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:
@@ -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 -----------------------------------------------------
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user