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:
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user