diff --git a/DESIGN.md b/DESIGN.md index 36d6f18..08a483e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -302,6 +302,7 @@ The `testium_assist` editor extension is a thin LSP client that spawns `testium Both Flatpak and AppImage export `TESTIUM_VERSION` from a launcher (Flatpak: launcher script in `org.testium.Testium.yaml`; AppImage: `runtime.env` in `AppImageBuilder.yml`). `get_testium_version()` checks `/.flatpak-info` / `APPIMAGE` and reads `TESTIUM_VERSION` rather than relying on package metadata or repo introspection. ## Recent fixes / notable changes +- Graceful item load: a self-loading item that fails to load its module/file (e.g. a `unittest` test file importing a missing module, or `pytest` not installed on the host) no longer aborts the **whole** test load. `TestSet._load_item()` wraps the item's `load()`, emits a `tm.print_warn(...)` at load time and records the reason in `item._load_error`; the `@test_run` wrapper turns a non-None `_load_error` into a clean run-time `FAILURE` (message printed once via `write_footer`). The rest of the campaign loads and runs normally. Applies to module-loading items (`unittest`, `pytest`); structural action loading stays fail-fast. - `console` item — serial robustness + richer `read_until`: (1) a failed serial `open()` now raises a clear `ETUMRuntimeError` ("Serial device '…' does not exist." / permission hint) instead of dumping a pyserial traceback, and a console whose open failed is safely "not open" (init `_thd=None` + `isOpened` guards in `readchar`/`read_nowait`/`close`) so later reads no longer crash with `AttributeError: '_thd'`; the action handlers show a one-liner for expected (`ETUMRuntimeError`) errors and keep the full traceback for unexpected ones. (2) `read_until`'s `expected` now accepts a **list of values** (match any) and a new `regex: true` flag treats each pattern as a Python regex (`re.search` over a bounded tail — `Console.REGEX_WINDOW`; limitation: cost/memory bounded, so a match only after a very long stream or beyond the window won't fire). Flatpak manifest now grants `--device=all` so serial adapters (`/dev/ttyUSB*`, `/dev/ttyACM*`) are visible in the sandbox. Validation: new `read_until` list/regex cases in `test/validation/items/console/test.tum`. - Subprocess RPC startup handshake: the `py_func`/`lua_func`/`eval_proc` worker now picks its own port (`bind 0`), announces it on stdout (`__TESTIUM_RPC_PORT__=`), and the parent connects only after reading it. Fixes intermittent Windows `failed to connect : timeout` and the matching `wait_ready()` hang; removes the reserve/close/rebind race and `SO_REUSEADDR`. See "Subprocess RPC startup handshake". - `build_all.sh`: builds the four heavy channels in parallel (serial prep for the shared venv + wheel), results in completion order, Ctrl+C kills the whole job tree; `--ram` puts the build scratch on tmpfs (`/dev/shm`) + skips UPX for fast builds on USB/SD storage (Flatpak excluded — rofiles-fuse can't mount tmpfs). See the "Building all channels" section. diff --git a/src/testium/interpreter/test_items/test_item.py b/src/testium/interpreter/test_items/test_item.py index 4320da8..0e2ddba 100644 --- a/src/testium/interpreter/test_items/test_item.py +++ b/src/testium/interpreter/test_items/test_item.py @@ -61,6 +61,13 @@ def test_run(f): self.run_test_init() + # The item could not be loaded (e.g. a missing module): FAIL at run. + # run_test_end -> write_footer prints the message. + if self._load_error is not None: + self.result.set(TestValue.FAILURE, self._load_error) + self.run_test_end() + return self.result + while self._is_paused: sleep(0.2) if self.isStopped() : @@ -151,6 +158,7 @@ class TestItem: self._expected_result = None self._no_fail = None self._is_stopped = False + self._load_error = None self._is_running = False self._is_breakpoint = False self._is_paused = False diff --git a/src/testium/interpreter/test_set.py b/src/testium/interpreter/test_set.py index 5c5e8e4..e6e0670 100644 --- a/src/testium/interpreter/test_set.py +++ b/src/testium/interpreter/test_set.py @@ -451,6 +451,20 @@ class TestSet: def rootItem(self): return self._rootItem + def _load_item(self, item): + """Run an item's self-load, deferring a failure (e.g. a missing module) + to a run-time FAILURE instead of aborting the whole test load.""" + try: + return item.load() + except Exception as e: + msg = getattr(e, "_message", None) or str(e) + item._load_error = msg + tm.print_warn( + f"'{item.cmd()}' item '{item.name()}' could not be loaded: " + f"{msg} (it will FAIL at run)." + ) + return {} + def load_test_recursively(self, tree_parent, parent_seq, file_name): ret = {} try: @@ -534,7 +548,7 @@ class TestSet: # case where the test item loads itself its descendants if it == cst_type.TYPE_UNITTEST: item.setTestDir(test_dir) - child = item.load() + child = self._load_item(item) elif issubclass(it.item_class, TestItemActions): child = item.load() # case where the test item is an items container