feat: graceful load failure for module-loading items
A self-loading item that can't load its module/file (unittest test file with a missing import, pytest not installed on the host, ...) no longer aborts the whole test load. TestSet._load_item() wraps load(), warns at load time and records item._load_error; @test_run turns it into a clean run-time FAILURE. The rest of the campaign loads and runs. Scoped to module-loading items (unittest; pytest once merged). Structural action loading stays fail-fast. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user