Compare commits
11 Commits
9db0f89522
...
358ade8c98
| Author | SHA1 | Date | |
|---|---|---|---|
| 358ade8c98 | |||
| 46bdb44cfb | |||
| 41519c97cb | |||
| b9475c6e9b | |||
| d3c5bd01e5 | |||
| 077e1a97c1 | |||
| 35ca0a8b45 | |||
| 4529da7aee | |||
| 8bd9b3e9d6 | |||
| a70b70db54 | |||
| d7f25718d0 |
123
CLAUDE.md
123
CLAUDE.md
@@ -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 |
|
||||||
@@ -109,9 +131,28 @@ All dialog items (`dialog_image`, `dialog_question`, `dialog_references`, `dialo
|
|||||||
| `src/testium/interpreter/test_items/test_item_parallel.py` | `parallel` and `parallel_branch` items |
|
| `src/testium/interpreter/test_items/test_item_parallel.py` | `parallel` and `parallel_branch` items |
|
||||||
| `src/testium/interpreter/utils/globdict.py` | Global variable dict |
|
| `src/testium/interpreter/utils/globdict.py` | Global variable dict |
|
||||||
| `src/testium/interpreter/utils/termlog.py` | Terminal color output |
|
| `src/testium/interpreter/utils/termlog.py` | Terminal color output |
|
||||||
| `src/lib/stdout_redirect.py` | `StdioRedirect` singleton (`stdio_redir`) |
|
| `src/testium/runtime/stdout_redirect.py` | `StdioRedirect` singleton (`stdio_redir`) |
|
||||||
| `src/lib/string_queue.py` | Thread-safe string buffer used for stdout redirection |
|
| `src/testium/runtime/string_queue.py` | Thread-safe string buffer used for stdout redirection |
|
||||||
| `src/testium/libs/testium.py` | Public API for test scripts (`tm.*`) |
|
| `src/testium/api/testium.py` | Public API for test scripts (`tm.*`) |
|
||||||
|
| `src/testium/py_func/` | Python subprocess for `py_func` items (sandboxed: imports only `runtime/` and `py_func/`) |
|
||||||
|
| `src/testium/lua_func/` | Lua subprocess scripts for `lua_func` items |
|
||||||
|
|
||||||
|
## Package layout
|
||||||
|
|
||||||
|
The whole project is a single Python package under `src/testium/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/testium/
|
||||||
|
├── __init__.py / __main__.py
|
||||||
|
├── runtime/ internal plumbing (jrpc, stdout_redirect, string_queue, tum_except, api)
|
||||||
|
├── api/ public SDK exposed to test scripts (`import api.testium as tm`)
|
||||||
|
├── interpreter/ test execution engine (NOT visible to py_func/lua_func)
|
||||||
|
├── main_win/ GUI (NOT visible to py_func/lua_func)
|
||||||
|
├── py_func/ subprocess code for python_func items
|
||||||
|
└── lua_func/ subprocess scripts for lua_func items (data files)
|
||||||
|
```
|
||||||
|
|
||||||
|
`subproc_path()` and `testium_path()` both return the package directory. The py_func subprocess is launched with cwd=that directory and `python3 py_func`. The contract that `py_func/` and `lua_func/` only depend on `runtime/` (no `interpreter`, `main_win`, `api`, `testium`) is enforced by `test/validation/items/isolation/`.
|
||||||
|
|
||||||
## GUI icons (main_win)
|
## GUI icons (main_win)
|
||||||
|
|
||||||
@@ -137,31 +178,71 @@ Icons are assigned once when the test file is loaded (not updated live on theme
|
|||||||
|
|
||||||
### `run` item
|
### `run` item
|
||||||
`src/testium/interpreter/test_items/test_item_run.py` — launches a `.tum` file in a new testium instance (`-b` in batch mode, `-r` in GUI mode). Result:
|
`src/testium/interpreter/test_items/test_item_run.py` — launches a `.tum` file in a new testium instance (`-b` in batch mode, `-r` in GUI mode). Result:
|
||||||
- **SUCCESS** if the sub-instance launched and ran to completion (exit code is ignored)
|
- **PASS** if the sub-instance launched and ran to completion (exit code is ignored)
|
||||||
- **FAILURE** if the file is not found, `wait_for_exec` is set without `start_time`/`end_time`, the time window was not reached, or any other launch error
|
- **FAIL** if the file is not found, `wait_for_exec` is set without `start_time`/`end_time`, the time window was not reached, or any other launch error
|
||||||
|
|
||||||
The sub-test's own pass/fail result is intentionally not propagated.
|
The sub-test's own pass/fail result is intentionally not propagated.
|
||||||
|
|
||||||
## Recent fixes (branch `parallel_execution`)
|
### Report exporters & plugins
|
||||||
- `test_item_parallel.py`: new `parallel` 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.
|
`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.
|
||||||
- `test_item_container.py`: new `TestItemContainer` base class extracted from Group/Cycle patterns
|
|
||||||
- `test_item_sleep.py`: interruptible loop (checks `self._is_stopped`) instead of blocking `time.sleep()` so `sync: any` can stop slow branches quickly
|
|
||||||
- `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_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.
|
|
||||||
- `__init__.py`: removed `-m`/`--terminal` mode
|
|
||||||
- `terminal.py`: deleted
|
|
||||||
|
|
||||||
## Recent fixes (branch `text_no_pyside`)
|
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:
|
||||||
- `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`
|
```toml
|
||||||
- `batch.py`: `control("loaded")` deadlock if `TestProcess` crashed before `cmd_th` started — fix: daemon thread + `threading.Event` + `is_alive()` polling
|
[project.entry-points."testium.exporters"]
|
||||||
- `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:]`).
|
my_format = "my_pkg:MyExporter"
|
||||||
- Dialog items: `auto_result`/`auto_value` now used in non-interactive text mode; dialogs without `auto_result` FAIL immediately in batch mode.
|
```
|
||||||
- `run` item: removed `stdout=PIPE` (caused deadlock with `multiprocessing` spawn); simplified result to SUCCESS on any completed subprocess.
|
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
|
||||||
|
- 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`.
|
||||||
|
- `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.
|
||||||
|
- 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.
|
||||||
|
- Report exporter plugin registry (`test_report.py`): `_EXPORTER_REGISTRY` + `entry_points("testium.exporters")` discovery. Missing format → info line, run continues.
|
||||||
|
- About dialog rework: `QVBoxLayout` (resizable), version + dirty/branch info in a `QLabel` (auto-sized), copyright + clickable EUPL-1.2 link.
|
||||||
|
- `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.
|
||||||
|
- `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.
|
||||||
|
- `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 on TestProcess crash — fix uses daemon thread + `threading.Event` + `is_alive()` polling.
|
||||||
|
- `termlog.py`: light/dark terminal auto-detection (`COLORFGBG`, OSC 11) + write residue bug.
|
||||||
|
- 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` deadlock; PASS on any completed subprocess.
|
||||||
|
- `unittest` item: renamed from `unittest_file`.
|
||||||
|
- GUI test tree: check and fold state preserved across same-file reloads.
|
||||||
|
- Licence: EUPL-1.2.
|
||||||
|
|
||||||
## Validation tests
|
## Validation tests
|
||||||
Located in `test/validation/`. Run with `-b` flag:
|
Located in `test/validation/`. Run with `-b` flag:
|
||||||
```
|
```
|
||||||
./run.sh -b -l mon_log.log -- test/validation/main.tum
|
./run.sh -b -- test/validation/main.tum
|
||||||
```
|
```
|
||||||
Parallel item tests: `test/validation/items/parallel/test.tum`
|
Parallel item tests: `test/validation/items/parallel/test.tum`
|
||||||
|
|
||||||
|
|||||||
101
CONTRIBUTING.md
101
CONTRIBUTING.md
@@ -45,7 +45,7 @@ For existing files, keep the header that is already there.
|
|||||||
3. Commit with a clear message (one logical change per commit).
|
3. Commit with a clear message (one logical change per commit).
|
||||||
4. Make sure the validation suite still passes:
|
4. Make sure the validation suite still passes:
|
||||||
```
|
```
|
||||||
./run.sh -b -l mon_log.log -- test/validation/main.tum
|
./run.sh -b -- test/validation/main.tum
|
||||||
```
|
```
|
||||||
5. Open a pull request against `main`.
|
5. Open a pull request against `main`.
|
||||||
|
|
||||||
@@ -56,6 +56,105 @@ For existing files, keep the header that is already there.
|
|||||||
- Add or update tests in `test/validation/` for new test items or behaviours
|
- Add or update tests in `test/validation/` for new test items or behaviours
|
||||||
- Update `CLAUDE.md` and the Sphinx manual for user-visible changes
|
- Update `CLAUDE.md` and the Sphinx manual for user-visible changes
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Debugging in VSCode
|
||||||
|
|
||||||
|
The recommended workflow:
|
||||||
|
|
||||||
|
1. Add a debug configuration to `.vscode/launch.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python : testium",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/src/testium",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"args": ["-g"],
|
||||||
|
"justMyCode": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. Install `debugpy` in the venv: `python -m pip install debugpy`.
|
||||||
|
3. Open the *Run and Debug* tab and press play. testium starts; load and
|
||||||
|
run a `.tum` file. Set breakpoints where you want to investigate.
|
||||||
|
|
||||||
|
### Qt GUI modification
|
||||||
|
|
||||||
|
UI files (`*.ui`) are edited in **Qt Creator**. After editing, regenerate
|
||||||
|
the corresponding Python and resource files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/qt_generate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Icons come from <https://github.com/free-icons/free-icons>.
|
||||||
|
|
||||||
|
### Sphinx documentation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install sphinx linuxdoc
|
||||||
|
doc/manual/sphinx/build_doc.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
PDF generation requires `texlive`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install texlive-full
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation suite
|
||||||
|
|
||||||
|
Batch mode (CI-friendly, headless):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh -b -- test/validation/main.tum
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI mode (loads the suite, click *Run* to execute and inspect the tree):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh test/validation/main.tum
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI run-and-close (executes the suite, then closes):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh -r -- test/validation/main.tum
|
||||||
|
```
|
||||||
|
|
||||||
|
Subset run via the `items` define (works in any mode):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh -b -d "items=['parallel','common']" -- test/validation/main.tum
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-distribution check
|
||||||
|
|
||||||
|
`package/deb/test_distro.sh` spins up a Docker/Podman container of the
|
||||||
|
target image, installs the expected system Python deps via apt (with
|
||||||
|
pip fallback for what is missing), installs the testium wheel and runs
|
||||||
|
the validation suite end-to-end. Currently green on `debian:bookworm`,
|
||||||
|
`debian:trixie`, `ubuntu:24.04`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./package/deb/test_distro.sh debian:trixie
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release procedure
|
||||||
|
|
||||||
|
1. Update `release_note.txt`.
|
||||||
|
2. Bump the version in `src/VERSION`.
|
||||||
|
3. Make sure the documentation is up to date — rebuild with
|
||||||
|
`doc/manual/sphinx/build_doc.sh` if needed.
|
||||||
|
4. Push and tag the commit with the new version.
|
||||||
|
5. Build the binary release: `package/pyinstaller/build.sh`.
|
||||||
|
6. Run the validation suite against each generated binary.
|
||||||
|
7. Confirm all validation results are green before publishing.
|
||||||
|
|
||||||
## Reporting security issues
|
## Reporting security issues
|
||||||
|
|
||||||
Please do **not** report security vulnerabilities through public GitHub
|
Please do **not** report security vulnerabilities through public GitHub
|
||||||
|
|||||||
283
README.md
283
README.md
@@ -1,185 +1,110 @@
|
|||||||
# Documentation
|
# testium
|
||||||
|
|
||||||
[See here](doc/manual/testium_manual.pdf).
|
testium is a YAML-driven test sequencer for hardware-in-the-loop and
|
||||||
|
integration testing. A test campaign is described in a `.tum` file as a tree
|
||||||
|
of items (checks, console interactions, Python/Lua functions, parallel blocks,
|
||||||
|
dialogs, …); testium executes the tree, captures results, and produces
|
||||||
|
reports in several formats.
|
||||||
|
|
||||||
# License
|
## Documentation
|
||||||
|
|
||||||
Copyright (c) 2025-2026 François Dausseur.
|
* [Quick start](doc/quick_start.md) — install and run your first test in
|
||||||
|
five minutes.
|
||||||
|
* [Tutorial](doc/tutorial.md) — guided walk-through of the most common
|
||||||
|
test items with a runnable example.
|
||||||
|
* [User manual (PDF)](doc/manual/testium_manual.pdf) — full reference.
|
||||||
|
* [`doc/examples/`](doc/examples/) — runnable `.tum` snippets.
|
||||||
|
|
||||||
|
## Pre-built releases
|
||||||
|
|
||||||
|
Pre-built artifacts are published at
|
||||||
|
<https://git.beafrancois.fr/v-and-v/testium/releases>:
|
||||||
|
|
||||||
|
* **Python wheel** (`testium-<version>-py3-none-any.whl`) — install with
|
||||||
|
`pip install testium-*.whl`. Lighter than the binary; pulls Python
|
||||||
|
dependencies from PyPI on install.
|
||||||
|
* **Self-contained Linux binary** (`testium`, built with PyInstaller) —
|
||||||
|
runnable directly, no Python installation required on the host. Lua
|
||||||
|
support still needs a system `lua` interpreter and the `lua-socket` /
|
||||||
|
`lua-cjson` modules.
|
||||||
|
* **Flatpak** — *coming soon.*
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
From a checkout of the repository:
|
||||||
|
|
||||||
|
| OS | Command |
|
||||||
|
|----|---------|
|
||||||
|
| Linux | `./run.sh` |
|
||||||
|
| Windows (cmd) | `run.bat` |
|
||||||
|
| Windows (PowerShell) | `run.ps1` |
|
||||||
|
|
||||||
|
The wrapper creates a Python virtual environment on first run and starts
|
||||||
|
testium in GUI mode. Add `-b path/to/test.tum` to run a test in batch mode.
|
||||||
|
|
||||||
|
## Manual installation
|
||||||
|
|
||||||
|
If the wrapper script does not fit your environment, set up testium manually:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r src/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Required Python packages (see `src/requirements.txt`):
|
||||||
|
`pyside6`, `pyserial`, `pyyaml`, `pexpect`, `gitpython`, `jinja2`, `colorama`,
|
||||||
|
`matplotlib`, `junit-xml`, `lxml`.
|
||||||
|
|
||||||
|
For tests using `lua_func` items, install Lua (>= 5.1) plus the `socket` and
|
||||||
|
`cjson` modules. On Debian/Ubuntu:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install lua5.4 lua-socket lua-cjson
|
||||||
|
```
|
||||||
|
|
||||||
|
Run testium:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 src/testium # GUI
|
||||||
|
python3 src/testium -b mytest.tum # batch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `wl_proxy_marshal_flags` symbol error
|
||||||
|
|
||||||
|
```
|
||||||
|
testium: symbol lookup error: ... undefined symbol: wl_proxy_marshal_flags
|
||||||
|
```
|
||||||
|
|
||||||
|
Force the X11 Qt backend:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export QT_QPA_PLATFORM=xcb
|
||||||
|
testium
|
||||||
|
```
|
||||||
|
|
||||||
|
### `xcb plugin missing`
|
||||||
|
|
||||||
|
```
|
||||||
|
qt.qpa.plugin: Could not load the Qt platform plugin "xcb"
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the missing system libraries:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install libxcb-cursor0 libicu-dev libxcb-cursor-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2025-2026 François Dausseur.
|
||||||
|
|
||||||
testium is distributed under the **European Union Public Licence v. 1.2
|
testium is distributed under the **European Union Public Licence v. 1.2
|
||||||
(EUPL-1.2)** — see the [LICENSE](LICENSE) file for the full text.
|
(EUPL-1.2)** — see [`LICENSE`](LICENSE) for the full text. SPDX:
|
||||||
|
`EUPL-1.2`.
|
||||||
|
|
||||||
SPDX identifier: `EUPL-1.2`
|
Contributions are accepted under the same licence (inbound = outbound).
|
||||||
|
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, debugging
|
||||||
Contributions are accepted under the same licence (inbound = outbound). See
|
workflow, and the release procedure.
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
||||||
|
|
||||||
# run testium
|
|
||||||
|
|
||||||
From the root path, on windows `cmd`:
|
|
||||||
|
|
||||||
run.bat
|
|
||||||
|
|
||||||
On windows powershell:
|
|
||||||
|
|
||||||
run.ps1
|
|
||||||
|
|
||||||
On linux:
|
|
||||||
|
|
||||||
./run.sh
|
|
||||||
|
|
||||||
The virtual environment is created if needed and *testium* is started.
|
|
||||||
|
|
||||||
# Manual setup
|
|
||||||
|
|
||||||
A python virtual environment should be created:
|
|
||||||
|
|
||||||
python3 -m venv <testium_venv>
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
In the virtual environment, the following modules must be installed:
|
|
||||||
|
|
||||||
* pyside6
|
|
||||||
* pyserial
|
|
||||||
* pyyaml
|
|
||||||
* pexpect
|
|
||||||
* gitpython
|
|
||||||
* jinja2
|
|
||||||
* colorama
|
|
||||||
* matplotlib
|
|
||||||
* junit-xml
|
|
||||||
* lxml
|
|
||||||
|
|
||||||
A `requirements.txt` file is also available in the git repository in the path `testium/src/`.
|
|
||||||
|
|
||||||
|
|
||||||
## run testium
|
|
||||||
|
|
||||||
from the testium path, execute
|
|
||||||
|
|
||||||
python3 -m src/testium
|
|
||||||
|
|
||||||
# Doc generation
|
|
||||||
|
|
||||||
## Install sphinx
|
|
||||||
|
|
||||||
pip install sphinx linuxdoc
|
|
||||||
|
|
||||||
## Generate the doc
|
|
||||||
|
|
||||||
Execute
|
|
||||||
|
|
||||||
doc/manual/sphinx/./build_doc.sh
|
|
||||||
|
|
||||||
This command works if texlive package has been installed on the system. It can be done by invoking the following command.
|
|
||||||
|
|
||||||
sudo apt install texlive-full
|
|
||||||
|
|
||||||
# QT GUI
|
|
||||||
|
|
||||||
## QT GUI modification
|
|
||||||
|
|
||||||
Open the ".ui" file with `qtcreator` and modify the gui. Then regenerate the python code.
|
|
||||||
|
|
||||||
On linux, a helper script has been created:
|
|
||||||
scripts/./qt_generate.sh
|
|
||||||
|
|
||||||
# Debugging
|
|
||||||
|
|
||||||
In order to debug testium or your python script executed within testium.
|
|
||||||
|
|
||||||
## In VSCODE
|
|
||||||
|
|
||||||
This is the prefered method :
|
|
||||||
|
|
||||||
1. Create a debug configuration like the following:
|
|
||||||
|
|
||||||
```
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Python : testium",
|
|
||||||
"type": "python",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder}/src/testium",
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"args": ["-g"],
|
|
||||||
"justMyCode": true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install debugpy module in python
|
|
||||||
|
|
||||||
python -m pip install debugpy
|
|
||||||
3. Then get to the "RUN AND DEBUG" tab and press the play button.
|
|
||||||
4. A testium window will pops up ; start execution of your tum.
|
|
||||||
5. Do not forget to put breakpoints where you want to investigate.
|
|
||||||
|
|
||||||
## Icons
|
|
||||||
|
|
||||||
Icons are coming from the following site: https://github.com/free-icons/free-icons.git
|
|
||||||
|
|
||||||
# testium Release
|
|
||||||
|
|
||||||
## Pre-requisite
|
|
||||||
|
|
||||||
A `python` virtual environment must have been set as described above.
|
|
||||||
|
|
||||||
### Install pyinstaller
|
|
||||||
|
|
||||||
Install `pyinstaller` package using pip.
|
|
||||||
|
|
||||||
## Generate the binary package
|
|
||||||
|
|
||||||
The procedure for a binary release is as follows:
|
|
||||||
|
|
||||||
1. update the `release_note.txt` file
|
|
||||||
2. modify the version in `src/VERSION` file
|
|
||||||
3. be sure that the documentation is up to date, and if not execute `doc/manual/sphinx/build_doc.sh` script
|
|
||||||
4. push modifications and create a tag with the new version on the git repository
|
|
||||||
5. generate an executable file by calling `package/pyinstaller/./build.sh`
|
|
||||||
6. run the complete validation test for each generated binary
|
|
||||||
7. check that all the validation results are OK
|
|
||||||
|
|
||||||
# Troubleshooting
|
|
||||||
|
|
||||||
## The testium exe crashes `wl_proxy_marshal_flags`
|
|
||||||
|
|
||||||
### Error message
|
|
||||||
|
|
||||||
/testium: symbol lookup error: /tmp/_MEIOhDCPF/libQt6WaylandClient.so.6: undefined symbol: wl_proxy_marshal_flags
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
|
|
||||||
Set the appropriate environment variable
|
|
||||||
|
|
||||||
export QT_QPA_PLATFORM=xcb
|
|
||||||
testium
|
|
||||||
|
|
||||||
## xcb plugin missing
|
|
||||||
|
|
||||||
### Error message
|
|
||||||
|
|
||||||
qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
|
|
||||||
A package is missing
|
|
||||||
|
|
||||||
sudo apt install libxcb-cursor0
|
|
||||||
sudo apt-get install libicu-dev
|
|
||||||
sudo apt-get install libxcb-cursor-dev
|
|
||||||
|
|
||||||
## The testium appimage crashes when opening a file
|
|
||||||
|
|
||||||
This is usually because wayland is defined as the default X server.
|
|
||||||
|
|
||||||
To change it :
|
|
||||||
|
|
||||||
* Disable Wayland by uncommenting WaylandEnable=false in the `/etc/gdm3/daemon.conf`
|
|
||||||
* Add `QT_QPA_PLATFORM=xcb` in `/etc/environment`
|
|
||||||
* After a reboot, check that the environment variable value returns `x11`:
|
|
||||||
|
|
||||||
$ echo $XDG_SESSION_TYPE
|
|
||||||
x11
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import libs.testium as tm
|
import py_func.tm as tm
|
||||||
|
|
||||||
def post_exec():
|
def post_exec():
|
||||||
print('Success !!!!')
|
print('Success !!!!')
|
||||||
|
|||||||
@@ -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:
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
66
doc/quick_start.md
Normal file
66
doc/quick_start.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Quick start
|
||||||
|
|
||||||
|
Five minutes from zero to a passing test.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
From a checkout of the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh --version # Linux
|
||||||
|
run.bat # Windows cmd
|
||||||
|
```
|
||||||
|
|
||||||
|
The wrapper creates a Python virtual environment on first run and verifies
|
||||||
|
testium starts. If you prefer a manual install, see the README.
|
||||||
|
|
||||||
|
## Your first test
|
||||||
|
|
||||||
|
Create `hello.tum`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
main:
|
||||||
|
name: hello world
|
||||||
|
steps:
|
||||||
|
- check:
|
||||||
|
name: 1 + 1 makes 2
|
||||||
|
values:
|
||||||
|
- <| 1 + 1 == 2 |>
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it in batch mode:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh -b -- hello.tum
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see something like:
|
||||||
|
|
||||||
|
```
|
||||||
|
-----> step "1 + 1 makes 2" started
|
||||||
|
Check passed
|
||||||
|
<----- step "1 + 1 makes 2" finished: PASS
|
||||||
|
Test run success.
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `==` with `!=` and re-run — the step now ends with **FAIL** and
|
||||||
|
the process exits with code 1.
|
||||||
|
|
||||||
|
## Open it in the GUI
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh hello.tum
|
||||||
|
```
|
||||||
|
|
||||||
|
The test tree appears in the left panel; click *Run test* in the toolbar.
|
||||||
|
Each item turns green or red live as it executes. Use `F1` on a selected
|
||||||
|
item to open its detail panel.
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
* [`doc/tutorial.md`](tutorial.md) — a guided walk-through of the most
|
||||||
|
common test items (`py_func`, `let`, `group`, `condition`, `report`).
|
||||||
|
* [`doc/examples/`](examples/) — runnable `.tum` snippets covering one
|
||||||
|
feature each.
|
||||||
|
* [`doc/manual/testium_manual.pdf`](manual/testium_manual.pdf) —
|
||||||
|
full reference manual.
|
||||||
223
doc/tutorial.md
Normal file
223
doc/tutorial.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# Tutorial — testing a small Python utility
|
||||||
|
|
||||||
|
This walk-through builds, step by step, a testium campaign that exercises
|
||||||
|
a small Python module. Each section adds one feature; you can follow
|
||||||
|
along by editing a single `.tum` file and re-running it.
|
||||||
|
|
||||||
|
If you have not yet run testium, start with [`quick_start.md`](quick_start.md).
|
||||||
|
|
||||||
|
## The code under test
|
||||||
|
|
||||||
|
Create `calc.py` next to your `.tum` file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def add(a, b):
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
def divide(a, b):
|
||||||
|
return a / b
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1 — a static check
|
||||||
|
|
||||||
|
The simplest item is `check`: it evaluates an expression and the test
|
||||||
|
passes iff the expression is truthy. Create `tutorial.tum`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
main:
|
||||||
|
name: calc.py campaign
|
||||||
|
steps:
|
||||||
|
- check:
|
||||||
|
name: addition is correct
|
||||||
|
values:
|
||||||
|
- <| 2 + 3 == 5 |>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<| ... |>` markers turn the body into a Python expression evaluated
|
||||||
|
at run time. Run it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./run.sh -b -- tutorial.tum
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2 — call your code with `py_func`
|
||||||
|
|
||||||
|
`check` only sees Python literals; to exercise `calc.py` we need a
|
||||||
|
`py_func` item. Replace the step:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- py_func:
|
||||||
|
name: add 2 and 3
|
||||||
|
file: calc.py
|
||||||
|
func_name: add
|
||||||
|
param: [2, 3]
|
||||||
|
expected_result: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
`expected_result` makes the item PASS only when the function returns
|
||||||
|
exactly that value.
|
||||||
|
|
||||||
|
The result is also stored in the global dict under `pfn_<name>`
|
||||||
|
(here `pfn_add 2 and 3`).
|
||||||
|
|
||||||
|
Anywhere in a `.tum`, `$(key)` is replaced at runtime by the value
|
||||||
|
stored in the global dict under `key`. A subsequent step can read the
|
||||||
|
result back with `$(pfn_<name>)`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- check:
|
||||||
|
name: result was 5
|
||||||
|
values:
|
||||||
|
- <| $(pfn_add 2 and 3) == 5 |>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3 — group several checks
|
||||||
|
|
||||||
|
Wrap the steps in a `group` to keep them visually together and let
|
||||||
|
testium report a per-group status:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
main:
|
||||||
|
name: calc.py campaign
|
||||||
|
steps:
|
||||||
|
- group:
|
||||||
|
name: add
|
||||||
|
steps:
|
||||||
|
- py_func:
|
||||||
|
name: 2 + 3
|
||||||
|
file: calc.py
|
||||||
|
func_name: add
|
||||||
|
param: [2, 3]
|
||||||
|
expected_result: 5
|
||||||
|
- py_func:
|
||||||
|
name: -1 + 1
|
||||||
|
file: calc.py
|
||||||
|
func_name: add
|
||||||
|
param: [-1, 1]
|
||||||
|
expected_result: 0
|
||||||
|
- group:
|
||||||
|
name: divide
|
||||||
|
steps:
|
||||||
|
- py_func:
|
||||||
|
name: 6 / 2
|
||||||
|
file: calc.py
|
||||||
|
func_name: divide
|
||||||
|
param: [6, 2]
|
||||||
|
expected_result: 3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
A group fails as soon as one of its steps fails (set
|
||||||
|
`stop_on_failure: false` to keep going).
|
||||||
|
|
||||||
|
## Step 4 — define a variable with `let`
|
||||||
|
|
||||||
|
Avoid hard-coding the same number twice with a variable:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- let:
|
||||||
|
name: define numerator
|
||||||
|
values:
|
||||||
|
- num: 6
|
||||||
|
- py_func:
|
||||||
|
name: divide num by 2
|
||||||
|
file: calc.py
|
||||||
|
func_name: divide
|
||||||
|
param:
|
||||||
|
- $(num)
|
||||||
|
- 2
|
||||||
|
expected_result: 3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
`$(num)` expands to the global dict entry — when the stored value is a
|
||||||
|
number it is substituted as a number, no need to wrap it in `<| ... |>`.
|
||||||
|
|
||||||
|
## Step 5 — conditional execution
|
||||||
|
|
||||||
|
Skip a step when a condition is false:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- py_func:
|
||||||
|
name: divide by zero only on linux
|
||||||
|
condition: <| "$(os)" == "Linux" |>
|
||||||
|
file: calc.py
|
||||||
|
func_name: divide
|
||||||
|
param: [1, 0]
|
||||||
|
```
|
||||||
|
|
||||||
|
Items skipped this way report `SKIP` and do not affect the overall
|
||||||
|
result.
|
||||||
|
|
||||||
|
## Step 6 — generate a report
|
||||||
|
|
||||||
|
Add a `report` block at the root of the file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
main:
|
||||||
|
name: calc.py campaign
|
||||||
|
steps:
|
||||||
|
# ... your steps here ...
|
||||||
|
|
||||||
|
report:
|
||||||
|
enabled: true
|
||||||
|
log_stored: true
|
||||||
|
export:
|
||||||
|
- junit:
|
||||||
|
path: ./reports
|
||||||
|
file_name: calc.xml
|
||||||
|
- html:
|
||||||
|
path: ./reports
|
||||||
|
file_name: calc.html
|
||||||
|
```
|
||||||
|
|
||||||
|
The `path` directory must exist before the test runs — testium does not
|
||||||
|
create it. Create it once:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p reports
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-run the test — `./reports/calc.xml` (CI-friendly) and
|
||||||
|
`./reports/calc.html` (human-friendly) are produced. Set
|
||||||
|
`log_stored: true` to include each item's captured stdout.
|
||||||
|
|
||||||
|
## Step 7 — share state between calls
|
||||||
|
|
||||||
|
By default each `py_func` runs in its own short-lived subprocess.
|
||||||
|
To keep state across calls, use `context_id`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- py_func:
|
||||||
|
name: open
|
||||||
|
file: calc.py
|
||||||
|
func_name: open_resource
|
||||||
|
context_id: my_ctx
|
||||||
|
- py_func:
|
||||||
|
name: use
|
||||||
|
file: calc.py
|
||||||
|
func_name: use_resource
|
||||||
|
context_id: my_ctx
|
||||||
|
```
|
||||||
|
|
||||||
|
Both steps share the same persistent Python interpreter, so `calc.py`
|
||||||
|
can store any object in module-level globals or in `tm.setgd()`.
|
||||||
|
|
||||||
|
To share data without `context_id`, write it to the testium global dict
|
||||||
|
via the JSON-RPC bridge:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import py_func.tm as tm
|
||||||
|
|
||||||
|
def producer():
|
||||||
|
tm.setgd("computed", 42)
|
||||||
|
|
||||||
|
def consumer():
|
||||||
|
return tm.gd("computed")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
* [`doc/examples/`](examples/) — one runnable `.tum` per feature
|
||||||
|
(cycles, dialogs, console, plots, parallel, run-of-tum, …).
|
||||||
|
* [`doc/manual/testium_manual.pdf`](manual/testium_manual.pdf) — full
|
||||||
|
reference manual covering every test item, every attribute and the
|
||||||
|
YAML syntax extensions.
|
||||||
141
package/deb/test_distro.sh
Executable file
141
package/deb/test_distro.sh
Executable 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 ==="
|
||||||
@@ -1,23 +1,50 @@
|
|||||||
# -*- 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'],
|
||||||
pathex=['../../src/testium',
|
pathex=['../../src/testium',
|
||||||
'../../src/testium/main_win/resources'],
|
'../../src/testium/main_win/resources'],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[ ('../../src/VERSION', '.'),
|
# py_func/ and runtime/ are bundled at the _MEIPASS root because the
|
||||||
('../../src/lua_func', 'lua_func'),
|
# py_func subprocess is launched with the *host* Python (not the
|
||||||
('../../src/py_func', 'py_func'),
|
# frozen interpreter): it needs the source files on disk to find them
|
||||||
('../../src/lib', 'lib')],
|
# 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', '.'),
|
||||||
|
('../../src/testium/lua_func', 'lua_func'),
|
||||||
|
('../../src/testium/py_func', 'py_func'),
|
||||||
|
('../../src/testium/runtime', 'runtime'),
|
||||||
|
(JUNIT_XML_DIR, 'junit_xml')],
|
||||||
hiddenimports=["git",
|
hiddenimports=["git",
|
||||||
"interpreter",
|
"interpreter",
|
||||||
"main_win",
|
"main_win",
|
||||||
"libs",
|
"runtime",
|
||||||
"libs.console",
|
"py_func",
|
||||||
"libs.termconsole",
|
"py_func.tm",
|
||||||
"libs.console_ssh",
|
"py_func.handle",
|
||||||
"libs.raw_tcp_console",
|
"py_func.func_call",
|
||||||
"libs.runtime_plot",
|
"api",
|
||||||
|
"api.console",
|
||||||
|
"api.termconsole",
|
||||||
|
"api.console_ssh",
|
||||||
|
"api.raw_tcp_console",
|
||||||
|
"api.runtime_plot",
|
||||||
|
"api.testium",
|
||||||
"matplotlib.backends.backend_pdf",
|
"matplotlib.backends.backend_pdf",
|
||||||
"telnetlib3",
|
"telnetlib3",
|
||||||
"serial",
|
"serial",
|
||||||
|
|||||||
@@ -27,4 +27,10 @@ if [ ! -d "$PY_VENV_DIR" ]; then
|
|||||||
python3 -m venv "$PY_VENV_DIR"
|
python3 -m venv "$PY_VENV_DIR"
|
||||||
source "$PY_VENV_DIR/bin/activate"
|
source "$PY_VENV_DIR/bin/activate"
|
||||||
pip install --extra-index-url https://pypi.python.org/pypi -r $REQ_PATH
|
pip install --extra-index-url https://pypi.python.org/pypi -r $REQ_PATH
|
||||||
|
# Validation suite plugin used to verify the report-exporter
|
||||||
|
# entry-points discovery end-to-end.
|
||||||
|
FAKE_EXPORTER_DIR="$(dirname "$REQ_PATH")/../test/validation/fake_exporter"
|
||||||
|
if [ -d "$FAKE_EXPORTER_DIR" ]; then
|
||||||
|
pip install -e "$FAKE_EXPORTER_DIR"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.1
|
0.2
|
||||||
@@ -13,7 +13,6 @@ license-files = ["../LICENSE"]
|
|||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)",
|
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"setuptools",
|
"setuptools",
|
||||||
@@ -36,7 +35,9 @@ testium = "testium:main"
|
|||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where=["."]
|
where=["."]
|
||||||
exclude=["lua_func", "py_func"]
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"testium.lua_func" = ["*.lua"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {file = ["VERSION"]}
|
version = {file = ["VERSION"]}
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ A {classname}.close() is missing somewhere in your code !'.format(classname=type
|
|||||||
if not sys.platform.startswith('win'):
|
if not sys.platform.startswith('win'):
|
||||||
# import SshConsole if pexpect is installed
|
# import SshConsole if pexpect is installed
|
||||||
try:
|
try:
|
||||||
from libs.console_ssh import SshConsole
|
from api.console_ssh import SshConsole
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
@@ -8,7 +8,7 @@ import os
|
|||||||
import pexpect
|
import pexpect
|
||||||
from pexpect import ExceptionPexpect, TIMEOUT, EOF, spawn
|
from pexpect import ExceptionPexpect, TIMEOUT, EOF, spawn
|
||||||
|
|
||||||
from libs.console import Console
|
from api.console import Console
|
||||||
|
|
||||||
# Exception classes used by this module.
|
# Exception classes used by this module.
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
import socket
|
import socket
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from libs.console import *
|
from api.console import *
|
||||||
|
|
||||||
class RawTCPConsole(Console):
|
class RawTCPConsole(Console):
|
||||||
TYPE = 'rawtcp'
|
TYPE = 'rawtcp'
|
||||||
@@ -16,9 +16,9 @@ import numpy as np
|
|||||||
import matplotlib.dates as mdates
|
import matplotlib.dates as mdates
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
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.eval import post_evaluate
|
from interpreter.utils.eval import post_evaluate
|
||||||
@@ -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(
|
||||||
@@ -10,7 +10,7 @@ import os
|
|||||||
|
|
||||||
ourPath = os.path.dirname(__file__)
|
ourPath = os.path.dirname(__file__)
|
||||||
sys.path.append(ourPath)
|
sys.path.append(ourPath)
|
||||||
from libs.console import (Console, BytesStore, TIMEOUT_NULL)
|
from api.console import (Console, BytesStore, TIMEOUT_NULL)
|
||||||
|
|
||||||
class TermConsole(Console):
|
class TermConsole(Console):
|
||||||
TYPE = 'term'
|
TYPE = 'term'
|
||||||
@@ -4,7 +4,7 @@ import sys
|
|||||||
import textwrap
|
import textwrap
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
import interpreter.utils.globdict as globdict
|
import interpreter.utils.globdict as globdict
|
||||||
from lib.tum_except import (ETUMSyntaxError)
|
from runtime.tum_except import (ETUMSyntaxError)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Console helper functions
|
# Console helper functions
|
||||||
@@ -14,7 +14,7 @@ def add_console(console):
|
|||||||
''' Function which adds a ``Console`` class instance to *testium*
|
''' Function which adds a ``Console`` class instance to *testium*
|
||||||
|
|
||||||
:param console: The ``Console`` instance.
|
:param console: The ``Console`` instance.
|
||||||
:type console: ``libs.console.Console`` or child class instance
|
:type console: ``api.console.Console`` or child class instance
|
||||||
:return: No returned value
|
:return: No returned value
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -48,7 +48,7 @@ def console(name):
|
|||||||
:param name: The name of the ``Console`` instance.
|
:param name: The name of the ``Console`` instance.
|
||||||
:type name: str
|
:type name: str
|
||||||
:return: The ``Console`` or child class object
|
:return: The ``Console`` or child class object
|
||||||
:rtype: ``libs.console.Console`` or child class instance
|
:rtype: ``api.console.Console`` or child class instance
|
||||||
"""
|
"""
|
||||||
cons = None
|
cons = None
|
||||||
for c in globdict.gd('console_instances', []):
|
for c in globdict.gd('console_instances', []):
|
||||||
@@ -65,7 +65,7 @@ def add_plot(plot: object) -> None:
|
|||||||
''' Function which adds a ``RuntimePlot`` class instance to *testium*
|
''' Function which adds a ``RuntimePlot`` class instance to *testium*
|
||||||
|
|
||||||
:param plot: The ``RuntimePlot`` instance.
|
:param plot: The ``RuntimePlot`` instance.
|
||||||
:type plot: ``libs.runtime_plot.RuntimePlot`` or child class instance
|
:type plot: ``api.runtime_plot.RuntimePlot`` or child class instance
|
||||||
:return: No returned value
|
:return: No returned value
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -99,7 +99,7 @@ def plot(name: str) -> object:
|
|||||||
:param name: The name of the ``RuntimePlot`` instance.
|
:param name: The name of the ``RuntimePlot`` instance.
|
||||||
:type name: str
|
:type name: str
|
||||||
:return: The ``RuntimePlot`` or child class object
|
:return: The ``RuntimePlot`` or child class object
|
||||||
:rtype: ``libs.runtime_plot.RuntimePlot`` or child class instance
|
:rtype: ``api.runtime_plot.RuntimePlot`` or child class instance
|
||||||
"""
|
"""
|
||||||
plot = None
|
plot = None
|
||||||
for g in globdict.gd('plot_instances', []):
|
for g in globdict.gd('plot_instances', []):
|
||||||
@@ -9,8 +9,8 @@ from multiprocessing import Queue
|
|||||||
|
|
||||||
from interpreter.process import TestProcess
|
from interpreter.process import TestProcess
|
||||||
from interpreter.utils.test_ctrl import TestSetController
|
from interpreter.utils.test_ctrl import TestSetController
|
||||||
from lib.tum_except import ETUMFileError, ETUMRuntimeError
|
from runtime.tum_except import ETUMFileError, ETUMRuntimeError
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
|
|
||||||
|
|
||||||
class Batch:
|
class Batch:
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from threading import Thread
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from lib.string_queue import StringQueue
|
from runtime.string_queue import StringQueue
|
||||||
from lib.tum_except import print_exception, ETUMRuntimeError, ETUMSyntaxError
|
from runtime.tum_except import print_exception, ETUMRuntimeError, ETUMSyntaxError
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
import interpreter.utils.globdict as globdict
|
import interpreter.utils.globdict as globdict
|
||||||
from interpreter.utils.params import expanse
|
from interpreter.utils.params import expanse
|
||||||
from interpreter.utils.test_ctrl import TestSetController
|
from interpreter.utils.test_ctrl import TestSetController
|
||||||
@@ -26,7 +26,7 @@ from interpreter.utils.test_init import (
|
|||||||
from interpreter.utils.constants import TestItemType as cst_type
|
from interpreter.utils.constants import TestItemType as cst_type
|
||||||
from interpreter.test_set import TestSet
|
from interpreter.test_set import TestSet
|
||||||
from interpreter.utils.include import TUMLoader, TUMLoaderNoIncludes, TUMLoaderRawIncludes
|
from interpreter.utils.include import TUMLoader, TUMLoaderNoIncludes, TUMLoaderRawIncludes
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
from interpreter.utils.template import template_to_test
|
from interpreter.utils.template import template_to_test
|
||||||
from interpreter.utils.yaml_load import yaml_load
|
from interpreter.utils.yaml_load import yaml_load
|
||||||
from interpreter.utils.py_eval import eval_process_init
|
from interpreter.utils.py_eval import eval_process_init
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
from interpreter.test_items.test_item import TestItem, test_run, test_data
|
from interpreter.test_items.test_item import TestItem, test_run, test_data
|
||||||
from interpreter.test_items.test_result import TestResult, TestValue
|
from interpreter.test_items.test_result import TestResult, TestValue
|
||||||
from interpreter.test_items.item_actions.action import TestItemAction
|
from interpreter.test_items.item_actions.action import TestItemAction
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ from time import sleep
|
|||||||
import yaml
|
import yaml
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from interpreter.test_items.test_result import TestResult, TestValue
|
from interpreter.test_items.test_result import TestResult, TestValue
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.params import TestItemParams
|
from interpreter.utils.params import TestItemParams
|
||||||
from interpreter.utils.constants import TestItemType as cst_type
|
from interpreter.utils.constants import TestItemType as cst_type
|
||||||
from interpreter.utils.eval import eval_to_boolean, evaluate, post_evaluate
|
from interpreter.utils.eval import eval_to_boolean, evaluate, post_evaluate
|
||||||
from lib.tum_except import ETUMSyntaxError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, item_load_context
|
||||||
|
|
||||||
LOG_TEST_STOP = '<----- step "{}" finished'
|
LOG_TEST_STOP = '<----- step "{}" finished'
|
||||||
LOG_TEST_START = '-----> step "{}" started'
|
LOG_TEST_START = '-----> step "{}" started'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from lib.tum_except import ETUMSyntaxError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, item_load_context
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from interpreter.utils.eval import evaluate
|
from interpreter.utils.eval import evaluate
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import item_load_context
|
from runtime.tum_except import item_load_context
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
|
|
||||||
class TestItemChoicesDialog(TestItemDialogBase):
|
class TestItemChoicesDialog(TestItemDialogBase):
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import os
|
|||||||
import importlib
|
import importlib
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
from interpreter.test_items.test_item import test_run
|
from interpreter.test_items.test_item import test_run
|
||||||
from interpreter.test_items.item_actions import TestItemActions
|
from interpreter.test_items.item_actions import TestItemActions
|
||||||
from interpreter.test_items.item_actions.action import TestItemAction
|
from interpreter.test_items.item_actions.action import TestItemAction
|
||||||
@@ -345,17 +345,17 @@ class TestItemConsole(TestItemActions):
|
|||||||
self.actions_token = {}
|
self.actions_token = {}
|
||||||
|
|
||||||
global console
|
global console
|
||||||
console = importlib.import_module("libs.console")
|
console = importlib.import_module("api.console")
|
||||||
|
|
||||||
if not sys.platform.startswith("win"):
|
if not sys.platform.startswith("win"):
|
||||||
global console_ssh
|
global console_ssh
|
||||||
console_ssh = importlib.import_module("libs.console_ssh")
|
console_ssh = importlib.import_module("api.console_ssh")
|
||||||
|
|
||||||
global termconsole
|
global termconsole
|
||||||
termconsole = importlib.import_module("libs.termconsole")
|
termconsole = importlib.import_module("api.termconsole")
|
||||||
|
|
||||||
global raw_tcp_console
|
global raw_tcp_console
|
||||||
raw_tcp_console = importlib.import_module("libs.raw_tcp_console")
|
raw_tcp_console = importlib.import_module("api.raw_tcp_console")
|
||||||
|
|
||||||
self.actions_token["console_name"] = self._prms.getParam(
|
self.actions_token["console_name"] = self._prms.getParam(
|
||||||
"console_name", required=True
|
"console_name", required=True
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from lib.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
||||||
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.test_items.test_item import TestItem, test_run
|
from interpreter.test_items.test_item import TestItem, test_run
|
||||||
from interpreter.test_items.test_result import TestResult, TestValue
|
from interpreter.test_items.test_result import TestResult, TestValue
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.params import TestItemParams
|
from interpreter.utils.params import TestItemParams
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.test_items.test_item import TestItem
|
from interpreter.test_items.test_item import TestItem
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||||
from interpreter.test_items.test_result import (TestValue)
|
from interpreter.test_items.test_result import (TestValue)
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import ETUMParamError, ETUMSyntaxError
|
from runtime.tum_except import ETUMParamError, ETUMSyntaxError
|
||||||
import interpreter.utils.version as git
|
import interpreter.utils.version as git
|
||||||
|
|
||||||
class TestItemGit(TestItem):
|
class TestItemGit(TestItem):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
class TestItemGroup(TestItem):
|
class TestItemGroup(TestItem):
|
||||||
def __init__(self, dict_cycle, parent = None, status_queue=None, filename=""):
|
def __init__(self, dict_cycle, parent = None, status_queue=None, filename=""):
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import item_load_context
|
from runtime.tum_except import item_load_context
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
|
|
||||||
class TestItemImageDialog(TestItemDialogBase):
|
class TestItemImageDialog(TestItemDialogBase):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
from interpreter.test_items.test_item import TestItem, test_run
|
from interpreter.test_items.test_item import TestItem, test_run
|
||||||
from interpreter.test_items.test_result import TestResult, TestValue
|
from interpreter.test_items.test_result import TestResult, TestValue
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import socket
|
|||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from libs.console import Console
|
from api.console import Console
|
||||||
|
|
||||||
|
|
||||||
def is_ip_address(address):
|
def is_ip_address(address):
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import time
|
|||||||
|
|
||||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||||
from lib.tum_except import ETUMSyntaxError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, item_load_context
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
|
|
||||||
class TestItemLet(TestItem):
|
class TestItemLet(TestItem):
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import traceback
|
|||||||
import pprint
|
import pprint
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from lib.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
||||||
from interpreter.test_items.test_item import TestItem, test_run
|
from interpreter.test_items.test_item import TestItem, test_run
|
||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.lua_func_exec import LuaFuncExecEngine
|
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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import item_load_context
|
from runtime.tum_except import item_load_context
|
||||||
|
|
||||||
|
|
||||||
class TestItemMsgDialog(TestItemDialogBase):
|
class TestItemMsgDialog(TestItemDialogBase):
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import item_load_context
|
from runtime.tum_except import item_load_context
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
|
|
||||||
class TestItemNoteDialog(TestItemDialogBase):
|
class TestItemNoteDialog(TestItemDialogBase):
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestResult, TestValue
|
from interpreter.test_items.test_result import TestResult, TestValue
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from interpreter.utils.eval import eval_to_boolean
|
from interpreter.utils.eval import eval_to_boolean
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
from lib.string_queue import StringQueue
|
from runtime.string_queue import StringQueue
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
|
|
||||||
|
|
||||||
class TestItemParallelBranch(TestItemContainer):
|
class TestItemParallelBranch(TestItemContainer):
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import time
|
|||||||
import pprint
|
import pprint
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from lib.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
||||||
from interpreter.test_items.test_item import TestItem, test_run
|
from interpreter.test_items.test_item import TestItem, test_run
|
||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
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.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import item_load_context
|
from runtime.tum_except import item_load_context
|
||||||
|
|
||||||
|
|
||||||
class TestItemQuestionDialog(TestItemDialogBase):
|
class TestItemQuestionDialog(TestItemDialogBase):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||||
from interpreter.test_items.test_result import (TestValue)
|
from interpreter.test_items.test_result import (TestValue)
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from interpreter.test_report.test_report import Export
|
from interpreter.test_report.test_report import Export
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import traceback
|
|||||||
|
|
||||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||||
from interpreter.test_items.test_result import (TestValue)
|
from interpreter.test_items.test_result import (TestValue)
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
||||||
|
|
||||||
|
|
||||||
def nowInBetween(start, end):
|
def nowInBetween(start, end):
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import importlib
|
|||||||
import traceback
|
import traceback
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from lib.tum_except import ETUMSyntaxError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, item_load_context
|
||||||
from interpreter.test_items.test_item import TestItem, test_run
|
from interpreter.test_items.test_item import TestItem, test_run
|
||||||
from interpreter.test_items.test_result import TestResult, TestValue
|
from interpreter.test_items.test_result import TestResult, TestValue
|
||||||
from interpreter.test_items.item_actions import TestItemActions
|
from interpreter.test_items.item_actions import TestItemActions
|
||||||
@@ -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("libs.runtime_plot")
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ from time import sleep
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from multiprocessing import Process, Pipe
|
from multiprocessing import Process, Pipe
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||||
from interpreter.test_items.test_result import (TestValue)
|
from interpreter.test_items.test_result import (TestValue)
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
||||||
|
|
||||||
class TestItemSleep(TestItem):
|
class TestItemSleep(TestItem):
|
||||||
"""sleep item usage.
|
"""sleep item usage.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import item_load_context
|
from runtime.tum_except import item_load_context
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
|
|
||||||
class TestItemTestedRefsDialog(TestItemDialogBase):
|
class TestItemTestedRefsDialog(TestItemDialogBase):
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ from unittest import (TestCase, TestSuite, TextTestRunner,
|
|||||||
TextTestResult)
|
TextTestResult)
|
||||||
from unittest.loader import defaultTestLoader
|
from unittest.loader import defaultTestLoader
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from lib.tum_except import (ETUMFileError)
|
from runtime.tum_except import (ETUMFileError)
|
||||||
from interpreter.utils.modules import load_source
|
from interpreter.utils.modules import load_source
|
||||||
from interpreter.test_items.test_item import (TestItem, test_run, LOG_TEST_STOP, LOG_TEST_START)
|
from interpreter.test_items.test_item import (TestItem, test_run, LOG_TEST_STOP, LOG_TEST_START)
|
||||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||||
from interpreter.test_items.test_item import test_data
|
from interpreter.test_items.test_item import test_data
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
|
|
||||||
class UnittestResult(TextTestResult):
|
class UnittestResult(TextTestResult):
|
||||||
"""Test result adapted for unittest test"""
|
"""Test result adapted for unittest test"""
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from interpreter.test_items.test_item import test_run
|
|||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
from interpreter.test_items.test_item_dialog_base import TestItemDialogBase, _is_text_mode, _is_interactive
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from lib.tum_except import item_load_context
|
from runtime.tum_except import item_load_context
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
|
|
||||||
class TestItemValueDialog(TestItemDialogBase):
|
class TestItemValueDialog(TestItemDialogBase):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from lib.tum_except import (ETUMRuntimeError)
|
from runtime.tum_except import (ETUMRuntimeError)
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import interpreter.test_report.test_report as tr
|
import interpreter.test_report.test_report as tr
|
||||||
from interpreter.utils.paths import prepare_file_to_save
|
from interpreter.utils.paths import prepare_file_to_save
|
||||||
import interpreter.utils.constants as cst
|
import interpreter.utils.constants as cst
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
|
|
||||||
class ReportExport:
|
class ReportExport:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from junit_xml import (TestSuite, TestCase)
|
from junit_xml import (TestSuite, TestCase)
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.test_items.test_result import (TestValue)
|
from interpreter.test_items.test_result import (TestValue)
|
||||||
import interpreter.test_report.report_export as rpe
|
import interpreter.test_report.report_export as rpe
|
||||||
import interpreter.test_report.test_report as tr
|
import interpreter.test_report.test_report as tr
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from functools import wraps
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from time import (time, sleep)
|
from time import (time, sleep)
|
||||||
import traceback
|
import traceback
|
||||||
from lib.tum_except import (ETUMRuntimeError, ETUMSyntaxError)
|
from runtime.tum_except import (ETUMRuntimeError, ETUMSyntaxError)
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
from interpreter.utils.params import (expanse)
|
from interpreter.utils.params import (expanse)
|
||||||
from interpreter.utils.paths import prepare_file_to_save
|
from interpreter.utils.paths import prepare_file_to_save
|
||||||
import interpreter.utils.constants as cst
|
import interpreter.utils.constants as cst
|
||||||
@@ -20,6 +20,52 @@ sqlite3.register_converter('JSON', convert_json)
|
|||||||
TEST_REPORT_FILE_REV = '0.1'
|
TEST_REPORT_FILE_REV = '0.1'
|
||||||
|
|
||||||
|
|
||||||
|
def _load_text():
|
||||||
|
from interpreter.test_report.report_export_txt import ReportExportTxt
|
||||||
|
return ReportExportTxt
|
||||||
|
|
||||||
|
def _load_json():
|
||||||
|
from interpreter.test_report.report_export_json import ReportExportJSON
|
||||||
|
return ReportExportJSON
|
||||||
|
|
||||||
|
def _load_junit():
|
||||||
|
try:
|
||||||
|
from interpreter.test_report.report_export_junit import ReportExportJUnit
|
||||||
|
return ReportExportJUnit
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
raise ETUMRuntimeError(
|
||||||
|
'Report format "junit" requires "junit_xml" — pip install junit-xml')
|
||||||
|
|
||||||
|
def _load_html():
|
||||||
|
try:
|
||||||
|
from interpreter.test_report.report_export_html import ReportExportHTML
|
||||||
|
return ReportExportHTML
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
raise ETUMRuntimeError(
|
||||||
|
'Report format "html" requires "lxml" — pip install lxml')
|
||||||
|
|
||||||
|
_EXPORTER_REGISTRY: dict = {
|
||||||
|
cst.REP_TYPE_TEXT: _load_text,
|
||||||
|
cst.REP_TYPE_JSON: _load_json,
|
||||||
|
cst.REP_TYPE_JUNIT: _load_junit,
|
||||||
|
cst.REP_TYPE_HTML: _load_html,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _discover_plugins():
|
||||||
|
try:
|
||||||
|
from importlib.metadata import entry_points
|
||||||
|
for ep in entry_points(group='testium.exporters'):
|
||||||
|
try:
|
||||||
|
cls = ep.load()
|
||||||
|
_EXPORTER_REGISTRY[ep.name] = lambda c=cls: c
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[testium] Failed to load report exporter plugin "{ep.name}": {e}')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_discover_plugins()
|
||||||
|
|
||||||
|
|
||||||
def tr_procedure(f):
|
def tr_procedure(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(self, *args, **kwds):
|
def wrapper(self, *args, **kwds):
|
||||||
@@ -82,28 +128,19 @@ class Export:
|
|||||||
else:
|
else:
|
||||||
path = os.path.join(path, fname)
|
path = os.path.join(path, fname)
|
||||||
|
|
||||||
if et == cst.REP_TYPE_TEXT:
|
if et == cst.REP_TYPE_SQLITE:
|
||||||
from interpreter.test_report.report_export_txt import ReportExportTxt
|
|
||||||
ReportExportTxt(name, con, path, pats, keys, no_header)
|
|
||||||
elif et == cst.REP_TYPE_JSON:
|
|
||||||
from interpreter.test_report.report_export_json import ReportExportJSON
|
|
||||||
ReportExportJSON(name, con, path, pats, keys, no_header)
|
|
||||||
elif et == cst.REP_TYPE_JUNIT:
|
|
||||||
try:
|
|
||||||
from interpreter.test_report.report_export_junit import ReportExportJUnit
|
|
||||||
ReportExportJUnit(name, con, path, pats, keys, no_header)
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise ETUMRuntimeError('"junit_xml" module not available')
|
|
||||||
elif et == cst.REP_TYPE_HTML:
|
|
||||||
try:
|
|
||||||
from interpreter.test_report.report_export_html import ReportExportHTML
|
|
||||||
ReportExportHTML(name, con, path, pats, keys, no_header)
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise ETUMRuntimeError('"lxml" module not available')
|
|
||||||
elif et == cst.REP_TYPE_SQLITE:
|
|
||||||
pass
|
pass
|
||||||
|
elif et in _EXPORTER_REGISTRY:
|
||||||
|
try:
|
||||||
|
cls = _EXPORTER_REGISTRY[et]()
|
||||||
|
cls(name, con, path, pats, keys, no_header)
|
||||||
|
except ETUMRuntimeError as e:
|
||||||
|
print(f'[report] Export skipped: {e}')
|
||||||
else:
|
else:
|
||||||
raise ETUMSyntaxError('Report export not recognized')
|
available = ', '.join(
|
||||||
|
sorted(_EXPORTER_REGISTRY.keys()) + [cst.REP_TYPE_SQLITE])
|
||||||
|
print(f'[report] Export skipped: format "{et}" not found. '
|
||||||
|
f'Available: {available}')
|
||||||
|
|
||||||
class TestReport:
|
class TestReport:
|
||||||
TEST_COLS = [[cst.DB_TEST_TIMESTAMP_START, 'INT'],
|
TEST_COLS = [[cst.DB_TEST_TIMESTAMP_START, 'INT'],
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import os
|
|||||||
import datetime
|
import datetime
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from interpreter.utils.params import expanse
|
from interpreter.utils.params import expanse
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
import interpreter.utils.settings as prefs
|
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 lib.tum_except import ETUMRuntimeError
|
from interpreter.utils import bins
|
||||||
|
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
|
||||||
from interpreter.utils.constants import TEST_TYPE_LIST
|
from interpreter.utils.constants import TEST_TYPE_LIST
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from lib.api import SUPPORTED_API
|
from runtime.api import SUPPORTED_API
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
# Fill the api_dict with the function of tm
|
# Fill the api_dict with the function of tm
|
||||||
api_dict = {k: getattr(tm, k) for k in SUPPORTED_API if hasattr(tm, k)}
|
api_dict = {k: getattr(tm, k) for k in SUPPORTED_API if hasattr(tm, k)}
|
||||||
|
|||||||
151
src/testium/interpreter/utils/bins.py
Normal file
151
src/testium/interpreter/utils/bins.py
Normal 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()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.py_eval import eval_exec
|
from interpreter.utils.py_eval import eval_exec
|
||||||
from lib.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
||||||
|
|
||||||
|
|
||||||
def evaluate(val, **replacement_dict):
|
def evaluate(val, **replacement_dict):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import yaml
|
import yaml
|
||||||
import os.path
|
import os.path
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.params import expanse
|
from interpreter.utils.params import expanse
|
||||||
from lib.tum_except import ETUMFileError
|
from runtime.tum_except import ETUMFileError
|
||||||
from interpreter.utils.template import template_to_test
|
from interpreter.utils.template import template_to_test
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from interpreter.utils.globdict import global_dict
|
from interpreter.utils.globdict import global_dict
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
from interpreter.utils.lua_process import LuaProcessBase
|
from interpreter.utils.lua_process import LuaProcessBase
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
|
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,92 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from lib.jrpc import JsonRpcClient
|
from runtime.jrpc import JsonRpcClient
|
||||||
from interpreter.utils.paths import subproc_path
|
from interpreter.utils.paths import subproc_path
|
||||||
from lib.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
|
||||||
|
from interpreter.utils.proc_drain import drain_to_log
|
||||||
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 +18,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
|
||||||
@@ -192,10 +94,14 @@ class LuaProcessBase:
|
|||||||
self._process = subprocess.Popen(
|
self._process = subprocess.Popen(
|
||||||
params, env=env, cwd=func_proc_path,
|
params, env=env, cwd=func_proc_path,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.PIPE,
|
||||||
restore_signals=False,
|
restore_signals=False,
|
||||||
)
|
)
|
||||||
|
# Route subprocess stdout/stderr (lua require failures, syntax
|
||||||
|
# errors, anything written to fd 1/2 before the in-script
|
||||||
|
# remote_print is set up) into the parent's log.
|
||||||
|
drain_to_log(self._process, prefix="[lua_func] ")
|
||||||
|
|
||||||
self._rpc = JsonRpcClient(
|
self._rpc = JsonRpcClient(
|
||||||
"localhost", self._port, req_handler=self._req_handler
|
"localhost", self._port, req_handler=self._req_handler
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import interpreter.utils.globdict as globdict
|
import interpreter.utils.globdict as globdict
|
||||||
from lib.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
||||||
|
|
||||||
glob_eval_func = None
|
glob_eval_func = None
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import testium
|
import testium
|
||||||
from interpreter.utils.params import expanse
|
from interpreter.utils.params import expanse
|
||||||
import subprocess
|
import subprocess
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
|
|
||||||
def testium_path():
|
def testium_path():
|
||||||
@@ -18,12 +18,9 @@ def testium_path():
|
|||||||
return str(Path(tp).parent.resolve())
|
return str(Path(tp).parent.resolve())
|
||||||
|
|
||||||
def subproc_path():
|
def subproc_path():
|
||||||
if getattr(sys, 'frozen', False):
|
# py_func and lua_func now live inside the testium package; their cwd
|
||||||
# Exécuté depuis le .exe
|
# is the testium package root, same as testium_path().
|
||||||
return sys._MEIPASS
|
return testium_path()
|
||||||
|
|
||||||
tp = inspect.getfile(inspect.getmodule(testium))
|
|
||||||
return str(Path(tp).parent.parent.resolve())
|
|
||||||
|
|
||||||
def prepare_file_to_save(file_name, file_ext=""):
|
def prepare_file_to_save(file_name, file_ext=""):
|
||||||
iname = file_name
|
iname = file_name
|
||||||
|
|||||||
48
src/testium/interpreter/utils/proc_drain.py
Normal file
48
src/testium/interpreter/utils/proc_drain.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Drain a subprocess stdout/stderr into testium's print pipeline.
|
||||||
|
|
||||||
|
Captured lines go through the parent's stdio_redir, so they reach the
|
||||||
|
test log AND the live output (terminal in batch mode, GUI text panel
|
||||||
|
in -r mode). This is essential for diagnosing early-startup errors
|
||||||
|
of py_func / lua_func subprocesses (missing modules, unhandled
|
||||||
|
exceptions before the in-process redirection kicks in, lua
|
||||||
|
``require`` failures, anything written to fd 1/2 directly).
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
def _drain_pipe(pipe, prefix):
|
||||||
|
try:
|
||||||
|
for raw in iter(pipe.readline, b""):
|
||||||
|
line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if prefix:
|
||||||
|
print(f"{prefix}{line}")
|
||||||
|
else:
|
||||||
|
print(line)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
pipe.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def drain_to_log(process, prefix=""):
|
||||||
|
"""Spawn daemon threads that read ``process.stdout`` and
|
||||||
|
``process.stderr`` line by line and print each line through the
|
||||||
|
parent's stdout (so it reaches the log + live output).
|
||||||
|
|
||||||
|
Each thread exits cleanly when the subprocess closes the
|
||||||
|
corresponding pipe (i.e. when it exits). Daemon flag ensures they
|
||||||
|
do not block testium exit.
|
||||||
|
"""
|
||||||
|
threads = []
|
||||||
|
for pipe in (process.stdout, process.stderr):
|
||||||
|
if pipe is None:
|
||||||
|
continue
|
||||||
|
t = threading.Thread(
|
||||||
|
target=_drain_pipe, args=(pipe, prefix), daemon=True,
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
return threads
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
from interpreter.utils.py_process import PyProcessBase
|
from interpreter.utils.py_process import PyProcessBase
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
import libs.testium as tm
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
from interpreter.utils.py_process import PyProcessBase
|
from interpreter.utils.py_process import PyProcessBase
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
from interpreter.test_items.test_result import TestValue
|
from interpreter.test_items.test_result import TestValue
|
||||||
|
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,77 +1,13 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import socket
|
import socket
|
||||||
from lib.jrpc import JsonRpcClient
|
from runtime.jrpc import JsonRpcClient
|
||||||
import libs.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 lib.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
|
||||||
|
from interpreter.utils.proc_drain import drain_to_log
|
||||||
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 +16,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
|
||||||
@@ -161,10 +78,15 @@ class PyProcessBase:
|
|||||||
self._process = subprocess.Popen(
|
self._process = subprocess.Popen(
|
||||||
params, env=env, cwd=func_proc_path,
|
params, env=env, cwd=func_proc_path,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.PIPE,
|
||||||
restore_signals=False,
|
restore_signals=False,
|
||||||
)
|
)
|
||||||
|
# Route subprocess stdout/stderr (early-startup errors,
|
||||||
|
# unhandled exceptions, anything written to fd 1/2 before the
|
||||||
|
# in-process JSON-RPC stdio_redir kicks in) into the parent's
|
||||||
|
# log.
|
||||||
|
drain_to_log(self._process, prefix="[py_func] ")
|
||||||
|
|
||||||
self._rpc = JsonRpcClient(
|
self._rpc = JsonRpcClient(
|
||||||
"localhost", self._port, req_handler=self._req_handler
|
"localhost", self._port, req_handler=self._req_handler
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import configparser
|
import configparser
|
||||||
import json
|
import json
|
||||||
import platform
|
import platform
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
|
|
||||||
SettingsCompany = 'Testium'
|
SettingsCompany = 'Testium'
|
||||||
SettingsApplication = 'testium'
|
SettingsApplication = 'testium'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from jinja2 import Template
|
|||||||
from jinja2.exceptions import TemplateSyntaxError, TemplateError, UndefinedError
|
from jinja2.exceptions import TemplateSyntaxError, TemplateError, UndefinedError
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
from interpreter.utils.yaml_load import print_yaml
|
from interpreter.utils.yaml_load import print_yaml
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
|
|
||||||
|
|
||||||
def template_to_test(filename: str, params: list):
|
def template_to_test(filename: str, params: list):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from multiprocessing import Queue
|
from multiprocessing import Queue
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
|
|
||||||
|
|
||||||
class TestSetController:
|
class TestSetController:
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import yaml
|
|||||||
import copy
|
import copy
|
||||||
|
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
import interpreter.utils.globdict as globdict
|
import interpreter.utils.globdict as globdict
|
||||||
import interpreter.utils.settings as prefs
|
import interpreter.utils.settings as prefs
|
||||||
from interpreter.utils.paths import testium_path
|
from interpreter.utils.paths import testium_path
|
||||||
from interpreter.utils.yaml_load import yaml_load
|
from interpreter.utils.yaml_load import yaml_load
|
||||||
from interpreter.utils import clear_recursively
|
from interpreter.utils import clear_recursively
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
from interpreter.utils.params import expanse, eval_func_init
|
from interpreter.utils.params import expanse, eval_func_init
|
||||||
from interpreter.utils.eval import evaluate
|
from interpreter.utils.eval import evaluate
|
||||||
from interpreter.utils.version import (
|
from interpreter.utils.version import (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
import interpreter.utils.settings as prefs
|
import interpreter.utils.settings as prefs
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
_cached_versions = {}
|
_cached_versions = {}
|
||||||
|
|
||||||
@@ -31,39 +31,42 @@ def get_version(path :str)-> str:
|
|||||||
return "Warning git not supported in your settings, version of {} unknown".format(path)
|
return "Warning git not supported in your settings, version of {} unknown".format(path)
|
||||||
|
|
||||||
def get_testium_version():
|
def get_testium_version():
|
||||||
# case where we're executing from an Appimage
|
# AppImage
|
||||||
if 'APPIMAGE' in os.environ:
|
if 'APPIMAGE' in os.environ:
|
||||||
ver = 'unknown'
|
ver = os.getenv('SEQUENCER_REV', 'unknown')
|
||||||
if 'SEQUENCER_REV' in os.environ:
|
return ver + " (binary release)"
|
||||||
ver = os.getenv('SEQUENCER_REV')
|
|
||||||
return (ver + " (binary release)")
|
|
||||||
|
|
||||||
# case where we're executing from pyinstaller exe
|
# PyInstaller frozen exe
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
file_path = os.path.join(sys._MEIPASS, "VERSION")
|
file_path = os.path.join(sys._MEIPASS, "VERSION")
|
||||||
with open(file_path, 'r') as file:
|
try:
|
||||||
ver = file.read()
|
with open(file_path, 'r') as f:
|
||||||
return (ver + " (binary release)")
|
ver = f.read().strip()
|
||||||
|
return ver + " (binary release)"
|
||||||
|
except OSError:
|
||||||
|
return "unknown (binary release)"
|
||||||
|
|
||||||
# Executed from sources
|
# Source checkout: prefer git revision when available
|
||||||
try:
|
if prefs.settings.git_supported:
|
||||||
if prefs.settings.git_supported:
|
try:
|
||||||
git = import_module("git")
|
git = import_module("git")
|
||||||
path = tm.get_main_dir()
|
return repo_rev(tm.get_main_dir())
|
||||||
try:
|
except Exception:
|
||||||
return repo_rev(path)
|
# Not a git repo (typical pip install): fall through.
|
||||||
except git.InvalidGitRepositoryError:
|
pass
|
||||||
pkg_rec = import_module("pkg_resources")
|
|
||||||
try:
|
# Pip-installed wheel: use the package metadata baked from VERSION
|
||||||
ret = pkg_rec.get_distribution("testium").version
|
try:
|
||||||
_cached_versions.update({path: ret})
|
from importlib.metadata import version as _pkg_version
|
||||||
return str(ret) + " (wheel release)"
|
from importlib.metadata import PackageNotFoundError
|
||||||
except:
|
try:
|
||||||
return "Warning : testium not versioned"
|
return _pkg_version("testium") + " (wheel release)"
|
||||||
else:
|
except PackageNotFoundError:
|
||||||
return "Warning git not supported in your settings, version of testium is unknown."
|
pass
|
||||||
except:
|
except ImportError:
|
||||||
return ("Unknown")
|
pass
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
def get_modifications(path : str)-> str:
|
def get_modifications(path : str)-> str:
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from yaml.parser import ParserError
|
from yaml.parser import ParserError
|
||||||
from yaml import load, Loader
|
from yaml import load, Loader
|
||||||
from yaml.scanner import ScannerError
|
from yaml.scanner import ScannerError
|
||||||
from libs.testium import print_debug
|
from api.testium import print_debug
|
||||||
from lib.tum_except import ETUMSyntaxError
|
from runtime.tum_except import ETUMSyntaxError
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'about_win.ui'
|
## Form generated from reading UI file 'about_win.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.11.0
|
## Created by: Qt User Interface Compiler version 6.10.2
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -16,39 +16,50 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||||
QFrame, QLabel, QPlainTextEdit, QSizePolicy,
|
QLabel, QSizePolicy, QVBoxLayout, QWidget)
|
||||||
QWidget)
|
|
||||||
import about_win_rc
|
import about_win_rc
|
||||||
|
|
||||||
class Ui_About(object):
|
class Ui_About(object):
|
||||||
def setupUi(self, About):
|
def setupUi(self, About):
|
||||||
if not About.objectName():
|
if not About.objectName():
|
||||||
About.setObjectName(u"About")
|
About.setObjectName(u"About")
|
||||||
About.resize(400, 247)
|
About.resize(500, 220)
|
||||||
self.buttonBox = QDialogButtonBox(About)
|
self.verticalLayout = QVBoxLayout(About)
|
||||||
self.buttonBox.setObjectName(u"buttonBox")
|
self.verticalLayout.setSpacing(6)
|
||||||
self.buttonBox.setGeometry(QRect(30, 200, 341, 32))
|
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||||
self.buttonBox.setOrientation(Qt.Horizontal)
|
self.verticalLayout.setContentsMargins(20, 16, 20, 16)
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Ok)
|
|
||||||
self.label = QLabel(About)
|
self.label = QLabel(About)
|
||||||
self.label.setObjectName(u"label")
|
self.label.setObjectName(u"label")
|
||||||
self.label.setGeometry(QRect(30, 20, 341, 31))
|
|
||||||
font = QFont()
|
font = QFont()
|
||||||
font.setPointSize(14)
|
font.setPointSize(14)
|
||||||
self.label.setFont(font)
|
self.label.setFont(font)
|
||||||
self.label.setWordWrap(True)
|
|
||||||
|
self.verticalLayout.addWidget(self.label)
|
||||||
|
|
||||||
self.labelVersion = QLabel(About)
|
self.labelVersion = QLabel(About)
|
||||||
self.labelVersion.setObjectName(u"labelVersion")
|
self.labelVersion.setObjectName(u"labelVersion")
|
||||||
self.labelVersion.setGeometry(QRect(30, 60, 341, 16))
|
self.labelVersion.setWordWrap(True)
|
||||||
self.plainTextEdit = QPlainTextEdit(About)
|
|
||||||
self.plainTextEdit.setObjectName(u"plainTextEdit")
|
self.verticalLayout.addWidget(self.labelVersion)
|
||||||
self.plainTextEdit.setGeometry(QRect(30, 100, 341, 91))
|
|
||||||
self.plainTextEdit.setFrameShape(QFrame.NoFrame)
|
self.labelCopyright = QLabel(About)
|
||||||
self.plainTextEdit.setFrameShadow(QFrame.Sunken)
|
self.labelCopyright.setObjectName(u"labelCopyright")
|
||||||
self.plainTextEdit.setReadOnly(True)
|
|
||||||
self.labelCesUnitVersion = QLabel(About)
|
self.verticalLayout.addWidget(self.labelCopyright)
|
||||||
self.labelCesUnitVersion.setObjectName(u"labelCesUnitVersion")
|
|
||||||
self.labelCesUnitVersion.setGeometry(QRect(30, 70, 341, 16))
|
self.labelLicence = QLabel(About)
|
||||||
|
self.labelLicence.setObjectName(u"labelLicence")
|
||||||
|
self.labelLicence.setOpenExternalLinks(True)
|
||||||
|
|
||||||
|
self.verticalLayout.addWidget(self.labelLicence)
|
||||||
|
|
||||||
|
self.buttonBox = QDialogButtonBox(About)
|
||||||
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
|
self.buttonBox.setOrientation(Qt.Horizontal)
|
||||||
|
self.buttonBox.setStandardButtons(QDialogButtonBox.Ok)
|
||||||
|
|
||||||
|
self.verticalLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
|
||||||
self.retranslateUi(About)
|
self.retranslateUi(About)
|
||||||
self.buttonBox.accepted.connect(About.accept)
|
self.buttonBox.accepted.connect(About.accept)
|
||||||
@@ -57,10 +68,10 @@ class Ui_About(object):
|
|||||||
# setupUi
|
# setupUi
|
||||||
|
|
||||||
def retranslateUi(self, About):
|
def retranslateUi(self, About):
|
||||||
About.setWindowTitle(QCoreApplication.translate("About", u"A propos", None))
|
About.setWindowTitle(QCoreApplication.translate("About", u"\u00c0 propos", None))
|
||||||
self.label.setText(QCoreApplication.translate("About", u"Testium", None))
|
self.label.setText(QCoreApplication.translate("About", u"Testium", None))
|
||||||
self.labelVersion.setText(QCoreApplication.translate("About", u"Version", None))
|
self.labelVersion.setText("")
|
||||||
self.plainTextEdit.setPlainText(QCoreApplication.translate("About", u"This gui was developed with the help of Qt by Fran\u00e7ois Dausseur.", None))
|
self.labelCopyright.setText(QCoreApplication.translate("About", u"\u00a9 2025-2026 Fran\u00e7ois Dausseur", None))
|
||||||
self.labelCesUnitVersion.setText(QCoreApplication.translate("About", u"Version", None))
|
self.labelLicence.setText(QCoreApplication.translate("About", u"Licensed under <a href=\"https://eupl.eu/1.2/en/\">EUPL-1.2</a>", None))
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
@@ -6,98 +6,79 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>400</width>
|
<width>500</width>
|
||||||
<height>247</height>
|
<height>220</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>A propos</string>
|
<string>À propos</string>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<property name="geometry">
|
<property name="leftMargin">
|
||||||
<rect>
|
<number>20</number>
|
||||||
<x>30</x>
|
|
||||||
<y>200</y>
|
|
||||||
<width>341</width>
|
|
||||||
<height>32</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
</property>
|
||||||
<property name="orientation">
|
<property name="topMargin">
|
||||||
<enum>Qt::Horizontal</enum>
|
<number>16</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="standardButtons">
|
<property name="rightMargin">
|
||||||
<set>QDialogButtonBox::Ok</set>
|
<number>20</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
<property name="bottomMargin">
|
||||||
<widget class="QLabel" name="label">
|
<number>16</number>
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>30</x>
|
|
||||||
<y>20</y>
|
|
||||||
<width>341</width>
|
|
||||||
<height>31</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
</property>
|
||||||
<property name="font">
|
<property name="spacing">
|
||||||
<font>
|
<number>6</number>
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<item>
|
||||||
<string>Testium</string>
|
<widget class="QLabel" name="label">
|
||||||
</property>
|
<property name="font">
|
||||||
<property name="wordWrap">
|
<font>
|
||||||
<bool>true</bool>
|
<pointsize>14</pointsize>
|
||||||
</property>
|
</font>
|
||||||
</widget>
|
</property>
|
||||||
<widget class="QLabel" name="labelVersion">
|
<property name="text">
|
||||||
<property name="geometry">
|
<string>Testium</string>
|
||||||
<rect>
|
</property>
|
||||||
<x>30</x>
|
</widget>
|
||||||
<y>60</y>
|
</item>
|
||||||
<width>341</width>
|
<item>
|
||||||
<height>16</height>
|
<widget class="QLabel" name="labelVersion">
|
||||||
</rect>
|
<property name="text">
|
||||||
</property>
|
<string/>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>Version</string>
|
<property name="wordWrap">
|
||||||
</property>
|
<bool>true</bool>
|
||||||
</widget>
|
</property>
|
||||||
<widget class="QPlainTextEdit" name="plainTextEdit">
|
</widget>
|
||||||
<property name="geometry">
|
</item>
|
||||||
<rect>
|
<item>
|
||||||
<x>30</x>
|
<widget class="QLabel" name="labelCopyright">
|
||||||
<y>100</y>
|
<property name="text">
|
||||||
<width>341</width>
|
<string>© 2025-2026 François Dausseur</string>
|
||||||
<height>91</height>
|
</property>
|
||||||
</rect>
|
</widget>
|
||||||
</property>
|
</item>
|
||||||
<property name="frameShape">
|
<item>
|
||||||
<enum>QFrame::NoFrame</enum>
|
<widget class="QLabel" name="labelLicence">
|
||||||
</property>
|
<property name="text">
|
||||||
<property name="frameShadow">
|
<string>Licensed under <a href="https://eupl.eu/1.2/en/">EUPL-1.2</a></string>
|
||||||
<enum>QFrame::Sunken</enum>
|
</property>
|
||||||
</property>
|
<property name="openExternalLinks">
|
||||||
<property name="readOnly">
|
<bool>true</bool>
|
||||||
<bool>true</bool>
|
</property>
|
||||||
</property>
|
</widget>
|
||||||
<property name="plainText">
|
</item>
|
||||||
<string>This gui was developed with the help of Qt by François Dausseur.</string>
|
<item>
|
||||||
</property>
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
</widget>
|
<property name="orientation">
|
||||||
<widget class="QLabel" name="labelCesUnitVersion">
|
<enum>Qt::Horizontal</enum>
|
||||||
<property name="geometry">
|
</property>
|
||||||
<rect>
|
<property name="standardButtons">
|
||||||
<x>30</x>
|
<set>QDialogButtonBox::Ok</set>
|
||||||
<y>70</y>
|
</property>
|
||||||
<width>341</width>
|
</widget>
|
||||||
<height>16</height>
|
</item>
|
||||||
</rect>
|
</layout>
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Version</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../resources/about_win.qrc"/>
|
<include location="../resources/about_win.qrc"/>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from interpreter.process import TestProcess
|
|||||||
from interpreter.utils.test_ctrl import TestSetController
|
from interpreter.utils.test_ctrl import TestSetController
|
||||||
from main_win.test_controller_service import TestControllerService
|
from main_win.test_controller_service import TestControllerService
|
||||||
import interpreter.utils.settings as prefs
|
import interpreter.utils.settings as prefs
|
||||||
from lib.tum_except import ETUMFileError, ETUMRuntimeError
|
from runtime.tum_except import ETUMFileError, ETUMRuntimeError
|
||||||
|
|
||||||
|
|
||||||
class TestFileManager:
|
class TestFileManager:
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ from PySide6.QtGui import (QFont, QFontInfo)
|
|||||||
from time import (time)
|
from time import (time)
|
||||||
|
|
||||||
from main_win.test_tree_items.common import (TEST_COLS, TEST_COLS_WITH_TIME)
|
from main_win.test_tree_items.common import (TEST_COLS, TEST_COLS_WITH_TIME)
|
||||||
from lib.tum_except import (ETUMFileError, ETUMSyntaxError)
|
from runtime.tum_except import (ETUMFileError, ETUMSyntaxError)
|
||||||
from main_win.test_controller_service import TestControllerService
|
from main_win.test_controller_service import TestControllerService
|
||||||
from main_win.test_tree_items.test_tree_item import make_tree_item
|
from main_win.test_tree_items.test_tree_item import make_tree_item
|
||||||
|
|
||||||
from interpreter.test_items.test_result import (TestValue)
|
from interpreter.test_items.test_result import (TestValue)
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
import interpreter.utils.settings as prefs
|
import interpreter.utils.settings as prefs
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from interpreter.utils.icons import icon_prefix
|
from interpreter.utils.icons import icon_prefix
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from PySide6.QtGui import (QIcon, QPixmap, QBrush, QColor)
|
|||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (QTreeWidgetItem)
|
from PySide6.QtWidgets import (QTreeWidgetItem)
|
||||||
from interpreter.utils.icons import icon_prefix
|
from interpreter.utils.icons import icon_prefix
|
||||||
from libs.testium import print_warn
|
from api.testium import print_warn
|
||||||
|
|
||||||
# Maps item_name (from TestItemType.item_name) to visual config.
|
# Maps item_name (from TestItemType.item_name) to visual config.
|
||||||
# Keys: icon (required), icon_on (optional 2nd state), expanded, unfoldable, no_breakpoint
|
# Keys: icon (required), icon_on (optional 2nd state), expanded, unfoldable, no_breakpoint
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from main_win.f1_win.d_f1_win import DialogF1
|
|||||||
from main_win.test_tree import QTestTree
|
from main_win.test_tree import QTestTree
|
||||||
|
|
||||||
from main_win.test_run.thread_output import ThreadTestOutput
|
from main_win.test_run.thread_output import ThreadTestOutput
|
||||||
from lib.string_queue import StringQueue
|
from runtime.string_queue import StringQueue
|
||||||
from interpreter.process import TestProcess
|
from interpreter.process import TestProcess
|
||||||
from interpreter.utils.test_ctrl import TestSetController
|
from interpreter.utils.test_ctrl import TestSetController
|
||||||
from interpreter.utils.icons import icon_prefix
|
from interpreter.utils.icons import icon_prefix
|
||||||
@@ -38,14 +38,14 @@ from interpreter.utils.icons import icon_prefix
|
|||||||
from main_win.test_run.outlog import OutLog
|
from main_win.test_run.outlog import OutLog
|
||||||
from main_win.test_run.test_run import ThreadTestStatus
|
from main_win.test_run.test_run import ThreadTestStatus
|
||||||
import interpreter.utils.settings as prefs
|
import interpreter.utils.settings as prefs
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.version import get_testium_version
|
|
||||||
from interpreter.utils.test_init import (
|
from interpreter.utils.test_init import (
|
||||||
env_init,
|
env_init,
|
||||||
locate_report_file,
|
locate_report_file,
|
||||||
)
|
)
|
||||||
from lib.tum_except import ETUMFileError, ETUMRuntimeError
|
from interpreter.utils.version import get_testium_version
|
||||||
|
from runtime.tum_except import ETUMFileError, ETUMRuntimeError
|
||||||
from main_win.test_controller_service import TestControllerService
|
from main_win.test_controller_service import TestControllerService
|
||||||
from main_win.test_runner import TestRunner, TestState
|
from main_win.test_runner import TestRunner, TestState
|
||||||
from main_win.test_file_manager import TestFileManager
|
from main_win.test_file_manager import TestFileManager
|
||||||
@@ -206,8 +206,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.d_about_win = QDialog()
|
self.d_about_win = QDialog()
|
||||||
self.about_win = Ui_About()
|
self.about_win = Ui_About()
|
||||||
self.about_win.setupUi(self.d_about_win)
|
self.about_win.setupUi(self.d_about_win)
|
||||||
self.about_win.labelVersion.setText("testium - " + get_testium_version())
|
self.about_win.labelVersion.setText(get_testium_version())
|
||||||
self.about_win.labelCesUnitVersion.setText("")
|
|
||||||
self.d_about_win.setModal(True)
|
self.d_about_win.setModal(True)
|
||||||
|
|
||||||
self.d_f1_win = DialogF1(self)
|
self.d_f1_win = DialogF1(self)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from PySide6.QtGui import QCursor, QDesktopServices, QFont
|
|||||||
|
|
||||||
from main_win.text_log_highlighter import TextLogHighlighter
|
from main_win.text_log_highlighter import TextLogHighlighter
|
||||||
|
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
class QTextLog(QPlainTextEdit):
|
class QTextLog(QPlainTextEdit):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
from py_func.tm import _init_api, _remote_print
|
from py_func.tm import _init_api, _remote_print
|
||||||
from lib.stdout_redirect import stdio_redir
|
from runtime.stdout_redirect import stdio_redir
|
||||||
|
|
||||||
|
|
||||||
class TcpStdOut:
|
class TcpStdOut:
|
||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import importlib
|
import importlib
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from lib.tum_except import ETUMRuntimeError, ETUMSyntaxError
|
from runtime.tum_except import ETUMRuntimeError, ETUMSyntaxError
|
||||||
from py_func import tm
|
from py_func import tm
|
||||||
|
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ import math
|
|||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from lib.jrpc import JsonRpcSrv
|
from runtime.jrpc import JsonRpcSrv
|
||||||
from lib.tum_except import ETUMRuntimeError, print_exception
|
from runtime.tum_except import ETUMRuntimeError, print_exception
|
||||||
import py_func.tm as tm
|
import py_func.tm as tm
|
||||||
from py_func.func_call import func_exec
|
from py_func.func_call import func_exec
|
||||||
|
|
||||||
@@ -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 {
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
import json
|
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 runtime.tum_except import ETUMRuntimeError
|
||||||
from lib.api import SUPPORTED_API
|
from runtime.api import SUPPORTED_API
|
||||||
|
|
||||||
thismodule = sys.modules[__name__]
|
thismodule = sys.modules[__name__]
|
||||||
_func_call_thread = None
|
_func_call_thread = None
|
||||||
@@ -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
|
||||||
0
src/testium/runtime/__init__.py
Normal file
0
src/testium/runtime/__init__.py
Normal 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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ import itertools
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
try:
|
try:
|
||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
except:
|
except:
|
||||||
import py_func.tm as tm
|
import py_func.tm as tm
|
||||||
|
|
||||||
from lib.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
|
|
||||||
"""Lightweight JSON-RPC 2.0 helpers over TCP sockets.
|
"""Lightweight JSON-RPC 2.0 helpers over TCP sockets.
|
||||||
|
|
||||||
@@ -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):
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from threading import (Thread, Event)
|
from threading import (Thread, Event)
|
||||||
from lib.string_queue import StringQueue
|
from runtime.string_queue import StringQueue
|
||||||
from time import (sleep)
|
from time import (sleep)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
def RetreiveData(console_name):
|
def RetreiveData(console_name):
|
||||||
print("--------------- retrieving data ---------------")
|
print("--------------- retrieving data ---------------")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import libs.testium as tm
|
import api.testium as tm
|
||||||
|
|
||||||
def RetreiveData(console_name):
|
def RetreiveData(console_name):
|
||||||
print("--------------- retrieving data ---------------")
|
print("--------------- retrieving data ---------------")
|
||||||
|
|||||||
42
test/validation/fake_exporter/fake_exporter/__init__.py
Normal file
42
test/validation/fake_exporter/fake_exporter/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""CSV report exporter — used as a real plugin by the testium validation suite.
|
||||||
|
|
||||||
|
Demonstrates the contract: take the SQLite connection, output path, optional
|
||||||
|
name/key filters, and produce the output. Has no dependency on testium
|
||||||
|
internals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
|
class FakeExporter:
|
||||||
|
COLUMNS = [
|
||||||
|
'timestamp_start',
|
||||||
|
'test_id',
|
||||||
|
'parent_id',
|
||||||
|
'level',
|
||||||
|
'test_name',
|
||||||
|
'test_type',
|
||||||
|
'report_key',
|
||||||
|
'result',
|
||||||
|
'message',
|
||||||
|
'duration',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, name, con, path, pats, keys, no_header=False):
|
||||||
|
clauses = []
|
||||||
|
for p in pats:
|
||||||
|
clauses.append(f'test_name LIKE "{p}"')
|
||||||
|
for k in keys:
|
||||||
|
clauses.append(f'report_key LIKE "{k}"')
|
||||||
|
where = ('WHERE ' + ' OR '.join(clauses) + ' ') if clauses else ''
|
||||||
|
cols = ', '.join(self.COLUMNS)
|
||||||
|
rows = con.execute(
|
||||||
|
f'SELECT {cols} FROM tests {where}ORDER BY timestamp_start'
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
with open(path, 'w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
if not no_header:
|
||||||
|
writer.writerow(self.COLUMNS)
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow(row)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user