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,94 @@
|
|
|
1
|
+
# pytestflow/steps/action_step.py
|
|
2
|
+
|
|
3
|
+
from prefect.artifacts import create_markdown_artifact
|
|
4
|
+
from pytestflow.core.core import StepWrapper
|
|
5
|
+
from pytestflow.core.pytestflow_states import PyTestflowDone, PyTestflowError
|
|
6
|
+
from pytestflow.core.utils import get_data_for_gui
|
|
7
|
+
from pytestflow.steps.common import get_metadata_from_prefect_context
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ActionStep(StepWrapper):
|
|
13
|
+
def __init__(self, fn, *, name=None, store_as=None, autowire=True, **task_kwargs):
|
|
14
|
+
super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
|
|
15
|
+
self.store_as = store_as
|
|
16
|
+
self.step_type = "action"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run(self, *args, **kwargs):
|
|
20
|
+
from pytestflow.core.context import ptf_context
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
# Execute user code (inside the Prefect task created by StepWrapper)
|
|
24
|
+
result = super()._run(*args, **kwargs)
|
|
25
|
+
|
|
26
|
+
# Optionally store the result in the context
|
|
27
|
+
if self.store_as:
|
|
28
|
+
ptf_context.locals[self.store_as] = result
|
|
29
|
+
|
|
30
|
+
result_data = {
|
|
31
|
+
"step_status": "done",
|
|
32
|
+
"output": result,
|
|
33
|
+
"step_type": self.step_type,
|
|
34
|
+
"pytestflow_timestamp": datetime.utcnow().isoformat(),
|
|
35
|
+
"store_as": self.store_as,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result_data.update(self.get_meta_info())
|
|
39
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
40
|
+
|
|
41
|
+
# Send End data to GUI
|
|
42
|
+
get_data_for_gui(self, result_data.get("end_time"), result_data)
|
|
43
|
+
|
|
44
|
+
# Prefect UI artifact (best-effort)
|
|
45
|
+
try:
|
|
46
|
+
safe_step_name = re.sub(r"[^a-z0-9\\-]", "-", self.name.lower())
|
|
47
|
+
artifact_md = (
|
|
48
|
+
f"### Step: `{self.name}`\n"
|
|
49
|
+
f"- **Output:** `{result}`\n"
|
|
50
|
+
f"- **Stored as:** `{self.store_as or 'None'}`\n"
|
|
51
|
+
f"- **Status:** ✅ DONE"
|
|
52
|
+
)
|
|
53
|
+
create_markdown_artifact(
|
|
54
|
+
key=f"result-{safe_step_name}",
|
|
55
|
+
markdown=artifact_md,
|
|
56
|
+
description=f"Action result for `{self.name}`",
|
|
57
|
+
)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
return PyTestflowDone(
|
|
62
|
+
ptf_result=result_data,
|
|
63
|
+
result=result,
|
|
64
|
+
message="Action step completed",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
result_data = {
|
|
69
|
+
"step_status": "error",
|
|
70
|
+
"error": str(e),
|
|
71
|
+
"step_type": "action",
|
|
72
|
+
"pytestflow_timestamp": datetime.utcnow().isoformat(),
|
|
73
|
+
"store_as": self.store_as,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
result_data.update(self.get_meta_info())
|
|
77
|
+
# optional: include Prefect metadata even on error (useful)
|
|
78
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
79
|
+
|
|
80
|
+
return PyTestflowError(
|
|
81
|
+
ptf_result=result_data,
|
|
82
|
+
message=f"Action step `{self.name}` failed",
|
|
83
|
+
result=None,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def action_step(*, name=None, store_as=None, autowire=True, **task_kwargs):
|
|
88
|
+
"""
|
|
89
|
+
Decorator factory for action steps.
|
|
90
|
+
"""
|
|
91
|
+
def decorator(fn):
|
|
92
|
+
# IMPORTANT: already task-wrapped by StepWrapper
|
|
93
|
+
return ActionStep(fn, name=name, store_as=store_as, autowire=autowire, **task_kwargs)
|
|
94
|
+
return decorator
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from prefect.context import get_run_context
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
def safe_getattr(obj, attr, default=None):
|
|
5
|
+
try:
|
|
6
|
+
return getattr(obj, attr, default)
|
|
7
|
+
except Exception:
|
|
8
|
+
return default
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_metadata_from_prefect_context():
|
|
12
|
+
metadata = {
|
|
13
|
+
"flow_run_id": None,
|
|
14
|
+
"flow_name": None,
|
|
15
|
+
"task_run_id": None,
|
|
16
|
+
"task_name": None,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# 1) Get task metadata from run context (works well inside tasks)
|
|
20
|
+
try:
|
|
21
|
+
run_ctx = get_run_context()
|
|
22
|
+
task_run = safe_getattr(run_ctx, "task_run", None)
|
|
23
|
+
|
|
24
|
+
metadata["task_run_id"] = safe_getattr(task_run, "id")
|
|
25
|
+
metadata["task_name"] = safe_getattr(task_run, "name")
|
|
26
|
+
except RuntimeError:
|
|
27
|
+
warnings.warn(
|
|
28
|
+
"Prefect context is unavailable. Metadata enrichment is skipped.",
|
|
29
|
+
RuntimeWarning,
|
|
30
|
+
)
|
|
31
|
+
return metadata
|
|
32
|
+
|
|
33
|
+
# 2) Get flow metadata from Prefect runtime (most reliable)
|
|
34
|
+
try:
|
|
35
|
+
from prefect.runtime import flow_run
|
|
36
|
+
metadata["flow_run_id"] = safe_getattr(flow_run, "id")
|
|
37
|
+
metadata["flow_name"] = safe_getattr(flow_run, "name")
|
|
38
|
+
except Exception:
|
|
39
|
+
# 3) Fallback: try flow_run from context if present
|
|
40
|
+
try:
|
|
41
|
+
run_ctx = get_run_context()
|
|
42
|
+
flow_run_obj = safe_getattr(run_ctx, "flow_run", None)
|
|
43
|
+
metadata["flow_run_id"] = metadata["flow_run_id"] or safe_getattr(flow_run_obj, "id")
|
|
44
|
+
metadata["flow_name"] = metadata["flow_name"] or safe_getattr(flow_run_obj, "name")
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
return metadata
|
|
49
|
+
|
|
50
|
+
def get_runtime_value(value):
|
|
51
|
+
return value() if callable(value) else value
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from prefect.artifacts import create_markdown_artifact
|
|
2
|
+
from pytestflow.core.core import StepWrapper
|
|
3
|
+
from pytestflow.core.pytestflow_states import PyTestflowPassed, PyTestflowFailed
|
|
4
|
+
from pytestflow.core.utils import get_data_for_gui
|
|
5
|
+
from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
|
|
6
|
+
from typing import Callable
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DFNumericLimitsStep(StepWrapper):
|
|
12
|
+
def __init__(self, fn, *, limits_df: pd.DataFrame, name=None, autowire=True, **task_kwargs):
|
|
13
|
+
super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
|
|
14
|
+
self.limits_df = limits_df
|
|
15
|
+
self.step_type = "df_numeric_limits"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _run(self, *args, **kwargs):
|
|
19
|
+
values = super()._run(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
limits_df = get_runtime_value(self.limits_df)
|
|
22
|
+
|
|
23
|
+
if not isinstance(values, (dict, pd.Series)):
|
|
24
|
+
raise TypeError("Return value must be dict or pd.Series")
|
|
25
|
+
|
|
26
|
+
results = []
|
|
27
|
+
overall_status = "passed"
|
|
28
|
+
|
|
29
|
+
for _, row in limits_df.iterrows():
|
|
30
|
+
varname = row["name"]
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
val = values[varname]
|
|
34
|
+
limit = row["limit"]
|
|
35
|
+
mode = row["mode"]
|
|
36
|
+
|
|
37
|
+
if mode == "ge":
|
|
38
|
+
status = "passed" if val >= limit else "failed"
|
|
39
|
+
|
|
40
|
+
elif mode == "le":
|
|
41
|
+
status = "passed" if val <= limit else "failed"
|
|
42
|
+
|
|
43
|
+
elif mode == "between":
|
|
44
|
+
low, high = limit
|
|
45
|
+
status = "passed" if low <= val <= high else "failed"
|
|
46
|
+
|
|
47
|
+
elif mode == "outside":
|
|
48
|
+
low, high = limit
|
|
49
|
+
status = "passed" if val < low or val > high else "failed"
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError(f"Unsupported mode: {mode}")
|
|
53
|
+
|
|
54
|
+
if status != "passed":
|
|
55
|
+
overall_status = "failed"
|
|
56
|
+
|
|
57
|
+
result = {
|
|
58
|
+
"name": varname,
|
|
59
|
+
"value": val,
|
|
60
|
+
"limit": limit,
|
|
61
|
+
"mode": mode,
|
|
62
|
+
"step_status": status,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
result = {
|
|
67
|
+
"name": varname,
|
|
68
|
+
"step_status": "error",
|
|
69
|
+
"error": str(e),
|
|
70
|
+
}
|
|
71
|
+
overall_status = "failed"
|
|
72
|
+
|
|
73
|
+
results.append(result)
|
|
74
|
+
|
|
75
|
+
result_data = {
|
|
76
|
+
"step_status": overall_status,
|
|
77
|
+
"output": values,
|
|
78
|
+
"per_variable": results,
|
|
79
|
+
"step_type": self.step_type,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result_data.update(self.get_meta_info())
|
|
83
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
84
|
+
|
|
85
|
+
# Send End data to GUI
|
|
86
|
+
get_data_for_gui(self, result_data.get("end_time"), result_data)
|
|
87
|
+
|
|
88
|
+
# Prefect UI artifact (best-effort)
|
|
89
|
+
try:
|
|
90
|
+
safe_step_name = re.sub(r"[^a-z0-9\-]", "-", self.name.lower())
|
|
91
|
+
|
|
92
|
+
md_lines = [
|
|
93
|
+
f"### Step: `{self.name}`",
|
|
94
|
+
f"- **Status:** {'✅ PASSED' if overall_status == 'passed' else '❌ FAILED'}",
|
|
95
|
+
"",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
if overall_status == "failed":
|
|
99
|
+
failed_results = [r for r in results if r.get("step_status") != "passed"]
|
|
100
|
+
|
|
101
|
+
md_lines += [
|
|
102
|
+
f"**{len(failed_results)}** variable(s) failed limits.",
|
|
103
|
+
"",
|
|
104
|
+
"| Variable | Value | Limit | Mode | Status |",
|
|
105
|
+
"|---|---|---|---|---|",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
for r in failed_results[:10]:
|
|
109
|
+
md_lines.append(
|
|
110
|
+
f"| {r.get('name')} | {r.get('value', 'NA')} | {r.get('limit', 'NA')} | "
|
|
111
|
+
f"{r.get('mode', 'NA')} | {r.get('step_status', 'error')} |"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if len(failed_results) > 10:
|
|
115
|
+
md_lines.append(
|
|
116
|
+
f"\n… and **{len(failed_results) - 10} more**. See full report for details."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
create_markdown_artifact(
|
|
120
|
+
key=f"result-{safe_step_name}",
|
|
121
|
+
markdown="\n".join(md_lines),
|
|
122
|
+
description=f"DataFrame numeric limits for `{self.name}`",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
PyTestflowPassed(ptf_result=result_data)
|
|
130
|
+
if overall_status == "passed"
|
|
131
|
+
else PyTestflowFailed(
|
|
132
|
+
ptf_result=result_data,
|
|
133
|
+
message=f"{self.name} failed"
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def df_numeric_limits_step(*, limits_df: pd.DataFrame, name=None, autowire=True, **task_kwargs):
|
|
139
|
+
"""
|
|
140
|
+
Decorator factory for DataFrame numeric limit steps.
|
|
141
|
+
"""
|
|
142
|
+
def decorator(fn: Callable):
|
|
143
|
+
# IMPORTANT: already task-wrapped by StepWrapper
|
|
144
|
+
return DFNumericLimitsStep(
|
|
145
|
+
fn,
|
|
146
|
+
limits_df=limits_df,
|
|
147
|
+
name=name,
|
|
148
|
+
autowire=autowire,
|
|
149
|
+
**task_kwargs,
|
|
150
|
+
)
|
|
151
|
+
return decorator
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from pytestflow.core.context import ptf_context
|
|
5
|
+
from pytestflow.core.core import StepWrapper
|
|
6
|
+
from pytestflow.core.pytestflow_states import PyTestflowDone, PyTestflowError
|
|
7
|
+
from pytestflow.core.utils import get_data_for_gui
|
|
8
|
+
from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FlowControlStep(StepWrapper):
|
|
12
|
+
def __init__(self, fn, *, next_steps={0:"next", 1:"next"}, name=None, store_as=None, autowire=True, **task_kwargs):
|
|
13
|
+
super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
|
|
14
|
+
self.next_steps = next_steps
|
|
15
|
+
self.store_as = store_as
|
|
16
|
+
self.step_type = "flow_control"
|
|
17
|
+
|
|
18
|
+
def _run(self, *args, **kwargs):
|
|
19
|
+
try:
|
|
20
|
+
result = super()._run(*args, **kwargs)
|
|
21
|
+
|
|
22
|
+
next_steps_map = get_runtime_value(self.next_steps)
|
|
23
|
+
store_as = get_runtime_value(self.store_as)
|
|
24
|
+
|
|
25
|
+
if store_as:
|
|
26
|
+
ptf_context.locals[store_as] = result
|
|
27
|
+
|
|
28
|
+
actual_next_steps = next_steps_map.get(result, "next")
|
|
29
|
+
ptf_context.locals["_ptf_next_step"] = actual_next_steps
|
|
30
|
+
|
|
31
|
+
result_data = {
|
|
32
|
+
"step_status": "done",
|
|
33
|
+
"output": result,
|
|
34
|
+
"step_type": self.step_type,
|
|
35
|
+
"pytestflow_timestamp": datetime.utcnow().isoformat(),
|
|
36
|
+
"store_as": store_as,
|
|
37
|
+
"flow_control_next_steps": next_steps_map,
|
|
38
|
+
"selected_next_step": actual_next_steps,
|
|
39
|
+
"retry_n": ptf_context.locals.get("retry_n"),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
result_data.update(self.get_meta_info())
|
|
43
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
44
|
+
|
|
45
|
+
get_data_for_gui(self, result_data.get("end_time"), result_data)
|
|
46
|
+
|
|
47
|
+
return PyTestflowDone(
|
|
48
|
+
ptf_result=result_data,
|
|
49
|
+
message="Flow control step completed",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
result_data = {
|
|
54
|
+
"step_status": "error",
|
|
55
|
+
"error": str(exc),
|
|
56
|
+
"step_type": self.step_type,
|
|
57
|
+
"pytestflow_timestamp": datetime.utcnow().isoformat(),
|
|
58
|
+
"store_as": get_runtime_value(self.store_as),
|
|
59
|
+
"retry_n": ptf_context.locals.get("retry_n"),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
result_data.update(self.get_meta_info())
|
|
63
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
64
|
+
|
|
65
|
+
return PyTestflowError(
|
|
66
|
+
ptf_result=result_data,
|
|
67
|
+
message=f"Flow control step `{self.name}` failed",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def flow_control_step(*, next_steps={0:"next", 1:"next"}, name=None, store_as=None, autowire=True, **task_kwargs):
|
|
72
|
+
"""
|
|
73
|
+
Decorator factory for control-only steps that steer sequence execution.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def decorator(fn: Callable):
|
|
77
|
+
return FlowControlStep(
|
|
78
|
+
fn,
|
|
79
|
+
name=name,
|
|
80
|
+
next_steps=next_steps,
|
|
81
|
+
store_as=store_as,
|
|
82
|
+
autowire=autowire,
|
|
83
|
+
**task_kwargs,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return decorator
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from pytestflow.core.core import StepWrapper
|
|
2
|
+
from pytestflow.core.pytestflow_states import PyTestflowDone
|
|
3
|
+
from pytestflow.core.utils import get_data_for_gui
|
|
4
|
+
from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from pytestflow.core.runtime_control import runtime_control
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessagePopUpStep(StepWrapper):
|
|
10
|
+
def __init__(self, fn, *, show_response_box=False, title="", msg="", buttons=[], name=None, store_as=None, autowire=True, **task_kwargs):
|
|
11
|
+
super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
|
|
12
|
+
self.step_type = "message_pop_up"
|
|
13
|
+
self.show_response_box = show_response_box
|
|
14
|
+
self.title = title
|
|
15
|
+
self.msg = msg
|
|
16
|
+
self.buttons = buttons
|
|
17
|
+
self.store_as = store_as
|
|
18
|
+
|
|
19
|
+
def _run(self, *args, **kwargs):
|
|
20
|
+
# Executes inside the Prefect task created by StepWrapper
|
|
21
|
+
value = super()._run(*args, **kwargs)
|
|
22
|
+
|
|
23
|
+
show_response_box = get_runtime_value(self.show_response_box)
|
|
24
|
+
title = get_runtime_value(self.title)
|
|
25
|
+
msg = get_runtime_value(self.msg)
|
|
26
|
+
buttons = get_runtime_value(self.buttons)
|
|
27
|
+
store_as = get_runtime_value(self.store_as)
|
|
28
|
+
|
|
29
|
+
popup_data = {
|
|
30
|
+
"isResponseBoxVisible": show_response_box,
|
|
31
|
+
"title": title,
|
|
32
|
+
"msg": msg,
|
|
33
|
+
"buttons": buttons,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
user_response = runtime_control.show_popup(popup_data)
|
|
37
|
+
|
|
38
|
+
if store_as:
|
|
39
|
+
from pytestflow.core.context import ptf_context
|
|
40
|
+
ptf_context.locals[store_as] = user_response
|
|
41
|
+
|
|
42
|
+
result_data = {
|
|
43
|
+
"step_status": "done",
|
|
44
|
+
"step_type": self.step_type,
|
|
45
|
+
"user_response": user_response,
|
|
46
|
+
"show_response_box": show_response_box,
|
|
47
|
+
"title": title,
|
|
48
|
+
"msg": msg,
|
|
49
|
+
"buttons": buttons,
|
|
50
|
+
"store_as": store_as,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result_data.update(self.get_meta_info())
|
|
54
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
55
|
+
|
|
56
|
+
# Send End data to GUI
|
|
57
|
+
get_data_for_gui(self, result_data.get("end_time"), result_data)
|
|
58
|
+
|
|
59
|
+
return PyTestflowDone(ptf_result=result_data)
|
|
60
|
+
def message_pop_up_step(*, show_response_box=False, title="TITLE_HERE", msg="MSG_HERE", buttons=["Ok"], name=None, store_as=None, autowire=True, **task_kwargs):
|
|
61
|
+
"""
|
|
62
|
+
Decorator factory for message pop-up steps.
|
|
63
|
+
"""
|
|
64
|
+
def decorator(fn: Callable):
|
|
65
|
+
# IMPORTANT: already task-wrapped by StepWrapper
|
|
66
|
+
return MessagePopUpStep(
|
|
67
|
+
fn,
|
|
68
|
+
show_response_box=show_response_box,
|
|
69
|
+
title=title,
|
|
70
|
+
msg=msg,
|
|
71
|
+
buttons=buttons,
|
|
72
|
+
name=name,
|
|
73
|
+
store_as=store_as,
|
|
74
|
+
autowire=autowire,
|
|
75
|
+
**task_kwargs)
|
|
76
|
+
return decorator
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# numeric_limit_step.py
|
|
2
|
+
from pytestflow.core.core import StepWrapper
|
|
3
|
+
from pytestflow.core.pytestflow_states import PyTestflowPassed, PyTestflowFailed
|
|
4
|
+
from pytestflow.core.utils import get_data_for_gui
|
|
5
|
+
from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
|
|
6
|
+
from typing import Callable
|
|
7
|
+
import re
|
|
8
|
+
from prefect.artifacts import create_markdown_artifact
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NumericLimitStep(StepWrapper):
|
|
12
|
+
def __init__(self, fn, *, limit, mode, name=None, store_as=None, autowire=True, **task_kwargs):
|
|
13
|
+
super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
|
|
14
|
+
self.limit = limit
|
|
15
|
+
self.mode = mode
|
|
16
|
+
self.store_as = store_as
|
|
17
|
+
self.step_type = "numeric_limit"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _run(self, *args, **kwargs):
|
|
21
|
+
from pytestflow.core.context import ptf_context
|
|
22
|
+
|
|
23
|
+
value = super()._run(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
limit = get_runtime_value(self.limit)
|
|
26
|
+
mode = get_runtime_value(self.mode)
|
|
27
|
+
store_as = get_runtime_value(self.store_as)
|
|
28
|
+
|
|
29
|
+
if not isinstance(value, (int, float)):
|
|
30
|
+
raise TypeError("Return value must be numeric")
|
|
31
|
+
|
|
32
|
+
if mode == "ge":
|
|
33
|
+
passed = value >= limit
|
|
34
|
+
|
|
35
|
+
elif mode == "le":
|
|
36
|
+
passed = value <= limit
|
|
37
|
+
|
|
38
|
+
elif mode == "between":
|
|
39
|
+
assert isinstance(limit, (list, tuple)) and len(limit) == 2
|
|
40
|
+
passed = limit[0] <= value <= limit[1]
|
|
41
|
+
|
|
42
|
+
elif mode == "outside":
|
|
43
|
+
assert isinstance(limit, (list, tuple)) and len(limit) == 2
|
|
44
|
+
passed = value < limit[0] or value > limit[1]
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"Unsupported mode: {mode}")
|
|
48
|
+
|
|
49
|
+
if store_as:
|
|
50
|
+
ptf_context.locals[store_as] = value
|
|
51
|
+
|
|
52
|
+
result_data = {
|
|
53
|
+
"measurement": value,
|
|
54
|
+
"limit": limit,
|
|
55
|
+
"mode": mode,
|
|
56
|
+
"store_as": store_as,
|
|
57
|
+
"step_status": "passed" if passed else "failed",
|
|
58
|
+
"output": value,
|
|
59
|
+
"step_type": self.step_type,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
result_data.update(self.get_meta_info())
|
|
63
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
64
|
+
|
|
65
|
+
# Send End data to GUI
|
|
66
|
+
get_data_for_gui(self, result_data.get("end_time"), result_data)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
safe_step_name = re.sub(r"[^a-z0-9\-]", "-", self.name.lower())
|
|
70
|
+
|
|
71
|
+
artifact_md = (
|
|
72
|
+
f"### Step: `{self.name}`\n"
|
|
73
|
+
f"- **Measurement:** `{value}`\n"
|
|
74
|
+
f"- **Limit ({mode}):** `{limit}`\n"
|
|
75
|
+
f"- **Status:** {'✅ PASSED' if passed else '❌ FAILED'}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
create_markdown_artifact(
|
|
79
|
+
key=f"result-{safe_step_name}",
|
|
80
|
+
markdown=artifact_md,
|
|
81
|
+
description=f"Numeric limit check for `{self.name}`",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
except RuntimeError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
PyTestflowPassed(ptf_result=result_data)
|
|
89
|
+
if passed
|
|
90
|
+
else PyTestflowFailed(
|
|
91
|
+
ptf_result=result_data,
|
|
92
|
+
message=f"{self.name} failed"
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def numeric_limit_step(*, limit, mode="ge", name=None, store_as=None, autowire=True, **task_kwargs):
|
|
98
|
+
def decorator(fn: Callable):
|
|
99
|
+
# IMPORTANT: return the step instance (already task-wrapped by StepWrapper)
|
|
100
|
+
return NumericLimitStep(
|
|
101
|
+
fn,
|
|
102
|
+
limit=limit,
|
|
103
|
+
mode=mode,
|
|
104
|
+
name=name,
|
|
105
|
+
store_as=store_as,
|
|
106
|
+
autowire=autowire,
|
|
107
|
+
**task_kwargs,
|
|
108
|
+
)
|
|
109
|
+
return decorator
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from pytestflow.core.core import StepWrapper
|
|
2
|
+
from pytestflow.core.pytestflow_states import PyTestflowPassed, PyTestflowFailed
|
|
3
|
+
from pytestflow.core.utils import get_data_for_gui
|
|
4
|
+
from pytestflow.steps.common import get_metadata_from_prefect_context
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PassFailStep(StepWrapper):
|
|
9
|
+
def __init__(self, fn, *, name=None, autowire=True, **task_kwargs):
|
|
10
|
+
super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
|
|
11
|
+
self.step_type = "pass_fail"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run(self, *args, **kwargs):
|
|
15
|
+
# Executes inside the Prefect task created by StepWrapper
|
|
16
|
+
value = super()._run(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
passed = bool(value)
|
|
19
|
+
|
|
20
|
+
result_data = {
|
|
21
|
+
"step_status": "passed" if passed else "failed",
|
|
22
|
+
"output": value,
|
|
23
|
+
"step_type": self.step_type,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
result_data.update(self.get_meta_info())
|
|
27
|
+
result_data.update(get_metadata_from_prefect_context())
|
|
28
|
+
|
|
29
|
+
# Send End data to GUI
|
|
30
|
+
get_data_for_gui(self, result_data.get("end_time"), result_data)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
PyTestflowPassed(ptf_result=result_data)
|
|
34
|
+
if passed
|
|
35
|
+
else PyTestflowFailed(
|
|
36
|
+
ptf_result=result_data,
|
|
37
|
+
message=f"{self.name} failed"
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def pass_fail_step(*, name=None, autowire=True, **task_kwargs):
|
|
43
|
+
"""
|
|
44
|
+
Decorator factory for pass/fail steps.
|
|
45
|
+
"""
|
|
46
|
+
def decorator(fn: Callable):
|
|
47
|
+
# IMPORTANT: already task-wrapped by StepWrapper
|
|
48
|
+
return PassFailStep(fn, name=name, autowire=autowire, **task_kwargs)
|
|
49
|
+
return decorator
|