Fix context subprocess leak and document py_func.tm helpers

- process.py: stop context_id engines in the inner finally block, before
  restore_gd() wipes _py_func_contexts/_lua_func_contexts from the global
  dict — engines were previously orphaned after every test run
- py_func/tm.py: add user-facing docstrings to gd/setgd/delgd; remove
  internal JSON-serialization details from the docs
- helper_lib.rst: auto-generate global variable helpers from py_func.tm
  (the actual subprocess API) instead of globdict
- conf.py: add src/ to Sphinx sys.path so py_func.tm is importable
- py_func_test_item.rst: simplify context sharing section, remove
  JSON-serializable/non-serializable distinction for end users
- Regenerated PDF manual

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 16:27:31 +02:00
parent d92f518e1e
commit 617f599f86
6 changed files with 59 additions and 46 deletions

View File

@@ -14,6 +14,7 @@ import os
import sys import sys
sys.path.insert(0, os.path.abspath('../../../../src/testium/')) sys.path.insert(0, os.path.abspath('../../../../src/testium/'))
sys.path.insert(0, os.path.abspath('../../../../src/'))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------

View File

@@ -21,7 +21,7 @@ Global variables helper functions
To manage values in the global variables dataset, the following testium library API To manage values in the global variables dataset, the following testium library API
must be used: must be used:
.. automodule:: interpreter.utils.globdict .. automodule:: py_func.tm
:members: gd, setgd, delgd :members: gd, setgd, delgd
:undoc-members: :undoc-members:
:no-index: :no-index:

View File

@@ -123,17 +123,12 @@ Each ``py_func`` item without a ``context_id`` runs in a dedicated subprocess th
is started and stopped around the call. State cannot be shared between two such is started and stopped around the call. State cannot be shared between two such
items using module-level variables. items using module-level variables.
Two mechanisms are available to share data across calls: Inside a ``py_func`` script, ``tm.setgd`` and ``tm.gd`` read and write the testium
global dictionary. Values stored this way are accessible from any subsequent test
**Using the testium global dictionary** item, including other ``py_func`` items, without requiring a shared subprocess.
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 .. code-block:: python
:caption: sharing a serializable value via the global dictionary :caption: sharing a value via the global dictionary
import py_func.tm as tm import py_func.tm as tm
@@ -144,36 +139,22 @@ items) without requiring a shared subprocess.
def consume(): def consume():
return tm.gd("my_shared_value", None) 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 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. reuse the same persistent subprocess. This allows sharing any Python object across
calls — including objects that cannot be transmitted to other processes.
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 .. code-block:: python
:caption: sharing a non-serializable object via ``context_id`` :caption: sharing an object via ``context_id``
import py_func.tm as tm import py_func.tm as tm
class _Connection: # not JSON-serializable
def __init__(self):
self.value = "open"
def open_connection(): def open_connection():
tm.setgd("conn", _Connection()) # stored locally in the subprocess tm.setgd("conn", MyConnection())
return "ok" return "ok"
def use_connection(): def use_connection():
conn = tm.gd("conn") # retrieved from the subprocess local dict conn = tm.gd("conn")
return conn.value return conn.status()
.. code-block:: yaml .. code-block:: yaml
:caption: ``py_func`` items sharing a persistent subprocess :caption: ``py_func`` items sharing a persistent subprocess

Binary file not shown.

View File

@@ -45,12 +45,21 @@ for k in SUPPORTED_API:
############################################################################### ###############################################################################
# gd/setgd/delgd with local-dict fallback for non-serializable values # gd/setgd/delgd with local-dict fallback for non-serializable values
def gd(*params): def gd(name, default=None):
key = params[0] if params else None """Return a value from the testium global dictionary.
if key is not None and key in _local_dict:
return _local_dict[key] The value is accessible from any test item and from any ``py_func``
subprocess, regardless of the ``context_id`` used.
:param name: Name of the entry to retrieve.
:type name: str
:param default: Value returned when the key is absent. Defaults to ``None``.
:return: The stored value, or *default* if not found.
"""
if name is not None and name in _local_dict:
return _local_dict[name]
if _func_call_thread is not None: if _func_call_thread is not None:
res = _func_call_thread.call("gd", params) res = _func_call_thread.call("gd", (name, default))
if "result" in res: if "result" in res:
return res["result"] return res["result"]
elif "error" in res: elif "error" in res:
@@ -60,14 +69,25 @@ def gd(*params):
raise ETUMRuntimeError("api not initialized") raise ETUMRuntimeError("api not initialized")
def setgd(*params): def setgd(name, value):
key = params[0] if params else None """Store a value in the testium global dictionary.
value = params[1] if len(params) > 1 else None
if key is not None and not _is_json_serializable(value): The stored value is accessible from any subsequent test item and from any
_local_dict[key] = value ``py_func`` subprocess via :func:`gd`.
When ``context_id`` is used on the ``py_func`` item, any Python object
(including those that cannot be transmitted to other processes) can be
stored and shared between calls running in the same subprocess.
:param name: Name of the entry to set.
:type name: str
:param value: Value to store.
"""
if name is not None and not _is_json_serializable(value):
_local_dict[name] = value
return None return None
if _func_call_thread is not None: if _func_call_thread is not None:
res = _func_call_thread.call("setgd", params) res = _func_call_thread.call("setgd", (name, value))
if "result" in res: if "result" in res:
return res["result"] return res["result"]
elif "error" in res: elif "error" in res:
@@ -77,13 +97,17 @@ def setgd(*params):
raise ETUMRuntimeError("api not initialized") raise ETUMRuntimeError("api not initialized")
def delgd(*params): def delgd(name):
key = params[0] if params else None """Remove an entry from the testium global dictionary.
if key is not None and key in _local_dict:
del _local_dict[key] :param name: Name of the entry to remove.
:type name: str
"""
if name is not None and name in _local_dict:
del _local_dict[name]
return None return None
if _func_call_thread is not None: if _func_call_thread is not None:
res = _func_call_thread.call("delgd", params) res = _func_call_thread.call("delgd", (name,))
if "result" in res: if "result" in res:
return res["result"] return res["result"]
elif "error" in res: elif "error" in res:

View File

@@ -265,6 +265,13 @@ Is the python exec path correct ?"""
test_set.run_post_exec() test_set.run_post_exec()
finally: finally:
self.__exec = False self.__exec = False
# Stop shared context engines before restore_gd wipes them
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()
# Sends signal to the GUI # Sends signal to the GUI
self.send_finished() self.send_finished()
restore_gd(gdict) restore_gd(gdict)