fix(load): report every test-load error with file, item path and cause

A structural mistake in a .tum (unknown item or action, a step holding two
items, a missing 'steps:' list, a scalar where a mapping is expected, ...)
used to surface as a bare Python traceback. At worst the unknown-action
formatter itself crashed with "'dict_keys' object is not subscriptable"
(action.keys()[0]), masking the real cause and leaving only the generic
"test process crashed for any reason".

The load path now validates each step and funnels every failure through a
located TUM file syntax error: the file, a breadcrumb to the item, the
offending value and the list of valid names. A problem inside an !include-d
file points to that file. A last-resort net in __loadTestTree turns any
unforeseen exception into a located error too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 14:40:15 +02:00
parent 6dc473de41
commit 1ea360e5a5
3 changed files with 160 additions and 52 deletions

View File

@@ -39,20 +39,36 @@ class TestItemActions(TestItem):
def load(self):
ret = {}
for action in self.dict_actions:
# Action should be only dict of length 1
if not isinstance(action, dict) or (not len(action) == 1):
if self.dict_actions is None:
self.dict_actions = []
if not isinstance(self.dict_actions, (list, tuple)):
raise ETUMSyntaxError(
f"The '{self.cmd()}' test item named '{self.name()}' action should be only dict of length = 1.",
f"The '{self.cmd()}' test item named '{self.name()}' expects a "
f"list of actions under 'steps' but got "
f"{type(self.dict_actions).__name__} ({self.dict_actions!r}).",
self.seqFilename()
)
known_actions = ", ".join(sorted(self.action_classes.keys())) or "(none)"
for action in self.dict_actions:
# Each action must be a single-key mapping ``{action_name: {...}}``.
if not isinstance(action, dict) or len(action) != 1:
raise ETUMSyntaxError(
f"The '{self.cmd()}' test item named '{self.name()}' has an "
f"invalid action: each action must be a single-key mapping "
f"('<action>: ...'), got {type(action).__name__} ({action!r}).",
self.seqFilename()
)
action_name = list(action.keys())[0]
if not (action_name in self.action_classes.keys()):
if action_name not in self.action_classes:
raise ETUMSyntaxError(
f"The '{self.cmd()}' test item named '{self.name()}' has an unknown action '{action.keys()[0]}'.",
f"The '{self.cmd()}' test item named '{self.name()}' has an "
f"unknown action '{action_name}'.\n"
f"Known actions: {known_actions}.",
self.seqFilename()
)
# NB: an action body is not necessarily a mapping — several actions
# accept a scalar shorthand (e.g. ``writeln: 'echo hi'``); the action
# class validates its own body. Pass it through untouched.
item = (self.action_classes[action_name])(
action_name,
action[action_name],

View File

@@ -3,7 +3,7 @@ import datetime
from queue import Queue
from interpreter.utils.params import expanse
import api.testium as tm
from runtime.tum_except import ETUMSyntaxError
from runtime.tum_except import ETUMSyntaxError, ETUMError
import interpreter.utils.settings as prefs
from interpreter.test_report.test_report import TestReport
from interpreter.utils.py_func_exec import PyFuncExecEngine
@@ -65,9 +65,22 @@ def _flatten_actions(actions, out, parent_seq_name):
f"Syntax error in '{parent_seq_name}' step number {idx+1}. Sequence definition: '{str(action)}'",
f
)
if not isinstance(sequence, list):
raise ETUMSyntaxError(
f"Invalid included sequence in '{parent_seq_name}' "
f"(step {idx+1}): expected a list of steps, got "
f"{type(sequence).__name__}.",
f
)
for s in sequence:
if isinstance(s, dict) and s:
s[list(s.keys())[0]]["seq_filename"] = f
# Propagate the source filename onto each included step. Only a
# single-key mapping with a mapping body can carry it; malformed
# entries are left untouched and reported by the loader below,
# with their real location.
if isinstance(s, dict) and len(s) == 1:
body = s[next(iter(s))]
if isinstance(body, dict):
body["seq_filename"] = f
_flatten_actions(sequence, out, parent_seq_name)
continue
@@ -390,7 +403,19 @@ class TestSet:
self._rootItem = (cst_type.TYPE_ROOT.item_class)(
dict_item=dict_main, status_queue=self.status_queue
)
try:
ret = self.load_test_recursively(self._rootItem, dict_main, filename)
except ETUMError:
# Already a located, user-readable testium error.
raise
except Exception as e:
# Last-resort net: turn any unforeseen failure into a located error
# rather than a bare traceback / 'crashed for any reason'.
raise ETUMSyntaxError(
f"Unexpected error while building the test tree: "
f"{type(e).__name__}: {e}",
filename
) from e
self.set_post_exec()
return ret
@@ -467,30 +492,43 @@ class TestSet:
def load_test_recursively(self, tree_parent, parent_seq, file_name):
ret = {}
path = _build_item_path(tree_parent)
if not isinstance(parent_seq, dict):
raise ETUMSyntaxError(
f"In: {path}\n"
f"The body of '{tree_parent.cmd()}' must be a mapping (with a "
f"'steps' list) but is {type(parent_seq).__name__} "
f"({parent_seq!r}).",
file_name
)
try:
parent_seq_name = parent_seq["name"]
except KeyError:
parent_seq["name"] = "sequence"
except TypeError:
raise ETUMSyntaxError(
f"No 'name' attribute in '{tree_parent.type()}' (a child of '{tree_parent.parent().name()}')",
file_name
)
parent_seq_name = "sequence"
try:
parent_seq_actions = parent_seq["steps"]
except KeyError:
raise ETUMSyntaxError(
f"No step list found for '{parent_seq_name}' sequence. \n" +
f"Check the syntax of the 'steps' parameter of the '{tree_parent.cmd()}' test item definition.",
f"In: {path}\n"
f"No 'steps' list found for the '{tree_parent.cmd()}' item "
f"'{parent_seq_name}'.\n"
f"A container item must declare its children under 'steps:'.",
file_name
)
# if action is a dictionary , we assume it is a single action
# that has not been nested in a list, so do it
if isinstance(parent_seq_actions, (dict)):
parent_seq_actions = [parent_seq_actions]
# an empty 'steps:' (None) is a valid, empty sequence
if parent_seq_actions is None:
parent_seq_actions = []
if not isinstance(parent_seq_actions, (list, tuple)):
raise ETUMSyntaxError(
f"No valid list of actions in sequence {parent_seq_name}",
f"In: {path}\n"
f"The 'steps' of '{parent_seq_name}' must be a list of test "
f"items but is {type(parent_seq_actions).__name__} "
f"({parent_seq_actions!r}).",
file_name
)
test_dir = tm.gd("test_directory")
@@ -502,10 +540,50 @@ class TestSet:
_flatten_actions(parent_seq_actions, flat_actions, parent_seq_name)
for action in flat_actions:
# Action is now for sure a dict of length 1
# After flattening, each step must be a single-key mapping
# '{item_cmd: {params...}}'. Anything else is a structural mistake
# in the .tum (a stray scalar, a missing '-' marker, an over- or
# under-indented block) — report it with its location instead of
# crashing on it below.
if not isinstance(action, dict):
raise ETUMSyntaxError(
f"In: {path}\n"
f"A step is not a valid test item: expected a "
f"'<item>: ...' mapping but got {type(action).__name__} "
f"({action!r}).\n"
f"Check the indentation and the '-' list markers of 'steps'.",
file_name
)
if len(action) != 1:
raise ETUMSyntaxError(
f"In: {path}\n"
f"A step must define exactly one test item but defines "
f"{len(action)}: {sorted(map(str, action.keys()))}.\n"
f"Each '-' step holds a single '<item>:'; the lines below it "
f"are probably its parameters and need one more indent level.",
file_name
)
k = list(action.keys())[0]
if action[k].get("seq_filename", None) is None:
action[k]["seq_filename"] = file_name
# The body of an item is its parameter mapping. A bare '<item>:'
# (None) is tolerated as an empty parameter set; a scalar or list is
# a structural mistake and is reported with its location.
body = action[k]
if body is None:
body = {}
action[k] = body
if not isinstance(body, dict):
raise ETUMSyntaxError(
f"In: {path}\n"
f"The body of test item '{k}' must be a mapping of "
f"parameters but is {type(body).__name__} ({body!r}).",
file_name
)
if body.get("seq_filename", None) is None:
body["seq_filename"] = file_name
seq_filename = body["seq_filename"]
executed = False
for it in TEST_TYPE_LIST:
@@ -517,32 +595,18 @@ class TestSet:
(it.item_class is None)
):
continue
if (it.item_cmd in action) or (
(cst.FOLDED_CHAR + it.item_cmd) in action
):
if k not in (it.item_cmd, cst.FOLDED_CHAR + it.item_cmd):
continue
executed = True
is_folded = False
action_name = it.item_cmd
# Check if a "." is before the cmd_name (meaning folded)
if (cst.FOLDED_CHAR + it.item_cmd) in action:
is_folded = True
action_name = cst.FOLDED_CHAR + it.item_cmd
seq_filename = action[action_name]["seq_filename"]
# A "." before the cmd name means the item is folded in the GUI
is_folded = k.startswith(cst.FOLDED_CHAR)
try:
item = (it.item_class)(
action[action_name],
body,
tree_parent,
self.status_queue,
filename=seq_filename
)
except ETUMSyntaxError as e:
path = _build_item_path(tree_parent)
raise ETUMSyntaxError(
f"In: {path}\n{e._message}",
e._file or seq_filename,
) from e
item.is_folded = is_folded
child = {}
# case where the test item loads itself its descendants
@@ -554,15 +618,42 @@ class TestSet:
# case where the test item is an items container
elif item.is_container:
child = self.load_test_recursively(
item, action[action_name], seq_filename
item, body, seq_filename
)
except ETUMSyntaxError as e:
# Already a syntax error: prepend the breadcrumb to its
# location (unless it already carries one from a deeper level).
msg = e._message
if not msg.lstrip().startswith("In:"):
msg = f"In: {path} > {k}\n{msg}"
raise ETUMSyntaxError(msg, e._file or seq_filename) from e
except ETUMError:
# Other testium errors (missing parameter, runtime, I/O)
# already carry structured context (item type, name,
# parameter, ...): let them through unchanged.
raise
except Exception as e:
# Anything unexpected: never let a raw Python error reach the
# user as 'crashed for any reason' — locate it precisely.
raise ETUMSyntaxError(
f"In: {path} > {k}\n"
f"Unexpected error while loading this item: "
f"{type(e).__name__}: {e}",
seq_filename
) from e
ret.update(test_data(item, child))
if not executed:
known = ", ".join(
t.item_cmd for t in TEST_TYPE_LIST
if t is not cst_type.TYPE_ROOT and t.item_class is not None
)
raise ETUMSyntaxError(
f"test item '{k}' is not known.",
action[k]["seq_filename"]
f"In: {path}\n"
f"'{k}' is not a known test item.\n"
f"Known items: {known}.",
seq_filename
)
return ret

View File

@@ -127,7 +127,8 @@ class TestFileManager:
del w.ts_controller
w.ts_controller = None
raise ETUMRuntimeError(
"Test could not be loaded (test process crashed for any reason)"
"Test could not be loaded. See the log above for the cause "
"(syntax error, missing file, missing module, ...)."
)
progress.setLabelText("Building test tree…")