pyprodtest 0.1.0__tar.gz

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 (46) hide show
  1. pyprodtest-0.1.0/LICENSE +21 -0
  2. pyprodtest-0.1.0/PKG-INFO +141 -0
  3. pyprodtest-0.1.0/README.md +116 -0
  4. pyprodtest-0.1.0/pyproject.toml +58 -0
  5. pyprodtest-0.1.0/setup.cfg +4 -0
  6. pyprodtest-0.1.0/src/_pyprodtest/__init__.py +0 -0
  7. pyprodtest-0.1.0/src/_pyprodtest/config.py +163 -0
  8. pyprodtest-0.1.0/src/_pyprodtest/decorator.py +47 -0
  9. pyprodtest-0.1.0/src/_pyprodtest/hooks.py +284 -0
  10. pyprodtest-0.1.0/src/_pyprodtest/input_acceptors.py +63 -0
  11. pyprodtest-0.1.0/src/_pyprodtest/observers/__init__.py +9 -0
  12. pyprodtest-0.1.0/src/_pyprodtest/observers/csv_report/__init__.py +5 -0
  13. pyprodtest-0.1.0/src/_pyprodtest/observers/csv_report/csv_observer.py +67 -0
  14. pyprodtest-0.1.0/src/_pyprodtest/observers/html_report/__init__.py +5 -0
  15. pyprodtest-0.1.0/src/_pyprodtest/observers/html_report/html_observer.py +69 -0
  16. pyprodtest-0.1.0/src/_pyprodtest/observers/html_report/templates/report.html +131 -0
  17. pyprodtest-0.1.0/src/_pyprodtest/observers/json_report/__init__.py +5 -0
  18. pyprodtest-0.1.0/src/_pyprodtest/observers/json_report/json_observer.py +48 -0
  19. pyprodtest-0.1.0/src/_pyprodtest/observers/pdf_report/__init__.py +5 -0
  20. pyprodtest-0.1.0/src/_pyprodtest/observers/pdf_report/pdf_observer.py +334 -0
  21. pyprodtest-0.1.0/src/_pyprodtest/observers/test_observer.py +24 -0
  22. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/__init__.py +5 -0
  23. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/app.py +41 -0
  24. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/input_acceptor.py +24 -0
  25. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/observer.py +23 -0
  26. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/server.py +71 -0
  27. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/state.py +106 -0
  28. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/static/app.js +290 -0
  29. pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/templates/index.html +52 -0
  30. pyprodtest-0.1.0/src/_pyprodtest/report_settings.py +23 -0
  31. pyprodtest-0.1.0/src/_pyprodtest/test_record.py +45 -0
  32. pyprodtest-0.1.0/src/_pyprodtest/web_assets/__init__.py +1 -0
  33. pyprodtest-0.1.0/src/_pyprodtest/web_assets/pico.min.css +4 -0
  34. pyprodtest-0.1.0/src/_pyprodtest/web_assets/theme.css +247 -0
  35. pyprodtest-0.1.0/src/pyprodtest/__init__.py +11 -0
  36. pyprodtest-0.1.0/src/pyprodtest.egg-info/PKG-INFO +141 -0
  37. pyprodtest-0.1.0/src/pyprodtest.egg-info/SOURCES.txt +44 -0
  38. pyprodtest-0.1.0/src/pyprodtest.egg-info/dependency_links.txt +1 -0
  39. pyprodtest-0.1.0/src/pyprodtest.egg-info/entry_points.txt +2 -0
  40. pyprodtest-0.1.0/src/pyprodtest.egg-info/requires.txt +5 -0
  41. pyprodtest-0.1.0/src/pyprodtest.egg-info/top_level.txt +2 -0
  42. pyprodtest-0.1.0/test/test_decorator.py +18 -0
  43. pyprodtest-0.1.0/test/test_hooks.py +171 -0
  44. pyprodtest-0.1.0/test/test_input_acceptors.py +19 -0
  45. pyprodtest-0.1.0/test/test_observers.py +133 -0
  46. pyprodtest-0.1.0/test/test_web_ui.py +165 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jaydan Cowell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyprodtest
