diff --git a/doc/manual/sphinx/source/conf.py b/doc/manual/sphinx/source/conf.py index a307446..05cce03 100644 --- a/doc/manual/sphinx/source/conf.py +++ b/doc/manual/sphinx/source/conf.py @@ -14,6 +14,7 @@ import os import sys sys.path.insert(0, os.path.abspath('../../../../src/testium/')) +sys.path.insert(0, os.path.abspath('../../../../src/')) # -- Project information ----------------------------------------------------- diff --git a/doc/manual/sphinx/source/helper_lib.rst b/doc/manual/sphinx/source/helper_lib.rst index 4203d15..900358a 100644 --- a/doc/manual/sphinx/source/helper_lib.rst +++ b/doc/manual/sphinx/source/helper_lib.rst @@ -21,7 +21,7 @@ Global variables helper functions To manage values in the global variables dataset, the following testium library API must be used: -.. automodule:: interpreter.utils.globdict +.. automodule:: py_func.tm :members: gd, setgd, delgd :undoc-members: :no-index: diff --git a/doc/manual/sphinx/source/test_items/py_func_test_item.rst b/doc/manual/sphinx/source/test_items/py_func_test_item.rst index 71e325e..b31be24 100644 --- a/doc/manual/sphinx/source/test_items/py_func_test_item.rst +++ b/doc/manual/sphinx/source/test_items/py_func_test_item.rst @@ -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 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. +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 +item, including other ``py_func`` items, without requiring a shared subprocess. .. 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 @@ -144,36 +139,22 @@ items) without requiring a shared subprocess. 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. +reuse the same persistent subprocess. This allows sharing any Python object across +calls — including objects that cannot be transmitted to other processes. .. 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 - class _Connection: # not JSON-serializable - def __init__(self): - self.value = "open" - def open_connection(): - tm.setgd("conn", _Connection()) # stored locally in the subprocess + tm.setgd("conn", MyConnection()) return "ok" def use_connection(): - conn = tm.gd("conn") # retrieved from the subprocess local dict - return conn.value + conn = tm.gd("conn") + return conn.status() .. code-block:: yaml :caption: ``py_func`` items sharing a persistent subprocess diff --git a/doc/manual/testium_manual.pdf b/doc/manual/testium_manual.pdf index 3042d8c..e143569 100644 Binary files a/doc/manual/testium_manual.pdf and b/doc/manual/testium_manual.pdf differ diff --git a/src/py_func/tm.py b/src/py_func/tm.py index d84566f..68cfed4 100644 --- a/src/py_func/tm.py +++ b/src/py_func/tm.py @@ -45,12 +45,21 @@ for k in SUPPORTED_API: ############################################################################### # 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] +def gd(name, default=None): + """Return a value from the testium global dictionary. + + 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: - res = _func_call_thread.call("gd", params) + res = _func_call_thread.call("gd", (name, default)) if "result" in res: return res["result"] elif "error" in res: @@ -60,14 +69,25 @@ def gd(*params): 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 +def setgd(name, value): + """Store a value in the testium global dictionary. + + The stored value is accessible from any subsequent test item and from any + ``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 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: return res["result"] elif "error" in res: @@ -77,13 +97,17 @@ def setgd(*params): 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] +def delgd(name): + """Remove an entry from the testium global dictionary. + + :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 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: return res["result"] elif "error" in res: diff --git a/src/testium/interpreter/process.py b/src/testium/interpreter/process.py index e17ace7..bb0112e 100644 --- a/src/testium/interpreter/process.py +++ b/src/testium/interpreter/process.py @@ -265,6 +265,13 @@ Is the python exec path correct ?""" test_set.run_post_exec() finally: 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 self.send_finished() restore_gd(gdict)