5 Commits

Author SHA1 Message Date
d92f518e1e Add context_id to py_func and lua_func for shared persistent subprocess
- py_func and lua_func items accept a context_id parameter; items sharing
  the same id reuse the same subprocess for the duration of the test run
- Subprocess-side tm.setgd/tm.gd use a local fallback dict for non-JSON-
  serializable values (py_func only); serializable values reach the main
  process global dict and are accessible from any test item or subprocess
- Shared subprocess engines are cleaned up in process.py finally block
- LuaProcessBase gains is_alive() (was missing, broke all lua_func items)
- Validation tests cover serializable sharing across different context ids,
  non-serializable sharing within the same context_id, and cross-item access
- RST documentation updated for both py_func and lua_func items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:02:36 +02:00
49354b8664 Merge branch 'feature/reload-without-restart'
Reload test without restarting testium (closes #18)
2026-04-18 15:24:11 +02:00
7383820aba Reload test without restarting testium (closes #18)
Replace os.execv restart in actionRefresh with file_manager.reload(),
leveraging the subprocess architecture so py_func modules are freshly
imported on each reload. Add a modal progress dialog with step labels
during loading. Fix checkbox reappearing on breakpoint with
show_checkboxes OFF.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 15:23:53 +02:00
67c879ab10 Fix checkboxes reappearing when setting a breakpoint with show_checkboxes OFF
itemChanged fires for any data change (including icon updates in the pause
column), causing on_testChecked to inadvertently restore CheckStateRole via
synchronizeEnabledState. Guard against non-checkbox column changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 14:55:18 +02:00
aa72e349c6 Merge branch 'refactor/ui-modularization'
- Extract TestControllerService service layer over TestSetController
- Split MainWindow into TestRunner and TestFileManager coordinators
- Merge 21 QTestTreeItem subclasses into a single factory
- Replace _test_started/_test_paused booleans with TestState enum
2026-04-18 14:46:06 +02:00
14 changed files with 426 additions and 70 deletions

View File

@@ -39,11 +39,14 @@ The ``lua_func`` test item is of the form:
Beside common test items attributes, lua_func item has specific attribute, some of which being mandatory. 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. * ``file``: the script file name that contains the function to be executed.
Only python script format is supported. Only Lua script format is supported.
* ``func_name``: The function name to be executed. * ``func_name``: The function name to be executed.
* ``param``: This is a list of parameters that are passed to the function * ``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 in the order they are presented in the script. These parameters are not
mandatory and are highly dependent of the function prototype. mandatory and are highly dependent of the function prototype.
* ``context_id``: Optional. When set, all ``lua_func`` items sharing the same
``context_id`` value run inside the same persistent Lua subprocess for the
duration of the test. See :ref:`lua_func context<sec_lua_func_context>` for details.
.. code-block:: yaml .. code-block:: yaml
:caption: ``lua_func`` test item example of usage :caption: ``lua_func`` test item example of usage
@@ -56,16 +59,71 @@ Beside common test items attributes, lua_func item has specific attribute, some
- $(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 ``pfn_<func_name>`` variable named ``lfn_<item_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 ``$(lfn_activity)`` In the example above, the global variable ``$(lfn_activity)``
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 methodName Lua function.
The ``lua_func`` will always result ``PASS``, except if the called function raises The ``lua_func`` will always result ``PASS``, except if the called function raises
and exception or if the ``expected_result`` attribute is used. an exception or if the ``expected_result`` attribute is used.
.. _sec_lua_func_context:
Sharing state between ``lua_func`` calls
------------------------------------------
Each ``lua_func`` item without a ``context_id`` runs in a dedicated subprocess that
is started and stopped around the call. Module-level variables are not preserved
between two such items.
Inside a ``lua_func`` script, the ``tm`` module exposes ``tm.setgd`` and ``tm.gd``
to read and write the testium global dictionary of the test process. Values stored
this way are accessible from any subsequent test item without requiring a shared
subprocess.
.. code-block:: lua
:caption: sharing a value via the global dictionary
local tm = require("tm")
local module = {}
function module.produce(val)
tm.setgd("my_shared_value", val)
return val
end
function module.consume()
return tm.gd("my_shared_value")
end
return module
When ``context_id`` is set, all ``lua_func`` items that share the same identifier
reuse the same persistent subprocess. This allows Lua-side state (upvalues, module
cache) to be retained across calls beyond what ``tm.setgd`` persists.
.. code-block:: yaml
:caption: ``lua_func`` items sharing a persistent subprocess
- lua_func:
name: produce value
file: my_script.lua
func_name: produce
context_id: my_context
param:
- hello
- lua_func:
name: consume value
file: my_script.lua
func_name: consume
context_id: my_context
expected_result: hello
The shared subprocess is automatically stopped at the end of the test run.
**Lua Interpreter environment setup** **Lua Interpreter environment setup**

View File

@@ -89,6 +89,9 @@ some of which being mandatory.
* ``param``: This is a list of parameters that are passed to the function * ``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 in the order they are presented in the script. These parameters are not
mandatory and are highly dependent of the function prototype. mandatory and are highly dependent of the function prototype.
* ``context_id``: Optional. When set, all ``py_func`` items sharing the same
``context_id`` value run inside the same persistent Python subprocess for the
duration of the test. See :ref:`py_func context<sec_py_func_context>` for details.
.. code-block:: yaml .. code-block:: yaml
:caption: ``py_func`` test item example of usage :caption: ``py_func`` test item example of usage
@@ -111,6 +114,86 @@ value of the funcToBeExecuted python function.
The ``py_func`` will always result ``PASS``, except if the called function raises The ``py_func`` will always result ``PASS``, except if the called function raises
and exception or if the ``expected_result`` attribute is used. and exception or if the ``expected_result`` attribute is used.
.. _sec_py_func_context:
Sharing state between ``py_func`` calls
------------------------------------------
Each ``py_func`` item without a ``context_id`` runs in a dedicated subprocess that
is started and stopped around the call. State cannot be shared between two such
items using module-level variables.
Two mechanisms are available to share data across calls:
**Using the testium global dictionary**
Inside a ``py_func`` script, the ``tm`` module exposes ``tm.setgd`` and ``tm.gd``
to read and write the testium global dictionary of the test process. Values stored
this way are accessible from any subsequent test item (including other ``py_func``
items) without requiring a shared subprocess.
.. code-block:: python
:caption: sharing a serializable value via the global dictionary
import py_func.tm as tm
def produce(val):
tm.setgd("my_shared_value", val)
return val
def consume():
return tm.gd("my_shared_value", None)
Values stored with ``tm.setgd`` must be JSON-serializable (str, int, float, list,
dict, bool, None). Non-serializable values (objects, connections, file handles…)
are handled transparently by the local fallback described below.
**Using a shared persistent subprocess (``context_id``)**
When ``context_id`` is set, all ``py_func`` items that share the same identifier
reuse the same subprocess. The subprocess is kept alive until the end of the test.
This is required for non-JSON-serializable objects (e.g. a socket connection, a
device handle). Calling ``tm.setgd`` with such a value stores it inside the
subprocess local dictionary instead of sending it to the main process. It can then
be retrieved with ``tm.gd`` from any subsequent call that runs in the same subprocess.
.. code-block:: python
:caption: sharing a non-serializable object via ``context_id``
import py_func.tm as tm
class _Connection: # not JSON-serializable
def __init__(self):
self.value = "open"
def open_connection():
tm.setgd("conn", _Connection()) # stored locally in the subprocess
return "ok"
def use_connection():
conn = tm.gd("conn") # retrieved from the subprocess local dict
return conn.value
.. code-block:: yaml
:caption: ``py_func`` items sharing a persistent subprocess
- py_func:
name: open connection
file: my_script.py
func_name: open_connection
context_id: my_context
expected_result: ok
- py_func:
name: use connection
file: my_script.py
func_name: use_connection
context_id: my_context
expected_result: open
The shared subprocess is automatically stopped at the end of the test run.
**Python Interpreter environment setup** **Python Interpreter environment setup**
Some global variables have an impact on the ``py_func`` test item behavior: Some global variables have an impact on the ``py_func`` test item behavior:

Binary file not shown.

View File

@@ -1,19 +1,26 @@
import json
import sys import sys
from py_func.handle import FuncHandler from py_func.handle import FuncHandler
from lib.tum_except import ETUMRuntimeError from lib.tum_except import ETUMRuntimeError
from lib.api import SUPPORTED_API from lib.api import SUPPORTED_API
thismodule = sys.modules[__name__] thismodule = sys.modules[__name__]
# Shared FuncHandler instance used to forward API calls. Remains None
# until `_init_api` is invoked.
_func_call_thread = None _func_call_thread = None
# Local storage for non-JSON-serializable values
_local_dict = {}
def _is_json_serializable(value):
try:
json.dumps(value)
return True
except (TypeError, ValueError):
return False
############################################################################### ###############################################################################
# Dynamically create module-level functions for each supported API name.
# Each generated function shares the implementation of `api_call` but
# has a distinct name used as the remote action identifier.
def _make_api(name): def _make_api(name):
def _wrapper(*params): def _wrapper(*params):
if _func_call_thread is not None: if _func_call_thread is not None:
@@ -31,21 +38,62 @@ def _make_api(name):
return _wrapper return _wrapper
for k in SUPPORTED_API: for k in SUPPORTED_API:
if k not in ('gd', 'setgd', 'delgd'):
setattr(thismodule, k, _make_api(k)) setattr(thismodule, k, _make_api(k))
###############################################################################
# gd/setgd/delgd with local-dict fallback for non-serializable values
def gd(*params):
key = params[0] if params else None
if key is not None and key in _local_dict:
return _local_dict[key]
if _func_call_thread is not None:
res = _func_call_thread.call("gd", params)
if "result" in res:
return res["result"]
elif "error" in res:
raise ETUMRuntimeError(f"api call to 'tm.gd' failed with error '{res['error']}'")
else:
raise ETUMRuntimeError("api call failure in jrpc client to be reported to testium support team.")
raise ETUMRuntimeError("api not initialized")
def setgd(*params):
key = params[0] if params else None
value = params[1] if len(params) > 1 else None
if key is not None and not _is_json_serializable(value):
_local_dict[key] = value
return None
if _func_call_thread is not None:
res = _func_call_thread.call("setgd", params)
if "result" in res:
return res["result"]
elif "error" in res:
raise ETUMRuntimeError(f"api call to 'tm.setgd' failed with error '{res['error']}'")
else:
raise ETUMRuntimeError("api call failure in jrpc client to be reported to testium support team.")
raise ETUMRuntimeError("api not initialized")
def delgd(*params):
key = params[0] if params else None
if key is not None and key in _local_dict:
del _local_dict[key]
return None
if _func_call_thread is not None:
res = _func_call_thread.call("delgd", params)
if "result" in res:
return res["result"]
elif "error" in res:
raise ETUMRuntimeError(f"api call to 'tm.delgd' failed with error '{res['error']}'")
else:
raise ETUMRuntimeError("api call failure in jrpc client to be reported to testium support team.")
raise ETUMRuntimeError("api not initialized")
def _init_api(host, port, timeout): def _init_api(host, port, timeout):
"""Start and initialize the remote function handler.
Starts a ``FuncHandler`` bound to ``port``, runs it and blocks until
it signals readiness.
Args:
port: port number or identifier passed to ``FuncHandler``.
Returns:
The initialized ``FuncHandler`` instance assigned to
``_func_call_thread``.
"""
global _func_call_thread global _func_call_thread
_func_call_thread = FuncHandler(host, port, timeout=timeout) _func_call_thread = FuncHandler(host, port, timeout=timeout)
return _func_call_thread return _func_call_thread
@@ -53,17 +101,10 @@ def _init_api(host, port, timeout):
############################################################################### ###############################################################################
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
handler with action name ``"print"`` and the provided values. Errors
during forwarding are ignored because printing is best-effort.
"""
if _func_call_thread is not None: if _func_call_thread is not None:
try: try:
_func_call_thread.call("print", values) _func_call_thread.call("print", values)
except: except:
# Best-effort: ignore forwarding failures
pass pass

View File

@@ -275,6 +275,13 @@ Is the python exec path correct ?"""
# Stop python eval execution process # Stop python eval execution process
eval_proc.stop() eval_proc.stop()
eval_proc.join() eval_proc.join()
# Stop shared func context engines (keep_context_id)
for engine in tm.gd("_py_func_contexts", {}).values():
engine.stop()
engine.join()
for engine in tm.gd("_lua_func_contexts", {}).values():
engine.stop()
engine.join()
except Exception as e: except Exception as e:
print_exception(e) print_exception(e)

View File

@@ -12,10 +12,13 @@ from interpreter.utils.lua_func_exec import LuaFuncExecEngine
from interpreter.utils.api_srv import api_request from interpreter.utils.api_srv import api_request
from interpreter.utils.constants import TestItemType as cst from interpreter.utils.constants import TestItemType as cst
_LUA_FUNC_CONTEXTS_KEY = "_lua_func_contexts"
class TestItemLuaFunc(TestItem): class TestItemLuaFunc(TestItem):
"""lua_func item usage. """lua_func item usage.
func file: func_file.lua, func_name: func, param: [$(variable1), [1, 2, 3], true] func file: func_file.lua, func_name: func, param: [$(variable1), [1, 2, 3], true]
Optional: context_id: <id> — share a persistent process with other lua_func items using the same id.
""" """
def __init__(self, dict_item, parent=None, status_queue=None, filename=""): def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
@@ -27,14 +30,26 @@ class TestItemLuaFunc(TestItem):
self.file_name = self._prms.getParam("file", required=True) self.file_name = self._prms.getParam("file", required=True)
self.func_name = self._prms.getParam("func_name", required=True) self.func_name = self._prms.getParam("func_name", required=True)
self.params = self._prms.getParamAll("param") self.params = self._prms.getParamAll("param")
self._context_id = self._prms.getParam("context_id", default=None, processed=False)
except: except:
raise ETUMSyntaxError( raise ETUMSyntaxError(
f"The '{self.cmd()}' test item named '{self.name()}' (child of '{self.parent.name()}') has a missing or wrong parameter", f"The '{self.cmd()}' test item named '{self.name()}' (child of '{self.parent.name()}') has a missing or wrong parameter",
self.seqFilename(), self.seqFilename(),
) )
# Lua functions call subprocess initialization
self._lua_func_proc = LuaFuncExecEngine(tm.gd("lua_bin", ""), api_request, 10) self._lua_func_proc = LuaFuncExecEngine(tm.gd("lua_bin", ""), api_request, 10)
def _get_engine(self):
"""Return (engine, persistent). If context_id is set, use a shared persistent engine."""
if self._context_id is None:
return self._lua_func_proc, False
ctx_id = self._prms.expanse(self._context_id)
contexts = tm.gd(_LUA_FUNC_CONTEXTS_KEY, {})
if ctx_id not in contexts:
contexts[ctx_id] = LuaFuncExecEngine(tm.gd("lua_bin", ""), api_request, 10)
tm.setgd(_LUA_FUNC_CONTEXTS_KEY, contexts)
return contexts[ctx_id], True
@test_run @test_run
def execute(self): def execute(self):
self.result.set( self.result.set(
@@ -48,8 +63,11 @@ class TestItemLuaFunc(TestItem):
print("Parameters list:") print("Parameters list:")
print(textwrap.indent(pprint.pformat(pl), " |")) print(textwrap.indent(pprint.pformat(pl), " |"))
self._lua_func_proc.start() engine, persistent = self._get_engine()
if not self._lua_func_proc.wait_ready(10):
if not engine.is_alive():
engine.start()
if not engine.wait_ready(10):
raise ETUMRuntimeError( raise ETUMRuntimeError(
f"""Impossible to start the external lua execution process. f"""Impossible to start the external lua execution process.
Is the lua path correct ? Is the lua path correct ?
@@ -59,11 +77,11 @@ Is the lua environnment well defined in the "LUA_PATH" and "LUA_CPATH" variables
) )
try: try:
success, ret = self._lua_func_proc.func_call(self.file_name, self.func_name, pl) success, ret = engine.func_call(self.file_name, self.func_name, pl)
finally: finally:
# Stops lua function execution process if not persistent:
self._lua_func_proc.stop() engine.stop()
self._lua_func_proc.join() engine.join()
if success == TestValue.SUCCESS: if success == TestValue.SUCCESS:
self.result.set(TestValue.SUCCESS) self.result.set(TestValue.SUCCESS)
@@ -73,7 +91,6 @@ Is the lua environnment well defined in the "LUA_PATH" and "LUA_CPATH" variables
print("Returned value:") print("Returned value:")
print(textwrap.indent(pprint.pformat(res), " |")) print(textwrap.indent(pprint.pformat(res), " |"))
# The result of the func test item is put in global dir and result
tm.setgd("lfn_" + self._name, res) tm.setgd("lfn_" + self._name, res)
self.result.value = res self.result.value = res
@@ -88,5 +105,5 @@ Is the lua environnment well defined in the "LUA_PATH" and "LUA_CPATH" variables
traceback.print_exception(*sys.exc_info()) traceback.print_exception(*sys.exc_info())
self.result.set( self.result.set(
TestValue.FAILURE, TestValue.FAILURE,
'Unrecoverable "py_func" item error from {}'.format(self.func_name), 'Unrecoverable "lua_func" item error from {}'.format(self.func_name),
) )

View File

@@ -12,10 +12,13 @@ from interpreter.utils.py_func_exec import PyFuncExecEngine
from interpreter.utils.api_srv import api_request from interpreter.utils.api_srv import api_request
from interpreter.utils.constants import TestItemType as cst from interpreter.utils.constants import TestItemType as cst
_PY_FUNC_CONTEXTS_KEY = "_py_func_contexts"
class TestItemPyFunc(TestItem): class TestItemPyFunc(TestItem):
"""py_func item usage. """py_func item usage.
func file: func_file.py, func_name: func, param: [$(variable1), [1, 2, 3], true] func file: func_file.py, func_name: func, param: [$(variable1), [1, 2, 3], true]
Optional: context_id: <id> — share a persistent process with other py_func items using the same id.
""" """
def __init__(self, dict_item, parent=None, status_queue=None, filename=""): def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
@@ -27,6 +30,7 @@ class TestItemPyFunc(TestItem):
self.file_name = self._prms.getParam("file", required=True) self.file_name = self._prms.getParam("file", required=True)
self.func_name = self._prms.getParam("func_name", required=True) self.func_name = self._prms.getParam("func_name", required=True)
self.params = self._prms.getParamAll("param") self.params = self._prms.getParamAll("param")
self._context_id = self._prms.getParam("context_id", default=None, processed=False)
except: except:
raise ETUMSyntaxError( raise ETUMSyntaxError(
f"The '{self.cmd()}' test item named '{self.name()}' (child of '{self.parent.name()}') has a missing or wrong parameter", f"The '{self.cmd()}' test item named '{self.name()}' (child of '{self.parent.name()}') has a missing or wrong parameter",
@@ -34,6 +38,18 @@ class TestItemPyFunc(TestItem):
) )
self._py_func_proc = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10) self._py_func_proc = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10)
def _get_engine(self):
"""Return (engine, persistent). If context_id is set, use a shared persistent engine."""
if self._context_id is None:
return self._py_func_proc, False
ctx_id = self._prms.expanse(self._context_id)
contexts = tm.gd(_PY_FUNC_CONTEXTS_KEY, {})
if ctx_id not in contexts:
contexts[ctx_id] = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10)
tm.setgd(_PY_FUNC_CONTEXTS_KEY, contexts)
return contexts[ctx_id], True
@test_run @test_run
def execute(self): def execute(self):
self.result.set( self.result.set(
@@ -47,20 +63,23 @@ class TestItemPyFunc(TestItem):
print("Parameters list:") print("Parameters list:")
print(textwrap.indent(pprint.pformat(pl), " |")) print(textwrap.indent(pprint.pformat(pl), " |"))
# start the process for executing external python engine, persistent = self._get_engine()
self._py_func_proc.start()
if not self._py_func_proc.wait_ready(): if not engine.is_alive():
engine.start()
if not engine.wait_ready():
raise ETUMRuntimeError( raise ETUMRuntimeError(
f"""Impossible to start the external python execution process. f"""Impossible to start the external python execution process.
Is the python path correct ? Is the python path correct ?
python_bin = {tm.gd("python_bin", "no python path defined")}""" python_bin = {tm.gd("python_bin", "no python path defined")}"""
) )
try: try:
success, ret = self._py_func_proc.func_call(self.file_name, self.func_name, pl) success, ret = engine.func_call(self.file_name, self.func_name, pl)
finally: finally:
# Stops python function execution process if not persistent:
self._py_func_proc.stop() engine.stop()
self._py_func_proc.join() engine.join()
if success == TestValue.SUCCESS: if success == TestValue.SUCCESS:
self.result.set(TestValue.SUCCESS) self.result.set(TestValue.SUCCESS)
@@ -70,7 +89,6 @@ python_bin = {tm.gd("python_bin", "no python path defined")}"""
print("Returned value:") print("Returned value:")
print(textwrap.indent(pprint.pformat(res), " |")) print(textwrap.indent(pprint.pformat(res), " |"))
# The result of the func test item is put in global dir and result
tm.setgd("pfn_" + self._name, res) tm.setgd("pfn_" + self._name, res)
self.result.value = res self.result.value = res

View File

@@ -221,6 +221,11 @@ class LuaProcessBase:
return self._rpc.wait_ready(timeout) return self._rpc.wait_ready(timeout)
return False return False
def is_alive(self):
if self._rpc is not None:
return self._rpc.is_alive()
return False
def stop(self): def stop(self):
""" """
Stops the RPC client. Stops the RPC client.

View File

@@ -3,7 +3,8 @@ import sys
import traceback import traceback
from queue import Empty from queue import Empty
from PySide6.QtWidgets import QApplication, QFileDialog from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QFileDialog, QProgressDialog
from interpreter.process import TestProcess from interpreter.process import TestProcess
from interpreter.utils.test_ctrl import TestSetController from interpreter.utils.test_ctrl import TestSetController
@@ -44,9 +45,25 @@ class TestFileManager:
self.load(file_name) self.load(file_name)
w.reconnect_signals() w.reconnect_signals()
def _make_progress(self, w):
progress = QProgressDialog("Starting test process…", None, 0, 0, w)
progress.setWindowTitle("Loading")
progress.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint)
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setMinimumWidth(320)
progress._force_close = False
progress.closeEvent = lambda e: e.accept() if progress._force_close else e.ignore()
return progress
def _close_progress(self, progress):
progress._force_close = True
progress.close()
def load(self, file_name: str) -> bool: def load(self, file_name: str) -> bool:
"""Load a test file. Returns True on success, False otherwise.""" """Load a test file. Returns True on success, False otherwise."""
w = self._win w = self._win
progress = None
try: try:
if not file_name: if not file_name:
raise ETUMFileError("No file to load") raise ETUMFileError("No file to load")
@@ -59,6 +76,10 @@ class TestFileManager:
if not os.path.isfile(file_name): if not os.path.isfile(file_name):
raise ETUMFileError("Could not find %s file" % file_name) raise ETUMFileError("Could not find %s file" % file_name)
progress = self._make_progress(w)
progress.show()
QApplication.processEvents()
w.testFile = None w.testFile = None
w.ts_controller = TestSetController() w.ts_controller = TestSetController()
w.test_service = TestControllerService(w.ts_controller) w.test_service = TestControllerService(w.ts_controller)
@@ -71,12 +92,14 @@ class TestFileManager:
self._defaults_for_process(), self._defaults_for_process(),
) )
w.test_proc.start() w.test_proc.start()
progress.setLabelText("Loading test file…")
while w.test_proc.is_alive(): while w.test_proc.is_alive():
try: try:
if w.test_service.loaded(timeout=1.0): if w.test_service.loaded(timeout=0.05):
break break
except Empty: except Empty:
w.test_service.clear() w.test_service.clear()
QApplication.processEvents()
if not w.test_proc.is_alive(): if not w.test_proc.is_alive():
del w.test_proc del w.test_proc
@@ -89,9 +112,14 @@ class TestFileManager:
"Test could not be loaded (test process crashed for any reason)" "Test could not be loaded (test process crashed for any reason)"
) )
progress.setLabelText("Building test tree…")
QApplication.processEvents()
test_data = w.test_service.tree() test_data = w.test_service.tree()
w.treeTests.clear() w.treeTests.clear()
QApplication.processEvents()
w.treeTests.loadTestRecursively(w.treeTests.invisibleRootItem(), test_data) w.treeTests.loadTestRecursively(w.treeTests.invisibleRootItem(), test_data)
self._close_progress(progress)
progress = None
w.treeTests.setFoldDefault() w.treeTests.setFoldDefault()
w.treeTests.updateTreeSkipState(w.test_service) w.treeTests.updateTreeSkipState(w.test_service)
@@ -109,6 +137,8 @@ class TestFileManager:
w.show_checkboxes() w.show_checkboxes()
return True return True
except: except:
if progress is not None:
self._close_progress(progress)
w.statusBar().showMessage("No test file could be loaded", 10000) w.statusBar().showMessage("No test file could be loaded", 10000)
w.treeTests.clear() w.treeTests.clear()
print(traceback.format_exc()) print(traceback.format_exc())

View File

@@ -8,7 +8,6 @@ from multiprocessing import Queue
from queue import Empty from queue import Empty
from threading import Thread from threading import Thread
import shutil import shutil
import ast
# Qt # Qt
from PySide6 import QtGui, QtWidgets from PySide6 import QtGui, QtWidgets
@@ -471,21 +470,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
@Slot() @Slot()
def on_actionRefresh_test_triggered(self): def on_actionRefresh_test_triggered(self):
self.on_exiting() if self.testFile:
args = [] self.file_manager.reload(self.testFile)
if not hasattr(sys, "frozen"):
args += [sys.executable]
args += [sys.argv[0]]
if len(self.defines) > 0:
for k, v in self.defines.items():
try:
val = ast.literal_eval(v)
except:
val = v
args += ["-d", f"{k}={val}"]
if (self.testFile is not None) and (isinstance(self.testFile, str)):
args += [self.testFile]
os.execv(sys.executable, args)
@Slot() @Slot()
def on_actionSave_report_triggered(self): def on_actionSave_report_triggered(self):
@@ -553,6 +539,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.reconnect_signals() self.reconnect_signals()
def on_testChecked(self, item, index): def on_testChecked(self, item, index):
if index != self.treeTests.cols['name']['index']:
return
self.checkSelect.setCheckState(Qt.PartiallyChecked) self.checkSelect.setCheckState(Qt.PartiallyChecked)
self.disconnect_signals() self.disconnect_signals()
try: try:

View File

@@ -32,5 +32,13 @@ function module.tuple_return(first, second)
return first, second return first, second
end end
function module.set_context_value(val)
tm.setgd("_lua_ctx_test_value", val)
return val
end
function module.get_context_value()
return tm.gd("_lua_ctx_test_value")
end
return module return module

View File

@@ -179,3 +179,36 @@
file: $(test_path)$(psep)lua_func.lua file: $(test_path)$(psep)lua_func.lua
func_name: tuple_return func_name: tuple_return
param: [ 0, "OK" ] param: [ 0, "OK" ]
- group:
name: context_id tests
steps:
- lua_func:
name: set context value
key: $(test)_PASS
file: $(test_path)$(psep)lua_func.lua
func_name: set_context_value
context_id: lua_ctx_test
param:
- hello lua
expected_result: hello lua
- lua_func:
name: get context value (same context_id)
key: $(test)_PASS
file: $(test_path)$(psep)lua_func.lua
func_name: get_context_value
context_id: lua_ctx_test
expected_result: hello lua
- lua_func:
name: get context value (no context_id, from main gd)
key: $(test)_PASS
file: $(test_path)$(psep)lua_func.lua
func_name: get_context_value
expected_result: hello lua
- lua_func:
name: get context value (different context_id)
key: $(test)_PASS
file: $(test_path)$(psep)lua_func.lua
func_name: get_context_value
context_id: lua_ctx_other
expected_result: hello lua

View File

@@ -27,3 +27,23 @@ def echo(param):
def tuple_return(first, second): def tuple_return(first, second):
return first, second return first, second
def set_context_value(val):
tm.setgd("_py_ctx_test_value", val)
return val
def get_context_value():
return tm.gd("_py_ctx_test_value", None)
class _NotSerializable:
def __init__(self, val):
self.val = val
def set_ns_value(val):
tm.setgd("_py_ctx_ns_value", _NotSerializable(val))
return val
def get_ns_value():
obj = tm.gd("_py_ctx_ns_value", None)
return obj.val if obj is not None else None

View File

@@ -189,3 +189,51 @@
func_name: tuple_return func_name: tuple_return
param: [ 0, "OK" ] param: [ 0, "OK" ]
expected_result: [0, "OK"] expected_result: [0, "OK"]
- group:
name: context_id tests
steps:
- py_func:
name: set serializable value
key: $(test)_PASS
file: $(test_path)$(psep)py_func.py
func_name: set_context_value
param:
- hello context
expected_result: hello context
- py_func:
name: get serializable value (same context_id)
key: $(test)_PASS
file: $(test_path)$(psep)py_func.py
func_name: get_context_value
context_id: ctx_test
expected_result: hello context
- py_func:
name: get serializable value (no context_id, from main gd)
key: $(test)_PASS
file: $(test_path)$(psep)py_func.py
func_name: get_context_value
expected_result: hello context
- py_func:
name: get serializable value (different context_id)
key: $(test)_PASS
file: $(test_path)$(psep)py_func.py
func_name: get_context_value
context_id: ctx_other
expected_result: hello context
- py_func:
name: set non-serializable value
key: $(test)_PASS
file: $(test_path)$(psep)py_func.py
func_name: set_ns_value
context_id: ctx_ns_test
param:
- hello ns
expected_result: hello ns
- py_func:
name: get non-serializable value (same context_id)
key: $(test)_PASS
file: $(test_path)$(psep)py_func.py
func_name: get_ns_value
context_id: ctx_ns_test
expected_result: hello ns