3
+ Version: 0.1.0
4
+ Summary: Pytest plugin for production testing
5
+ Author: Jaydan Cowell
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jaydanc/pyprodtest
8
+ Project-URL: Documentation, https://github.com/jaydanc/pyprodtest#readme
9
+ Project-URL: Issues, https://github.com/jaydanc/pyprodtest/issues
10
+ Project-URL: Repository, https://github.com/jaydanc/pyprodtest.git
11
+ Keywords: production-testing,pytest,test-automation
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.13
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: flask>=3.1.2
20
+ Requires-Dist: jinja2>=3.1.6
21
+ Requires-Dist: pyyaml>=6.0.2
22
+ Requires-Dist: pytest>=8.4.2
23
+ Requires-Dist: reportlab>=5.0.1
24
+ Dynamic: license-file
25
+
26
+ # PyProdTest
27
+
28
+ PyProdTest is an early-stage pytest plugin for running production tests. It adds
29
+ test metadata, operator prompts, a live browser view, ordered test plans, and
30
+ HTML, JSON, CSV, and PDF reports.
31
+
32
+ See the [architecture notes](doc/design.md) for the current design and known
33
+ diagram gaps. The earlier prototype is available on the
34
+ [`prototype` branch](https://github.com/jaydanc/pyprodtest/tree/prototype).
35
+
36
+ ## Add to a uv project
37
+
38
+ From an existing Python 3.13+ uv project, add the published package:
39
+
40
+ ```powershell
41
+ uv add pyprodtest
42
+ uv run pytest
43
+ ```
44
+
45
+ pytest discovers the installed plugin through its `pytest11` entry point, so no
46
+ extra plugin flag is needed. Until the first PyPI release, add a checkout
47
+ directly instead:
48
+
49
+ ```powershell
50
+ uv add "pyprodtest @ git+https://github.com/jaydanc/pyprodtest.git"
51
+ ```
52
+
53
+ The live operator page opens automatically at `http://127.0.0.1:8765`. At the
54
+ end of the run, PyProdTest writes timestamped HTML, JSON, CSV, and PDF reports.
55
+
56
+ ## Add production-test metadata
57
+
58
+ Decorators are optional and may be combined:
59
+
60
+ ```python
61
+ from pyprodtest import info, req, step
62
+
63
+
64
+ @info("Status light", "Confirm the device is ready")
65
+ @req("REQ-42")
66
+ @step("Check that the status light is green")
67
+ def test_status_light(input) -> None:
68
+ assert input("Is the status light green?", bool)
69
+ ```
70
+
71
+ Use `input(prompt)` for text or `input(prompt, bool)` for a yes/no response.
72
+ The live UI handles prompts when enabled; otherwise PyProdTest uses the
73
+ console.
74
+
75
+ ## Configure a run
76
+
77
+ Create `pyprodtest.yaml` in the pytest project root:
78
+
79
+ ```yaml
80
+ name: Device acceptance
81
+
82
+ ui:
83
+ enabled: true
84
+ host: 127.0.0.1
85
+ port: 8765
86
+
87
+ reports:
88
+ output: reports/device-acceptance
89
+ html: true
90
+ json: true
91
+ csv: false
92
+ pdf: true
93
+
94
+ tests:
95
+ - test/integration/serial_test.py
96
+ - test/integration/status_light_test.py
97
+ ```
98
+
99
+ `tests` accepts file paths and pytest node IDs. Only matching tests run, in the
100
+ listed order; an unmatched entry is an error. Use
101
+ `uv run pytest --collect-only -q` to inspect node IDs.
102
+
103
+ The extensionless `reports.output` value receives a session timestamp and each
104
+ enabled report extension. Set `ui.port` to `0` to choose a free port, or set
105
+ `ui.enabled` to `false` for console-only operation.
106
+
107
+ ## Adjust reporting from a test
108
+
109
+ The session-scoped `report` fixture can change the destination or disable all
110
+ reports for the current run:
111
+
112
+ ```python
113
+ def test_identify_device(input, report) -> None:
114
+ serial = input("Device serial number")
115
+ report.path = f"reports/{serial}"
116
+ report.name = "acceptance"
117
+ report.enabled = True
118
+ ```
119
+
120
+ The final fixture values are used when the session ends. Set
121
+ `report.enabled = False` to suppress report generation; the YAML format toggles
122
+ still select which formats are enabled.
123
+
124
+ PyProdTest also streams log messages at `INFO` and above. pytest's
125
+ `--log-cli-level=LEVEL` option overrides that default.
126
+
127
+ ## Development
128
+
129
+ Clone the repository and use its locked `uv` environment:
130
+
131
+ ```powershell
132
+ uv sync
133
+ uv run ruff format --check .
134
+ uv run ruff check .
135
+ uv run pytest -p pyprodtest
136
+ ```
137
+
138
+ To test a local checkout from another uv project, use
139
+ `uv add --editable ../pyprodtest`. Maintainers can create the source and wheel
140
+ distributions with `uv build --no-sources`; publishing requires a configured
141
+ PyPI project and `uv publish` credentials.
@@ -0,0 +1,116 @@
1
+ # PyProdTest
2
+
3
+ PyProdTest is an early-stage pytest plugin for running production tests. It adds
4
+ test metadata, operator prompts, a live browser view, ordered test plans, and
5
+ HTML, JSON, CSV, and PDF reports.
6
+
7
+ See the [architecture notes](doc/design.md) for the current design and known
8
+ diagram gaps. The earlier prototype is available on the
9
+ [`prototype` branch](https://github.com/jaydanc/pyprodtest/tree/prototype).
10
+
11
+ ## Add to a uv project
12
+
13
+ From an existing Python 3.13+ uv project, add the published package:
14
+
15
+ ```powershell
16
+ uv add pyprodtest
17
+ uv run pytest
18
+ ```
19
+
20
+ pytest discovers the installed plugin through its `pytest11` entry point, so no
21
+ extra plugin flag is needed. Until the first PyPI release, add a checkout
22
+ directly instead:
23
+
24
+ ```powershell
25
+ uv add "pyprodtest @ git+https://github.com/jaydanc/pyprodtest.git"
26
+ ```
27
+
28
+ The live operator page opens automatically at `http://127.0.0.1:8765`. At the
29
+ end of the run, PyProdTest writes timestamped HTML, JSON, CSV, and PDF reports.
30
+
31
+ ## Add production-test metadata
32
+
33
+ Decorators are optional and may be combined:
34
+
35
+ ```python
36
+ from pyprodtest import info, req, step
37
+
38
+
39
+ @info("Status light", "Confirm the device is ready")
40
+ @req("REQ-42")
41
+ @step("Check that the status light is green")
42
+ def test_status_light(input) -> None:
43
+ assert input("Is the status light green?", bool)
44
+ ```
45
+
46
+ Use `input(prompt)` for text or `input(prompt, bool)` for a yes/no response.
47
+ The live UI handles prompts when enabled; otherwise PyProdTest uses the
48
+ console.
49
+
50
+ ## Configure a run
51
+
52
+ Create `pyprodtest.yaml` in the pytest project root:
53
+
54
+ ```yaml
55
+ name: Device acceptance
56
+
57
+ ui:
58
+ enabled: true
59
+ host: 127.0.0.1
60
+ port: 8765
61
+
62
+ reports:
63
+ output: reports/device-acceptance
64
+ html: true
65
+ json: true
66
+ csv: false
67
+ pdf: true
68
+
69
+ tests:
70
+ - test/integration/serial_test.py
71
+ - test/integration/status_light_test.py
72
+ ```
73
+
74
+ `tests` accepts file paths and pytest node IDs. Only matching tests run, in the
75
+ listed order; an unmatched entry is an error. Use
76
+ `uv run pytest --collect-only -q` to inspect node IDs.
77
+
78
+ The extensionless `reports.output` value receives a session timestamp and each
79
+ enabled report extension. Set `ui.port` to `0` to choose a free port, or set
80
+ `ui.enabled` to `false` for console-only operation.
81
+
82
+ ## Adjust reporting from a test
83
+
84
+ The session-scoped `report` fixture can change the destination or disable all
85
+ reports for the current run:
86
+
87
+ ```python
88
+ def test_identify_device(input, report) -> None:
89
+ serial = input("Device serial number")
90
+ report.path = f"reports/{serial}"
91
+ report.name = "acceptance"
92
+ report.enabled = True
93
+ ```
94
+
95
+ The final fixture values are used when the session ends. Set
96
+ `report.enabled = False` to suppress report generation; the YAML format toggles
97
+ still select which formats are enabled.
98
+
99
+ PyProdTest also streams log messages at `INFO` and above. pytest's
100
+ `--log-cli-level=LEVEL` option overrides that default.
101
+
102
+ ## Development
103
+
104
+ Clone the repository and use its locked `uv` environment:
105
+
106
+ ```powershell
107
+ uv sync
108
+ uv run ruff format --check .
109
+ uv run ruff check .
110
+ uv run pytest -p pyprodtest
111
+ ```
112
+
113
+ To test a local checkout from another uv project, use
114
+ `uv add --editable ../pyprodtest`. Maintainers can create the source and wheel
115
+ distributions with `uv build --no-sources`; publishing requires a configured
116
+ PyPI project and `uv publish` credentials.
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "pyprodtest"
3
+ version = "0.1.0"
4
+ description = "Pytest plugin for production testing"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [
10
+ { name = "Jaydan Cowell" },
11
+ ]
12
+ keywords = ["production-testing", "pytest", "test-automation"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Framework :: Pytest",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.13",
18
+ ]
19
+ dependencies = [
20
+ "flask>=3.1.2",
21
+ "jinja2>=3.1.6",
22
+ "pyyaml>=6.0.2",
23
+ "pytest>=8.4.2",
24
+ "reportlab>=5.0.1",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/jaydanc/pyprodtest"
29
+ Documentation = "https://github.com/jaydanc/pyprodtest#readme"
30
+ Issues = "https://github.com/jaydanc/pyprodtest/issues"
31
+ Repository = "https://github.com/jaydanc/pyprodtest.git"
32
+
33
+ [build-system]
34
+ requires = ["setuptools>=77"]
35
+ build-backend = "setuptools.build_meta"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "ruff>=0.12.12",
40
+ ]
41
+
42
+ [tool.ruff]
43
+ target-version = "py313"
44
+
45
+ [tool.ruff.lint]
46
+ select = ["E4", "E7", "E9", "F", "I"]
47
+
48
+ [tool.setuptools.package-data]
49
+ "_pyprodtest.observers.html_report" = ["templates/*.html"]
50
+ "_pyprodtest.web_assets" = ["*.css"]
51
+ "_pyprodtest.observers.web_ui" = ["templates/*.html", "static/*.js"]
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
55
+ include = ["pyprodtest*", "_pyprodtest*"]
56
+
57
+ [project.entry-points.pytest11]
58
+ pyprodtest = "_pyprodtest.hooks"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,163 @@
1
+ """Load and validate project configuration from pyprodtest.yaml."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ import yaml
8
+
9
+ DEFAULT_UI_NAME = "Production test execution"
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class UiConfig:
14
+ """Configuration for the live operator UI."""
15
+
16
+ enabled: bool = True
17
+ host: str = "127.0.0.1"
18
+ port: int = 8765
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ReportsConfig:
23
+ """Configuration shared by the final report observers."""
24
+
25
+ output: str = "pyprodtest-report"
26
+ html: bool = True
27
+ json: bool = True
28
+ csv: bool = True
29
+ pdf: bool = True
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class PyProdTestConfig:
34
+ """Validated settings for one PyProdTest session."""
35
+
36
+ name: str = DEFAULT_UI_NAME
37
+ tests: list[str] | None = None
38
+ ui: UiConfig = field(default_factory=UiConfig)
39
+ reports: ReportsConfig = field(default_factory=ReportsConfig)
40
+
41
+
42
+ def load_config(rootpath: Path) -> PyProdTestConfig:
43
+ """Load pyprodtest.yaml from the pytest project root."""
44
+ config_path = rootpath / "pyprodtest.yaml"
45
+ if not config_path.exists():
46
+ return PyProdTestConfig()
47
+
48
+ try:
49
+ document = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
50
+ except yaml.YAMLError as error:
51
+ raise pytest.UsageError(
52
+ f"Invalid PyProdTest configuration {config_path}: {error}"
53
+ ) from error
54
+ if not isinstance(document, dict):
55
+ raise pytest.UsageError(
56
+ f"PyProdTest configuration {config_path} must contain a YAML mapping"
57
+ )
58
+
59
+ name = _non_empty_string(document.get("name", DEFAULT_UI_NAME), "name")
60
+ tests = _load_tests(document.get("tests"))
61
+ ui = _mapping(document.get("ui", {}), "ui")
62
+ reports = _mapping(document.get("reports", {}), "reports")
63
+ return PyProdTestConfig(
64
+ name=name,
65
+ tests=tests,
66
+ ui=UiConfig(
67
+ enabled=_boolean(ui.get("enabled", True), "ui.enabled"),
68
+ host=_non_empty_string(ui.get("host", "127.0.0.1"), "ui.host"),
69
+ port=_port(ui.get("port", 8765)),
70
+ ),
71
+ reports=ReportsConfig(
72
+ output=_non_empty_string(
73
+ reports.get("output", "pyprodtest-report"), "reports.output"
74
+ ),
75
+ html=_boolean(reports.get("html", True), "reports.html"),
76
+ json=_boolean(reports.get("json", True), "reports.json"),
77
+ csv=_boolean(reports.get("csv", True), "reports.csv"),
78
+ pdf=_boolean(reports.get("pdf", True), "reports.pdf"),
79
+ ),
80
+ )
81
+
82
+
83
+ def apply_test_plan(
84
+ config: pytest.Config, items: list[pytest.Item], plan: list[str] | None
85
+ ) -> None:
86
+ """Select and order collected items using the configured test plan."""
87
+ if plan is None:
88
+ return
89
+
90
+ ranked = [(item, _selection_index(item.nodeid, plan)) for item in items]
91
+ matched = {plan[index] for _, index in ranked if index < len(plan)}
92
+ unmatched = [selection for selection in plan if selection not in matched]
93
+ if unmatched:
94
+ formatted = "\n - ".join(unmatched)
95
+ raise pytest.UsageError(
96
+ f"PyProdTest plan entries matched no tests:\n - {formatted}"
97
+ )
98
+
99
+ ranked.sort(key=lambda entry: entry[1])
100
+ selected = [item for item, index in ranked if index < len(plan)]
101
+ deselected = [item for item, index in ranked if index == len(plan)]
102
+ items[:] = selected
103
+ if deselected:
104
+ config.hook.pytest_deselected(items=deselected)
105
+
106
+
107
+ def _load_tests(value: object) -> list[str] | None:
108
+ if value is None:
109
+ return None
110
+ if not isinstance(value, list) or not all(
111
+ isinstance(selection, str) and selection for selection in value
112
+ ):
113
+ raise pytest.UsageError(
114
+ "PyProdTest configuration 'tests' must be a list of paths or node IDs"
115
+ )
116
+ return [selection.replace("\\", "/") for selection in value]
117
+
118
+
119
+ def _mapping(value: object, setting: str) -> dict[str, object]:
120
+ if not isinstance(value, dict):
121
+ raise pytest.UsageError(
122
+ f"PyProdTest configuration '{setting}' must be a mapping"
123
+ )
124
+ return value
125
+
126
+
127
+ def _boolean(value: object, setting: str) -> bool:
128
+ if not isinstance(value, bool):
129
+ raise pytest.UsageError(
130
+ f"PyProdTest configuration '{setting}' must be true or false"
131
+ )
132
+ return value
133
+
134
+
135
+ def _non_empty_string(value: object, setting: str) -> str:
136
+ if not isinstance(value, str) or not value.strip():
137
+ raise pytest.UsageError(
138
+ f"PyProdTest configuration '{setting}' must be a non-empty string"
139
+ )
140
+ return value.strip()
141
+
142
+
143
+ def _port(value: object) -> int:
144
+ if not isinstance(value, int) or isinstance(value, bool) or not 0 <= value <= 65535:
145
+ raise pytest.UsageError(
146
+ "PyProdTest configuration 'ui.port' must be an integer from 0 to 65535"
147
+ )
148
+ return value
149
+
150
+
151
+ def _selection_index(nodeid: str, plan: list[str]) -> int:
152
+ return next(
153
+ (index for index, selection in enumerate(plan) if _matches(nodeid, selection)),
154
+ len(plan),
155
+ )
156
+
157
+
158
+ def _matches(nodeid: str, selection: str) -> bool:
159
+ return (
160
+ nodeid == selection
161
+ or nodeid.startswith(f"{selection}::")
162
+ or nodeid.startswith(f"{selection}[")
163
+ )
@@ -0,0 +1,47 @@
1
+ """
2
+ Additional decorators for PyProdTest tests.
3
+ These decorators add metadata to test functions and are optional.
4
+ """
5
+
6
+
7
+ def info(name, desc):
8
+ """
9
+ Adds name and description metadata to a test function.
10
+ """
11
+
12
+ def wrapper(fn):
13
+ fn.test_meta = getattr(fn, "test_meta", {})
14
+ fn.test_meta["name"] = name
15
+ fn.test_meta["desc"] = desc
16
+ return fn
17
+
18
+ return wrapper
19
+
20
+
21
+ def req(*reqs):
22
+ """
23
+ Adds requirement IDs to the metadata.
24
+ """
25
+
26
+ def wrapper(fn):
27
+ fn.test_meta = getattr(fn, "test_meta", {})
28
+ fn.test_meta["requirements"] = list(reqs)
29
+ return fn
30
+
31
+ return wrapper
32
+
33
+
34
+ def step(*steps):
35
+ """
36
+ Adds step metadata to the metadata
37
+ """
38
+
39
+ def wrapper(fn):
40
+ fn.test_meta = getattr(fn, "test_meta", {})
41
+ existing_steps = fn.test_meta.get("steps", [])
42
+ # Decorators are applied from the bottom upwards, so prepend each
43
+ # group to retain the order in which stacked decorators are written.
44
+ fn.test_meta["steps"] = [*steps, *existing_steps]
45
+ return fn
46
+
47
+ return wrapper