pytestflow 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bootstrap_templates/__init__.py +0 -0
- bootstrap_templates/config.yaml +13 -0
- bootstrap_templates/custom_step_types/__init__.py +0 -0
- bootstrap_templates/custom_step_types/custom_step_template.py +7 -0
- bootstrap_templates/process_models/__init__.py +0 -0
- bootstrap_templates/process_models/reporting/README.md +48 -0
- bootstrap_templates/process_models/reporting/__init__.py +0 -0
- bootstrap_templates/process_models/reporting/default_report.html.j2 +122 -0
- bootstrap_templates/process_models/reporting/html_report.py +331 -0
- bootstrap_templates/process_models/sequential_model.py +141 -0
- bootstrap_templates/test_sequences/__init__.py +0 -0
- bootstrap_templates/test_sequences/basic_sequence.py +62 -0
- bootstrap_templates/test_sequences/message_box_and_flow_control.py +169 -0
- bootstrap_templates/test_sequences/motherboard_test_sequence.py +125 -0
- bootstrap_templates/test_sequences/step_types_quickstart.py +168 -0
- pytestflow/README.md +13 -0
- pytestflow/__init__.py +19 -0
- pytestflow/backend/__init__.py +2 -0
- pytestflow/backend/event_bus.py +27 -0
- pytestflow/backend/frontend/assets/full_logo-D1DRTUt8.svg +21 -0
- pytestflow/backend/frontend/assets/index-480TOyh4.js +2 -0
- pytestflow/backend/frontend/assets/index-qEI3VAQU.css +1 -0
- pytestflow/backend/frontend/index.html +14 -0
- pytestflow/backend/frontend/logo.svg +21 -0
- pytestflow/backend/handlers.py +214 -0
- pytestflow/backend/report_manager.py +15 -0
- pytestflow/backend/sequences_info.py +130 -0
- pytestflow/backend/start_backend.py +118 -0
- pytestflow/backend/uuids_handler.py +67 -0
- pytestflow/backend/websocket_gateway.py +91 -0
- pytestflow/cli.py +183 -0
- pytestflow/config/__init__.py +0 -0
- pytestflow/config/config_manager.py +44 -0
- pytestflow/core/README.md +110 -0
- pytestflow/core/__init__.py +15 -0
- pytestflow/core/context.py +41 -0
- pytestflow/core/core.py +112 -0
- pytestflow/core/pytestflow_states.py +88 -0
- pytestflow/core/runtime_control.py +164 -0
- pytestflow/core/seq_file_runner.py +38 -0
- pytestflow/core/sequence.py +404 -0
- pytestflow/core/utils.py +81 -0
- pytestflow/flow_utils/README.md +6 -0
- pytestflow/flow_utils/__init__.py +0 -0
- pytestflow/flow_utils/conditions.py +0 -0
- pytestflow/flow_utils/transitions.py +0 -0
- pytestflow/starter_here.md +43 -0
- pytestflow/steps/README.md +43 -0
- pytestflow/steps/__init__.py +15 -0
- pytestflow/steps/action_step.py +94 -0
- pytestflow/steps/common.py +51 -0
- pytestflow/steps/df_numeric_limits.py +151 -0
- pytestflow/steps/flow_control.py +86 -0
- pytestflow/steps/message_pop_up.py +76 -0
- pytestflow/steps/numeric_limit.py +109 -0
- pytestflow/steps/pass_fail.py +49 -0
- pytestflow/steps/string_check.py +104 -0
- pytestflow/steps/waveform_limit.py +170 -0
- pytestflow-0.2.0.dist-info/METADATA +73 -0
- pytestflow-0.2.0.dist-info/RECORD +63 -0
- pytestflow-0.2.0.dist-info/WHEEL +5 -0
- pytestflow-0.2.0.dist-info/entry_points.txt +2 -0
- pytestflow-0.2.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import uuid
|
|
3
|
+
from prefect import flow
|
|
4
|
+
from typing import Any, Callable, List, Iterable
|
|
5
|
+
from pytestflow.core.pytestflow_states import (
|
|
6
|
+
PyTestflowPassed, PyTestflowFailed, PyTestflowDone, PyTestflowError, PyTestflowState
|
|
7
|
+
)
|
|
8
|
+
from pytestflow.core.context import ptf_context
|
|
9
|
+
import inspect
|
|
10
|
+
|
|
11
|
+
from pytestflow.core.utils import get_data_for_gui
|
|
12
|
+
from pytestflow.core.runtime_control import runtime_control
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Sequence:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
name: str,
|
|
19
|
+
steps: List[Callable] = None,
|
|
20
|
+
default_parameters: dict = None,
|
|
21
|
+
allow_parent_mutation: bool = False,
|
|
22
|
+
apply_throttle: bool = True,
|
|
23
|
+
):
|
|
24
|
+
self.name = name
|
|
25
|
+
self.steps = steps or []
|
|
26
|
+
self.allow_parent_mutation = allow_parent_mutation
|
|
27
|
+
self.apply_throttle = apply_throttle
|
|
28
|
+
self.results: List[tuple[str, PyTestflowState]] = []
|
|
29
|
+
self._locals_stack: List[dict] = [] # Stack to manage ptf_context.locals
|
|
30
|
+
self.default_parameters = default_parameters or {}
|
|
31
|
+
self.max_transitions = 10_000
|
|
32
|
+
|
|
33
|
+
def _is_passed(self, state: PyTestflowState) -> bool:
|
|
34
|
+
return isinstance(state, (PyTestflowPassed, PyTestflowDone))
|
|
35
|
+
|
|
36
|
+
def add_step(self, fn: Callable, position: int = None):
|
|
37
|
+
if position is None:
|
|
38
|
+
self.steps.append(fn)
|
|
39
|
+
else:
|
|
40
|
+
self.steps.insert(position, fn)
|
|
41
|
+
|
|
42
|
+
def remove_step(self, name_or_fn):
|
|
43
|
+
name = name_or_fn if isinstance(name_or_fn, str) else name_or_fn.__name__
|
|
44
|
+
self.steps = [s for s in self.steps if getattr(s, '__name__', '') != name]
|
|
45
|
+
|
|
46
|
+
def force_step_status(self, step_name: str, status: str):
|
|
47
|
+
def forced():
|
|
48
|
+
match status.lower():
|
|
49
|
+
case "passed":
|
|
50
|
+
return PyTestflowPassed(ptf_result={"step_status": "forced_pass"}, message=f"{step_name} forced pass")
|
|
51
|
+
case "failed":
|
|
52
|
+
return PyTestflowFailed(ptf_result={"step_status": "forced_fail"}, message=f"{step_name} forced fail")
|
|
53
|
+
case "done" | "skipped":
|
|
54
|
+
return PyTestflowDone(ptf_result={"step_status": f"{status.lower()}"}, message=f"{step_name} {status.lower()}")
|
|
55
|
+
case _:
|
|
56
|
+
raise ValueError(f"Unsupported status: {status}")
|
|
57
|
+
forced.__name__ = step_name + "_forced"
|
|
58
|
+
self.add_step(forced)
|
|
59
|
+
|
|
60
|
+
def _exec_step(self, step_fn: Callable) -> tuple[str, PyTestflowState]:
|
|
61
|
+
step_name = getattr(step_fn, "name", getattr(step_fn, "__name__", repr(step_fn)))
|
|
62
|
+
print(f"➡️ Executing step: {step_name}")
|
|
63
|
+
|
|
64
|
+
if inspect.iscoroutinefunction(step_fn):
|
|
65
|
+
raise TypeError("Async step functions are not supported in this base Sequence class.")
|
|
66
|
+
|
|
67
|
+
# Check if the step is a Sequence
|
|
68
|
+
if isinstance(step_fn, Sequence):
|
|
69
|
+
# Push current locals onto the stack
|
|
70
|
+
# Snapshot caller state
|
|
71
|
+
caller_snapshot = ptf_context.locals.copy()
|
|
72
|
+
self._locals_stack.append(caller_snapshot)
|
|
73
|
+
|
|
74
|
+
#initialize called sequence context
|
|
75
|
+
ptf_context.locals.clear() # Clear locals for the subsequence
|
|
76
|
+
|
|
77
|
+
if step_fn.allow_parent_mutation:
|
|
78
|
+
# Live pointer to caller's local space
|
|
79
|
+
ptf_context.locals["__caller__"] = caller_snapshot
|
|
80
|
+
else:
|
|
81
|
+
# Read-only wrapper around a disposable copy
|
|
82
|
+
class ReadOnlyDict(dict):
|
|
83
|
+
def __setitem__(self, k, v): raise RuntimeError("Caller context is read-only")
|
|
84
|
+
def update(self, *a, **kw): raise RuntimeError("Caller context is read-only")
|
|
85
|
+
|
|
86
|
+
ptf_context.locals["__caller__"] = ReadOnlyDict(caller_snapshot.copy()) # disposable copy
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Run the subsequence
|
|
90
|
+
state = step_fn.run(return_state=True) # returns PyTestflowState
|
|
91
|
+
#step_name = state.ptf_result['step_name']
|
|
92
|
+
finally:
|
|
93
|
+
# Restore parent locals
|
|
94
|
+
ptf_context.locals = self._locals_stack.pop()
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
# Execute a regular step
|
|
98
|
+
try:
|
|
99
|
+
state = step_fn()
|
|
100
|
+
#step_name = state.ptf_result['step_name']
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
print(f"💥 Step {step_name} raised an exception: {exc}")
|
|
103
|
+
state = PyTestflowError(ptf_result={"step_status": "error", "error": str(exc)}, message=str(exc))
|
|
104
|
+
|
|
105
|
+
return step_name, state
|
|
106
|
+
|
|
107
|
+
def _get_step_name(self, step_fn: Callable) -> str:
|
|
108
|
+
return getattr(step_fn, "name", getattr(step_fn, "__name__", repr(step_fn)))
|
|
109
|
+
|
|
110
|
+
def _read_next_step_directive(self) -> Any:
|
|
111
|
+
return ptf_context.locals.pop("_ptf_next_step", "next")
|
|
112
|
+
|
|
113
|
+
def _resolve_next_index(
|
|
114
|
+
self,
|
|
115
|
+
*,
|
|
116
|
+
directive: Any,
|
|
117
|
+
current_index: int,
|
|
118
|
+
step_name_to_index: dict[str, int],
|
|
119
|
+
step_count: int,
|
|
120
|
+
) -> tuple[int, str | None]:
|
|
121
|
+
if directive is None:
|
|
122
|
+
return current_index + 1, None
|
|
123
|
+
|
|
124
|
+
if isinstance(directive, str):
|
|
125
|
+
token = directive.strip()
|
|
126
|
+
token_lower = token.lower()
|
|
127
|
+
|
|
128
|
+
if token == "" or token_lower == "next":
|
|
129
|
+
return current_index + 1, None
|
|
130
|
+
if token_lower in {"end", "stop"}:
|
|
131
|
+
return step_count, None
|
|
132
|
+
if token in step_name_to_index:
|
|
133
|
+
return step_name_to_index[token], None
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
current_index + 1,
|
|
137
|
+
f"Invalid _ptf_next_step '{directive}'. Expected 'next', 'end', a valid step name, or an integer index.",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if isinstance(directive, int):
|
|
141
|
+
if 0 <= directive < step_count:
|
|
142
|
+
return directive, None
|
|
143
|
+
if directive == step_count:
|
|
144
|
+
return step_count, None
|
|
145
|
+
return (
|
|
146
|
+
current_index + 1,
|
|
147
|
+
f"Invalid _ptf_next_step index '{directive}'. Expected range [0, {step_count}]",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
current_index + 1,
|
|
152
|
+
f"Invalid _ptf_next_step type '{type(directive).__name__}'. Expected str or int.",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _flow_control_error_state(self, message: str) -> PyTestflowError:
|
|
156
|
+
return PyTestflowError(
|
|
157
|
+
ptf_result={"step_status": "error", "error": message, "step_type": "flow_control"},
|
|
158
|
+
message=message,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _run_steps_with_flow_control(
|
|
162
|
+
self,
|
|
163
|
+
*,
|
|
164
|
+
steps: List[Callable],
|
|
165
|
+
runtime_start_index: int = 0,
|
|
166
|
+
) -> tuple[List[tuple[str, PyTestflowState]], int]:
|
|
167
|
+
results: List[tuple[str, PyTestflowState]] = []
|
|
168
|
+
step_name_to_index = {self._get_step_name(step): idx for idx, step in enumerate(steps)}
|
|
169
|
+
current_index = 0
|
|
170
|
+
transitions = 0
|
|
171
|
+
|
|
172
|
+
ptf_context.locals.pop("_ptf_next_step", None)
|
|
173
|
+
|
|
174
|
+
while 0 <= current_index < len(steps):
|
|
175
|
+
if transitions >= self.max_transitions:
|
|
176
|
+
msg = (
|
|
177
|
+
f"Sequence '{self.name}' exceeded max transitions ({self.max_transitions}). "
|
|
178
|
+
"Possible infinite loop in flow control."
|
|
179
|
+
)
|
|
180
|
+
results.append(("__flow_control__", self._flow_control_error_state(msg)))
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
runtime_control.checkpoint_before_step(
|
|
184
|
+
runtime_start_index + transitions,
|
|
185
|
+
apply_throttle=self.apply_throttle,
|
|
186
|
+
)
|
|
187
|
+
step_fn = steps[current_index]
|
|
188
|
+
name, state = self._exec_step(step_fn)
|
|
189
|
+
results.append((name, state))
|
|
190
|
+
transitions += 1
|
|
191
|
+
|
|
192
|
+
directive = self._read_next_step_directive()
|
|
193
|
+
next_index, error = self._resolve_next_index(
|
|
194
|
+
directive=directive,
|
|
195
|
+
current_index=current_index,
|
|
196
|
+
step_name_to_index=step_name_to_index,
|
|
197
|
+
step_count=len(steps),
|
|
198
|
+
)
|
|
199
|
+
if error:
|
|
200
|
+
results.append(("__flow_control__", self._flow_control_error_state(error)))
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
current_index = next_index
|
|
204
|
+
|
|
205
|
+
return results, runtime_start_index + transitions
|
|
206
|
+
|
|
207
|
+
@flow(name="SequenceRun", persist_result=False)
|
|
208
|
+
def run(self, parameters: dict | None = None) -> PyTestflowState:
|
|
209
|
+
print(f"\n▶️ Running Sequence: {self.name}")
|
|
210
|
+
self.results = []
|
|
211
|
+
|
|
212
|
+
# Initialize context for this sequence run
|
|
213
|
+
merged_parameters = {**self.default_parameters, **(parameters or {})}
|
|
214
|
+
ptf_context.locals.update(merged_parameters)
|
|
215
|
+
self.results, _ = self._run_steps_with_flow_control(
|
|
216
|
+
steps=self.steps,
|
|
217
|
+
runtime_start_index=0,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
all_passed = all(self._is_passed(s) for _, s in self.results)
|
|
221
|
+
|
|
222
|
+
if all_passed:
|
|
223
|
+
overall = PyTestflowPassed(
|
|
224
|
+
ptf_result={"step_status": "sequence_passed"},
|
|
225
|
+
message=f"Sequence {self.name} passed",
|
|
226
|
+
children=self.results,
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
overall = PyTestflowFailed(
|
|
230
|
+
ptf_result={"step_status": "sequence_failed"},
|
|
231
|
+
message=f"Sequence {self.name} failed",
|
|
232
|
+
children=self.results,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
print(f"✅ Sequence {self.name} finished with status: {overall.name}")
|
|
236
|
+
return overall
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@flow(name="SequenceRunStep")
|
|
240
|
+
def run_step(self, names: str | Iterable[str]) -> PyTestflowState:
|
|
241
|
+
if isinstance(names, str):
|
|
242
|
+
wanted = {names}
|
|
243
|
+
else:
|
|
244
|
+
wanted = set(names)
|
|
245
|
+
|
|
246
|
+
print(f"\n▶️ Running Sequence.run_step on {self.name} for: {', '.join(wanted)}")
|
|
247
|
+
selected = [s for s in self.steps if getattr(s, "__name__", "") in wanted]
|
|
248
|
+
|
|
249
|
+
results, _ = self._run_steps_with_flow_control(
|
|
250
|
+
steps=selected,
|
|
251
|
+
runtime_start_index=0,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
all_passed = all(self._is_passed(s) for _, s in results)
|
|
255
|
+
|
|
256
|
+
if all_passed:
|
|
257
|
+
overall = PyTestflowPassed(
|
|
258
|
+
ptf_result={"step_status": "sequence_partial_passed"},
|
|
259
|
+
message=f"Sequence {self.name} (partial) passed",
|
|
260
|
+
children=results,
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
overall = PyTestflowFailed(
|
|
264
|
+
ptf_result={"step_status": "sequence_partial_failed"},
|
|
265
|
+
message=f"Sequence {self.name} (partial) failed",
|
|
266
|
+
children=results,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return overall
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class TestSequence(Sequence):
|
|
273
|
+
def __init__(
|
|
274
|
+
self,
|
|
275
|
+
name: str,
|
|
276
|
+
setup_steps: List[Callable] | None = None,
|
|
277
|
+
main_steps: List[Callable] | None = None,
|
|
278
|
+
cleanup_steps: List[Callable] | None = None,
|
|
279
|
+
):
|
|
280
|
+
super().__init__(name=name, steps=[])
|
|
281
|
+
self.static_uuid: uuid.UUID | None = None
|
|
282
|
+
self.setup_steps = setup_steps or []
|
|
283
|
+
self.main_steps = main_steps or []
|
|
284
|
+
self.cleanup_steps = cleanup_steps or []
|
|
285
|
+
|
|
286
|
+
def _run_section(self, label: str, steps: List[Callable], start_index: int = 0) -> tuple[List[tuple[str, PyTestflowState]], int]:
|
|
287
|
+
print(f"\n🔧 Running {label} for {self.name}")
|
|
288
|
+
return self._run_steps_with_flow_control(
|
|
289
|
+
steps=steps,
|
|
290
|
+
runtime_start_index=start_index,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
@flow(name="TestSequenceRun")
|
|
294
|
+
def run(self) -> PyTestflowState:
|
|
295
|
+
print(f"\n▶️ Running Test Sequence: {self.name}")
|
|
296
|
+
|
|
297
|
+
# Send start to GUI
|
|
298
|
+
get_data_for_gui(self, datetime.utcnow())
|
|
299
|
+
|
|
300
|
+
all_steps = [*self.setup_steps, *self.main_steps, *self.cleanup_steps]
|
|
301
|
+
children, _ = self._run_steps_with_flow_control(
|
|
302
|
+
steps=all_steps,
|
|
303
|
+
runtime_start_index=0,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
all_passed = all(self._is_passed(state) for _, state in children)
|
|
307
|
+
|
|
308
|
+
if all_passed:
|
|
309
|
+
overall = PyTestflowPassed(
|
|
310
|
+
ptf_result={"step_status": "test_sequence_passed"},
|
|
311
|
+
message=f"Test Sequence {self.name} passed",
|
|
312
|
+
children=children,
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
overall = PyTestflowFailed(
|
|
316
|
+
ptf_result={"step_status": "test_sequence_failed"},
|
|
317
|
+
message=f"Test Sequence {self.name} failed",
|
|
318
|
+
children=children,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Send end to GUI
|
|
322
|
+
result_data = {
|
|
323
|
+
"step_status": "passed" if all_passed else "failed",
|
|
324
|
+
"step_type": "sub_sequence",
|
|
325
|
+
}
|
|
326
|
+
get_data_for_gui(self, datetime.utcnow(), result_data)
|
|
327
|
+
|
|
328
|
+
print(f"✅ Test Sequence {self.name} finished with status: {overall.name}")
|
|
329
|
+
return overall
|
|
330
|
+
|
|
331
|
+
@flow(name="TestSequenceRunStep")
|
|
332
|
+
def run_step(self, names: str | Iterable[str]) -> PyTestflowState:
|
|
333
|
+
if isinstance(names, str):
|
|
334
|
+
wanted = {names}
|
|
335
|
+
else:
|
|
336
|
+
wanted = set(names)
|
|
337
|
+
|
|
338
|
+
print(f"\n▶️ Running TestSequence.run_step on {self.name} for main steps: {', '.join(wanted)}")
|
|
339
|
+
|
|
340
|
+
children: List[tuple[str, PyTestflowState]] = []
|
|
341
|
+
step_index = 0
|
|
342
|
+
|
|
343
|
+
setup_res, step_index = self._run_section("setup", self.setup_steps, start_index=step_index)
|
|
344
|
+
children.extend(setup_res)
|
|
345
|
+
|
|
346
|
+
selected_main = [s for s in self.main_steps if getattr(s, "__name__", "") in wanted]
|
|
347
|
+
main_res, step_index = self._run_section("main (selected)", selected_main, start_index=step_index)
|
|
348
|
+
children.extend(main_res)
|
|
349
|
+
|
|
350
|
+
cleanup_res, step_index = self._run_section("cleanup", self.cleanup_steps, start_index=step_index)
|
|
351
|
+
children.extend(cleanup_res)
|
|
352
|
+
|
|
353
|
+
all_passed = all(self._is_passed(state) for _, state in children)
|
|
354
|
+
|
|
355
|
+
if all_passed:
|
|
356
|
+
overall = PyTestflowPassed(
|
|
357
|
+
ptf_result={"step_status": "test_sequence_partial_passed"},
|
|
358
|
+
message=f"Test Sequence {self.name} (selected) passed",
|
|
359
|
+
children=children,
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
overall = PyTestflowFailed(
|
|
363
|
+
ptf_result={"step_status": "test_sequence_partial_failed"},
|
|
364
|
+
message=f"Test Sequence {self.name} (selected) failed",
|
|
365
|
+
children=children,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return overall
|
|
369
|
+
|
|
370
|
+
def run_step(self, step_name: str, return_state: bool = False):
|
|
371
|
+
"""
|
|
372
|
+
Run a specific step in the sequence, including setup and cleanup steps.
|
|
373
|
+
"""
|
|
374
|
+
from prefect import flow
|
|
375
|
+
|
|
376
|
+
# Find the step by name
|
|
377
|
+
step = next(
|
|
378
|
+
(s for s in self.setup_steps + self.main_steps + self.cleanup_steps if s.name == step_name),
|
|
379
|
+
None,
|
|
380
|
+
)
|
|
381
|
+
if not step:
|
|
382
|
+
raise ValueError(f"Step '{step_name}' not found in sequence '{self.name}'.")
|
|
383
|
+
|
|
384
|
+
@flow(name=f"Test Sequence {self.name} (selected)")
|
|
385
|
+
def sequence_flow():
|
|
386
|
+
steps_to_run = [*self.setup_steps, step, *self.cleanup_steps]
|
|
387
|
+
results, _ = self._run_steps_with_flow_control(
|
|
388
|
+
steps=steps_to_run,
|
|
389
|
+
runtime_start_index=0,
|
|
390
|
+
)
|
|
391
|
+
return results
|
|
392
|
+
|
|
393
|
+
# Run the flow
|
|
394
|
+
flow_state = sequence_flow(return_state=return_state)
|
|
395
|
+
|
|
396
|
+
# Build the result
|
|
397
|
+
result_children = [
|
|
398
|
+
(name, state) for name, state in flow_state.result()
|
|
399
|
+
]
|
|
400
|
+
result = PyTestflowPassed(children=result_children) if all(
|
|
401
|
+
state.is_completed() for _, state in result_children
|
|
402
|
+
) else PyTestflowFailed(children=result_children)
|
|
403
|
+
|
|
404
|
+
return result
|
pytestflow/core/utils.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# # core/utils.py
|
|
2
|
+
from inspect import signature, Parameter
|
|
3
|
+
from pytestflow.core.context import ptf_context
|
|
4
|
+
import json
|
|
5
|
+
from pytestflow.core.runtime_control import runtime_control
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_data_for_gui(step, timestamp, result=None):
|
|
9
|
+
overall_step_result = ''
|
|
10
|
+
status_to_send = 'started'
|
|
11
|
+
info = ""
|
|
12
|
+
if result is not None:
|
|
13
|
+
# means end of step
|
|
14
|
+
status_to_send = 'finished'
|
|
15
|
+
overall_step_result = result['step_status']
|
|
16
|
+
# get info (step type dependant)
|
|
17
|
+
step_type = result['step_type']
|
|
18
|
+
if step_type == 'action':
|
|
19
|
+
info = json.dumps({"Output": result['output']}, default=str)
|
|
20
|
+
elif step_type == 'numeric_limit':
|
|
21
|
+
info = json.dumps({"Output": result['output'], "limit": result['limit'],"mode": result['mode'] }, default=str)
|
|
22
|
+
if step_type == 'pass_fail':
|
|
23
|
+
info = json.dumps({"Output": result['output']}, default=str)
|
|
24
|
+
elif step_type == 'string_check':
|
|
25
|
+
info = json.dumps({"Output": result['output'],
|
|
26
|
+
"expected": result['expected'],
|
|
27
|
+
"match": result['match'],
|
|
28
|
+
"case_sensitive": result['case_sensitive'] },
|
|
29
|
+
default=str)
|
|
30
|
+
data_to_send = {
|
|
31
|
+
"cmd": "update_step_status",
|
|
32
|
+
"args": {
|
|
33
|
+
"StepUUID": str(step.static_uuid),
|
|
34
|
+
"SelectStepOnGUI": True,
|
|
35
|
+
"Status": status_to_send,
|
|
36
|
+
"Result": overall_step_result,
|
|
37
|
+
"Timestamp": timestamp if isinstance(timestamp, str) else timestamp.isoformat(),
|
|
38
|
+
"Info": info
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if step.static_uuid is None:
|
|
43
|
+
print("**************************************************")
|
|
44
|
+
print("step name:", step.name)
|
|
45
|
+
print("**************************************************")
|
|
46
|
+
|
|
47
|
+
# Send to FE via runtime_control queue (in-process)
|
|
48
|
+
runtime_control.send_gui_update(data_to_send)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_args(func, explicit_args, explicit_kwargs):
|
|
53
|
+
"""
|
|
54
|
+
Autowires missing parameters from context by name.
|
|
55
|
+
If a parameter is 'ctx', injects the context object.
|
|
56
|
+
"""
|
|
57
|
+
sig = signature(func)
|
|
58
|
+
|
|
59
|
+
# Early return for parameter-less functions
|
|
60
|
+
if len(sig.parameters) == 0:
|
|
61
|
+
return (), {}
|
|
62
|
+
|
|
63
|
+
bound = sig.bind_partial(*explicit_args, **explicit_kwargs)
|
|
64
|
+
|
|
65
|
+
for pname, param in sig.parameters.items():
|
|
66
|
+
# Skip *args and **kwargs
|
|
67
|
+
if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if pname in bound.arguments:
|
|
71
|
+
continue
|
|
72
|
+
if pname == "ctx":
|
|
73
|
+
bound.arguments[pname] = ptf_context
|
|
74
|
+
elif pname in ptf_context.locals:
|
|
75
|
+
bound.arguments[pname] = ptf_context.locals[pname]
|
|
76
|
+
elif pname in ptf_context.globals:
|
|
77
|
+
bound.arguments[pname] = ptf_context.globals[pname]
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError(f"Could not autowire parameter '{pname}'")
|
|
80
|
+
|
|
81
|
+
return bound.args, bound.kwargs
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Flow Utilities
|
|
2
|
+
|
|
3
|
+
Helper functions for orchestrating complex flows on top of Prefect. These tools
|
|
4
|
+
complement `Sequence` and `ProcessModel` classes by providing branching,
|
|
5
|
+
conditional execution, and data-movement utilities while preserving the
|
|
6
|
+
framework philosophy of ordered Prefect flows executing `Step` tasks.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Starter Here
|
|
2
|
+
|
|
3
|
+
## 1) Install (beta)
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pipx install "git+https://github.com/Alberto-Manzoni/PyTestFlow.git@v0.2.0-beta.1"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Source repositories:
|
|
10
|
+
- Engine: https://github.com/Alberto-Manzoni/PyTestFlow
|
|
11
|
+
- Frontend: https://github.com/Alberto-Manzoni/PyTestFlow-FrontEnd
|
|
12
|
+
|
|
13
|
+
## 2) Start backend + GUI host
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pytestflow-am start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Optional (open browser automatically):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pytestflow-am start --open
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 3) Open GUI
|
|
26
|
+
|
|
27
|
+
Go to `http://127.0.0.1:8000/`.
|
|
28
|
+
|
|
29
|
+
## 4) Run the motherboard sequence
|
|
30
|
+
|
|
31
|
+
In the GUI:
|
|
32
|
+
1. Select the motherboard test sequence.
|
|
33
|
+
2. Launch the run.
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
- The backend exposes WebSocket on `ws://127.0.0.1:8765`.
|
|
38
|
+
- If frontend assets are in a local folder, set `PYTESTFLOW_FRONTEND_DIR` to that folder (must contain `index.html`).
|
|
39
|
+
- If you need this guide in your current folder, run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pytestflow-am quickstart
|
|
43
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# PyTestFlow Steps
|
|
2
|
+
|
|
3
|
+
The `pytestflow.steps` package provides ready-made decorators layered on top of
|
|
4
|
+
`pytestflow.core.step`.
|
|
5
|
+
|
|
6
|
+
See the [project README](../../readme.md) for overview usage.
|
|
7
|
+
|
|
8
|
+
## Available step decorators
|
|
9
|
+
|
|
10
|
+
| Decorator | Import path | Purpose |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `numeric_limit_step` | `pytestflow.steps` | Validate one numeric value against a limit/range. |
|
|
13
|
+
| `df_numeric_limits_step` | `pytestflow.steps` | Apply numeric limits to multiple values (dict/series). |
|
|
14
|
+
| `pass_fail_step` | `pytestflow.steps` | Convert boolean outcome into pass/fail state. |
|
|
15
|
+
| `string_check_step` | `pytestflow.steps` | Compare string output with expected value/match mode. |
|
|
16
|
+
| `waveform_limit_step` | `pytestflow.steps` | Validate waveform points against lower/upper masks. |
|
|
17
|
+
| `flow_control_step` | `pytestflow.steps` | Publish control metadata and steer sequence transitions. |
|
|
18
|
+
| `action_step` | `pytestflow.steps.action_step` | Run side-effect actions and optionally `store_as` in context. |
|
|
19
|
+
| `message_pop_up_step` | `pytestflow.steps.message_pop_up` | Trigger frontend-driven popup interactions. |
|
|
20
|
+
| `msgbox_step_qt` | `pytestflow.steps.utility` | Show local Qt message dialog and capture input. |
|
|
21
|
+
|
|
22
|
+
## Creating custom steps
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pytestflow.core import step
|
|
26
|
+
from pytestflow.core.pytestflow_states import PyTestflowDone
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def my_step(name=None):
|
|
30
|
+
def decorator(fn):
|
|
31
|
+
@step(name=name or fn.__name__)
|
|
32
|
+
def wrapper(*args, **kwargs):
|
|
33
|
+
data = fn(*args, **kwargs)
|
|
34
|
+
return PyTestflowDone(ptf_result={"step_status": "done", "output": data})
|
|
35
|
+
return wrapper
|
|
36
|
+
return decorator
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Best practices
|
|
40
|
+
|
|
41
|
+
- Keep step functions small and deterministic when possible.
|
|
42
|
+
- Return `PyTestflowState` subclasses for expected test outcomes.
|
|
43
|
+
- Use `store_as` and `ptf_context.locals` for explicit handoff between steps.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .numeric_limit import numeric_limit_step
|
|
2
|
+
from .string_check import string_check_step
|
|
3
|
+
from .waveform_limit import waveform_limit_step
|
|
4
|
+
from .df_numeric_limits import df_numeric_limits_step
|
|
5
|
+
from .pass_fail import pass_fail_step
|
|
6
|
+
from .flow_control import flow_control_step
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"numeric_limit_step",
|
|
10
|
+
"string_check_step",
|
|
11
|
+
"waveform_limit_step",
|
|
12
|
+
"df_numeric_limits_step",
|
|
13
|
+
"pass_fail_step",
|
|
14
|
+
"flow_control_step",
|
|
15
|
+
]
|