doc and various fixes of lua for windows

This commit is contained in:
2026-01-03 19:29:14 +01:00
parent 487156785a
commit 7954f2cb2b
9 changed files with 283 additions and 63 deletions

View File

@@ -5,6 +5,7 @@ import threading
import itertools
from time import sleep
from typing import Callable, Any
import libs.testium as tm
from interpreter.utils.tum_except import ETUMRuntimeError
@@ -53,8 +54,14 @@ Notes:
class JsonRpcConnection:
def __init__(self, name, conn: socket.socket, req_handler: Callable[..., Any], timeout=0.2, dbg_out=None):
def __init__(
self,
name,
conn: socket.socket,
req_handler: Callable[..., Any],
timeout=0.2,
dbg_out=None,
):
self.name = name
self.conn = conn
if not callable(req_handler):
@@ -120,9 +127,9 @@ class JsonRpcConnection:
def _dispatch(self, msg):
if "method" in msg:
# request to be sent
meth=msg["method"]
params=msg.get("params", None)
rid=msg.get("id", None)
meth = msg["method"]
params = msg.get("params", None)
rid = msg.get("id", None)
threading.Thread(
target=self._handle_request, args=(meth, params, rid), daemon=True
@@ -168,10 +175,9 @@ class JsonRpcConnection:
The send operation is protected by a lock to avoid interleaving when
multiple threads attempt to write to the underlying socket.
"""
msg = json.dumps(obj) + "\n"
data = (msg).encode()
self.print_info("sending : " + msg)
self.print_info(f"sending : " + msg)
with self.send_lock:
self.conn.sendall(data)
@@ -217,6 +223,7 @@ class JsonRpcConnection:
def join(self):
self.recv_thread.join()
class JsonRpcBase(threading.Thread):
"""Threaded base class for simple JSON-RPC server/client helpers.
@@ -236,7 +243,14 @@ class JsonRpcBase(threading.Thread):
- `call()` raises `ETUMRuntimeError` if no active connection exists.
"""
def __init__(self, host, 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__()
self._host = host
self._port = port
@@ -276,7 +290,9 @@ class JsonRpcBase(threading.Thread):
self._rpc.stop()
def connect(self, sock):
self._rpc = JsonRpcConnection(self.name, sock, self.handle_request, dbg_out=self.dbg_out)
self._rpc = JsonRpcConnection(
self.name, sock, self.handle_request, dbg_out=self.dbg_out
)
self._event_ready.set()
def wait_ready(self, timeout=None):
@@ -292,6 +308,7 @@ class JsonRpcBase(threading.Thread):
if self._rpc is not None:
self._rpc.dbg_out = dbg_out
class JsonRpcSrv(JsonRpcBase):
"""Single-connection JSON-RPC server.
@@ -307,7 +324,7 @@ class JsonRpcSrv(JsonRpcBase):
The server will raise `ETUMRuntimeError` on accept/connect timeout.
"""
def __init__(self, host, port, req_handler = None, timeout=10):
def __init__(self, host, port, req_handler=None, timeout=10):
super().__init__(host, port, req_handler, timeout)
self.name = f"JsonRpcSvr_{port}"
@@ -332,7 +349,6 @@ class JsonRpcSrv(JsonRpcBase):
while True:
try:
conn, addr = sock.accept()
self.print_info("Client connected")
break
except socket.timeout:
t -= tslice
@@ -369,11 +385,58 @@ class JsonRpcClient(JsonRpcBase):
resp = clt.call('method', {'a': 1})
"""
def __init__(self, host, port, req_handler = None, timeout=10):
def __init__(self, host, port, req_handler=None, timeout=10):
super().__init__(host, port, req_handler, timeout)
self.name = f"JsonRpcClt_{port}"
def run(self):
if tm.OS() == "Windows":
self.run_win()
else:
self.run_lin()
def run_win(self):
# TCP/IP socket creation
tslice = 1
t = self._timeout
sock = None
try:
while t >= 0:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(tslice)
# Link of the socket at the configured port
try:
sock.connect((self._host, self._port))
break
except socket.timeout:
sock.close()
t -= tslice
if t < 0:
raise ETUMRuntimeError(
f"{self.name}: failed to connect : timeout"
)
else:
sleep(tslice)
except socket.error as e:
raise ETUMRuntimeError(f"{self.name}: failed to connect : {e}")
self.print_info("Connected to server")
self.connect(sock)
while self._rpc.running:
# Sleep a short time to avoid a busy loop and allow
# the receiver thread to process messages.
sleep(0.1)
finally:
if sock is not None:
sock.close()
if self._rpc is not None:
self._rpc.stop()
self._rpc.join()
self.print_info("closed")
def run_lin(self):
# TCP/IP socket creation
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
@@ -385,10 +448,12 @@ class JsonRpcClient(JsonRpcBase):
try:
sock.connect((self._host, self._port))
break
except OSError as e:
except Exception as e:
t -= tslice
if t < 0:
raise ETUMRuntimeError(f"{self.name}: failed to connect : {e}")
raise ETUMRuntimeError(
f"{self.name}: failed to connect : {e}"
)
else:
sleep(tslice)

View File

@@ -1,4 +1,5 @@
import os
import sys
import shutil
import subprocess
import socket
@@ -12,12 +13,36 @@ function_call_process = None
def lua_func_call_init(lua_path, request_handler, timeout):
"""
Initializes the global Lua function execution process.
Args:
lua_path (str): Path to the Lua interpreter executable. If empty, uses system default.
request_handler: Handler for JSON-RPC requests.
timeout (int): Timeout for operations in seconds.
Returns:
LuaFuncExecEngine: The initialized engine instance.
Raises:
ETUMRuntimeError: If the Lua path is invalid or no interpreter is found.
"""
global function_call_process
function_call_process = LuaFuncExecEngine(lua_path, request_handler, timeout)
return function_call_process
def is_lua_interpreter(path: str, timeout=2) -> bool:
"""
Checks if the given path points to a valid Lua interpreter.
Args:
path (str): Path to the executable to check.
timeout (int, optional): Timeout for the subprocess in seconds. Defaults to 2.
Returns:
bool: True if the path is a Lua interpreter, False otherwise.
"""
try:
result = subprocess.run(
[path, "-v"],
@@ -32,8 +57,25 @@ def is_lua_interpreter(path: str, timeout=2) -> bool:
class LuaFuncExecEngine:
"""
Engine for executing Lua functions via a subprocess and JSON-RPC communication.
This class manages a Lua interpreter subprocess, handles RPC communication,
and executes specified functions with parameters.
"""
def __init__(self, lua_path="", request_handler=None, timeout=10):
"""
Initializes the Lua function execution engine.
Args:
lua_path (str, optional): Path to the Lua interpreter. Defaults to system path.
request_handler: Handler for JSON-RPC requests.
timeout (int, optional): Timeout for operations in seconds. Defaults to 10.
Raises:
ETUMRuntimeError: If the Lua path is invalid or no interpreter is found.
"""
if lua_path != "":
if shutil.which(lua_path) is None:
raise ETUMRuntimeError(
@@ -61,27 +103,49 @@ class LuaFuncExecEngine:
def start(self):
"""
run the subprocess to execute the python functions of the test.
Starts the Lua subprocess for function execution.
Sets up environment variables, binds a socket for communication,
and initializes the JSON-RPC client.
Raises:
ETUMRuntimeError: If the subprocess is already started.
"""
# 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.")
func_proc_path = os.path.join(tm.gd("testium_path"),"lua_func")
# POpen config
CUST_ENV = {
"PATH": {"replace": False},
"LUA_PATH": {"replace": True},
"LUA_CPATH": {"replace": True},
}
lua_env = tm.gd("lua_env", {})
env = os.environ.copy()
for k, v in CUST_ENV.items():
e = lua_env.get(k, "")
if e != "":
if v["replace"]:
env[k] = e
else:
env[k] = e + ";" + env.get(k, "")
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", {})
tm.print_debug(f"lua_env : {lua_env}")
# POpen params
params = [self._lpath, "main.lua", "--timeout", f"{self._timeout}", "--host", "127.0.0.1", "--port", f"{self._port}"]
if tm.debug_enabled():
if tm.debug_enabled() and tm.gd("debug_rpc", False):
params.append("--verbose")
self._process = subprocess.Popen(
params, env=lua_env, cwd=func_proc_path
params, env=env, cwd=func_proc_path
)
# Port was reserved until the sub-process is started. Now released.
@@ -89,24 +153,57 @@ class LuaFuncExecEngine:
sock.close()
self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler)
if tm.debug_enabled():
self._rpc.dbg_out = sys.stdout
self._rpc.start()
def join(self):
"""
Joins the RPC thread and resets the process state.
"""
if self._rpc is not None:
self._rpc.join()
self._rpc = None
self._process = None
def wait_ready(self, timeout=None):
"""
Waits for the RPC client to be ready.
Args:
timeout (float, optional): Timeout in seconds. Defaults to None.
Returns:
bool: True if ready, False otherwise.
"""
if self._rpc is not None and self._rpc.is_alive():
return self._rpc.wait_ready(timeout)
return False
def stop(self):
"""
Stops the RPC client.
"""
if self._rpc is not None:
self._rpc.stop()
def func_call(self, file: str, func_name: str, params: list, verbose: bool = True):
"""
Calls a Lua function via RPC and returns the result.
Args:
file (str): Path to the Lua file containing the function.
func_name (str): Name of the function to call.
params (list): List of parameters to pass to the function.
verbose (bool, optional): Whether to enable verbose output. Defaults to True.
Returns:
tuple: (TestValue.SUCCESS, (result, reported_values)) on success,
(TestValue.FAILURE, error_message) on failure.
Raises:
ETUMRuntimeError: If the RPC call fails or no process is active.
"""
if (self._rpc is not None) and self._rpc.is_alive():
answer = self._rpc.call(
"func_call",
@@ -143,7 +240,21 @@ class LuaFuncExecEngine:
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"""
"""
Executes a Lua function using the global function call process.
Args:
file (str): Path to the Lua file containing the function.
func_name (str): Name of the function to call.
params (list): List of parameters to pass to the function.
verbose (bool, optional): Whether to enable verbose output. Defaults to True.
Returns:
tuple: (success_status, result_or_error) where success_status is TestValue.SUCCESS or FAILURE.
Raises:
ETUMRuntimeError: If no function execution process is active.
"""
global function_call_process
if function_call_process is not None:

View File

@@ -119,7 +119,6 @@ def _sys_app_path_win(app_name):
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
data = ""
sys_python_path = data.splitlines()
tm.print_debug("data = ", data)
for l in sys_python_path:
if f"{app_name}.exe" in l:
return l

View File

@@ -76,7 +76,8 @@ class PyFuncExecEngine:
func_proc_path = tm.gd("testium_path")
params = [self._ppath, "-m", "py_func", "-p", f"{self._port}", "-t", f"{self._timeout}"]
if tm.debug_enabled():
if tm.debug_enabled() and tm.gd("debug_rpc", False):
params.append("-v")
self._process = subprocess.Popen(
@@ -88,6 +89,8 @@ class PyFuncExecEngine:
sock.close()
self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler)
if tm.debug_enabled():
self._rpc.dbg_out = sys.stdout
self._rpc.start()
def join(self):