added debug features to the lua and python jrpc servers.

Added doc of lua func item
This commit is contained in:
2025-12-31 13:38:19 +01:00
parent f54e09098a
commit 2e9c5e6c52
17 changed files with 129 additions and 54 deletions

View File

@@ -166,7 +166,7 @@ The test items returning a value are:
* :ref:`dialog_value test items<sec_dialog_value_test_item>` * :ref:`dialog_value test items<sec_dialog_value_test_item>`
* :ref:`py_func test item<sec_func_item>` * :ref:`py_func test item<sec_py_func_item>`
* :ref:`dialog_choices test item<sec_dialog_choices_test_item>` * :ref:`dialog_choices test item<sec_dialog_choices_test_item>`

View File

@@ -0,0 +1,65 @@
.. _sec_lua_func_item:
**lua_func** test item
============================================================
The ``lua_func`` test item is used to execute custom lua 5.4 scripts with the given
input parameters.
The ``lua_func`` test item is of the form:
.. code-block:: lua
:caption: ``lua_func`` python function example
:name: script_file.lua
local module = {}
function module.dummy_func(param1, param2, param4, param4):
...
return 10
return module
.. code-block:: yaml
:caption: corresponding ``lua_func`` tum extract
- lua_func:
name: lua function test item
file: script_file.lua
func_name: dummy_func
param:
- 123
- 0.123
- True
- $(global_dict_key)
expected_result: 10
**Attributes**
Beside common test items attributes, lua_func item has specific attribute, some of which being mandatory.
* ``file``: the script file name that contains the function to be executed.
Only python script format is supported.
* ``func_name``: The function name to be executed.
* ``param``: This is a list of parameters that are passed to the function
in the order they are presented in the script. These parameters are not
mandatory and are highly dependent of the function prototype.
.. code-block:: yaml
:caption: ``lua_func`` test item example of usage
- lua_func:
name: activity
file: script_name.lua
func_name: methodName
param:
- $(my_param)
The result of the function (after eventual post treatment) is stored in the global
variable named ``pfn_<func_name>``
(See :ref:`global variables<sec_global_variables>` for more detail
on how to access to global variables from test items and scripts).
In the example above, the global variable ``$(lfn_activity)``
would be created at the end of the item execution. It would contain the resulting
value of the funcToBeExecuted python function.

View File

@@ -1,4 +1,4 @@
.. _sec_func_item: .. _sec_py_func_item:
**py_func** test item **py_func** test item
============================================================ ============================================================
@@ -99,10 +99,10 @@ Beside common test items attributes, py_func item has specific attribute, some o
- $(my_param) - $(my_param)
The result of the function (after eventual post treatment) is stored in the global The result of the function (after eventual post treatment) is stored in the global
variable named ``fn_<func_name>`` variable named ``pfn_<func_name>``
(See :ref:`global variables<sec_global_variables>` for more detail (See :ref:`global variables<sec_global_variables>` for more detail
on how to access to global variables from test items and scripts). on how to access to global variables from test items and scripts).
In the example above, the global variable ``$(fn_function test item)`` In the example above, the global variable ``$(pfn_function test item)``
would be created at the end of the item execution. It would contain the resulting would be created at the end of the item execution. It would contain the resulting
value of the funcToBeExecuted python function. value of the funcToBeExecuted python function.

View File

