perf(load): flatten step list in one pass; fix nested-list duplication

load_test_recursively expanded nested lists and included 'sequence'
entries by splicing each into the step list and rebuilding the whole
list every time (O(n^2)). The list branch also rebuilt after an in-place
splice, duplicating entries when a nested list held more than one item.

Replace both with a single linear _flatten_actions pass. Build phase
~12% faster at 6k items; the real fix is the duplication (a nested
2-element list now yields a,b,c,d not a,b,c,c,d). Validation suite
identical (post-exec SUCCESS, same verdicts/tracebacks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 14:40:46 +02:00
parent 5adba7fcd5
commit f02616dc3a

View File

@@ -29,6 +29,51 @@ def _build_item_path(item) -> str:
return " > ".join(reversed(parts)) return " > ".join(reversed(parts))
def _flatten_actions(actions, out, parent_seq_name):
"""Expand nested lists and included ``sequence`` entries into ``out`` as a
flat list of single test-item dicts, propagating each sequence's source
filename onto its items.
Replaces the previous approach, which spliced each entry into the step
list and rebuilt the whole list every time (O(n^2) over the step list, and
a rebuild that duplicated entries when a nested list held more than one
element). This single forward pass is linear.
"""
for idx, action in enumerate(actions):
# a bare list raises its elements to the same level
if isinstance(action, (list, tuple)):
_flatten_actions(action, out, parent_seq_name)
continue
# a NoneType (e.g. pointing at an unused alias) contributes nothing
if action is None:
continue
# a 'sequence' (an included file) is spliced in, with its filename
# propagated onto each of its items
if isinstance(action, dict) and "sequence" in action:
sequence = action["sequence"]["data"]
f = action["sequence"]["filename"]
if isinstance(sequence, dict):
sequence = [{k: v} for k, v in sequence.items()]
# Case of an empty sequence
elif sequence is None:
tm.print_info(
f"An empty sequence is loaded in '{parent_seq_name}'."
)
sequence = []
elif not isinstance(sequence, list):
raise ETUMSyntaxError(
f"Syntax error in '{parent_seq_name}' step number {idx+1}. Sequence definition: '{str(action)}'",
f
)
for s in sequence:
if isinstance(s, dict) and s:
s[list(s.keys())[0]]["seq_filename"] = f
_flatten_actions(sequence, out, parent_seq_name)
continue
out.append(action)
class TestSet: class TestSet:
def __init__( def __init__(
self, self,
@@ -434,56 +479,16 @@ class TestSet:
f"No valid list of actions in sequence {parent_seq_name}", f"No valid list of actions in sequence {parent_seq_name}",
file_name file_name
) )
# first we merged to the same level 'sequence dict entries and list within the list
counter = 0
test_dir = tm.gd("test_directory") test_dir = tm.gd("test_directory")
la = len(parent_seq_actions)
while counter < la:
action = parent_seq_actions[counter]
# if action is a list raise up to the the same level,
# ie insert action element into the parent_seq_actions
if isinstance(action, (list, tuple)):
parent_seq_actions[counter : counter + 1] = action
parent_seq_actions = (
parent_seq_actions[:counter]
+ action
+ parent_seq_actions[counter + 1 :]
)
la = len(parent_seq_actions)
continue
# if action is a NoneType skip and continue
# (when pointing to an unused alias for instance)
if action is None:
counter += 1
continue
# if action is a sequence we insert its entry into the action list
if "sequence" in action:
sequence = action["sequence"]["data"]
f = action["sequence"]["filename"]
if isinstance(sequence, dict):
sequence = [{k: v} for k, v in sequence.items()]
# Case of an empty sequence
elif sequence is None:
tm.print_info(
f"An empty sequence is loaded in '{parent_seq_name}'."
)
sequence = []
elif not isinstance(sequence, list):
raise ETUMSyntaxError(
f"Syntax error in '{parent_seq_name}' step number {counter+1}. Sequence definition: '{str(action)}'",
f
)
for s in sequence:
s[list(s.keys())[0]]["seq_filename"] = f
parent_seq_actions = (
parent_seq_actions[:counter]
+ sequence
+ parent_seq_actions[counter + 1 :]
)
la = len(parent_seq_actions)
continue
# Action is now for sure a list of dict of length 1 # Flatten nested lists and included 'sequence' entries to the same level
# in one linear pass (was an in-place splice + full list rebuild per
# entry: O(n^2) over the step list).
flat_actions = []
_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
k = list(action.keys())[0] k = list(action.keys())[0]
if action[k].get("seq_filename", None) is None: if action[k].get("seq_filename", None) is None:
action[k]["seq_filename"] = file_name action[k]["seq_filename"] = file_name
@@ -546,8 +551,6 @@ class TestSet:
action[k]["seq_filename"] action[k]["seq_filename"]
) )
counter += 1
return ret return ret
def tree(self): def tree(self):