From f54e09098ab71ad28461a255ce1044a44cb1d262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Tue, 30 Dec 2025 19:30:19 +0100 Subject: [PATCH] close to the lua func goal. --- doc/examples/lua_func.lua | 8 +- .../interpreter/utils/lua_func_exec.py | 2 +- src/testium/lua_func/handle.lua | 22 ++++- src/testium/lua_func/json-rpc.lua | 94 ++++++++++++------- src/testium/lua_func/main.lua | 24 +---- src/testium/lua_func/tm.lua | 2 +- src/testium/lua_func/utils.lua | 8 +- 7 files changed, 95 insertions(+), 65 deletions(-) diff --git a/doc/examples/lua_func.lua b/doc/examples/lua_func.lua index c12958e..9b4e38c 100644 --- a/doc/examples/lua_func.lua +++ b/doc/examples/lua_func.lua @@ -1,6 +1,10 @@ tm = require("tm") -function func_to_executed(param) +local module = {} + +function module.func_to_be_executed(param) -- return tm.gd(param) return param -end \ No newline at end of file +end + +return module \ No newline at end of file diff --git a/src/testium/interpreter/utils/lua_func_exec.py b/src/testium/interpreter/utils/lua_func_exec.py index e30d9e8..0b773e9 100644 --- a/src/testium/interpreter/utils/lua_func_exec.py +++ b/src/testium/interpreter/utils/lua_func_exec.py @@ -76,7 +76,7 @@ class LuaFuncExecEngine: 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 + [self._lpath, "main.lua", "--host", "127.0.0.1", "--port", f"{self._port}"], env=lua_env, cwd=func_proc_path ) # Port was reserved until the sub-process is started. Now released. diff --git a/src/testium/lua_func/handle.lua b/src/testium/lua_func/handle.lua index 323339a..fae3e13 100644 --- a/src/testium/lua_func/handle.lua +++ b/src/testium/lua_func/handle.lua @@ -33,20 +33,34 @@ local function _get_func_by_path(file_path, func_name) return target_func end -function handle.func_call(file, fname, params) +function handle.func_call(params) + local file = params.file + local fname = params.fname + local prms = params.params + local res = nil + local succ, ret + local pfile = file -- 1. modify the file path if it is relative - if utils.is_relative_path(file) then + if utils.is_relative_path(pfile) 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)) + utils.log("func_call function found '%s', '%s'", file, fname) + succ, ret = pcall(func, table.unpack(prms)) + utils.log("func_call returned '%s'", tostring(ret)) + + if succ then + res = ret + else + err = ret + end end -- 4. Returns result diff --git a/src/testium/lua_func/json-rpc.lua b/src/testium/lua_func/json-rpc.lua index 33377a2..2d31ab4 100644 --- a/src/testium/lua_func/json-rpc.lua +++ b/src/testium/lua_func/json-rpc.lua @@ -4,12 +4,14 @@ local utils = require("utils") local JSONRPC = {} JSONRPC.__index = JSONRPC -function JSONRPC.new(send_fn) +function JSONRPC.new(sock) local self = setmetatable({}, JSONRPC) - self.send_raw = send_fn -- 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.pending = {} -- Requests sent to client waiting for response self.next_id = 1 + + self.sock:settimeout(0.2) return self end @@ -25,8 +27,9 @@ function JSONRPC:handle_message(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) + if (msg.id ~= nil) and (msg.result ~= nil or msg.error ~= nil) then + self.pending[msg.id] = msg + return end -- 2. Check if it's a Request @@ -38,67 +41,72 @@ end --- INTERNAL: Handle requests from the client function JSONRPC:_handle_request(req) local method = self.methods[req.method] + local ok, ret + local res, err 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) + utils.log("calling '%s'", method) + ok, ret = pcall(method, req.params) + utils.log("returned '%s', '%s'", tostring(ok), tostring(res)) -- 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 }) + res, err = ret + if res == nil then + self:_send_error(req.id, -32603, "Internal error: " .. tostring(err)) + else + self:_send({ jsonrpc = "2.0", result = {returned = res}, id = req.id }) + end else - self:_send_error(req.id, -32603, "Internal error: " .. tostring(result)) + self:_send_error(req.id, -32603, "Internal error: " .. tostring(ret)) 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 +-- 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) +function JSONRPC:call(method, params) 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) + -- ---- Wait for response (re-entrant loop) + while true do + self:poll() - -- Pause execution here until 'resume' is called - return coroutine.yield() - end) - return coroutine.resume(callco, method, params) + if self.pending[id] then + local resp = self.pending[id] + self.pending[id] = nil + + if resp.error then + error(resp.error.message) + end + return resp.result + end + end end function JSONRPC:_send(data) local j = json.encode(data) utils.log("sending: '%s'", j) - self.send_raw(j) + return self.sock:send(j .. "\n") end function JSONRPC:_send_error(id, code, message) @@ -109,4 +117,24 @@ function JSONRPC:_send_error(id, code, message) }) end +function JSONRPC:poll() + local line, err = self.sock:receive("*l") + + if line then + self:handle_message(line) + elseif err ~= "timeout" and err ~= nil then + utils.log("Connection ended: %s", err) + return false + end + return true +end + +function JSONRPC:loop() + while true do + if not self:poll() then + break + end + end +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 index d60426e..a7b2cb5 100644 --- a/src/testium/lua_func/main.lua +++ b/src/testium/lua_func/main.lua @@ -59,6 +59,7 @@ local socket = require("socket") local JSONRPC = require("json-rpc") -- The module from the previous response local utils = require("utils") local tm = require("tm") +local handle = require("handle") utils.verbose = config.verbose @@ -75,35 +76,18 @@ if err then 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) +local rpc = JSONRPC.new(client_sock) tm._init_api(rpc) utils.setup_remote_print(rpc) --- Example: Send a request TO the client immediately upon connection -print("Welcome to the lua server") - -local tdir = tm.gd("test_directory") +rpc:register("func_call", handle.func_call) -- Communication Loop for this client -while true do - local line, err = client_sock:receive() -- Read until newline - if err == "closed" then - utils.log("Connection ended: %s", err) - break - elseif err then - socket.sleep(0.01) - else - rpc:handle_message(line) - end -end +rpc:loop() client_sock:close() utils.log("Server stopped") diff --git a/src/testium/lua_func/tm.lua b/src/testium/lua_func/tm.lua index 45a3acf..af0d3ec 100644 --- a/src/testium/lua_func/tm.lua +++ b/src/testium/lua_func/tm.lua @@ -14,7 +14,7 @@ function tm._init_api(rpc) local function _api_request(fname, ...) local args = {...} - return tm._rpc:call_sync(fname, args) + return tm._rpc:call(fname, args) end for _, fname in ipairs(SUPPORTED_API) do diff --git a/src/testium/lua_func/utils.lua b/src/testium/lua_func/utils.lua index 1bb6a8f..3e1f0cd 100644 --- a/src/testium/lua_func/utils.lua +++ b/src/testium/lua_func/utils.lua @@ -19,18 +19,18 @@ 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 + if string.sub(path, 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 + if string.match(path, "^%a:[/\\]") or string.match(path, "^%a:$") then return true end -- 3. Check for Windows UNC/Network paths (starts with \\ or //) - if path:match("^[/\\][/\\]") then + if string.match(path, "^[/\\][/\\]") then return true end @@ -61,7 +61,7 @@ function utils.setup_remote_print(rpc) message = message .. "\n" end pcall(function() - rpc:call_sync("print", message ) + rpc:call("print", message ) end) -- Optional: Still print to the server's local console -- utils.log("[Remote Log Sent]: " .. message)