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.
Files changed (63) hide show
  1. bootstrap_templates/__init__.py +0 -0
  2. bootstrap_templates/config.yaml +13 -0
  3. bootstrap_templates/custom_step_types/__init__.py +0 -0
  4. bootstrap_templates/custom_step_types/custom_step_template.py +7 -0
  5. bootstrap_templates/process_models/__init__.py +0 -0
  6. bootstrap_templates/process_models/reporting/README.md +48 -0
  7. bootstrap_templates/process_models/reporting/__init__.py +0 -0
  8. bootstrap_templates/process_models/reporting/default_report.html.j2 +122 -0
  9. bootstrap_templates/process_models/reporting/html_report.py +331 -0
  10. bootstrap_templates/process_models/sequential_model.py +141 -0
  11. bootstrap_templates/test_sequences/__init__.py +0 -0
  12. bootstrap_templates/test_sequences/basic_sequence.py +62 -0
  13. bootstrap_templates/test_sequences/message_box_and_flow_control.py +169 -0
  14. bootstrap_templates/test_sequences/motherboard_test_sequence.py +125 -0
  15. bootstrap_templates/test_sequences/step_types_quickstart.py +168 -0
  16. pytestflow/README.md +13 -0
  17. pytestflow/__init__.py +19 -0
  18. pytestflow/backend/__init__.py +2 -0
  19. pytestflow/backend/event_bus.py +27 -0
  20. pytestflow/backend/frontend/assets/full_logo-D1DRTUt8.svg +21 -0
  21. pytestflow/backend/frontend/assets/index-480TOyh4.js +2 -0
  22. pytestflow/backend/frontend/assets/index-qEI3VAQU.css +1 -0
  23. pytestflow/backend/frontend/index.html +14 -0
  24. pytestflow/backend/frontend/logo.svg +21 -0
  25. pytestflow/backend/handlers.py +214 -0
  26. pytestflow/backend/report_manager.py +15 -0
  27. pytestflow/backend/sequences_info.py +130 -0
  28. pytestflow/backend/start_backend.py +118 -0
  29. pytestflow/backend/uuids_handler.py +67 -0
  30. pytestflow/backend/websocket_gateway.py +91 -0
  31. pytestflow/cli.py +183 -0
  32. pytestflow/config/__init__.py +0 -0
  33. pytestflow/config/config_manager.py +44 -0
  34. pytestflow/core/README.md +110 -0
  35. pytestflow/core/__init__.py +15 -0
  36. pytestflow/core/context.py +41 -0
  37. pytestflow/core/core.py +112 -0
  38. pytestflow/core/pytestflow_states.py +88 -0
  39. pytestflow/core/runtime_control.py +164 -0
  40. pytestflow/core/seq_file_runner.py +38 -0
  41. pytestflow/core/sequence.py +404 -0
  42. pytestflow/core/utils.py +81 -0
  43. pytestflow/flow_utils/README.md +6 -0
  44. pytestflow/flow_utils/__init__.py +0 -0
  45. pytestflow/flow_utils/conditions.py +0 -0
  46. pytestflow/flow_utils/transitions.py +0 -0
  47. pytestflow/starter_here.md +43 -0
  48. pytestflow/steps/README.md +43 -0
  49. pytestflow/steps/__init__.py +15 -0
  50. pytestflow/steps/action_step.py +94 -0
  51. pytestflow/steps/common.py +51 -0
  52. pytestflow/steps/df_numeric_limits.py +151 -0
  53. pytestflow/steps/flow_control.py +86 -0
  54. pytestflow/steps/message_pop_up.py +76 -0
  55. pytestflow/steps/numeric_limit.py +109 -0
  56. pytestflow/steps/pass_fail.py +49 -0
  57. pytestflow/steps/string_check.py +104 -0
  58. pytestflow/steps/waveform_limit.py +170 -0
  59. pytestflow-0.2.0.dist-info/METADATA +73 -0
  60. pytestflow-0.2.0.dist-info/RECORD +63 -0
  61. pytestflow-0.2.0.dist-info/WHEEL +5 -0
  62. pytestflow-0.2.0.dist-info/entry_points.txt +2 -0
  63. 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
@@ -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
+ ]