reporting-labs 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.
- reporting_labs-0.1.0/LICENSE +21 -0
- reporting_labs-0.1.0/PKG-INFO +163 -0
- reporting_labs-0.1.0/README.md +127 -0
- reporting_labs-0.1.0/pyproject.toml +50 -0
- reporting_labs-0.1.0/setup.cfg +4 -0
- reporting_labs-0.1.0/src/reporting_labs/__init__.py +163 -0
- reporting_labs-0.1.0/src/reporting_labs/_version.py +1 -0
- reporting_labs-0.1.0/src/reporting_labs/capture/__init__.py +0 -0
- reporting_labs-0.1.0/src/reporting_labs/capture/http.py +181 -0
- reporting_labs-0.1.0/src/reporting_labs/core/__init__.py +0 -0
- reporting_labs-0.1.0/src/reporting_labs/core/collector.py +391 -0
- reporting_labs-0.1.0/src/reporting_labs/core/context.py +44 -0
- reporting_labs-0.1.0/src/reporting_labs/core/envdetect.py +194 -0
- reporting_labs-0.1.0/src/reporting_labs/core/explain.py +414 -0
- reporting_labs-0.1.0/src/reporting_labs/core/history.py +45 -0
- reporting_labs-0.1.0/src/reporting_labs/core/logo.py +26 -0
- reporting_labs-0.1.0/src/reporting_labs/core/mask.py +263 -0
- reporting_labs-0.1.0/src/reporting_labs/core/model.py +175 -0
- reporting_labs-0.1.0/src/reporting_labs/core/options.py +230 -0
- reporting_labs-0.1.0/src/reporting_labs/core/render.py +78 -0
- reporting_labs-0.1.0/src/reporting_labs/core/writer.py +79 -0
- reporting_labs-0.1.0/src/reporting_labs/playwright_support.py +448 -0
- reporting_labs-0.1.0/src/reporting_labs/py.typed +0 -0
- reporting_labs-0.1.0/src/reporting_labs/pytest_plugin.py +407 -0
- reporting_labs-0.1.0/src/reporting_labs/robot/__init__.py +3 -0
- reporting_labs-0.1.0/src/reporting_labs/robot/listener.py +293 -0
- reporting_labs-0.1.0/src/reporting_labs/selenium_support.py +234 -0
- reporting_labs-0.1.0/src/reporting_labs/template.html +1694 -0
- reporting_labs-0.1.0/src/reporting_labs/transport.py +64 -0
- reporting_labs-0.1.0/src/reporting_labs.egg-info/PKG-INFO +163 -0
- reporting_labs-0.1.0/src/reporting_labs.egg-info/SOURCES.txt +38 -0
- reporting_labs-0.1.0/src/reporting_labs.egg-info/dependency_links.txt +1 -0
- reporting_labs-0.1.0/src/reporting_labs.egg-info/entry_points.txt +2 -0
- reporting_labs-0.1.0/src/reporting_labs.egg-info/requires.txt +14 -0
- reporting_labs-0.1.0/src/reporting_labs.egg-info/top_level.txt +1 -0
- reporting_labs-0.1.0/tests/test_collector.py +83 -0
- reporting_labs-0.1.0/tests/test_explain.py +91 -0
- reporting_labs-0.1.0/tests/test_mask.py +81 -0
- reporting_labs-0.1.0/tests/test_options_env.py +63 -0
- reporting_labs-0.1.0/tests/test_plugin.py +111 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Naveen Khunteta
|
|
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,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reporting-labs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A beautiful test report in one HTML file, for pytest, Playwright, Selenium and Robot Framework. It tells you what broke, who owns it, and whether it is new.
|
|
5
|
+
Author-email: Naveen Khunteta <naveenanimation20@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://reportinglabs.dev
|
|
8
|
+
Project-URL: Documentation, https://reportinglabs.dev/get-started/python
|
|
9
|
+
Project-URL: Repository, https://github.com/naveenautomationlabs/reporting-labs-python
|
|
10
|
+
Project-URL: Issues, https://github.com/naveenautomationlabs/reporting-labs-python/issues
|
|
11
|
+
Keywords: pytest,playwright,selenium,robotframework,test-report,html-report,reporting,allure-alternative
|
|
12
|
+
Classifier: Framework :: Pytest
|
|
13
|
+
Classifier: Framework :: Robot Framework
|
|
14
|
+
Classifier: Framework :: Robot Framework :: Tool
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: pytest
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "pytest"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-rerunfailures>=12.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-timeout>=2.1; extra == "dev"
|
|
30
|
+
Requires-Dist: requests>=2.25; extra == "dev"
|
|
31
|
+
Requires-Dist: httpx>=0.23; extra == "dev"
|
|
32
|
+
Requires-Dist: playwright>=1.40; extra == "dev"
|
|
33
|
+
Requires-Dist: selenium>=4.10; extra == "dev"
|
|
34
|
+
Requires-Dist: robotframework>=6.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# reportingLabs for Python
|
|
38
|
+
|
|
39
|
+
**One beautiful HTML report for pytest, Playwright, Selenium and Robot Framework.** It tells you what broke,
|
|
40
|
+
who owns it, and whether it is new. The whole report is a single self-contained HTML file: no server, no
|
|
41
|
+
upload, no login. Open it in a browser, attach it to a CI job, or drop it in Slack.
|
|
42
|
+
|
|
43
|
+
The report is byte-for-byte the same layout as the [Node.js](https://github.com/naveenautomationlabs/reporting-labs)
|
|
44
|
+
and [Java](https://github.com/naveenautomationlabs/reporting-labs-java) reporters, so one triage habit works
|
|
45
|
+
across a whole company. Nothing from Node is needed at run time: the report is built entirely in Python.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install reporting-labs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
That is all. The pytest plugin turns on the moment the package is installed. For Robot Framework, add one
|
|
54
|
+
listener flag (below).
|
|
55
|
+
|
|
56
|
+
## pytest
|
|
57
|
+
|
|
58
|
+
Run your tests as you always do:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pytest
|
|
62
|
+
open reporting-labs/index.html # run again to see the trend and flaky history
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Add detail with a few optional helpers:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import pytest
|
|
69
|
+
from reporting_labs import meta, log, test_data, step, api, attach
|
|
70
|
+
|
|
71
|
+
@pytest.mark.meta(priority="P1", owner="asha", feature="checkout", story="SHOP-231")
|
|
72
|
+
def test_checkout(page):
|
|
73
|
+
test_data({"username": "demo", "password": "S3cret"}, "Login") # sensitive keys are masked
|
|
74
|
+
log("cart total before coupons: 99.00")
|
|
75
|
+
with step("Apply coupon"):
|
|
76
|
+
...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| What you get | How |
|
|
80
|
+
|---|---|
|
|
81
|
+
| A row per test: passed, failed, skipped (with the reason), xfail / xpass | automatic |
|
|
82
|
+
| Flaky, with every attempt | `pytest-rerunfailures` |
|
|
83
|
+
| Timed out | `pytest-timeout` |
|
|
84
|
+
| Parallel runs merged into one report | `pytest-xdist` (`-n auto`) |
|
|
85
|
+
| Priority, owner, feature charts and filters | `@pytest.mark.meta(...)` or `meta(...)` |
|
|
86
|
+
| Every `requests` / `httpx` call in the API tab | automatic |
|
|
87
|
+
| Captured stdout / stderr, with secrets masked | automatic |
|
|
88
|
+
|
|
89
|
+
Turn it off for a run with `-p no:reporting_labs` or `--no-rl`.
|
|
90
|
+
|
|
91
|
+
## Playwright
|
|
92
|
+
|
|
93
|
+
Install `pytest-playwright` and write tests as usual. Every action, every `expect()` and a screenshot of the
|
|
94
|
+
failing page land in the report with no extra code. pytest-playwright's trace and video are attached too:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pytest --tracing retain-on-failure --video retain-on-failure
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The sync and async APIs both work. See `examples/pytest-playwright`.
|
|
101
|
+
|
|
102
|
+
## Selenium
|
|
103
|
+
|
|
104
|
+
Install `selenium` and write tests as usual. Every driver command (open, find, click, type, run script,
|
|
105
|
+
perform actions, Appium taps) becomes a step, with the element named by the locator that found it, and a
|
|
106
|
+
screenshot of the browser is attached when a test fails. No wrappers. See `examples/pytest-selenium`.
|
|
107
|
+
|
|
108
|
+
## Robot Framework
|
|
109
|
+
|
|
110
|
+
Add one listener flag:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
robot --listener reporting_labs.RobotListener tests/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
One row per test case, suites as the path, every keyword a step, setup and teardown as hooks, tags as
|
|
117
|
+
filters, and the screenshots SeleniumLibrary or the Browser library embed in the log attached to the test.
|
|
118
|
+
Options go after a colon: `--listener reporting_labs.RobotListener:title=Checkout:output=reports/rl`.
|
|
119
|
+
See `examples/robot-selenium`.
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
Every option is optional. Put them in `reporting-labs.config.json` next to where you run, or under
|
|
124
|
+
`[tool.reporting-labs]` in `pyproject.toml`. Keys may be written `camelCase` (as in the Node reporter) or
|
|
125
|
+
`snake_case`.
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"title": "Checkout regression",
|
|
130
|
+
"project": { "name": "ShopLite", "version": "2.4.0", "team": "QA Platform" },
|
|
131
|
+
"metadata": { "env": "local" },
|
|
132
|
+
"logo": "logo.png",
|
|
133
|
+
"palette": "lab",
|
|
134
|
+
"links": { "story": "https://acme.atlassian.net/browse/{id}" },
|
|
135
|
+
"maskKeys": ["otp", "pan"],
|
|
136
|
+
"history": { "enabled": true, "keep": 30 }
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Common options: `title`, `logo`, `accent`, `theme` (`auto` / `light` / `dark`), `palette`
|
|
141
|
+
(`lab` / `ocean` / `ember` / `mono`), `outputFolder`, `metadata`, `project`, `env`, `dimensions`,
|
|
142
|
+
`links`, `maskKeys`, `maskValues`, `maskFromEnv`, `history`, `open` (`on-failure` / `always` / `never`).
|
|
143
|
+
|
|
144
|
+
**The environment chip** is found for you from `ENV`, `TEST_ENV`, `APP_ENV`, `TARGET_ENV`,
|
|
145
|
+
`CI_ENVIRONMENT_NAME`, or any variable ending in `_ENV`, so a config that says `local` still labels the
|
|
146
|
+
pipeline's reports `dev`, `qa`, `stage`. Point it at your own variable with `"envVar": "TARGET"`.
|
|
147
|
+
|
|
148
|
+
**Runtime overrides**, read from the environment and winning over the config, so CI can label a run without
|
|
149
|
+
touching the file: `REPORTING_LABS_METADATA_<KEY>` sets a header chip
|
|
150
|
+
(`REPORTING_LABS_METADATA_ENV=qa`), and `REPORTING_LABS_TITLE`, `_THEME`, `_PALETTE`, `_ACCENT`, `_LOGO`
|
|
151
|
+
set the matching option.
|
|
152
|
+
|
|
153
|
+
## Secrets are masked
|
|
154
|
+
|
|
155
|
+
Passwords, tokens, cookies, auth headers, API keys (Stripe, GitHub, AWS, Google, GitLab, npm, SendGrid),
|
|
156
|
+
JWTs and Luhn-valid card numbers are blanked in logs, request and response bodies, error messages, test
|
|
157
|
+
data and step titles. A value seen once as a secret (`password=...`, a `PASSWORD` env var, or `maskValues`)
|
|
158
|
+
is blanked everywhere it later appears. `maskKeys` adds your own keys; `maskFromEnv` (on by default) learns
|
|
159
|
+
the values of `PASSWORD` / `*_TOKEN` / `*_SECRET` environment variables.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT. The report is a file you own; nothing leaves your machine or your CI.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# reportingLabs for Python
|
|
2
|
+
|
|
3
|
+
**One beautiful HTML report for pytest, Playwright, Selenium and Robot Framework.** It tells you what broke,
|
|
4
|
+
who owns it, and whether it is new. The whole report is a single self-contained HTML file: no server, no
|
|
5
|
+
upload, no login. Open it in a browser, attach it to a CI job, or drop it in Slack.
|
|
6
|
+
|
|
7
|
+
The report is byte-for-byte the same layout as the [Node.js](https://github.com/naveenautomationlabs/reporting-labs)
|
|
8
|
+
and [Java](https://github.com/naveenautomationlabs/reporting-labs-java) reporters, so one triage habit works
|
|
9
|
+
across a whole company. Nothing from Node is needed at run time: the report is built entirely in Python.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install reporting-labs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That is all. The pytest plugin turns on the moment the package is installed. For Robot Framework, add one
|
|
18
|
+
listener flag (below).
|
|
19
|
+
|
|
20
|
+
## pytest
|
|
21
|
+
|
|
22
|
+
Run your tests as you always do:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pytest
|
|
26
|
+
open reporting-labs/index.html # run again to see the trend and flaky history
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Add detail with a few optional helpers:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import pytest
|
|
33
|
+
from reporting_labs import meta, log, test_data, step, api, attach
|
|
34
|
+
|
|
35
|
+
@pytest.mark.meta(priority="P1", owner="asha", feature="checkout", story="SHOP-231")
|
|
36
|
+
def test_checkout(page):
|
|
37
|
+
test_data({"username": "demo", "password": "S3cret"}, "Login") # sensitive keys are masked
|
|
38
|
+
log("cart total before coupons: 99.00")
|
|
39
|
+
with step("Apply coupon"):
|
|
40
|
+
...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| What you get | How |
|
|
44
|
+
|---|---|
|
|
45
|
+
| A row per test: passed, failed, skipped (with the reason), xfail / xpass | automatic |
|
|
46
|
+
| Flaky, with every attempt | `pytest-rerunfailures` |
|
|
47
|
+
| Timed out | `pytest-timeout` |
|
|
48
|
+
| Parallel runs merged into one report | `pytest-xdist` (`-n auto`) |
|
|
49
|
+
| Priority, owner, feature charts and filters | `@pytest.mark.meta(...)` or `meta(...)` |
|
|
50
|
+
| Every `requests` / `httpx` call in the API tab | automatic |
|
|
51
|
+
| Captured stdout / stderr, with secrets masked | automatic |
|
|
52
|
+
|
|
53
|
+
Turn it off for a run with `-p no:reporting_labs` or `--no-rl`.
|
|
54
|
+
|
|
55
|
+
## Playwright
|
|
56
|
+
|
|
57
|
+
Install `pytest-playwright` and write tests as usual. Every action, every `expect()` and a screenshot of the
|
|
58
|
+
failing page land in the report with no extra code. pytest-playwright's trace and video are attached too:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pytest --tracing retain-on-failure --video retain-on-failure
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The sync and async APIs both work. See `examples/pytest-playwright`.
|
|
65
|
+
|
|
66
|
+
## Selenium
|
|
67
|
+
|
|
68
|
+
Install `selenium` and write tests as usual. Every driver command (open, find, click, type, run script,
|
|
69
|
+
perform actions, Appium taps) becomes a step, with the element named by the locator that found it, and a
|
|
70
|
+
screenshot of the browser is attached when a test fails. No wrappers. See `examples/pytest-selenium`.
|
|
71
|
+
|
|
72
|
+
## Robot Framework
|
|
73
|
+
|
|
74
|
+
Add one listener flag:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
robot --listener reporting_labs.RobotListener tests/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
One row per test case, suites as the path, every keyword a step, setup and teardown as hooks, tags as
|
|
81
|
+
filters, and the screenshots SeleniumLibrary or the Browser library embed in the log attached to the test.
|
|
82
|
+
Options go after a colon: `--listener reporting_labs.RobotListener:title=Checkout:output=reports/rl`.
|
|
83
|
+
See `examples/robot-selenium`.
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
Every option is optional. Put them in `reporting-labs.config.json` next to where you run, or under
|
|
88
|
+
`[tool.reporting-labs]` in `pyproject.toml`. Keys may be written `camelCase` (as in the Node reporter) or
|
|
89
|
+
`snake_case`.
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"title": "Checkout regression",
|
|
94
|
+
"project": { "name": "ShopLite", "version": "2.4.0", "team": "QA Platform" },
|
|
95
|
+
"metadata": { "env": "local" },
|
|
96
|
+
"logo": "logo.png",
|
|
97
|
+
"palette": "lab",
|
|
98
|
+
"links": { "story": "https://acme.atlassian.net/browse/{id}" },
|
|
99
|
+
"maskKeys": ["otp", "pan"],
|
|
100
|
+
"history": { "enabled": true, "keep": 30 }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Common options: `title`, `logo`, `accent`, `theme` (`auto` / `light` / `dark`), `palette`
|
|
105
|
+
(`lab` / `ocean` / `ember` / `mono`), `outputFolder`, `metadata`, `project`, `env`, `dimensions`,
|
|
106
|
+
`links`, `maskKeys`, `maskValues`, `maskFromEnv`, `history`, `open` (`on-failure` / `always` / `never`).
|
|
107
|
+
|
|
108
|
+
**The environment chip** is found for you from `ENV`, `TEST_ENV`, `APP_ENV`, `TARGET_ENV`,
|
|
109
|
+
`CI_ENVIRONMENT_NAME`, or any variable ending in `_ENV`, so a config that says `local` still labels the
|
|
110
|
+
pipeline's reports `dev`, `qa`, `stage`. Point it at your own variable with `"envVar": "TARGET"`.
|
|
111
|
+
|
|
112
|
+
**Runtime overrides**, read from the environment and winning over the config, so CI can label a run without
|
|
113
|
+
touching the file: `REPORTING_LABS_METADATA_<KEY>` sets a header chip
|
|
114
|
+
(`REPORTING_LABS_METADATA_ENV=qa`), and `REPORTING_LABS_TITLE`, `_THEME`, `_PALETTE`, `_ACCENT`, `_LOGO`
|
|
115
|
+
set the matching option.
|
|
116
|
+
|
|
117
|
+
## Secrets are masked
|
|
118
|
+
|
|
119
|
+
Passwords, tokens, cookies, auth headers, API keys (Stripe, GitHub, AWS, Google, GitLab, npm, SendGrid),
|
|
120
|
+
JWTs and Luhn-valid card numbers are blanked in logs, request and response bodies, error messages, test
|
|
121
|
+
data and step titles. A value seen once as a secret (`password=...`, a `PASSWORD` env var, or `maskValues`)
|
|
122
|
+
is blanked everywhere it later appears. `maskKeys` adds your own keys; `maskFromEnv` (on by default) learns
|
|
123
|
+
the values of `PASSWORD` / `*_TOKEN` / `*_SECRET` environment variables.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT. The report is a file you own; nothing leaves your machine or your CI.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reporting-labs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A beautiful test report in one HTML file, for pytest, Playwright, Selenium and Robot Framework. It tells you what broke, who owns it, and whether it is new."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Naveen Khunteta", email = "naveenanimation20@gmail.com" }]
|
|
13
|
+
keywords = ["pytest", "playwright", "selenium", "robotframework", "test-report", "html-report", "reporting", "allure-alternative"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Framework :: Pytest",
|
|
16
|
+
"Framework :: Robot Framework",
|
|
17
|
+
"Framework :: Robot Framework :: Tool",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Topic :: Software Development :: Testing",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
pytest = ["pytest>=7.0"]
|
|
28
|
+
dev = ["pytest>=7.0", "pytest-xdist>=3.0", "pytest-rerunfailures>=12.0", "pytest-timeout>=2.1", "requests>=2.25", "httpx>=0.23", "playwright>=1.40", "selenium>=4.10", "robotframework>=6.0"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://reportinglabs.dev"
|
|
32
|
+
Documentation = "https://reportinglabs.dev/get-started/python"
|
|
33
|
+
Repository = "https://github.com/naveenautomationlabs/reporting-labs-python"
|
|
34
|
+
Issues = "https://github.com/naveenautomationlabs/reporting-labs-python/issues"
|
|
35
|
+
|
|
36
|
+
[project.entry-points.pytest11]
|
|
37
|
+
reporting_labs = "reporting_labs.pytest_plugin"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.dynamic]
|
|
40
|
+
version = { attr = "reporting_labs._version.__version__" }
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
reporting_labs = ["template.html", "py.typed"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
addopts = "-p no:reporting_labs"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""reportingLabs for Python: one HTML report for pytest, Playwright, Selenium and Robot Framework.
|
|
2
|
+
|
|
3
|
+
Helpers to call from a test:
|
|
4
|
+
|
|
5
|
+
from reporting_labs import meta, log, test_data, step, api, attach
|
|
6
|
+
|
|
7
|
+
meta(priority="P1", owner="asha", feature="checkout")
|
|
8
|
+
log("cart total before coupons: 99.00")
|
|
9
|
+
test_data({"username": "demo", "password": "x"}, "Login")
|
|
10
|
+
with step("Apply coupon"):
|
|
11
|
+
...
|
|
12
|
+
api(method="GET", url="/v1/orders", status=200, duration=120, response_body=body)
|
|
13
|
+
attach("screenshot", png_bytes, "image/png")
|
|
14
|
+
|
|
15
|
+
Secrets (password=..., Bearer ..., tokens) are masked in the report.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import functools
|
|
20
|
+
import inspect
|
|
21
|
+
import json
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from typing import Any, Callable, Dict, Iterator, Optional, Union
|
|
24
|
+
|
|
25
|
+
from ._version import __version__
|
|
26
|
+
from .core import context
|
|
27
|
+
from .core.model import ErrorInfo, now_ms
|
|
28
|
+
|
|
29
|
+
__all__ = ["meta", "log", "test_data", "step", "api", "attach", "RobotListener", "__version__"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def __getattr__(name: str) -> Any:
|
|
33
|
+
# `robot --listener reporting_labs.RobotListener`: imported lazily so a pytest project never loads Robot code
|
|
34
|
+
if name == "RobotListener":
|
|
35
|
+
from .robot.listener import ReportingLabsListener
|
|
36
|
+
return ReportingLabsListener
|
|
37
|
+
raise AttributeError(name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def meta(**values: Any) -> None:
|
|
41
|
+
"""Attach report metadata to the current test: priority, severity, owner, feature, epic, story, issue... or any key.
|
|
42
|
+
A value can be a list (one chip per value) or a dict (the fields of a multi-parameter link, see `links`)."""
|
|
43
|
+
a = context.current()
|
|
44
|
+
if a is None:
|
|
45
|
+
return
|
|
46
|
+
for k, v in values.items():
|
|
47
|
+
if v is not None:
|
|
48
|
+
a.meta[str(k).lower()] = v
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def log(message: Any, *rest: Any) -> None:
|
|
52
|
+
"""Add a timestamped log line to the current test. Lines containing "error"/"fail" show red, "warn" amber."""
|
|
53
|
+
a = context.current()
|
|
54
|
+
if a is None:
|
|
55
|
+
return
|
|
56
|
+
parts = [str(message)] + [r if isinstance(r, str) else json.dumps(r, default=str) for r in rest]
|
|
57
|
+
a.log(" ".join(parts))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_data(data: Any, name: str = "Test data") -> None:
|
|
61
|
+
"""Attach the data this test uses: a dict (key/value block), a list of dicts (table) or a CSV string (table).
|
|
62
|
+
Sensitive keys are masked."""
|
|
63
|
+
a = context.current()
|
|
64
|
+
if a is not None:
|
|
65
|
+
a.test_data(data, name)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def api(method: str, url: str, status: Optional[int] = None, duration: Optional[float] = None, name: Optional[str] = None,
|
|
69
|
+
request_headers: Optional[Dict[str, str]] = None, request_body: Any = None,
|
|
70
|
+
response_headers: Optional[Dict[str, str]] = None, response_body: Any = None) -> None:
|
|
71
|
+
"""Record an API call by hand. requests and httpx calls are recorded automatically."""
|
|
72
|
+
a = context.current()
|
|
73
|
+
if a is None:
|
|
74
|
+
return
|
|
75
|
+
call: Dict[str, Any] = {"method": str(method).upper(), "url": str(url)}
|
|
76
|
+
if status is not None:
|
|
77
|
+
call["status"] = int(status)
|
|
78
|
+
if duration is not None:
|
|
79
|
+
call["duration"] = float(duration)
|
|
80
|
+
if name:
|
|
81
|
+
call["name"] = name
|
|
82
|
+
if request_headers:
|
|
83
|
+
call["requestHeaders"] = dict(request_headers)
|
|
84
|
+
if request_body is not None:
|
|
85
|
+
call["requestBody"] = request_body
|
|
86
|
+
if response_headers:
|
|
87
|
+
call["responseHeaders"] = dict(response_headers)
|
|
88
|
+
if response_body is not None:
|
|
89
|
+
call["responseBody"] = response_body
|
|
90
|
+
a.api_call(call)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def attach(name: str, body: Union[bytes, str, None] = None, content_type: Optional[str] = None, path: Optional[str] = None) -> None:
|
|
94
|
+
"""Attach a file or bytes to the current test: a screenshot, a downloaded PDF, a log. Images are shown inline."""
|
|
95
|
+
a = context.current()
|
|
96
|
+
if a is None:
|
|
97
|
+
return
|
|
98
|
+
if isinstance(body, str):
|
|
99
|
+
body = body.encode("utf-8")
|
|
100
|
+
content_type = content_type or "text/plain"
|
|
101
|
+
if content_type is None:
|
|
102
|
+
ext = (path or name).lower().rsplit(".", 1)[-1] if "." in (path or name) else ""
|
|
103
|
+
content_type = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "webp": "image/webp", "gif": "image/gif", "webm": "video/webm",
|
|
104
|
+
"mp4": "video/mp4", "txt": "text/plain", "log": "text/plain", "json": "application/json", "html": "text/html", "csv": "text/csv",
|
|
105
|
+
"zip": "application/zip", "pdf": "application/pdf"}.get(ext, "application/octet-stream")
|
|
106
|
+
a.attach(name, body, content_type, path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
test_data.__test__ = False # pytest must not collect the helper when a test module imports it
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _StepContext:
|
|
113
|
+
"""`with step("title"):` and `@step("title")` on a function (sync or async)."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, title: str, category: str = "test.step") -> None:
|
|
116
|
+
self.title, self.category = title, category
|
|
117
|
+
self._st = None
|
|
118
|
+
self._attempt = None
|
|
119
|
+
|
|
120
|
+
def __enter__(self) -> "_StepContext":
|
|
121
|
+
self._attempt = context.current()
|
|
122
|
+
if self._attempt is not None:
|
|
123
|
+
self._st = self._attempt.begin_step(self.title, self.category)
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def __exit__(self, et, ev, tb) -> bool:
|
|
127
|
+
if self._attempt is not None and self._st is not None:
|
|
128
|
+
self._attempt.end_step(self._st, f"{et.__name__}: {ev}" if et else None)
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
async def __aenter__(self) -> "_StepContext":
|
|
132
|
+
return self.__enter__()
|
|
133
|
+
|
|
134
|
+
async def __aexit__(self, et, ev, tb) -> bool:
|
|
135
|
+
return self.__exit__(et, ev, tb)
|
|
136
|
+
|
|
137
|
+
def __call__(self, fn: Callable) -> Callable:
|
|
138
|
+
if inspect.iscoroutinefunction(fn):
|
|
139
|
+
@functools.wraps(fn)
|
|
140
|
+
async def aw(*args: Any, **kw: Any) -> Any:
|
|
141
|
+
async with _StepContext(self.title or fn.__name__, self.category):
|
|
142
|
+
return await fn(*args, **kw)
|
|
143
|
+
return aw
|
|
144
|
+
|
|
145
|
+
@functools.wraps(fn)
|
|
146
|
+
def w(*args: Any, **kw: Any) -> Any:
|
|
147
|
+
with _StepContext(self.title or fn.__name__, self.category):
|
|
148
|
+
return fn(*args, **kw)
|
|
149
|
+
return w
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def step(title: Union[str, Callable, None] = None, category: str = "test.step") -> Any:
|
|
153
|
+
"""A named step in the report. Use as a context manager or a decorator; steps nest.
|
|
154
|
+
|
|
155
|
+
with step("Log in as admin"):
|
|
156
|
+
page.fill("#user", "admin")
|
|
157
|
+
|
|
158
|
+
@step("Open the cart")
|
|
159
|
+
def open_cart(page): ...
|
|
160
|
+
"""
|
|
161
|
+
if callable(title):
|
|
162
|
+
return _StepContext(title.__name__, category)(title)
|
|
163
|
+
return _StepContext(str(title or "step"), category)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|