diff --git a/doc/examples/param.yaml b/doc/examples/param.yaml index 3e5fcac..41514ed 100644 --- a/doc/examples/param.yaml +++ b/doc/examples/param.yaml @@ -8,3 +8,10 @@ global_loop_param_num: [1, 2, 3] # Plot parameters plot_log_path: /tmp/testium_plot/$(testrun_date)/$(testrun_time)/ + +python_path: $(home)/tmp/tum_venv/bin/python3 + +lua_path: /usr/bin/lua +lua_env: + LUA_PATH: /usr/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;/usr/lib/lua/5.4/?.lua;/usr/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua;/home/francois/.luarocks/share/lua/5.4/?.lua;/home/francois/.luarocks/share/lua/5.4/?/init.lua + LUA_CPATH: /usr/local/lib/lua/5.4/?.so;/usr/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so;/usr/lib/lua/5.4/loadall.so;./?.so;/home/francois/.luarocks/lib/lua/5.4/?.so diff --git a/doc/manual/sphinx/source/test_items/func_test_item.rst b/doc/manual/sphinx/source/test_items/py_func_test_item.rst similarity index 97% rename from doc/manual/sphinx/source/test_items/func_test_item.rst rename to doc/manual/sphinx/source/test_items/py_func_test_item.rst index 701706b..619fd78 100644 --- a/doc/manual/sphinx/source/test_items/func_test_item.rst +++ b/doc/manual/sphinx/source/test_items/py_func_test_item.rst @@ -26,7 +26,7 @@ From this class it is possible to define some custom reported values with the fo import py_func.tm as tm - class TestItemFunc(tm.FunctionItem) + class TestItemPyFunc(tm.FunctionItem) def exec(param1, param2, param4, param4): ... @@ -42,7 +42,7 @@ The ``exec`` method of the ``FunctionItem`` derived class is executed while runn - py_func: name: function test item file: scriptTestFile.py - func_name: TestItemFunc + func_name: TestItemPyFunc param: - 123 - 0.123 diff --git a/doc/manual/sphinx/source/tum_syntax.rst b/doc/manual/sphinx/source/tum_syntax.rst index 87270c3..046f1a0 100644 --- a/doc/manual/sphinx/source/tum_syntax.rst +++ b/doc/manual/sphinx/source/tum_syntax.rst @@ -231,7 +231,7 @@ step list attributes. test_items/dialog_question_test_item.rst test_items/dialog_reference_test_item.rst test_items/dialog_value_test_item.rst - test_items/func_test_item.rst + test_items/py_func_test_item.rst test_items/git_test_item.rst test_items/group_test_item.rst test_items/json-rpc_test_item.rst diff --git a/src/testium/interpreter/process.py b/src/testium/interpreter/process.py index 1e390da..94c4d8a 100644 --- a/src/testium/interpreter/process.py +++ b/src/testium/interpreter/process.py @@ -22,7 +22,8 @@ from interpreter.utils.test_init import ( from interpreter.test_set import TestSet from interpreter.utils.stdout_redirect import stdio_redir from interpreter.utils.tum_except import print_exception -from interpreter.utils.func_exec import func_call_init +from interpreter.utils.py_func_exec import py_func_call_init +from interpreter.utils.lua_func_exec import lua_func_call_init from interpreter.utils.api_srv import api_request @@ -81,8 +82,9 @@ class TestProcess(Process): test_set.report_path = locate_report_file(test_set.report_path) - # Python functions call subprocess initialization - fproc = func_call_init(tm.gd("python_path", ""), api_request) + # Python & lua functions call subprocess initialization + py_fproc = py_func_call_init(tm.gd("python_path", ""), api_request) + lua_fproc = lua_func_call_init(tm.gd("lua_path", "/usr/bin/lua"), api_request) self.__loaded = True @@ -99,8 +101,10 @@ class TestProcess(Process): try: test_run_init() print(test_run_header()) - fproc.start() - fproc.wait_ready() + py_fproc.start() + lua_fproc.start() + lua_fproc.wait_ready() + py_fproc.wait_ready() test_set.execute() finally: if test_set.success(): @@ -111,8 +115,10 @@ class TestProcess(Process): test_set.run_post_exec() finally: # Stop function execution process - fproc.stop() - fproc.join() + py_fproc.stop() + lua_fproc.stop() + lua_fproc.join() + py_fproc.join() self.__exec = False # Sends signal to the GUI self.send_finished() diff --git a/src/testium/interpreter/test_items/test_item_cycle.py b/src/testium/interpreter/test_items/test_item_cycle.py index 78a8b5e..c359fa7 100644 --- a/src/testium/interpreter/test_items/test_item_cycle.py +++ b/src/testium/interpreter/test_items/test_item_cycle.py @@ -1,7 +1,7 @@ import traceback from interpreter.utils.tum_except import ETUMSyntaxError, ETUMRuntimeError -from interpreter.utils.func_exec import func_exec +from interpreter.utils.py_func_exec import py_func_exec from interpreter.test_items.test_item import TestItem, test_run from interpreter.test_items.test_result import TestResult, TestValue import libs.testium as tm @@ -204,7 +204,7 @@ class TestItemCycle(TestItem): pl = self._prms.expanse(param_list) else: pl = [self._currentLoop] - fsucc, res = func_exec(file, func, pl) + fsucc, res = py_func_exec(file, func, pl) if fsucc == TestValue.SUCCESS: fres, _ = res if fres: diff --git a/src/testium/interpreter/test_items/test_item_func.py b/src/testium/interpreter/test_items/test_item_lua_func.py similarity index 96% rename from src/testium/interpreter/test_items/test_item_func.py rename to src/testium/interpreter/test_items/test_item_lua_func.py index 59d9abb..912d965 100644 --- a/src/testium/interpreter/test_items/test_item_func.py +++ b/src/testium/interpreter/test_items/test_item_lua_func.py @@ -7,12 +7,12 @@ import textwrap from interpreter.test_items.test_item import TestItem, test_run from interpreter.test_items.test_result import TestValue import libs.testium as tm -from interpreter.utils.func_exec import func_exec +from interpreter.utils.lua_func_exec import lua_func_exec from interpreter.utils.tum_except import ETUMSyntaxError from interpreter.utils.constants import TestItemType as cst -class TestItemFunc(TestItem): +class TestItemLuaFunc(TestItem): """py_func item usage. func file: func_file.py, func_name: func, param: [$(variable1), [1, 2, 3], true] """ diff --git a/src/testium/interpreter/test_items/test_item_py_func.py b/src/testium/interpreter/test_items/test_item_py_func.py new file mode 100644 index 0000000..167c8fb --- /dev/null +++ b/src/testium/interpreter/test_items/test_item_py_func.py @@ -0,0 +1,77 @@ +import sys +import traceback + +import pprint +import textwrap + +from interpreter.test_items.test_item import TestItem, test_run +from interpreter.test_items.test_result import TestValue +import libs.testium as tm +from interpreter.utils.py_func_exec import py_func_exec +from interpreter.utils.tum_except import ETUMSyntaxError +from interpreter.utils.constants import TestItemType as cst + + +class TestItemPyFunc(TestItem): + """py_func item usage. + func file: func_file.py, func_name: func, param: [$(variable1), [1, 2, 3], true] + """ + + def __init__(self, dict_item, parent=None, status_queue=None, filename=""): + self._name = cst.TYPE_FUNCTION.item_name + super().__init__(dict_item, parent, status_queue, filename=filename) + self._type = cst.TYPE_FUNCTION + self.is_container = False + try: + self.file_name = self._prms.getParam("file", required=True) + self.func_name = self._prms.getParam("func_name", required=True) + self.params = self._prms.getParamAll("param") + except: + raise ETUMSyntaxError( + f"The '{self.cmd()}' test item named '{self.name()}' (child of '{self.parent.name()}') has a missing or wrong parameter", + self.seqFilename(), + ) + + @test_run + def execute(self): + self.result.set( + TestValue.FAILURE, "an exception occured during function execution." + ) + try: + self.file_name = self._prms.expanse(self.file_name) + self.func_name = self._prms.expanse(self.func_name) + param_list = self._prms.getParamFromList(self.params) + pl = self._prms.expanse(param_list) + if tm.debug_enabled(): + tm.print_debug("Parameters list:") + tm.print_debug(textwrap.indent(pprint.pformat(pl), " |")) + success, ret = py_func_exec(self.file_name, self.func_name, pl) + + if success == TestValue.SUCCESS: + self.result.set(TestValue.SUCCESS) + res, reported_values = ret + reported_values = {**reported_values, "returned": res} + self.result.reported = ret[1] + + if tm.debug_enabled(): + tm.print_debug("Returned value:") + tm.print_debug(textwrap.indent(pprint.pformat(res), " |")) + + # The result of the func test item is put in global dir and result + tm.setgd("fn_" + self._name, res) + self.result.value = res + + else: + self.result.set(TestValue.FAILURE, ret) + if tm.debug_enabled(): + tm.print_debug("Failed:") + tm.print_debug(textwrap.indent(pprint.pformat(ret), " |")) + + return + + except: + traceback.print_exception(*sys.exc_info()) + self.result.set( + TestValue.FAILURE, + 'Unrecoverable "py_func" item error from {}'.format(self.func_name), + ) diff --git a/src/testium/interpreter/test_set.py b/src/testium/interpreter/test_set.py index f507f19..d07eacf 100644 --- a/src/testium/interpreter/test_set.py +++ b/src/testium/interpreter/test_set.py @@ -8,7 +8,7 @@ from interpreter.utils.tum_except import ( ) import interpreter.utils.settings as prefs from interpreter.test_report.test_report import TestReport -from interpreter.utils.func_exec import func_exec +from interpreter.utils.py_func_exec import py_func_exec from interpreter.utils.constants import TestItemType as cst_type import interpreter.utils.constants as cst from interpreter.utils.constants import TEST_TYPE_LIST @@ -331,13 +331,13 @@ class TestSet: tm.print_debug(f'Post-execution from: "{post_exec_file}"') if self.rootItem().result.success: # tests backup is done here - succ, res = func_exec(post_exec_file, "post_exec", []) + succ, res = py_func_exec(post_exec_file, "post_exec", []) if not succ == TestValue.SUCCESS: tm.print_debug( f"Test success but the \"post_exec\" function failed: {res}" ) else: - succ, res = func_exec(post_exec_file, "post_exec_fail", []) + succ, res = py_func_exec(post_exec_file, "post_exec_fail", []) if not succ == TestValue.SUCCESS: tm.print_debug( f"Test failed but the \"post_exec_fail\" function failed: {res}" diff --git a/src/testium/interpreter/utils/lua_func_exec.py b/src/testium/interpreter/utils/lua_func_exec.py new file mode 100644 index 0000000..e30d9e8 --- /dev/null +++ b/src/testium/interpreter/utils/lua_func_exec.py @@ -0,0 +1,153 @@ +import os +import shutil +import subprocess +import socket +import libs.testium as tm +from interpreter.utils.tum_except import ETUMRuntimeError +from interpreter.utils.jrpc import JsonRpcClient +from interpreter.test_items.test_result import TestValue + +function_call_process = None + + +def lua_func_call_init(lua_path, request_handler): + global function_call_process + function_call_process = LuaFuncExecEngine(lua_path, request_handler) + return function_call_process + + +def lua_version(path: str): + result = subprocess.run( + [path, "-v"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + vers = ((result.stdout.split(" "))[1]).split(".") + return (vers[0], vers[1], vers[2]) + + +def is_lua_interpreter(path: str, timeout=2) -> bool: + try: + result = subprocess.run( + [path, "-v"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=timeout, + ) + return (result.returncode == 0) and (result.stdout.startswith("Lua")) + except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired): + return False + + +class LuaFuncExecEngine: + + def __init__(self, lua_path="", request_handler=None): + if shutil.which(lua_path) is None: + raise ETUMRuntimeError( + f"The passed lua path is not pointing to an executable: '{lua_path}'" + ) + + if not is_lua_interpreter(lua_path): + raise ETUMRuntimeError( + f"The passed executable is not a lua interpreter: '{lua_path}'" + ) + + self._lpath = lua_path + self._req_handler = request_handler + self._process = None + self._port = 0 + self._rpc = None + + def start(self): + """ + run the subprocess to execute the python functions of the test. + """ + # This thread is not closed until new test is loaded + if self._process is not None: + raise ETUMRuntimeError("The function subprocess has already been started.") + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(("localhost", 0)) + self._port = sock.getsockname()[1] + + func_proc_path = os.path.join(tm.gd("testium_path"),"lua_func") + lua_env = tm.gd("lua_env", {}) + + self._process = subprocess.Popen( + [self._lpath, "main.lua", "--host", "localhost", "--port", f"{self._port}"], env=lua_env, cwd=func_proc_path + ) + + # Port was reserved until the sub-process is started. Now released. + if sock is not None: + sock.close() + + self._rpc = JsonRpcClient(self._port, req_handler=self._req_handler) + self._rpc.start() + + def join(self): + if self._rpc is not None: + self._rpc.join() + self._rpc = None + self._process = None + + def wait_ready(self, timeout=None): + if self._rpc is not None and self._rpc.is_alive(): + return self._rpc.wait_ready(timeout) + return False + + def stop(self): + if self._rpc is not None: + self._rpc.stop() + + def func_call(self, file: str, func_name: str, params: list, verbose: bool = True): + if (self._rpc is not None) and self._rpc.is_alive(): + answer = self._rpc.call( + "func_call", + { + "file": file, + "fname": func_name, + "params": params, + "verbose": verbose, + }, + ) + if "result" in answer: + reported_values = answer["result"].get("reported_values", {}) + if "returned_value" in answer["result"]: + res = answer["result"]["returned_value"] + return TestValue.SUCCESS, (res, reported_values) + else: + raise ETUMRuntimeError( + "Unexepected py_func jrpc result. To be reported to testium support team." + ) + + # In case an error was encountered in the called function + elif "error" in answer: + msg = f"{answer["error"]}" + return TestValue.FAILURE, msg + + else: + raise ETUMRuntimeError( + "Unexepected py_func call failure to be reported to testium support team." + ) + else: + raise ETUMRuntimeError( + "No function execution process active. To be reported to testium support team." + ) + + +def lua_func_exec(file: str, func_name: str, params: list, verbose: bool = True): + """Executes a python function and returns its result and reported values""" + global function_call_process + + if function_call_process is not None: + success, result = function_call_process.func_call( + file, func_name, params, verbose + ) + else: + raise ETUMRuntimeError( + "No function execution process active. To be reported to testium support team." + ) + + return success, result \ No newline at end of file diff --git a/src/testium/interpreter/utils/func_exec.py b/src/testium/interpreter/utils/py_func_exec.py similarity index 95% rename from src/testium/interpreter/utils/func_exec.py rename to src/testium/interpreter/utils/py_func_exec.py index c98246c..1e2970f 100644 --- a/src/testium/interpreter/utils/func_exec.py +++ b/src/testium/interpreter/utils/py_func_exec.py @@ -10,9 +10,9 @@ from interpreter.test_items.test_result import TestValue function_call_process = None -def func_call_init(python_path, request_handler): +def py_func_call_init(python_path, request_handler): global function_call_process - function_call_process = FuncExecEngine(python_path, request_handler) + function_call_process = PyFuncExecEngine(python_path, request_handler) return function_call_process @@ -40,7 +40,7 @@ def is_python_interpreter(path: str, timeout=2) -> bool: return False -class FuncExecEngine: +class PyFuncExecEngine: def __init__(self, python_path="", request_handler=None): if python_path != "": @@ -140,7 +140,7 @@ class FuncExecEngine: ) -def func_exec(file: str, func_name: str, params: list, verbose: bool = True): +def py_func_exec(file: str, func_name: str, params: list, verbose: bool = True): """Executes a python function and returns its result and reported values""" global function_call_process diff --git a/src/testium/interpreter/utils/test_init.py b/src/testium/interpreter/utils/test_init.py index 89ba304..05317b5 100644 --- a/src/testium/interpreter/utils/test_init.py +++ b/src/testium/interpreter/utils/test_init.py @@ -32,7 +32,7 @@ from interpreter.test_items.test_item_cycle import TestItemCycle from interpreter.test_items.test_item_runtime_plot import TestItemPlot from interpreter.test_items.test_item_group import TestItemGroup from interpreter.test_items.test_item_git import TestItemGit -from interpreter.test_items.test_item_func import TestItemFunc +from interpreter.test_items.test_item_py_func import TestItemPyFunc from interpreter.test_items.test_item_let import TestItemLet from interpreter.test_items.test_item_check import TestItemCheckValue from interpreter.test_items.test_item_json_rpc import TestItemJSON_RPC @@ -51,7 +51,7 @@ from interpreter.test_items.test_item_report import TestItemReport def _constants_init(): cst.TYPE_CONSOLE.item_class = TestItemConsole cst.TYPE_CYCLE.item_class = TestItemCycle - cst.TYPE_FUNCTION.item_class = TestItemFunc + cst.TYPE_FUNCTION.item_class = TestItemPyFunc cst.TYPE_GIT.item_class = TestItemGit cst.TYPE_GRAPH.item_class = TestItemPlot cst.TYPE_GROUP.item_class = TestItemGroup diff --git a/src/testium/libs/runtime_plot.py b/src/testium/libs/runtime_plot.py index 4e057de..22af316 100644 --- a/src/testium/libs/runtime_plot.py +++ b/src/testium/libs/runtime_plot.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta, timezone from interpreter.test_items.test_result import TestValue from interpreter.utils.tum_except import ETUMRuntimeError -from interpreter.utils.func_exec import func_exec +from interpreter.utils.py_func_exec import py_func_exec from interpreter.utils.eval import post_evaluate from interpreter.utils.periodic_timer import PeriodicTimer from interpreter.utils.paths import abs_path_from_file, prepare_file_to_save @@ -272,7 +272,7 @@ class RuntimePlotPeriodic(PeriodicTimer): self.on_timer_event() def on_timer_event(self): - succ, ret = func_exec(self.file, self.func_name, self.args) + succ, ret = py_func_exec(self.file, self.func_name, self.args) if succ == TestValue.SUCCESS: res, _ = ret res = post_evaluate(self.post_eval, res) diff --git a/src/testium/lua_func/handle.lua b/src/testium/lua_func/handle.lua new file mode 100644 index 0000000..323339a --- /dev/null +++ b/src/testium/lua_func/handle.lua @@ -0,0 +1,56 @@ +local utils = require("utils") +local tm = require("tm") + +local handle = {} + +local function _get_func_by_path(file_path, func_name) + -- 1. Load the file from the path + -- loadfile returns a 'chunk' (a function that runs the file's code) + local chunk, load_err = loadfile(file_path) + + if not chunk then + return nil, "Failed to load file: " .. tostring(load_err) + end + + -- 2. Execute the chunk to get the module's return value + -- Most Lua modules end with 'return { ... }' + local ok, module = pcall(chunk) + + if not ok then + return nil, "Error executing file: " .. tostring(module) + end + + -- 3. Validate the module is a table and contains the function + if type(module) ~= "table" then + return nil, "Module did not return a table (returned " .. type(module) .. ")" + end + + local target_func = module[func_name] + if type(target_func) ~= "function" then + return nil, "Function '" .. func_name .. "' not found in " .. file_path + end + + return target_func +end + +function handle.func_call(file, fname, params) + local pfile = file + -- 1. modify the file path if it is relative + if utils.is_relative_path(file) then + local td = tm.gd("test_directory") + pfile = utils.join_paths(td, file) + end + -- 2. retrieve the function "fname" + local func, err = _get_func_by_path(pfile, fname) + + -- 3. Execute the function + local res = nil + if err == nil then + succ, res = pcall(func, table.unpack(params)) + end + + -- 4. Returns result + return res, err +end + +return handle diff --git a/src/testium/lua_func/json-rpc.lua b/src/testium/lua_func/json-rpc.lua new file mode 100644 index 0000000..b0e0f53 --- /dev/null +++ b/src/testium/lua_func/json-rpc.lua @@ -0,0 +1,108 @@ +local json = require("cjson") + +local JSONRPC = {} +JSONRPC.__index = JSONRPC + +function JSONRPC.new(send_fn) + local self = setmetatable({}, JSONRPC) + self.send_raw = send_fn -- Function to transmit string data to transport (TCP/Websocket) + self.methods = {} -- Methods the server provides to the client + self.pending = {} -- Requests sent to client waiting for response + self.next_id = 1 + return self +end + +--- Register a method the client can call +function JSONRPC:register(name, callback) + self.methods[name] = callback +end + +--- Handle incoming raw data from the transport layer +function JSONRPC:handle_message(raw_data) + local ok, msg = pcall(json.decode, raw_data) + if not ok then return self:_send_error(nil, -32700, "Parse error") end + + -- 1. Check if it's a Response (has 'result' or 'error' and 'id') + if msg.result ~= nil or msg.error ~= nil then + return self:_handle_response(msg) + end + + -- 2. Check if it's a Request + if msg.method then + return self:_handle_request(msg) + end +end + +--- INTERNAL: Handle requests from the client +function JSONRPC:_handle_request(req) + local method = self.methods[req.method] + if not method then + if req.id then self:_send_error(req.id, -32601, "Method not found") end + return + end + + local ok, result = pcall(method, req.params) + + -- Only send response if it's not a Notification (notifications have no ID) + if req.id then + if ok then + self:_send({ jsonrpc = "2.0", result = result, id = req.id }) + else + self:_send_error(req.id, -32603, "Internal error: " .. tostring(result)) + 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 +function JSONRPC:call(method, params, callback) + local id = self.next_id + self.next_id = self.next_id + 1 + + if callback then + self.pending[id] = callback + end + + self:_send({ + jsonrpc = "2.0", + method = method, + params = params, + id = id + }) +end + +function JSONRPC:call_sync(method, params) + local callco = coroutine.create(function(m, p) + local co = coroutine.running() + -- Call the async version, but use the callback to resume this coroutine + self:call(m, p, function(err, res) + coroutine.resume(co, err, res) + end) + + -- Pause execution here until 'resume' is called + return coroutine.yield() + end) + return coroutine.resume(callco, method, params) +end + +function JSONRPC:_send(data) + self.send_raw(json.encode(data)) +end + +function JSONRPC:_send_error(id, code, message) + self:_send({ + jsonrpc = "2.0", + error = { code = code, message = message }, + id = id + }) +end + +return JSONRPC \ No newline at end of file diff --git a/src/testium/lua_func/main.lua b/src/testium/lua_func/main.lua new file mode 100644 index 0000000..5738ba0 --- /dev/null +++ b/src/testium/lua_func/main.lua @@ -0,0 +1,111 @@ +-- ========================= +-- Options par défaut +-- ========================= +local config = { + host = "0.0.0.0", + port = 9000, + timeout = 60, + verbose = true, +} + +local function usage() + print([[ +Usage: lua lua_func [options] + +Options: + --host Adresse d'écoute (default: 0.0.0.0) + --port Port TCP (default: 9000) + --timeout Timeout client en secondes (default: 60) + --verbose Logs détaillés + --help Affiche cette aide +]]) + os.exit(0) +end + +-- ========================= +-- Parsing des arguments +-- ========================= +local i = 1 +while i <= #arg do + local a = arg[i] + + if a == "--host" then + i = i + 1 + config.host = arg[i] + + elseif a == "--port" then + i = i + 1 + config.port = tonumber(arg[i]) + + elseif a == "--timeout" then + i = i + 1 + config.timeout = tonumber(arg[i]) + + elseif a == "--verbose" then + config.verbose = true + + elseif a == "--help" then + usage() + + else + print("Unknown option:", a) + usage() + end + + i = i + 1 +end + +local socket = require("socket") +local JSONRPC = require("json-rpc") -- The module from the previous response +local utils = require("utils") + +utils.verbose = config.verbose + +-- Create the master socket +local server_sock = assert(socket.bind(config.host, config.port)) +utils.log("listening on %s:%d", config.host, config.port) + +server_sock:settimeout(config.timeout) -- Prevents hanging on dead connections + +-- Main Server Loop +local client_sock, err = server_sock:accept() +if err then + utils.log("connection failed: %s", err) + os.exit(0) +end + +client_sock:settimeout(10) -- Prevents hanging on dead connections + +utils.log("Client connected!") + +-- Initialize the RPC instance for this specific connection +local rpc = JSONRPC.new(function(data) + client_sock:send(data .. "\n") -- Standard JSON-RPC uses newline delimiters over TCP +end) + +utils.setup_remote_print(rpc) + +-- Define Server Methods +rpc:register("echo", function(params) + return params +end) + +-- Example: Send a request TO the client immediately upon connection +rpc:call("greet", { msg = "Welcome to the server" }, function(err, res) + if not err then print("Client replied to greeting:", res) end +end) + +-- Communication Loop for this client +while true do + local line, err = client_sock:receive() -- Read until newline + if err == "closed" then + utils.log("Connection closed:", err) + break + elseif err then + socket.sleep(0.01) + else + rpc:handle_message(line) + end +end + +client_sock:close() diff --git a/src/testium/lua_func/tm.lua b/src/testium/lua_func/tm.lua new file mode 100644 index 0000000..45a3acf --- /dev/null +++ b/src/testium/lua_func/tm.lua @@ -0,0 +1,29 @@ + +local tm = {} + +local SUPPORTED_API = { + "gd", + "setgd", + "delgd", +} + +-- underlying function + +function tm._init_api(rpc) + tm._rpc = rpc + + local function _api_request(fname, ...) + local args = {...} + return tm._rpc:call_sync(fname, args) + end + + for _, fname in ipairs(SUPPORTED_API) do + -- create a closure that calls common_handler with fname + tm[fname] = function(...) + return _api_request(fname, ...) + end + end + +end + +return tm diff --git a/src/testium/lua_func/utils.lua b/src/testium/lua_func/utils.lua new file mode 100644 index 0000000..d8d96c4 --- /dev/null +++ b/src/testium/lua_func/utils.lua @@ -0,0 +1,67 @@ +local utils = {} + +utils.verbose = false + +function utils.log(fmt, ...) + if utils.verbose then + -- print("[lua_func server]", ...) + io.stdout:write(string.format("[lua_func server] - " .. fmt .. "\n", ...)) + end +end + +utils.sep = package.config:sub(1,1) + +function utils.join_paths(p1, p2) + return p1 .. utils.sep .. p2 +end + +function utils.is_absolute_path(path) + if not path or path == "" then return false end + + -- 1. Check for POSIX absolute path (starts with /) + if path:sub(1, 1) == "/" then + return true + end + + -- 2. Check for Windows drive letter (e.g., C:\ or D:/) + -- Pattern: %a (letter) followed by : (colon) + if path:match("^%a:[/\\]") or path:match("^%a:$") then + return true + end + + -- 3. Check for Windows UNC/Network paths (starts with \\ or //) + if path:match("^[/\\][/\\]") then + return true + end + + return false +end + +function utils.is_relative_path(path) + return not utils.is_absolute_path(path) +end + +function utils.setup_remote_print(rpc) + -- Store the original print if you still need to log to the server console + _G.native_print = _G.native_print or _G.print + + -- Define the new local print + _G.print = function (...) + local args = table.pack(...) + local output = {} + + for i = 1, args.n do + table.insert(output, tostring(args[i])) + end + + local message = table.concat(output, "\t") + + pcall(function() + rpc:call_sync("print", message ) + end) + -- Optional: Still print to the server's local console + -- utils.log("[Remote Log Sent]: " .. message) + end +end + +return utils \ No newline at end of file diff --git a/src/testium/py_func/__init__.py b/src/testium/py_func/__init__.py index 1db779e..f5010c7 100755 --- a/src/testium/py_func/__init__.py +++ b/src/testium/py_func/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python import multiprocessing -from py_func.tm import _init_api, remote_print +from py_func.tm import _init_api, _remote_print from interpreter.utils.stdout_redirect import stdio_redir @@ -9,7 +9,7 @@ class TcpStdOut: pass def write(self, s: str) -> None: - remote_print(s) + _remote_print(s) def flush(self): pass @@ -30,7 +30,7 @@ def main(): outstream = TcpStdOut() stdio_redir.redirect(outstream) # debug the server - # thrd_api.dbg_out = stdio_redir.ini_stdout + thrd_api.dbg_out = stdio_redir.ini_stdout try: while thrd_api.is_alive(): thrd_api.join(1) diff --git a/src/testium/py_func/tm.py b/src/testium/py_func/tm.py index b2f01d3..c41ee1d 100644 --- a/src/testium/py_func/tm.py +++ b/src/testium/py_func/tm.py @@ -70,7 +70,7 @@ def _init_api(port): ############################################################################### -def remote_print(*values): +def _remote_print(*values): """Forward print-like output to the remote handler. If a ``_func_call_thread`` is available, this function calls the