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.
- pyprodtest-0.1.0/LICENSE +21 -0
- pyprodtest-0.1.0/PKG-INFO +141 -0
- pyprodtest-0.1.0/README.md +116 -0
- pyprodtest-0.1.0/pyproject.toml +58 -0
- pyprodtest-0.1.0/setup.cfg +4 -0
- pyprodtest-0.1.0/src/_pyprodtest/__init__.py +0 -0
- pyprodtest-0.1.0/src/_pyprodtest/config.py +163 -0
- pyprodtest-0.1.0/src/_pyprodtest/decorator.py +47 -0
- pyprodtest-0.1.0/src/_pyprodtest/hooks.py +284 -0
- pyprodtest-0.1.0/src/_pyprodtest/input_acceptors.py +63 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/__init__.py +9 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/csv_report/__init__.py +5 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/csv_report/csv_observer.py +67 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/html_report/__init__.py +5 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/html_report/html_observer.py +69 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/html_report/templates/report.html +131 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/json_report/__init__.py +5 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/json_report/json_observer.py +48 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/pdf_report/__init__.py +5 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/pdf_report/pdf_observer.py +334 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/test_observer.py +24 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/__init__.py +5 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/app.py +41 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/input_acceptor.py +24 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/observer.py +23 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/server.py +71 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/state.py +106 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/static/app.js +290 -0
- pyprodtest-0.1.0/src/_pyprodtest/observers/web_ui/templates/index.html +52 -0
- pyprodtest-0.1.0/src/_pyprodtest/report_settings.py +23 -0
- pyprodtest-0.1.0/src/_pyprodtest/test_record.py +45 -0
- pyprodtest-0.1.0/src/_pyprodtest/web_assets/__init__.py +1 -0
- pyprodtest-0.1.0/src/_pyprodtest/web_assets/pico.min.css +4 -0
- pyprodtest-0.1.0/src/_pyprodtest/web_assets/theme.css +247 -0
- pyprodtest-0.1.0/src/pyprodtest/__init__.py +11 -0
- pyprodtest-0.1.0/src/pyprodtest.egg-info/PKG-INFO +141 -0
- pyprodtest-0.1.0/src/pyprodtest.egg-info/SOURCES.txt +44 -0
- pyprodtest-0.1.0/src/pyprodtest.egg-info/dependency_links.txt +1 -0
- pyprodtest-0.1.0/src/pyprodtest.egg-info/entry_points.txt +2 -0
- pyprodtest-0.1.0/src/pyprodtest.egg-info/requires.txt +5 -0
- pyprodtest-0.1.0/src/pyprodtest.egg-info/top_level.txt +2 -0
- pyprodtest-0.1.0/test/test_decorator.py +18 -0
- pyprodtest-0.1.0/test/test_hooks.py +171 -0
- pyprodtest-0.1.0/test/test_input_acceptors.py +19 -0
- pyprodtest-0.1.0/test/test_observers.py +133 -0
- pyprodtest-0.1.0/test/test_web_ui.py +165 -0
pyprodtest-0.1.0/LICENSE
ADDED
|
@@ -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"
|
|
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
|