lua and python bin detection rationalized: bins.py module created.

Added some api accessible from python and lua sub_processes. Now the tests only access to py_func.tm instead of direct api.testium module access.

Corrected some f"xxx" to allow working with old python (bookworm).

Changed param.yaml of the test to allow lua to work in all situations.

Various other small fixes for frozen app, wheel.

Tested in all situations, and OK. Ready for tag !

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 10:16:56 +02:00
parent 077e1a97c1
commit d3c5bd01e5
30 changed files with 585 additions and 312 deletions

View File

@@ -92,11 +92,33 @@ All dialog items (`dialog_image`, `dialog_question`, `dialog_references`, `dialo
- Per-item log capture (`stdio_redir.read()`) is naturally race-free thanks to per-thread buffers (see `StdoutProxy`). - Per-item log capture (`stdio_redir.read()`) is naturally race-free thanks to per-thread buffers (see `StdoutProxy`).
### Thread-aware stdout (`StdoutProxy`) ### Thread-aware stdout (`StdoutProxy`)
`src/lib/stdout_redirect.py` — when `log_stored: True`, `intercept()` installs a `StdoutProxy` as `sys.stdout`/`sys.stderr` instead of a single shared `StringQueue`. The proxy: `src/testium/runtime/stdout_redirect.py` — when `log_stored: True`, `intercept()` installs a `StdoutProxy` as `sys.stdout`/`sys.stderr` instead of a single shared `StringQueue`. The proxy:
- Holds one `StringQueue` per thread (registered via `register_thread(buffer=...)`). The main thread uses a default buffer; each parallel branch's thread registers its own at start and unregisters at end. `stdio_redir.read()` reads the calling thread's buffer → `addTest()` of an item running in branch X reads X's clean, non-interleaved output. - Holds one `StringQueue` per thread (registered via `register_thread(buffer=...)`). The main thread uses a default buffer; each parallel branch's thread registers its own at start and unregisters at end. `stdio_redir.read()` reads the calling thread's buffer → `addTest()` of an item running in branch X reads X's clean, non-interleaved output.
- For the live stream (terminal in batch / GUI panel), prefixes every line emitted from a branch's thread with `[<branch_name>] ` so concurrent branches stay readable. - For the live stream (terminal in batch / GUI panel), prefixes every line emitted from a branch's thread with `[<branch_name>] ` so concurrent branches stay readable.
- Exposes `write` / `writeln` / `flush` (Python 3.14's `unittest` calls `stream.writeln()` directly without `_WritelnDecorator`). - Exposes `write` / `writeln` / `flush` (Python 3.14's `unittest` calls `stream.writeln()` directly without `_WritelnDecorator`).
### Subprocess API contract (py_func / lua_func)
User test scripts running inside a `py_func` or `lua_func` subprocess **must** use the JSON-RPC bridge to interact with testium state:
- Python: `import py_func.tm as tm` — auto-generates wrappers for every function in `runtime/api.py:SUPPORTED_API`. `tm.gd`/`tm.setgd`/`tm.delgd` go through JSON-RPC to the parent.
- Lua: `local tm = require("tm")` — same idea on the Lua side.
`api.testium` is the *main-process* implementation; it is **not** exposed to subprocesses by design (not bundled in PyInstaller, not on the subprocess `PYTHONPATH` in pip-installed mode either when isolation is preserved). An import attempt from a subprocess script is a code smell and is detected by `test/validation/items/isolation/`.
To add a new API call usable from subprocesses:
1. Add the function to `api/testium.py`
2. Add its name to `SUPPORTED_API` in `runtime/api.py`
3. It is auto-exposed via JSON-RPC by `interpreter/utils/api_srv.py` and auto-wrapped by `py_func/tm.py:_make_api`
### External interpreter resolution (`bins.py`)
`src/testium/interpreter/utils/bins.py` — single source of truth for the paths to the external Python and Lua interpreters used by subprocesses.
- `python_bin()` / `lua_bin()` : resolve once, cache in memory. User can override via the `python_bin` / `lua_bin` global dict keys (typically populated from the YAML config). Falls back to discovery on PATH (candidates: `python3`/`python` and `lua`/`lua5.5`/`lua5.4`/`lua5.3`/`lua5.2`/`lua5.1`).
- `ensure(*names)` : called by `TestSet._validate_runtime_deps()` at test load. Always requires `python` (the eval engine always runs); requires `lua` only if a `lua_func` item is in the tree. Fails fast with a clear error citing tried candidates and override key.
Engines (`PyProcessBase`, `LuaProcessBase`, `EvalExecEngine`) call `bins.python_bin()`/`bins.lua_bin()` themselves — call sites never pass an explicit binary path.
## Key files ## Key files
| Path | Role | | Path | Role |
@@ -161,23 +183,61 @@ Icons are assigned once when the test file is loaded (not updated live on theme
The sub-test's own pass/fail result is intentionally not propagated. The sub-test's own pass/fail result is intentionally not propagated.
### Report exporters & plugins
`src/testium/interpreter/test_report/test_report.py``_EXPORTER_REGISTRY` dict maps a format name (cmd key in the YAML `report.export`) to a lazy loader. Built-ins: `text`, `json`, `junit` (needs `junit_xml`), `html` (needs `lxml`). `sqlite` is the storage layer, no-op as an export.
Third-party plugins are discovered at module import via `importlib.metadata.entry_points(group="testium.exporters")` — installing a wheel that declares such an entry point is enough, no testium config change needed:
```toml
[project.entry-points."testium.exporters"]
my_format = "my_pkg:MyExporter"
```
Exporter contract: `__init__(self, name, con, path, pats, keys, no_header=False)` — the class does its work in `__init__` and writes to `path`.
Behaviour on errors:
- Unknown format → info line `[report] Export skipped: format "X" not found. Available: ...`, run continues.
- Optional dependency missing → same info line with a pip-install hint, run continues.
A real-world test plugin lives at `test/validation/fake_exporter/` (CSV exporter, auto-installed by `scripts/build_env.sh` and exercised by `test/validation/items/report_plugin/`).
## Packaging
Three distribution channels coexist, sharing the single `src/testium/` package:
| Channel | Where | Notes |
|---------|-------|-------|
| Wheel (`pip install`) | `src/pyproject.toml` | Vanilla Python package; entry point `testium = "testium:main"` |
| PyInstaller binary | `package/pyinstaller/` | Single ~130 MB binary. `py_func`, `runtime`, `lua_func` bundled at `_MEIPASS` root so the **host** Python can find them when launched as `python3 py_func`. `api`/`interpreter` are **not** exposed (subprocess isolation). |
| Flatpak | `package/flatpak/` | (Existing recipe, not actively maintained in current refactor wave.) |
The `.deb` work-in-progress lives in `package/deb/`:
- `test_distro.sh debian:bookworm | debian:trixie | ubuntu:24.04` spins up a Docker/Podman container, reports system package availability, falls back to pip for what's missing (`pyside6` on bookworm/ubuntu, `telnetlib3`, `junit_xml`), runs the validation suite. Currently green on the three targets.
## Recent fixes / notable changes ## Recent fixes / notable changes
- `parallel` item: new item with `sync: all|any`, `wait_for`, daemon threads, `_stop_branch_recursively()`. Each branch thread registers a per-thread stdout buffer with `stdio_redir.register_thread(...)` so its log capture and live-output prefix work in isolation. - Restructure: single `src/testium/` Python package (was 4 sibling top-levels: `testium`, `lib`, `py_func`, `lua_func`). `lib/``runtime/`, `libs/``api/`. `pip install` now produces a clean `site-packages/testium/` with no top-level pollution; `.lua` files travel via `package_data`.
- `parallel_branch` icon: distinct single-arrow icon (`parallel_branch.png`) separate from the parallel container's three-arrow icon. - `bins.py`: centralised resolution + cache of external `python3` / `lua` binaries. Replaces the scattered `tm.gd("python_bin")`/`tm.gd("lua_bin")` dance and the duplicated discovery logic in `py_process.py`/`lua_process.py`. Validates at test load via `TestSet._validate_runtime_deps()` so missing interpreters fail fast.
- `parallel` F1 panel: `steps` stripped from each branch dict so the panel shows per-branch attributes without duplicating the tree. - Subprocess API contract: user scripts in `py_func`/`lua_func` use the JSON-RPC bridge (`py_func.tm` / Lua `tm`) — never `api.testium` / `interpreter.*` directly. `SUPPORTED_API` extended with `OS`, `get_main_dir`, `init_timestamp`, `timestamp`, `timestamp_as_sec` so subprocess scripts have the same surface as main-process code.
- `test_item_container.py`: new `TestItemContainer` base class extracted from Group/Cycle patterns - Report exporter plugin registry (`test_report.py`): `_EXPORTER_REGISTRY` + `entry_points("testium.exporters")` discovery. Missing format → info line, run continues.
- `test_item_sleep.py`: interruptible loop (checks `self._is_stopped`) instead of blocking `time.sleep()` so `sync: any` can stop slow branches quickly - About dialog rework: `QVBoxLayout` (resizable), version + dirty/branch info in a `QLabel` (auto-sized), copyright + clickable EUPL-1.2 link.
- `stdout_redirect.py`: rewrote `intercept()` to install a `StdoutProxy` (thread-aware: per-thread capture buffers + branch-prefixed live output). Adds `writeln()` for Python 3.14 unittest compatibility. - `test_ctrl.control()`: drain stale responses (left over from polled `loaded()` after `clear()` race) instead of failing on a wrong cmd key — fixes a "Unexpected return error in test set controller" seen in GUI mode after a fast reload.
- `test_report.py`: `check_same_thread=False` + lock around the SQLite `INSERT` for parallel branch concurrency. Log capture itself is race-free thanks to per-thread buffers. - `lua_process.py`: stderr no longer DEVNULL'd so actual Lua errors (missing `cjson`/`socket`) surface instead of "Connection refused".
- `run_post_exec`: failure message uses `print_warn` (was `print_debug` — silent in non-debug runs).
- Python 3.11 compat: replaced PEP 701 nested-quote f-strings (e.g. `f"... {d["k"]} ..."`) with single-quote inner strings or string concatenation.
- `parallel` item: new item with `sync: all|any`, `wait_for`, daemon threads, `_stop_branch_recursively()`. Each branch thread registers a per-thread stdout buffer.
- `parallel_branch` icon: distinct single-arrow icon (`parallel_branch.png`).
- `parallel` F1 panel: `steps` stripped from each branch dict.
- `test_item_container.py`: shared base class extracted from Group/Cycle.
- `test_item_sleep.py`: interruptible loop so `sync: any` can stop slow branches quickly.
- `stdout_redirect.py`: `StdoutProxy` (thread-aware buffers + branch-prefixed live output, `writeln()` for Python 3.14 unittest).
- `test_report.py`: thread-safe SQLite INSERT for parallel branch concurrency.
- `terminal.py`: deleted — `-m`/`--terminal` mode removed. - `terminal.py`: deleted — `-m`/`--terminal` mode removed.
- `batch.py`: premature loop exit when `gd_update` messages (no `"id"` key) were mistaken for the "finished" signal — fix: `"id" in m and m["id"] is None` - `batch.py`: premature finish bug on `gd_update` (no `"id"` key) — fix uses `"id" in m and m["id"] is None`.
- `batch.py`: `control("loaded")` deadlock if `TestProcess` crashed before `cmd_th` started — fix: daemon thread + `threading.Event` + `is_alive()` polling - `batch.py`: `control("loaded")` deadlock on TestProcess crash — fix uses daemon thread + `threading.Event` + `is_alive()` polling.
- `termlog.py`: `COLOR_DEFAULT = Fore.WHITE` invisible on light terminals; added auto-detection + light palette. Also fixed `write()` residue accumulation bug (`s[pos:]``s[pos+1:]`). - `termlog.py`: light/dark terminal auto-detection (`COLORFGBG`, OSC 11) + write residue bug.
- Dialog items: `auto_result`/`auto_value` now used in non-interactive text mode; dialogs without `auto_result` FAIL immediately in batch mode. - Dialog items: `auto_result`/`auto_value` for non-interactive text mode; dialogs without `auto_result` FAIL immediately in batch.
- `run` item: renamed `tum_fime``tum`; removed `stdout=PIPE` (caused deadlock with `multiprocessing` spawn); result simplified to PASS on any completed subprocess. - `run` item: renamed `tum_fime``tum`; removed `stdout=PIPE` deadlock; PASS on any completed subprocess.
- `unittest` item: renamed from `unittest_file` (cmd key, display name, Python constant `TYPE_UNITTEST_FILE``TYPE_UNITTEST`). - `unittest` item: renamed from `unittest_file`.
- GUI test tree: check and fold state preserved across same-file reloads (`test_file_manager.py`). - GUI test tree: check and fold state preserved across same-file reloads.
- Licence: EUPL-1.2 (`LICENSE`, `CONTRIBUTING.md`, `pyproject.toml`). - Licence: EUPL-1.2.
## Validation tests ## Validation tests
Located in `test/validation/`. Run with `-b` flag: Located in `test/validation/`. Run with `-b` flag:

View File

@@ -1,5 +1,5 @@
import api.testium as tm import py_func.tm as tm
def post_exec(): def post_exec():
print('Success !!!!') print('Success !!!!')

View File

@@ -4,7 +4,12 @@ Python helper library
====================== ======================
A python library including helper function for python modules called from A python library including helper function for python modules called from
testium. testium ``py_func`` items.
User scripts run inside the ``py_func`` subprocess and interact with testium
through a JSON-RPC bridge — the ``py_func.tm`` module. They must **not**
import ``api.testium`` or ``interpreter.*`` directly: those are main-process
modules and may not even be reachable in a packaged build (PyInstaller, .deb).
To include the support of this library in a python script, the following To include the support of this library in a python script, the following
line must be included in the script header: line must be included in the script header:
@@ -18,58 +23,38 @@ line must be included in the script header:
Global variables helper functions 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:
must be used:
.. automodule:: py_func.tm .. automodule:: py_func.tm
:members: gd, setgd, delgd :members: gd, setgd, delgd
:undoc-members: :undoc-members:
:no-index: :no-index:
Console helper functions
------------------------
Every opened console instance is added to a list with the
key ``console_instances`` of the global variables.
The instance is removed from the list on close step of the ``console`` test item.
To manage consoles from within ``py_func`` python functions,
the following testium library API can be used:
.. automodule:: libs.testium
:members: add_console, remove_console, console
:undoc-members:
:no-index:
Plot helper functions Plot helper functions
------------------------ ------------------------
Every opened plot window instance is added to a list with the Add values to a running plot or read the last value from it:
key ``plot_instances`` of the global variables.
The instance is removed from the list on close step of the ``plot`` test item. .. automodule:: py_func.tm
:members: add_plot_values, last_plot_value
To manage plots from within ``py_func`` python functions,
the following testium library API can be used:
.. automodule:: libs.testium
:members: add_plot, remove_plot, plot, add_plot_values, last_plot_value
:undoc-members: :undoc-members:
:no-index: :no-index:
Console and plot **lifecycle** management (``add_console``, ``remove_console``,
``console``, ``add_plot``, ``remove_plot``, ``plot``) is performed by the
``console`` and ``plot`` test items themselves — not from user ``py_func``
scripts. Use those test items to open/close consoles and plots.
Other helper functions Other helper functions
------------------------ ------------------------
.. automodule:: libs.testium .. automodule:: py_func.tm
:members: OS, get_main_dir, timestamp, timestamp_as_sec :members: OS, get_main_dir, init_timestamp, timestamp, timestamp_as_sec, text_mode
:undoc-members: :undoc-members:
:no-index: :no-index:
Debug mode Debug mode
------------------------ ------------------------
.. automodule:: libs.testium The ``test_debug`` global variable controls debug-only output. Read or write
:members: debug_enabled, enable_debug, print_debug, print_info, print_warn it via ``tm.gd("test_debug")`` / ``tm.setgd("test_debug", True)``.
:undoc-members:
:no-index:

View File

@@ -6,18 +6,25 @@ Reports
If a report is required (in addition to the log), the ``report`` YAML element If a report is required (in addition to the log), the ``report`` YAML element
must be added at the root of the TUM main test file. must be added at the root of the TUM main test file.
The ``report`` YAML element has the following form: The ``report`` element accepts a single export or a list of them under the
``export`` key. Each export entry uses the format name as its key:
.. code-block:: yaml .. code-block:: yaml
:caption: reports global settings :caption: reports global settings — multiple exports
report: report:
enabled: True enabled: True
file_name: $(test_name).rep log_stored: True
path: $(home)/reports export:
pattern: "Console%" - sqlite:
export: junit path: $(home)/reports
log_stored: False file_name: $(test_name).db
- junit:
path: $(home)/reports
file_name: $(test_name).xml
- html:
path: $(home)/reports
file_name: $(test_name).html
.. table:: report attributes .. table:: report attributes
:widths: 20, 30, 50 :widths: 20, 30, 50
@@ -27,21 +34,93 @@ The ``report`` YAML element has the following form:
+-----------------+-----------------------+-------------------------------------------+ +-----------------+-----------------------+-------------------------------------------+
| ``enabled`` | ``True`` | Report activated | | ``enabled`` | ``True`` | Report activated |
+-----------------+-----------------------+-------------------------------------------+ +-----------------+-----------------------+-------------------------------------------+
| ``file_name`` | / | Report file name | | ``log_stored`` | ``False`` | When ``True``, captures stdout per test |
| | | item so exports (html, json) can include |
| | | the log of each item. |
+-----------------+-----------------------+-------------------------------------------+ +-----------------+-----------------------+-------------------------------------------+
| ``path`` | ``$(report_path)`` | Report storage path By default, it uses | | ``export`` | / | One export entry or a list of them. Each |
| | | the default one set in the | | | | entry's key is the format name (see |
| | | preferences. | | | | below). |
+-----------------+-----------------------+-------------------------------------------+ +-----------------+-----------------------+-------------------------------------------+
| ``pattern`` | / | The pattern in SQL wildachars syntax |
| | | to be applied on test names to | Each export entry supports the following sub-attributes:
| | | selected reported tests. |
.. table:: export attributes
:widths: 20, 30, 50
+-----------------+-----------------------+-------------------------------------------+ +-----------------+-----------------------+-------------------------------------------+
| ``export`` | / | The type of export. For exemple junit. | | Attribute | default value | Description |
| | | By default, the sqlite format is |
| | | used to generate reports. |
+-----------------+-----------------------+-------------------------------------------+ +-----------------+-----------------------+-------------------------------------------+
| ``log_stored`` | / | Defines if the output log of each | | ``path`` | ``$(report_path)`` | Output directory. |
| | | test is accessible to generate the |
| | | report export. |
+-----------------+-----------------------+-------------------------------------------+ +-----------------+-----------------------+-------------------------------------------+
| ``file_name`` | / | Output file name. May include |
| | | ``$(...)`` global-dict expansions. |
+-----------------+-----------------------+-------------------------------------------+
| ``pattern`` | / | One or more SQL ``LIKE`` patterns |
| | | applied on the test ``name``. |
+-----------------+-----------------------+-------------------------------------------+
| ``key`` | / | One or more SQL ``LIKE`` patterns |
| | | applied on the test ``key`` |
| | | (the per-item ``key`` attribute). |
+-----------------+-----------------------+-------------------------------------------+
Built-in formats
^^^^^^^^^^^^^^^^
* ``sqlite`` — raw SQLite database (storage layer; selecting it persists the run).
* ``text`` — simple indented text dump of the test tree.
* ``json`` — full report as JSON: ``{"header": {...}, "tests": [...]}``.
* ``junit`` — JUnit XML (requires the ``junit_xml`` Python package).
* ``html`` — single HTML page with header, results table and per-item logs (requires ``lxml``).
If a format is unknown or its optional dependency is missing, the export is
skipped with an ``[report] Export skipped: ...`` info line on stdout — the
test run is **not** interrupted.
.. _sec_reports_plugins:
Custom export formats (plugins)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A third-party Python package can register additional export formats via the
``testium.exporters`` setuptools entry point group. Once installed in the same
Python environment as testium, the format is auto-detected at startup and can
be referenced from the YAML by its declared name.
Plugin contract — a class with this constructor signature:
.. code-block:: python
:caption: minimal exporter contract
class MyExporter:
def __init__(self, name, con, path, pats, keys, no_header=False):
# name : str — report name
# con : sqlite3.Connection (read) — tables: header, tests
# path : str — output file path (already expansed)
# pats : list[str] — LIKE filters on test_name (may be empty)
# keys : list[str] — LIKE filters on report_key (may be empty)
# no_header : bool — skip header section (set by the inline
# `report` test item)
... # do the work in __init__ and write to `path`
Tables and columns of the SQLite report:
* ``header(key TEXT, value TEXT)`` — keys: ``report_version``, ``test_file``,
``test_name``, ``test_result``, ``test_revision``, ``testium_version``,
``testrun_date``, ``testrun_time``, ``test_duration``.
* ``tests`` — 12 columns: ``timestamp_start``, ``test_id``, ``parent_id``,
``level``, ``test_name``, ``test_type``, ``report_key``, ``result``
(``PASS``/``FAIL``/``SKIP``), ``message``, ``duration`` (ms),
``log`` (captured stdout when ``log_stored: True``), ``data`` (JSON of
values reported via ``self.reportValue(...)``).
Declaration in the plugin's ``pyproject.toml``:
.. code-block:: toml
:caption: registering an exporter via entry-points
[project.entry-points."testium.exporters"]
my_format = "my_pkg:MyExporter"
The plugin is then usable in any ``.tum`` report block as ``my_format:``
no testium configuration change required.

View File

@@ -13,7 +13,7 @@ class ``py_func`` item
This is the normal way of calling some custom python code. This is the normal way of calling some custom python code.
A class must be defined and derived from ``FunctionItem`` from the ``libs.testium`` module. A class must be defined and derived from ``FunctionItem`` from the ``py_func.tm`` module.
From this class it is possible to define some custom reported values with the following API From this class it is possible to define some custom reported values with the following API

Binary file not shown.

141
package/deb/test_distro.sh Executable file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# test_distro.sh — verify testium runs on a target Debian/Ubuntu distrib.
#
# Spins up a Docker container of the requested image, checks which expected
# system Python packages are available (apt), installs them, installs the
# testium wheel, and runs a smoke test that exercises batch mode + py_func
# subprocess.
#
# Usage:
# ./test_distro.sh debian:bookworm
# ./test_distro.sh debian:trixie
# ./test_distro.sh ubuntu:24.04
set -euo pipefail
IMAGE="${1:?Usage: $0 <image> e.g. debian:bookworm | debian:trixie | ubuntu:24.04}"
ROOT=$(realpath "$(dirname "$0")/../..")
# Container runtime: prefer docker if available, fall back to podman
if command -v docker >/dev/null 2>&1; then
CTR=docker
elif command -v podman >/dev/null 2>&1; then
CTR=podman
else
echo "ERROR: neither docker nor podman is installed" >&2
exit 1
fi
echo "[host] Using $CTR"
# --- Build the wheel on the host if it does not already exist
WHEEL_DIR="$ROOT/src/dist"
PYTHON_HOST="$ROOT/test/tmp/.venv/bin/python3"
[ -x "$PYTHON_HOST" ] || PYTHON_HOST=python3
if ! ls "$WHEEL_DIR"/testium-*.whl >/dev/null 2>&1; then
echo "[host] Building wheel..."
(cd "$ROOT/src" && "$PYTHON_HOST" -m build --wheel >/dev/null)
fi
WHEEL=$(ls "$WHEEL_DIR"/testium-*.whl | head -1)
WHEEL_NAME=$(basename "$WHEEL")
echo "[host] Using $WHEEL_NAME"
# Expected system Python packages on the target distrib
APT_PACKAGES=(
python3
python3-pip
python3-setuptools
python3-pyside6.qtwidgets
python3-yaml
python3-jinja2
python3-colorama
python3-git
python3-pexpect
python3-matplotlib
python3-lxml
python3-serial
python3-telnetlib3
lua5.4
lua-cjson
lua-socket
git
)
echo "=== Testing on $IMAGE ==="
$CTR run --rm \
-v "$ROOT:/testium:ro" \
-e WHEEL_NAME="$WHEEL_NAME" \
-e PACKAGES="${APT_PACKAGES[*]}" \
"$IMAGE" \
bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# 1. Availability check
echo
echo "--- System package availability ---"
AVAILABLE=()
MISSING=()
for pkg in $PACKAGES; do
if apt-cache show "$pkg" >/dev/null 2>&1; then
AVAILABLE+=("$pkg")
echo " OK $pkg"
else
MISSING+=("$pkg")
echo " MISSING $pkg"
fi
done
echo
# 2. Install available packages
echo "--- Installing system packages ---"
apt-get install -qq -y --no-install-recommends "${AVAILABLE[@]}" ca-certificates >/dev/null
# 3. Map missing apt packages to their PyPI equivalents and pip-install
# them as a fallback (kept minimal so the run is still a "system"
# install for the most part)
declare -A PIP_FALLBACK=(
[python3-pyside6.qtwidgets]=pyside6
[python3-telnetlib3]=telnetlib3
)
# junit_xml has no Debian package — install it via pip so the
# validation post_execution.py can import it.
EXTRA_PIP=(junit-xml)
PIP_PKGS=()
for m in "${MISSING[@]}"; do
fallback="${PIP_FALLBACK[$m]:-}"
if [ -n "$fallback" ]; then
PIP_PKGS+=("$fallback")
fi
done
PIP_PKGS+=("${EXTRA_PIP[@]}")
if [ ${#PIP_PKGS[@]} -gt 0 ]; then
echo "--- Installing missing deps via pip: ${PIP_PKGS[*]} ---"
pip install --break-system-packages "${PIP_PKGS[@]}" >/dev/null
fi
# 4. Install testium wheel
echo "--- Installing testium wheel ---"
pip install --break-system-packages --no-deps "/testium/src/dist/$WHEEL_NAME" >/dev/null
# 5. Install the fake_exporter plugin (needed by the report_plugin
# validation test which exercises entry-points discovery).
# Copy it first because /testium is mounted read-only and the
# setuptools backend touches its build dir.
echo "--- Installing testium-fake-exporter (test plugin) ---"
cp -r /testium/test/validation/fake_exporter /tmp/fake_exporter
pip install --break-system-packages /tmp/fake_exporter >/dev/null
# 6. Run the full validation suite. Outputs are streamed live so
# progress is visible — the suite takes a couple of minutes.
# Reports go to /tmp/testium-validation since /testium is RO.
echo "--- Running validation suite ---"
mkdir -p /tmp/testium-validation
cd /testium
testium -b -o \
-d "validation_report_path=/tmp/testium-validation/" \
-- test/validation/main.tum
'
echo "=== $IMAGE: PASS ==="

View File

@@ -1,4 +1,13 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
import os
# junit_xml is imported by post_exec scripts running under the *host* Python,
# not the frozen interpreter — so bundling it via hiddenimports alone is not
# enough. We also drop its source files at the _MEIPASS root so the host
# python3 finds them via the PYTHONPATH that py_process.py sets to
# tstium_path (= _MEIPASS when frozen).
import junit_xml as _junit_xml
JUNIT_XML_DIR = os.path.dirname(_junit_xml.__file__)
a = Analysis( a = Analysis(
['../../src/testium/__main__.py'], ['../../src/testium/__main__.py'],
@@ -9,10 +18,18 @@ a = Analysis(
# py_func subprocess is launched with the *host* Python (not the # py_func subprocess is launched with the *host* Python (not the
# frozen interpreter): it needs the source files on disk to find them # frozen interpreter): it needs the source files on disk to find them
# via cwd=subproc_path() and `python3 py_func` + `from runtime.*`. # via cwd=subproc_path() and `python3 py_func` + `from runtime.*`.
# py_func/, lua_func/ and runtime/ are bundled at the _MEIPASS root
# because the py_func subprocess is launched with the *host* Python
# (not the frozen interpreter): it needs the source files on disk to
# find them via cwd=subproc_path() and `python3 py_func` +
# `from runtime.*`. api/ and interpreter/ are intentionally NOT
# exposed: user py_func scripts must go through py_func.tm
# (JSON-RPC bridge) for any testium API call.
datas=[('../../src/VERSION', '.'), datas=[('../../src/VERSION', '.'),
('../../src/testium/lua_func', 'lua_func'), ('../../src/testium/lua_func', 'lua_func'),
('../../src/testium/py_func', 'py_func'), ('../../src/testium/py_func', 'py_func'),
('../../src/testium/runtime', 'runtime')], ('../../src/testium/runtime', 'runtime'),
(JUNIT_XML_DIR, 'junit_xml')],
hiddenimports=["git", hiddenimports=["git",
"interpreter", "interpreter",
"main_win", "main_win",

View File

@@ -270,7 +270,7 @@ class RuntimePlotPeriodic(PeriodicTimer):
self.func_name = func_name self.func_name = func_name
self.args = args self.args = args
self.post_eval = post_eval self.post_eval = post_eval
self.proc = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10) self.proc = PyFuncExecEngine(api_request, 10)
self.proc.start() self.proc.start()
if not self.proc.wait_ready(10): if not self.proc.wait_ready(10):
raise ETUMRuntimeError( raise ETUMRuntimeError(

View File

@@ -211,7 +211,7 @@ class TestProcess(Process):
env_init() env_init()
# Creation of the python evaluation process for loading of the complete test # Creation of the python evaluation process for loading of the complete test
eval_proc = eval_process_init("", api_request, 10, test_dir) eval_proc = eval_process_init(api_request, 10, test_dir)
eval_proc.start() eval_proc.start()
tm.print_debug(f"python bin is: '{eval_proc.python_bin}'.") tm.print_debug(f"python bin is: '{eval_proc.python_bin}'.")
if not eval_proc.wait_ready(10): if not eval_proc.wait_ready(10):

View File

@@ -207,7 +207,7 @@ then considered as 'False'""")
else: else:
pl = [self._currentLoop] pl = [self._currentLoop]
proc = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10) proc = PyFuncExecEngine(api_request, 10)
proc.start() proc.start()
if not proc.wait_ready(10): if not proc.wait_ready(10):
raise ETUMRuntimeError( raise ETUMRuntimeError(

View File

@@ -31,7 +31,7 @@ class TestItemLuaFunc(TestItem):
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) self._context_id = self._prms.getParam("context_id", default=None, processed=False)
self._lua_func_proc = LuaFuncExecEngine(tm.gd("lua_bin", ""), api_request, 10) self._lua_func_proc = LuaFuncExecEngine(api_request, 10)
def _get_engine(self): def _get_engine(self):
"""Return (engine, persistent). If context_id is set, use a shared persistent engine.""" """Return (engine, persistent). If context_id is set, use a shared persistent engine."""
@@ -41,7 +41,7 @@ class TestItemLuaFunc(TestItem):
ctx_id = self._prms.expanse(self._context_id) ctx_id = self._prms.expanse(self._context_id)
contexts = tm.gd(_LUA_FUNC_CONTEXTS_KEY, {}) contexts = tm.gd(_LUA_FUNC_CONTEXTS_KEY, {})
if ctx_id not in contexts: if ctx_id not in contexts:
contexts[ctx_id] = LuaFuncExecEngine(tm.gd("lua_bin", ""), api_request, 10) contexts[ctx_id] = LuaFuncExecEngine(api_request, 10)
tm.setgd(_LUA_FUNC_CONTEXTS_KEY, contexts) tm.setgd(_LUA_FUNC_CONTEXTS_KEY, contexts)
return contexts[ctx_id], True return contexts[ctx_id], True

View File

@@ -31,7 +31,7 @@ class TestItemPyFunc(TestItem):
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) self._context_id = self._prms.getParam("context_id", default=None, processed=False)
self._py_func_proc = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10) self._py_func_proc = PyFuncExecEngine(api_request, 10)
def _get_engine(self): def _get_engine(self):
"""Return (engine, persistent). If context_id is set, use a shared persistent engine.""" """Return (engine, persistent). If context_id is set, use a shared persistent engine."""
@@ -41,7 +41,7 @@ class TestItemPyFunc(TestItem):
ctx_id = self._prms.expanse(self._context_id) ctx_id = self._prms.expanse(self._context_id)
contexts = tm.gd(_PY_FUNC_CONTEXTS_KEY, {}) contexts = tm.gd(_PY_FUNC_CONTEXTS_KEY, {})
if ctx_id not in contexts: if ctx_id not in contexts:
contexts[ctx_id] = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10) contexts[ctx_id] = PyFuncExecEngine(api_request, 10)
tm.setgd(_PY_FUNC_CONTEXTS_KEY, contexts) tm.setgd(_PY_FUNC_CONTEXTS_KEY, contexts)
return contexts[ctx_id], True return contexts[ctx_id], True

View File

@@ -40,6 +40,7 @@ class TestItemPlotActionOpen(TestItemPlotAction):
try: try:
gname = self._prms.expanse(self.token) gname = self._prms.expanse(self.token)
lpath = self._prms.expanse(self._log_path) lpath = self._prms.expanse(self._log_path)
runtime_plot = importlib.import_module("api.runtime_plot")
gr = runtime_plot.RuntimePlot(gname, lpath) gr = runtime_plot.RuntimePlot(gname, lpath)
tm.add_plot(gr) tm.add_plot(gr)
@@ -233,6 +234,3 @@ class TestItemPlot(TestItemActions):
) )
self.actions_token = self._prms.getParam("plot_name", required=True) self.actions_token = self._prms.getParam("plot_name", required=True)
global runtime_plot
runtime_plot = importlib.import_module("api.runtime_plot")

View File

@@ -58,7 +58,6 @@ def _discover_plugins():
try: try:
cls = ep.load() cls = ep.load()
_EXPORTER_REGISTRY[ep.name] = lambda c=cls: c _EXPORTER_REGISTRY[ep.name] = lambda c=cls: c
print(f'[testium] Loaded report exporter plugin: "{ep.name}"')
except Exception as e: except Exception as e:
print(f'[testium] Failed to load report exporter plugin "{ep.name}": {e}') print(f'[testium] Failed to load report exporter plugin "{ep.name}": {e}')
except Exception: except Exception:

View File

@@ -8,6 +8,7 @@ import interpreter.utils.settings as prefs
from interpreter.test_report.test_report import TestReport from interpreter.test_report.test_report import TestReport
from interpreter.utils.py_func_exec import PyFuncExecEngine 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 import bins
from runtime.tum_except import ETUMRuntimeError from runtime.tum_except import ETUMRuntimeError
from interpreter.utils.constants import TestItemType as cst_type from interpreter.utils.constants import TestItemType as cst_type
import interpreter.utils.constants as cst import interpreter.utils.constants as cst
@@ -49,6 +50,28 @@ class TestSet:
self._tree = self.__loadTestTree(tum_fime) self._tree = self.__loadTestTree(tum_fime)
self.dict_report = self._testdict.get("report", None) self.dict_report = self._testdict.get("report", None)
self.set_post_exec() self.set_post_exec()
self._validate_runtime_deps()
def _validate_runtime_deps(self):
"""Resolve external interpreters needed by this test tree and fail
early with a clear message if any is missing.
Python is always required (the eval engine always runs). Lua is
only required when at least one ``lua_func`` item is present.
"""
needed = ["python"]
if self.__has_item_type(self._rootItem, cst_type.TYPE_LUA_FUNCTION):
needed.append("lua")
bins.ensure(*needed)
def __has_item_type(self, parent, item_type):
for i in range(parent.childCount()):
child = parent.child(i)
if child.type() == item_type.item_name:
return True
if self.__has_item_type(child, item_type):
return True
return False
def execute(self): def execute(self):
self._report = TestReport(self.dict_report) self._report = TestReport(self.dict_report)
@@ -352,7 +375,7 @@ class TestSet:
tm.print_debug(f' No file: "{post_exec_file}".') tm.print_debug(f' No file: "{post_exec_file}".')
return return
proc = PyFuncExecEngine(tm.gd("python_bin", ""), api_request, 10) proc = PyFuncExecEngine(api_request, 10)
# start the process for executing external python # start the process for executing external python
proc.start() proc.start()
try: try:
@@ -367,13 +390,13 @@ class TestSet:
# tests backup is done here # tests backup is done here
succ, res = proc.func_call(post_exec_file, "post_exec", []) succ, res = proc.func_call(post_exec_file, "post_exec", [])
if not succ == TestValue.SUCCESS: if not succ == TestValue.SUCCESS:
tm.print_debug( tm.print_warn(
f"Test success but the \"post_exec\" function failed: {res}" f"Test success but the \"post_exec\" function failed: {res}"
) )
else: else:
succ, res = proc.func_call(post_exec_file, "post_exec_fail", []) succ, res = proc.func_call(post_exec_file, "post_exec_fail", [])
if not succ == TestValue.SUCCESS: if not succ == TestValue.SUCCESS:
tm.print_debug( tm.print_warn(
f"Test failed but the \"post_exec_fail\" function failed: {res}" f"Test failed but the \"post_exec_fail\" function failed: {res}"
) )
finally: finally:

View File

@@ -0,0 +1,151 @@
"""Centralised resolution of external interpreter paths (Python, Lua).
The user can override the path through the global dict via the keys
``python_bin`` and ``lua_bin`` (typically populated from a YAML config).
When unset, the system PATH is searched for known candidates.
Resolution is cached in memory: each interpreter is resolved at most
once per testium process. Subsequent calls return the cached value.
Public API
----------
``python_bin()`` : resolved python3 path (or "" if missing)
``lua_bin()`` : resolved lua >= 5.1 path (or "" if missing)
``ensure(*names)`` : resolve every name and raise a clear error if
any is missing — meant for early validation at
test load time
``reset()`` : clear the cache (mostly useful for tests)
"""
import shutil
import subprocess
import api.testium as tm
from interpreter.utils.paths import sys_app_path_lin, sys_app_path_win
from runtime.tum_except import ETUMRuntimeError
# ---------- Discovery primitives ---------------------------------------------
_PYTHON_CANDIDATES = ["python3", "python"]
_LUA_CANDIDATES = ["lua", "lua5.5", "lua5.4", "lua5.3", "lua5.2", "lua5.1"]
def _which(name):
func = sys_app_path_win if tm.OS() == "Windows" else sys_app_path_lin
return func(name)
def _python_version(path):
cmd = [path, "-c", "import sys; print(sys.version_info[:3])"]
try:
r = subprocess.run(
cmd, capture_output=True, text=True,
encoding=tm.sys_encoding(), timeout=10,
)
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
return None
try:
return eval(r.stdout)
except Exception:
return None
def _is_python3(path):
v = _python_version(path)
return v is not None and v[0] == 3
def _lua_version(path):
try:
r = subprocess.run(
[path, "-v"], capture_output=True, text=True, timeout=10,
)
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
return None
# On Windows the version banner goes to stderr.
line = r.stdout or r.stderr
try:
major, minor, _patch = line.split(" ")[1].split(".")
return (int(major), int(minor))
except (IndexError, ValueError):
return None
def _is_lua51(path):
v = _lua_version(path)
return v is not None and v >= (5, 1)
# ---------- Resolver ---------------------------------------------------------
# (display name, globdict override key, candidate list, validator)
_SPECS = {
"python": ("Python 3", "python_bin", _PYTHON_CANDIDATES, _is_python3),
"lua": ("Lua 5.1+", "lua_bin", _LUA_CANDIDATES, _is_lua51),
}
_resolved = {}
def _resolve(name):
if name in _resolved:
return _resolved[name]
display, gd_key, candidates, validator = _SPECS[name]
override = tm.gd(gd_key, "") or ""
path = ""
if override:
if shutil.which(override) and validator(override):
path = override
else:
tm.print_warn(
f"Configured {display} interpreter '{override}' is not usable; "
f"falling back to discovery."
)
if not path:
for c in candidates:
p = _which(c)
if not p:
continue
if validator(p):
path = p
break
_resolved[name] = path
return path
def python_bin():
return _resolve("python")
def lua_bin():
return _resolve("lua")
def ensure(*names):
"""Resolve each of the given names; raise if any is missing.
Meant to be called at test load with the set of interpreters the
test tree actually needs, so the user gets a clear error before
execution starts instead of deep inside an engine spawn.
"""
missing = []
for n in names:
if not _resolve(n):
display, gd_key, candidates, _ = _SPECS[n]
missing.append(
f" - {display}: tried {candidates} on PATH, none usable. "
f"Set '{gd_key}' in the YAML config to override."
)
if missing:
raise ETUMRuntimeError(
"Required external interpreter(s) not found:\n" + "\n".join(missing)
)
def reset():
_resolved.clear()

View File

@@ -29,7 +29,7 @@ class LuaFuncExecEngine(LuaProcessBase):
# In case an error was encountered in the called function # In case an error was encountered in the called function
elif "error" in answer: elif "error" in answer:
msg = f"{answer["error"]}" msg = f"{answer['error']}"
return TestValue.FAILURE, msg return TestValue.FAILURE, msg
else: else:

View File

@@ -1,6 +1,5 @@
import os import os
import sys import sys
import shutil
import subprocess import subprocess
import socket import socket
@@ -8,85 +7,7 @@ import api.testium as tm
from runtime.jrpc import JsonRpcClient from runtime.jrpc import JsonRpcClient
from interpreter.utils.paths import subproc_path from interpreter.utils.paths import subproc_path
from runtime.tum_except import ETUMRuntimeError from runtime.tum_except import ETUMRuntimeError
from interpreter.utils.paths import sys_app_path_lin, sys_app_path_win from interpreter.utils import bins
def _lua_version(path: str):
cmd = f'"{path}" -v'
try:
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
encoding=tm.sys_encoding(),
timeout=10,
)
# Under windows, the output is on stderr
data = result.stdout or result.stderr
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired) as e:
data = ""
try:
vers = ((data.split(" "))[1]).split(".")
if len(vers) != 3:
vers = (0, 0, 0)
except:
vers = (0, 0, 0)
return tuple(vers)
def _is_lua51(lua_bin):
res = False
v = _lua_version(lua_bin)
if (v[0] == "5") and (v[1] >= "1"):
res = True
return res
def _sys_lua_bin():
sys_lua_bin = tm.gd("_sys_lua_bin", "")
if sys_lua_bin != "":
return sys_lua_bin
cur_os = tm.OS()
if cur_os == "Windows":
func = sys_app_path_win
else:
func = sys_app_path_lin
sys_lua_bin = func("lua")
if (sys_lua_bin != "") and not _is_lua51(sys_lua_bin):
tm.print_debug(f"'{sys_lua_bin}' not a lua 5.1 min.")
sys_lua_bin = ""
tm.print_debug(f"lua bin is: '{sys_lua_bin}'.")
tm.setgd("_sys_lua_bin", sys_lua_bin)
return sys_lua_bin
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout,
)
return (result.returncode == 0) and (
(result.stdout.startswith("Lua") or result.stderr.startswith("Lua"))
)
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
return False
class LuaProcessBase: class LuaProcessBase:
@@ -96,35 +17,15 @@ class LuaProcessBase:
"LUA_CPATH": {"replace": True}, "LUA_CPATH": {"replace": True},
} }
def __init__(self, lua_bin="", request_handler=None, timeout=10): def __init__(self, request_handler=None, timeout=10):
""" """Initializes the Lua function execution engine.
Initializes the Lua function execution engine.
Args:
lua_bin (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: Raises:
ETUMRuntimeError: If the Lua path is invalid or no interpreter is found. ETUMRuntimeError: If no Lua >= 5.1 interpreter is found.
""" """
if lua_bin != "": self._lbin = bins.lua_bin()
if shutil.which(lua_bin) is None: if not self._lbin:
raise ETUMRuntimeError( raise ETUMRuntimeError("No valid Lua 5.1+ interpreter found")
f"The passed lua path is not pointing to an executable: '{lua_bin}'"
)
if not _is_lua_interpreter(lua_bin):
raise ETUMRuntimeError(
f"The passed executable is not a lua interpreter: '{lua_bin}'"
)
else:
lua_bin = _sys_lua_bin()
if lua_bin == "":
raise ETUMRuntimeError(f"No valid lua interpreter found")
tm.setgd("lua_bin", lua_bin)
self._lbin = lua_bin
self._req_handler = request_handler self._req_handler = request_handler
self._process = None self._process = None
self._port = 0 self._port = 0

View File

@@ -6,9 +6,9 @@ import api.testium as tm
eval_process = None eval_process = None
def eval_process_init(python_bin, request_handler, timeout, python_path): def eval_process_init(request_handler, timeout, python_path):
global eval_process global eval_process
eval_process = EvalExecEngine(python_bin, request_handler, timeout, python_path) eval_process = EvalExecEngine(request_handler, timeout, python_path)
return eval_process return eval_process

View File

@@ -29,7 +29,7 @@ class PyFuncExecEngine(PyProcessBase):
# In case an error was encountered in the called function # In case an error was encountered in the called function
elif "error" in answer: elif "error" in answer:
msg = f"{answer["error"]}" msg = f"{answer['error']}"
return TestValue.FAILURE, msg return TestValue.FAILURE, msg
else: else:

View File

@@ -1,77 +1,12 @@
import os import os
import shutil
import sys import sys
import subprocess import subprocess
import socket import socket
from runtime.jrpc import JsonRpcClient from runtime.jrpc import JsonRpcClient
import api.testium as tm import api.testium as tm
from interpreter.utils.paths import sys_app_path_lin, sys_app_path_win
from runtime.tum_except import ETUMRuntimeError from runtime.tum_except import ETUMRuntimeError
from interpreter.utils.paths import testium_path, subproc_path from interpreter.utils.paths import testium_path, subproc_path
from interpreter.utils import bins
def _python_version(path: str):
cmd = f'"{path}" -c "import sys; print(sys.version_info[:3])"'
try:
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
encoding=tm.sys_encoding(),
timeout=10,
)
data = result.stdout
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired) as e:
tm.print_debug(str(e))
data = ""
return eval(data)
def _is_python3(python_bin):
try:
v = _python_version(python_bin)
if v[0] == 3:
res = True
except:
res = False
return res
def _is_python_interpreter(path: str, timeout=2) -> bool:
try:
result = subprocess.run(
[path, "-c", "import sys; print(sys.executable)"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout,
)
return result.returncode == 0
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
return False
def _sys_python_bin():
sys_python_bin = ""
cur_os = tm.OS()
if cur_os == "Windows":
func = sys_app_path_win
else:
func = sys_app_path_lin
exe = ["python3", "python"]
for e in exe:
sys_python_bin = func(e)
if sys_python_bin == "":
continue
if _is_python3(sys_python_bin):
break
sys_python_bin = ""
return sys_python_bin
class PyProcessBase: class PyProcessBase:
@@ -80,29 +15,10 @@ class PyProcessBase:
"PYTHONPATH": {"replace": True}, "PYTHONPATH": {"replace": True},
} }
def __init__(self, python_bin="", request_handler=None, timeout=10, python_path=""): def __init__(self, request_handler=None, timeout=10, python_path=""):
self._pbin = python_bin self._pbin = bins.python_bin()
if (self._pbin is not None) and (self._pbin != ""): if not self._pbin:
raise ETUMRuntimeError("No valid Python 3 interpreter found")
if shutil.which(self._pbin) is None:
raise ETUMRuntimeError(
f"The passed python path is not pointing to an executable: '{self._pbin}'"
)
if not _is_python_interpreter(self._pbin):
raise ETUMRuntimeError(
f"The passed executable is not a python interpreter: '{self._pbin}'"
)
else:
self._pbin = tm.gd("_cached_python_bin", "")
if self._pbin == "":
self._pbin = _sys_python_bin()
tm.setgd("_cached_python_bin", self._pbin)
if self._pbin == "":
raise ETUMRuntimeError(f"No valid python interpreter found")
self._ppath = python_path self._ppath = python_path
self._req_handler = request_handler self._req_handler = request_handler
self._process = None self._process = None

View File

@@ -25,12 +25,17 @@ class TestSetController:
if "timeout" in args: if "timeout" in args:
timeout = args.pop("timeout") timeout = args.pop("timeout")
self._test_ctrl.put({cmd: args}) self._test_ctrl.put({cmd: args})
res = self._test_resp.get(block, timeout) # Drain stale responses (left over from earlier polled commands that
if isinstance(res, tuple): # we had given up on waiting). They can land in the queue after our
raise ETUMRuntimeError(f"Test set command '{cmd}' failed: '{res[1]}'") # clear() because the TestProcess may have pulled their request
if isinstance(res, dict) and not cmd in res.keys(): # before the clear, processed them, and pushed the response after.
raise ETUMRuntimeError(f"Unexpected return error in test set controller") while True:
return res[cmd] res = self._test_resp.get(block, timeout)
if isinstance(res, tuple):
raise ETUMRuntimeError(f"Test set command '{cmd}' failed: '{res[1]}'")
if isinstance(res, dict) and cmd in res.keys():
return res[cmd]
# Anything else is a stale response — discard and keep waiting.
def clear(self): def clear(self):
while True: while True:

View File

@@ -41,7 +41,7 @@ class FuncHandler(JsonRpcSrv):
except Exception as e: except Exception as e:
tb = traceback.format_exc() tb = traceback.format_exc()
return { return {
"error": f"bad jrpc req handler 'func_call' arguments ({"\n".join(tb.splitlines())}). To be reported to testium support team." "error": "bad jrpc req handler 'func_call' arguments (" + "\n".join(tb.splitlines()) + "). To be reported to testium support team."
} }
if method == "eval": if method == "eval":
try: try:
@@ -57,7 +57,7 @@ class FuncHandler(JsonRpcSrv):
except Exception as e: except Exception as e:
tb = traceback.format_exc() tb = traceback.format_exc()
return { return {
"error": f"bad jrpc req handler 'eval' arguments ({"\n".join(tb.splitlines())}). To be reported to testium support team." "error": "bad jrpc req handler 'eval' arguments (" + "\n".join(tb.splitlines()) + "). To be reported to testium support team."
} }
else: else:
return { return {

View File

@@ -28,7 +28,7 @@ def _make_api(name):
if "result" in res: if "result" in res:
ret_val = res["result"] ret_val = res["result"]
elif "error" in res: elif "error" in res:
raise ETUMRuntimeError(f"api call to 'tm.{name}' failed with error '{res["error"]}'") raise ETUMRuntimeError(f"api call to 'tm.{name}' failed with error '{res['error']}'")
else: else:
raise ETUMRuntimeError("api call failure in jrpc client to be reported to testium support team.") raise ETUMRuntimeError("api call failure in jrpc client to be reported to testium support team.")
return ret_val return ret_val

View File

@@ -6,5 +6,10 @@ SUPPORTED_API = [
"add_plot_values", "add_plot_values",
"last_plot_value", "last_plot_value",
"text_mode", "text_mode",
"OS",
"get_main_dir",
"init_timestamp",
"timestamp",
"timestamp_as_sec",
] ]

View File

@@ -145,7 +145,7 @@ class JsonRpcConnection:
self.pending[msg["id"]]["response"] = msg self.pending[msg["id"]]["response"] = msg
self.pending[msg["id"]]["event"].set() self.pending[msg["id"]]["event"].set()
else: else:
self.print_info(f"msg id '{msg["id"]}' inconsistency") self.print_info(f"msg id '{msg['id']}' inconsistency")
# ---------- Handler ---------- # ---------- Handler ----------
def _handle_request(self, meth, params, rid=None): def _handle_request(self, meth, params, rid=None):

View File

@@ -1,4 +1,4 @@
import api.testium as libtm import py_func.tm as libtm
def check_os(expected_os): def check_os(expected_os):

View File

@@ -1,5 +1,5 @@
import time import time
import api.testium as tm import py_func.tm as tm
def sleep_func(duration): def sleep_func(duration):

View File

@@ -30,17 +30,10 @@ linux_prompt: "$ "
inc_no_template: "inc no template" inc_no_template: "inc no template"
inc_with_template: "inc with template" inc_with_template: "inc with template"
lua_rev: 5.5 # LUA_PATH / LUA_CPATH are intentionally NOT set: the lua interpreter's
# compiled-in defaults already point to the version-matching directories
LUA_PATH_Linux: /usr/share/lua/$(lua_rev)/?.lua;/usr/local/share/lua/$(lua_rev)/?.lua;/usr/local/share/lua/$(lua_rev)/?/init.lua;/usr/share/lua/$(lua_rev)/?/init.lua;/usr/local/lib/lua/$(lua_rev)/?.lua;/usr/local/lib/lua/$(lua_rev)/?/init.lua;/usr/lib/lua/$(lua_rev)/?.lua;/usr/lib/lua/$(lua_rev)/?/init.lua;./?.lua;./?/init.lua;/home/francois/.luarocks/share/lua/$(lua_rev)/?.lua;/home/francois/.luarocks/share/lua/$(lua_rev)/?/init.lua # (/usr/share/lua/X.Y, /usr/lib/.../lua/X.Y) where the system packages
LUA_CPATH_Linux: /usr/local/lib/lua/$(lua_rev)/?.so;/usr/lib/lua/$(lua_rev)/?.so;/usr/local/lib/lua/$(lua_rev)/loadall.so;/usr/lib/lua/$(lua_rev)/loadall.so;./?.so;/home/francois/.luarocks/lib/lua/$(lua_rev)/?.so # lua-cjson and lua-socket install their files. Hard-coding a `lua_rev`
PATH_Linux: # here would break as soon as the host's lua differs from that value
# (which is exactly what happened on Debian Bookworm with lua5.5
LUA_PATH_Windows: ;.\?.lua;C:\Lua\5.1\lua\?.lua;C:\Lua\5.1\lua\?\init.lua;C:\Lua\5.1\?.lua;C:\Lua\5.1\?\init.lua;C:\Lua\5.1\lua\?.luac # vs. lua-cjson built for lua5.4).
LUA_CPATH_Windows: .\?.dll;C:\Lua\5.1\?.dll;C:\Lua\5.1\loadall.dll;C:\Lua\5.1\clibs\?.dll;C:\Lua\5.1\clibs\loadall.dll;.\?51.dll;C:\Lua\5.1\?51.dll;C:\Lua\5.1\clibs\?51.dll
PATH_Windows: ""
lua_env:
PATH: $(PATH_$(os))
LUA_PATH: $(LUA_PATH_$(os))
LUA_CPATH: $(LUA_CPATH_$(os))