@@ -142,7 +142,8 @@ library API (see :ref:`helper library<sec_python_helper_library>`)
* ``ts_end_<item_name>``: timestamp at the end of test item execution (see :ref:`sec_item_common`), * ``ts_end_<item_name>``: timestamp at the end of test item execution (see :ref:`sec_item_common`),
* ``ts_duration_<item_name>``: duration of test item execution in seconds (see :ref:`sec_item_common`), * ``ts_duration_<item_name>``: duration of test item execution in seconds (see :ref:`sec_item_common`),
* ``cn_<test_name>``: console test item result (see section :ref:`sec_console_test_item`), * ``cn_<test_name>``: console test item result (see section :ref:`sec_console_test_item`),
* ``fn_<func_name>``: py_func test item result (see section :ref:`sec_func_item`), * ``pfn_<func_name>``: py_func test item result (see section :ref:`sec_py_func_item`),
* ``lfn_<func_name>``: lua_func test item result (see section :ref:`sec_lua_func_item`),
* ``cs_<test_name>``: dialog_choices test item result (see section :ref:`sec_dialog_choices_test_item`), * ``cs_<test_name>``: dialog_choices test item result (see section :ref:`sec_dialog_choices_test_item`),
* ``loop_param``: loop iterator (available from within a loop item, * ``loop_param``: loop iterator (available from within a loop item,
see :ref:`sec_loop_item`), see :ref:`sec_loop_item`),
@@ -237,6 +238,7 @@ step list attributes.
test_items/json-rpc_test_item.rst test_items/json-rpc_test_item.rst
test_items/let_test_item.rst test_items/let_test_item.rst
test_items/loop_test_item.rst test_items/loop_test_item.rst
test_items/lua_func_test_item.rst
test_items/plot_test_item.rst test_items/plot_test_item.rst
test_items/report_test_item.rst test_items/report_test_item.rst
test_items/run_test_item.rst test_items/run_test_item.rst

Binary file not shown.

View File

@@ -8,7 +8,7 @@ from interpreter.utils.eval import evaluate
class TestItemCheckValue(TestItem): class TestItemCheckValue(TestItem):
"""check item usage. """check item usage.
check usage:{check: {name: check my func output, steps: ['$(fn_echo) < 5']}} check usage:{check: {name: check my func output, steps: ['$(pfn_echo) < 5']}}
""" """
def __init__(self, dict_item, parent = None, status_queue=None, filename=""): def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
self._name = cst.TYPE_CHECK.item_name self._name = cst.TYPE_CHECK.item_name

View File

@@ -50,6 +50,7 @@ class TestItemLuaFunc(TestItem):
if success == TestValue.SUCCESS: if success == TestValue.SUCCESS:
self.result.set(TestValue.SUCCESS) self.result.set(TestValue.SUCCESS)
res, reported_values = ret res, reported_values = ret
print(res)
reported_values = {**reported_values, "returned": res} reported_values = {**reported_values, "returned": res}
self.result.reported = ret[1] self.result.reported = ret[1]
@@ -58,7 +59,7 @@ class TestItemLuaFunc(TestItem):
tm.print_debug(textwrap.indent(pprint.pformat(res), " |")) tm.print_debug(textwrap.indent(pprint.pformat(res), " |"))
# The result of the func test item is put in global dir and result # The result of the func test item is put in global dir and result
tm.setgd("fn_" + self._name, res) tm.setgd("lfn_" + self._name, res)
self.result.value = res self.result.value = res
else: else:

View File

@@ -58,7 +58,7 @@ class TestItemPyFunc(TestItem):
tm.print_debug(textwrap.indent(pprint.pformat(res), " |")) tm.print_debug(textwrap.indent(pprint.pformat(res), " |"))
# The result of the func test item is put in global dir and result # The result of the func test item is put in global dir and result
tm.setgd("fn_" + self._name, res) tm.setgd("pfn_" + self._name, res)
self.result.value = res self.result.value = res
else: else:

View File

@@ -38,7 +38,7 @@ Usage example (server):
Usage example (client): Usage example (client):
clt = JsonRpcClient(port) clt = JsonRpcClient(host, port)
clt.start() clt.start()
result = clt.call('method_name', {'foo': 'bar'}) result = clt.call('method_name', {'foo': 'bar'})
@@ -236,8 +236,9 @@ class JsonRpcBase(threading.Thread):
- `call()` raises `ETUMRuntimeError` if no active connection exists. - `call()` raises `ETUMRuntimeError` if no active connection exists.
""" """
def __init__(self, port, req_handler: Callable[[dict], Any]=None, timeout=10, dbg_out=None): def __init__(self, host, port, req_handler: Callable[[dict], Any]=None, timeout=10, dbg_out=None):
super().__init__() super().__init__()
self._host = host
self._port = port self._port = port
self._timeout = timeout self._timeout = timeout
self._rpc = None self._rpc = None
@@ -306,8 +307,8 @@ class JsonRpcSrv(JsonRpcBase):
The server will raise `ETUMRuntimeError` on accept/connect timeout. The server will raise `ETUMRuntimeError` on accept/connect timeout.
""" """
def __init__(self, port, req_handler = None, timeout=10): def __init__(self, host, port, req_handler = None, timeout=10):
super().__init__(port, req_handler, timeout) super().__init__(host, port, req_handler, timeout)
self.name = f"JsonRpcSvr_{port}" self.name = f"JsonRpcSvr_{port}"
def run(self): def run(self):
@@ -318,12 +319,13 @@ class JsonRpcSrv(JsonRpcBase):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Link of the socket at the configured port # Link of the socket at the configured port
sock.bind(("localhost", self._port)) sock.bind((self._host, self._port))
sock.settimeout(self._timeout) sock.settimeout(self._timeout)
# Listens incoming connections # Listens incoming connections
sock.listen(1) sock.listen(1)
self.print_info(f"listening on {self._host}:{self._port}")
self.print_info("awaiting connection") self.print_info("awaiting connection")
tslice = 0.2 tslice = 0.2
@@ -331,6 +333,7 @@ class JsonRpcSrv(JsonRpcBase):
while True: while True:
try: try:
conn, addr = sock.accept() conn, addr = sock.accept()
self.print_info("Client connected")
except socket.timeout: except socket.timeout:
if t >= 0: if t >= 0:
sleep(tslice) sleep(tslice)
@@ -364,13 +367,13 @@ class JsonRpcClient(JsonRpcBase):
Typical usage:: Typical usage::
clt = JsonRpcClient(port) clt = JsonRpcClient(host, port)
clt.start() clt.start()
resp = clt.call('method', {'a': 1}) resp = clt.call('method', {'a': 1})
""" """
def __init__(self, port, req_handler = None, timeout=10): def __init__(self, host, port, req_handler = None, timeout=10):
super().__init__(port, req_handler, timeout) super().__init__(host, port, req_handler, timeout)
self.name = f"JsonRpcClt_{port}" self.name = f"JsonRpcClt_{port}"
def run(self): def run(self):
@@ -383,7 +386,7 @@ class JsonRpcClient(JsonRpcBase):
t = self._timeout t = self._timeout
while True: while True:
try: try:
sock.connect(("localhost", self._port)) sock.connect((self._host, self._port))
except OSError: except OSError:
t -= tslice t -= tslice
if t >= 0: if t >= 0:

View File

@@ -75,15 +75,20 @@ class LuaFuncExecEngine:
func_proc_path = os.path.join(tm.gd("testium_path"),"lua_func") func_proc_path = os.path.join(tm.gd("testium_path"),"lua_func")
lua_env = tm.gd("lua_env", {}) lua_env = tm.gd("lua_env", {})
params = [self._lpath, "main.lua", "--host", "127.0.0.1", "--port", f"{self._port}"]
if tm.debug_enabled():
params.append("--verbose")
self._process = subprocess.Popen( self._process = subprocess.Popen(
[self._lpath, "main.lua", "--host", "127.0.0.1", "--port", f"{self._port}"], env=lua_env, cwd=func_proc_path params, env=lua_env, cwd=func_proc_path
) )
# Port was reserved until the sub-process is started. Now released. # Port was reserved until the sub-process is started. Now released.
if sock is not None: if sock is not None:
sock.close() sock.close()
self._rpc = JsonRpcClient(self._port, req_handler=self._req_handler) self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler)
self._rpc.start() self._rpc.start()
def join(self): def join(self):

View File

@@ -78,15 +78,19 @@ class PyFuncExecEngine:
func_proc_path = tm.gd("testium_path") func_proc_path = tm.gd("testium_path")
params = [self._ppath, "-m", "py_func", "-p", f"{self._port}"]
if tm.debug_enabled():
params.append("-v")
self._process = subprocess.Popen( self._process = subprocess.Popen(
[self._ppath, "-m", "py_func", "-p", f"{self._port}"], cwd=func_proc_path params, cwd=func_proc_path
) )
# Port was reserved until the sub-process is started. Now released. # Port was reserved until the sub-process is started. Now released.
if sock is not None: if sock is not None:
sock.close() sock.close()
self._rpc = JsonRpcClient(self._port, req_handler=self._req_handler) self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler)
self._rpc.start() self._rpc.start()
def join(self): def join(self):

View File

@@ -27,7 +27,7 @@ local function _get_func_by_path(file_path, func_name)
local target_func = module[func_name] local target_func = module[func_name]
if type(target_func) ~= "function" then if type(target_func) ~= "function" then
return nil, "Function '" .. func_name .. "' not found in " .. file_path return nil, "Function '" .. func_name .. "' not found in '" .. file_path .. "'"
end end
return target_func return target_func
@@ -52,9 +52,10 @@ function handle.func_call(params)
-- 3. Execute the function -- 3. Execute the function
if err == nil then if err == nil then
print(string.format("Function executed from '%s'", pfile))
utils.log("func_call function found '%s', '%s'", file, fname) utils.log("func_call function found '%s', '%s'", file, fname)
succ, ret = pcall(func, table.unpack(prms)) succ, ret = pcall(func, table.unpack(prms))
utils.log("func_call returned '%s'", tostring(ret)) utils.log("func_call returned '%s', '%s'", tostring(succ), tostring(ret))
if succ then if succ then
res = ret res = ret

View File

@@ -6,9 +6,9 @@ JSONRPC.__index = JSONRPC
function JSONRPC.new(sock) function JSONRPC.new(sock)
local self = setmetatable({}, JSONRPC) local self = setmetatable({}, JSONRPC)
self.sock = sock -- Function to transmit string data to transport (TCP/Websocket) self.sock = sock -- Function to transmit string data to transport (TCP/Websocket)
self.methods = {} -- Methods the server provides to the client self.methods = {} -- Methods the server provides to the client
self.pending = {} -- Requests sent to client waiting for response self.pending = {} -- Requests sent to client waiting for response
self.next_id = 1 self.next_id = 1
self.sock:settimeout(0.2) self.sock:settimeout(0.2)
@@ -24,7 +24,7 @@ end
function JSONRPC:handle_message(raw_data) function JSONRPC:handle_message(raw_data)
utils.log("received: '%s'", raw_data) utils.log("received: '%s'", raw_data)
local ok, msg = pcall(json.decode, raw_data) local ok, msg = pcall(json.decode, raw_data)
if not ok then return self:_send_error(nil, -32700, "Parse error") end if not ok then return self:_send_error(nil, "received message parse error in lua server") end
-- 1. Check if it's a Response (has 'result' or 'error' and 'id') -- 1. Check if it's a Response (has 'result' or 'error' and 'id')
if (msg.id ~= nil) and (msg.result ~= nil or msg.error ~= nil) then if (msg.id ~= nil) and (msg.result ~= nil or msg.error ~= nil) then
@@ -44,37 +44,27 @@ function JSONRPC:_handle_request(req)
local ok, ret local ok, ret
local res, err local res, err
if not method then if not method then
if req.id then self:_send_error(req.id, -32601, "Method not found") end if req.id then self:_send_error(req.id, string.format("Method '%s' not registered in lua server")) end
return return
end end
utils.log("calling '%s'", method) ok, ret, err = pcall(method, req.params)
ok, ret = pcall(method, req.params) utils.log("Call returned ok='%s', ret='%s'", tostring(ok), tostring(ret))
utils.log("returned '%s', '%s'", tostring(ok), tostring(res))
-- Only send response if it's not a Notification (notifications have no ID) -- Only send response if it's not a Notification (notifications have no ID)
if req.id then if req.id then
if ok then if ok then
res, err = ret res = ret
if res == nil then if res == nil then
self:_send_error(req.id, -32603, "Internal error: " .. tostring(err)) self:_send_error(req.id, tostring(err))
else else
self:_send({ jsonrpc = "2.0", result = {returned = res}, id = req.id }) self:_send({ jsonrpc = "2.0", result = { returned_value = res }, id = req.id })
end end
else else
self:_send_error(req.id, -32603, "Internal error: " .. tostring(ret)) self:_send_error(req.id, tostring(err))
end end
end end
end end
--- INTERNAL: Handle responses to requests WE sent
-- function JSONRPC:_handle_response(res)
-- local callback = self.pending[res.id]
-- if callback then
-- callback(res.error, res.result)
-- self.pending[res.id] = nil
-- end
-- end
--- Call a method on the client --- Call a method on the client
function JSONRPC:call(method, params) function JSONRPC:call(method, params)
local id = self.next_id local id = self.next_id
@@ -109,10 +99,10 @@ function JSONRPC:_send(data)
return self.sock:send(j .. "\n") return self.sock:send(j .. "\n")
end end
function JSONRPC:_send_error(id, code, message) function JSONRPC:_send_error(id, message)
self:_send({ self:_send({
jsonrpc = "2.0", jsonrpc = "2.0",
error = { code = code, message = message }, error = message,
id = id id = id
}) })
end end
@@ -137,4 +127,4 @@ function JSONRPC:loop()
end end
end end
return JSONRPC return JSONRPC

View File

@@ -5,7 +5,7 @@ local config = {
host = "0.0.0.0", host = "0.0.0.0",
port = 9000, port = 9000,
timeout = 60, timeout = 60,
verbose = true, verbose = false,
} }
local function usage() local function usage()

View File

@@ -22,15 +22,19 @@ def main():
import argparse import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-i", "--ip", type=str, help="Ip address or hostname to listen to",
default="localhost")
parser.add_argument("-p", "--port", type=int, help="port to listen to", parser.add_argument("-p", "--port", type=int, help="port to listen to",
default="/etc/jsonrpc-echo.conf") default=9000)
parser.add_argument("-v", "--verbose", action='store_true', help="port to listen to")
args = parser.parse_args() args = parser.parse_args()
thrd_api = _init_api(args.port) thrd_api = _init_api(args.ip, args.port)
outstream = TcpStdOut() outstream = TcpStdOut()
stdio_redir.redirect(outstream) stdio_redir.redirect(outstream)
# debug the server # debug the server
# thrd_api.dbg_out = stdio_redir.ini_stdout if args.verbose:
thrd_api.dbg_out = stdio_redir.ini_stdout
try: try:
while thrd_api.is_alive(): while thrd_api.is_alive():
thrd_api.join(1) thrd_api.join(1)

View File

@@ -46,7 +46,7 @@ def func_exec(file: str, func_name: str, params: list, verbose: bool=True):
reported_values = {} reported_values = {}
mod = func_module(file) mod = func_module(file)
if verbose: if verbose:
print("Function executed from {}".format( print("Function executed from '{}'".format(
inspect.getabsfile(mod))) inspect.getabsfile(mod)))
# check of the FunctionItem descendants # check of the FunctionItem descendants

View File

@@ -49,7 +49,7 @@ def _make_api(name):
for k in SUPPORTED_API: for k in SUPPORTED_API:
setattr(thismodule, k, _make_api(k)) setattr(thismodule, k, _make_api(k))
def _init_api(port): def _init_api(host, port):
"""Start and initialize the remote function handler. """Start and initialize the remote function handler.
Starts a ``FuncHandler`` bound to ``port``, runs it and blocks until Starts a ``FuncHandler`` bound to ``port``, runs it and blocks until
@@ -63,7 +63,7 @@ def _init_api(port):
``_func_call_thread``. ``_func_call_thread``.
""" """
global _func_call_thread global _func_call_thread
_func_call_thread = FuncHandler(port) _func_call_thread = FuncHandler(host, port)
_func_call_thread.start() _func_call_thread.start()
_func_call_thread.wait_ready() _func_call_thread.wait_ready()
return _func_call_thread return _func_call_thread