kensho-pytest 0.1.1__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.
- kensho_pytest-0.1.1/PKG-INFO +167 -0
- kensho_pytest-0.1.1/README.md +144 -0
- kensho_pytest-0.1.1/pyproject.toml +42 -0
- kensho_pytest-0.1.1/setup.cfg +4 -0
- kensho_pytest-0.1.1/src/kensho_pytest/__init__.py +223 -0
- kensho_pytest-0.1.1/src/kensho_pytest/_schema.py +237 -0
- kensho_pytest-0.1.1/src/kensho_pytest/_state.py +74 -0
- kensho_pytest-0.1.1/src/kensho_pytest/plugin.py +921 -0
- kensho_pytest-0.1.1/src/kensho_pytest/py.typed +0 -0
- kensho_pytest-0.1.1/src/kensho_pytest.egg-info/PKG-INFO +167 -0
- kensho_pytest-0.1.1/src/kensho_pytest.egg-info/SOURCES.txt +13 -0
- kensho_pytest-0.1.1/src/kensho_pytest.egg-info/dependency_links.txt +1 -0
- kensho_pytest-0.1.1/src/kensho_pytest.egg-info/entry_points.txt +2 -0
- kensho_pytest-0.1.1/src/kensho_pytest.egg-info/requires.txt +1 -0
- kensho_pytest-0.1.1/src/kensho_pytest.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kensho-pytest
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Kensho reporter for pytest — writes kensho-results/ so the Kensho CLI can generate a rich HTML report.
|
|
5
|
+
Author: KaizenReport
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/kaizenreport/kensho
|
|
8
|
+
Project-URL: Source, https://github.com/kaizenreport/kensho
|
|
9
|
+
Keywords: pytest,kensho,kaizenreport,test-report,reporter
|
|
10
|
+
Classifier: Framework :: Pytest
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: pytest>=7.0
|
|
23
|
+
|
|
24
|
+
# kensho-pytest
|
|
25
|
+
|
|
26
|
+
A pytest plugin that emits the canonical [Kensho v1](../schema) JSON format.
|
|
27
|
+
Run your tests, then point the `kensho` CLI at `kensho-results/` to get a
|
|
28
|
+
self-contained static HTML report.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install kensho-pytest
|
|
34
|
+
# or, in this monorepo:
|
|
35
|
+
pip install -e packages/pytest
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The plugin auto-registers via the `pytest11` entry point — no `conftest.py`
|
|
39
|
+
edits required.
|
|
40
|
+
|
|
41
|
+
## Run
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pytest
|
|
45
|
+
# => kensho-results/run.json + kensho-results/cases/*.json
|
|
46
|
+
|
|
47
|
+
# Generate the HTML report (uses the JS CLI from the same monorepo):
|
|
48
|
+
npx kensho generate
|
|
49
|
+
npx kensho open
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## CLI flags
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
--kensho-output PATH Output dir (default: ./kensho-results)
|
|
56
|
+
--kensho-project-name STR Project name in run.json
|
|
57
|
+
--kensho-project-slug STR Project slug (lowercase, alnum + dash/underscore)
|
|
58
|
+
--kensho-run-id STR Override the auto-generated run id
|
|
59
|
+
--kensho-no-severity-from-marks Don't promote @pytest.mark.<severity> to case.severity
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`pytest.ini` / `pyproject.toml` `[tool.pytest.ini_options]` keys are also
|
|
63
|
+
respected: `kensho_output`, `kensho_project_name`, `kensho_project_slug`.
|
|
64
|
+
|
|
65
|
+
## What it produces
|
|
66
|
+
|
|
67
|
+
- `kensho-results/run.json` — run manifest (project, env, totals, timing).
|
|
68
|
+
- `kensho-results/cases/<stableId>.json` — one file per test case.
|
|
69
|
+
- `kensho-results/attachments/<caseId>/...` — files registered via
|
|
70
|
+
`kensho_pytest.attach`.
|
|
71
|
+
|
|
72
|
+
Each case gets a **stable id** (`tc_<16 hex>`) hashed from its `nodeid` +
|
|
73
|
+
file path, so test history correlates across runs and across adapters
|
|
74
|
+
(the JS adapters use the same FNV-1a-based hash).
|
|
75
|
+
|
|
76
|
+
## Markers we read
|
|
77
|
+
|
|
78
|
+
| Marker | Effect |
|
|
79
|
+
| ----------------------------------------------- | --------------------------------------------------------- |
|
|
80
|
+
| `@pytest.mark.severity('critical')` | Sets `case.severity`. Allowed values: `blocker`, `critical`, `normal`, `minor`, `trivial`. |
|
|
81
|
+
| `@pytest.mark.blocker` / `critical` / `normal` / `minor` / `trivial` | Shorthand alias for the above. |
|
|
82
|
+
| `@pytest.mark.feature('Cart')` | Sets `case.behavior.feature`. |
|
|
83
|
+
| `@pytest.mark.epic('Checkout')` | Sets `case.behavior.epic`. |
|
|
84
|
+
| `@pytest.mark.story('Empty cart shows CTA')` | Sets `case.behavior.scenario`. |
|
|
85
|
+
| `@pytest.mark.description('Long-form...')` | Sets `case.description`. |
|
|
86
|
+
| `@pytest.mark.owner('alice')` | Sets `case.owner`. |
|
|
87
|
+
| `@pytest.mark.kensho_label(team='cart')` | Adds free-form `key=value` to `case.labels` (string only). |
|
|
88
|
+
| `@pytest.mark.kensho_link(kind='jira', url='https://…', label='PROJ-123')` | Adds an entry to `case.links`. |
|
|
89
|
+
| `@pytest.mark.parametrize(...)` | Each parametrized case gets `case.parameters[]`. |
|
|
90
|
+
|
|
91
|
+
Inline `@tag` syntax in test names (`def test_foo_at_smoke()` or
|
|
92
|
+
`pytest.param(..., id="x@smoke")`) is also picked up as a tag, mirroring
|
|
93
|
+
the JS adapters.
|
|
94
|
+
|
|
95
|
+
## Helper API
|
|
96
|
+
|
|
97
|
+
Import the helpers from `kensho_pytest`. All four are no-ops outside a
|
|
98
|
+
running test, so it's safe to call them from shared utility code.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import kensho_pytest as kensho
|
|
102
|
+
|
|
103
|
+
def test_login(page):
|
|
104
|
+
with kensho.step("open the login page"):
|
|
105
|
+
page.goto("/login")
|
|
106
|
+
|
|
107
|
+
with kensho.step("submit credentials"):
|
|
108
|
+
page.fill("#user", "demo")
|
|
109
|
+
page.click("text=Sign in")
|
|
110
|
+
# nesting works — child steps roll up under the parent
|
|
111
|
+
with kensho.step("verify redirect"):
|
|
112
|
+
assert page.url.endswith("/home")
|
|
113
|
+
|
|
114
|
+
kensho.label("team", "growth")
|
|
115
|
+
kensho.link("https://jira.example.com/browse/PROJ-123",
|
|
116
|
+
kind="jira", label_text="PROJ-123")
|
|
117
|
+
|
|
118
|
+
page.screenshot(path="/tmp/login.png")
|
|
119
|
+
kensho.attach("/tmp/login.png", kind="screenshot")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Helper | What it does |
|
|
123
|
+
| ----------------------------------------- | ------------------------------------------------------- |
|
|
124
|
+
| `with kensho.step(title, action=None):` | Opens a Kensho step. Nests automatically. On exception the step is marked `fail` and the exception re-raises. |
|
|
125
|
+
| `kensho.attach(path, kind=None, name=None, mime_type=None)` | Copies the file into `kensho-results/attachments/<caseId>/` and registers it on the current case (or current step, if one is open). |
|
|
126
|
+
| `kensho.label(key, value)` | Adds a string label to the case. |
|
|
127
|
+
| `kensho.link(url, kind=None, label_text=None)` | Adds a hyperlink to the case. |
|
|
128
|
+
| `kensho.current_case_id()` | Returns the stable case id of the running test, or `None`. |
|
|
129
|
+
|
|
130
|
+
## Captured output → logs
|
|
131
|
+
|
|
132
|
+
Pytest's captured stdout / stderr / `caplog` for each phase (setup, call,
|
|
133
|
+
teardown) is converted to entries in `case.logs[]`. The `t` field is the
|
|
134
|
+
millisecond offset from the test start.
|
|
135
|
+
|
|
136
|
+
## Errors
|
|
137
|
+
|
|
138
|
+
`call` failures map to `status: 'fail'`; `setup` / `teardown` failures map
|
|
139
|
+
to `status: 'broken'` (these are infrastructure, not real assertion
|
|
140
|
+
failures). Skips at any phase map to `status: 'skip'`. The full
|
|
141
|
+
`longrepr` is preserved in `case.errors[].stack`; the first line becomes
|
|
142
|
+
`case.errors[].message`.
|
|
143
|
+
|
|
144
|
+
## Environment auto-detected
|
|
145
|
+
|
|
146
|
+
GitHub Actions, CircleCI, GitLab CI, Jenkins, Buildkite, Azure DevOps —
|
|
147
|
+
CI provider, branch, commit, run URL, OS, architecture, Python version.
|
|
148
|
+
|
|
149
|
+
Pass `KR_AUTHOR`, `KR_COMMIT_MSG`, `KR_STAGE`, `KR_BASE_URL`,
|
|
150
|
+
`KR_APP_VERSION`, `KR_BUILD_NUMBER`, `KR_RELEASE`, `KR_REGION`,
|
|
151
|
+
`KR_LOCALE`, `KR_TRIGGER`, or `KR_FEATURE` as env vars to populate the
|
|
152
|
+
matching fields on `run.env`.
|
|
153
|
+
|
|
154
|
+
## Design notes
|
|
155
|
+
|
|
156
|
+
* Zero runtime dependencies beyond `pytest` itself. The schema lives in
|
|
157
|
+
the JS workspace; we vendor the minimum (id-hashing, status mapping,
|
|
158
|
+
env capture) inline so the adapter installs in seconds.
|
|
159
|
+
* The reporter never raises out of a hook — a broken adapter must not
|
|
160
|
+
break a test run. All errors come out as `warnings.warn`.
|
|
161
|
+
* IDs are stable across adapters: the FNV-1a hash matches the JS
|
|
162
|
+
`stableCaseId` byte-for-byte, so `pytest` and `playwright` runs of
|
|
163
|
+
the same suite roll up to the same history on the platform.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
Apache-2.0.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# kensho-pytest
|
|
2
|
+
|
|
3
|
+
A pytest plugin that emits the canonical [Kensho v1](../schema) JSON format.
|
|
4
|
+
Run your tests, then point the `kensho` CLI at `kensho-results/` to get a
|
|
5
|
+
self-contained static HTML report.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install kensho-pytest
|
|
11
|
+
# or, in this monorepo:
|
|
12
|
+
pip install -e packages/pytest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The plugin auto-registers via the `pytest11` entry point — no `conftest.py`
|
|
16
|
+
edits required.
|
|
17
|
+
|
|
18
|
+
## Run
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pytest
|
|
22
|
+
# => kensho-results/run.json + kensho-results/cases/*.json
|
|
23
|
+
|
|
24
|
+
# Generate the HTML report (uses the JS CLI from the same monorepo):
|
|
25
|
+
npx kensho generate
|
|
26
|
+
npx kensho open
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI flags
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
--kensho-output PATH Output dir (default: ./kensho-results)
|
|
33
|
+
--kensho-project-name STR Project name in run.json
|
|
34
|
+
--kensho-project-slug STR Project slug (lowercase, alnum + dash/underscore)
|
|
35
|
+
--kensho-run-id STR Override the auto-generated run id
|
|
36
|
+
--kensho-no-severity-from-marks Don't promote @pytest.mark.<severity> to case.severity
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`pytest.ini` / `pyproject.toml` `[tool.pytest.ini_options]` keys are also
|
|
40
|
+
respected: `kensho_output`, `kensho_project_name`, `kensho_project_slug`.
|
|
41
|
+
|
|
42
|
+
## What it produces
|
|
43
|
+
|
|
44
|
+
- `kensho-results/run.json` — run manifest (project, env, totals, timing).
|
|
45
|
+
- `kensho-results/cases/<stableId>.json` — one file per test case.
|
|
46
|
+
- `kensho-results/attachments/<caseId>/...` — files registered via
|
|
47
|
+
`kensho_pytest.attach`.
|
|
48
|
+
|
|
49
|
+
Each case gets a **stable id** (`tc_<16 hex>`) hashed from its `nodeid` +
|
|
50
|
+
file path, so test history correlates across runs and across adapters
|
|
51
|
+
(the JS adapters use the same FNV-1a-based hash).
|
|
52
|
+
|
|
53
|
+
## Markers we read
|
|
54
|
+
|
|
55
|
+
| Marker | Effect |
|
|
56
|
+
| ----------------------------------------------- | --------------------------------------------------------- |
|
|
57
|
+
| `@pytest.mark.severity('critical')` | Sets `case.severity`. Allowed values: `blocker`, `critical`, `normal`, `minor`, `trivial`. |
|
|
58
|
+
| `@pytest.mark.blocker` / `critical` / `normal` / `minor` / `trivial` | Shorthand alias for the above. |
|
|
59
|
+
| `@pytest.mark.feature('Cart')` | Sets `case.behavior.feature`. |
|
|
60
|
+
| `@pytest.mark.epic('Checkout')` | Sets `case.behavior.epic`. |
|
|
61
|
+
| `@pytest.mark.story('Empty cart shows CTA')` | Sets `case.behavior.scenario`. |
|
|
62
|
+
| `@pytest.mark.description('Long-form...')` | Sets `case.description`. |
|
|
63
|
+
| `@pytest.mark.owner('alice')` | Sets `case.owner`. |
|
|
64
|
+
| `@pytest.mark.kensho_label(team='cart')` | Adds free-form `key=value` to `case.labels` (string only). |
|
|
65
|
+
| `@pytest.mark.kensho_link(kind='jira', url='https://…', label='PROJ-123')` | Adds an entry to `case.links`. |
|
|
66
|
+
| `@pytest.mark.parametrize(...)` | Each parametrized case gets `case.parameters[]`. |
|
|
67
|
+
|
|
68
|
+
Inline `@tag` syntax in test names (`def test_foo_at_smoke()` or
|
|
69
|
+
`pytest.param(..., id="x@smoke")`) is also picked up as a tag, mirroring
|
|
70
|
+
the JS adapters.
|
|
71
|
+
|
|
72
|
+
## Helper API
|
|
73
|
+
|
|
74
|
+
Import the helpers from `kensho_pytest`. All four are no-ops outside a
|
|
75
|
+
running test, so it's safe to call them from shared utility code.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import kensho_pytest as kensho
|
|
79
|
+
|
|
80
|
+
def test_login(page):
|
|
81
|
+
with kensho.step("open the login page"):
|
|
82
|
+
page.goto("/login")
|
|
83
|
+
|
|
84
|
+
with kensho.step("submit credentials"):
|
|
85
|
+
page.fill("#user", "demo")
|
|
86
|
+
page.click("text=Sign in")
|
|
87
|
+
# nesting works — child steps roll up under the parent
|
|
88
|
+
with kensho.step("verify redirect"):
|
|
89
|
+
assert page.url.endswith("/home")
|
|
90
|
+
|
|
91
|
+
kensho.label("team", "growth")
|
|
92
|
+
kensho.link("https://jira.example.com/browse/PROJ-123",
|
|
93
|
+
kind="jira", label_text="PROJ-123")
|
|
94
|
+
|
|
95
|
+
page.screenshot(path="/tmp/login.png")
|
|
96
|
+
kensho.attach("/tmp/login.png", kind="screenshot")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Helper | What it does |
|
|
100
|
+
| ----------------------------------------- | ------------------------------------------------------- |
|
|
101
|
+
| `with kensho.step(title, action=None):` | Opens a Kensho step. Nests automatically. On exception the step is marked `fail` and the exception re-raises. |
|
|
102
|
+
| `kensho.attach(path, kind=None, name=None, mime_type=None)` | Copies the file into `kensho-results/attachments/<caseId>/` and registers it on the current case (or current step, if one is open). |
|
|
103
|
+
| `kensho.label(key, value)` | Adds a string label to the case. |
|
|
104
|
+
| `kensho.link(url, kind=None, label_text=None)` | Adds a hyperlink to the case. |
|
|
105
|
+
| `kensho.current_case_id()` | Returns the stable case id of the running test, or `None`. |
|
|
106
|
+
|
|
107
|
+
## Captured output → logs
|
|
108
|
+
|
|
109
|
+
Pytest's captured stdout / stderr / `caplog` for each phase (setup, call,
|
|
110
|
+
teardown) is converted to entries in `case.logs[]`. The `t` field is the
|
|
111
|
+
millisecond offset from the test start.
|
|
112
|
+
|
|
113
|
+
## Errors
|
|
114
|
+
|
|
115
|
+
`call` failures map to `status: 'fail'`; `setup` / `teardown` failures map
|
|
116
|
+
to `status: 'broken'` (these are infrastructure, not real assertion
|
|
117
|
+
failures). Skips at any phase map to `status: 'skip'`. The full
|
|
118
|
+
`longrepr` is preserved in `case.errors[].stack`; the first line becomes
|
|
119
|
+
`case.errors[].message`.
|
|
120
|
+
|
|
121
|
+
## Environment auto-detected
|
|
122
|
+
|
|
123
|
+
GitHub Actions, CircleCI, GitLab CI, Jenkins, Buildkite, Azure DevOps —
|
|
124
|
+
CI provider, branch, commit, run URL, OS, architecture, Python version.
|
|
125
|
+
|
|
126
|
+
Pass `KR_AUTHOR`, `KR_COMMIT_MSG`, `KR_STAGE`, `KR_BASE_URL`,
|
|
127
|
+
`KR_APP_VERSION`, `KR_BUILD_NUMBER`, `KR_RELEASE`, `KR_REGION`,
|
|
128
|
+
`KR_LOCALE`, `KR_TRIGGER`, or `KR_FEATURE` as env vars to populate the
|
|
129
|
+
matching fields on `run.env`.
|
|
130
|
+
|
|
131
|
+
## Design notes
|
|
132
|
+
|
|
133
|
+
* Zero runtime dependencies beyond `pytest` itself. The schema lives in
|
|
134
|
+
the JS workspace; we vendor the minimum (id-hashing, status mapping,
|
|
135
|
+
env capture) inline so the adapter installs in seconds.
|
|
136
|
+
* The reporter never raises out of a hook — a broken adapter must not
|
|
137
|
+
break a test run. All errors come out as `warnings.warn`.
|
|
138
|
+
* IDs are stable across adapters: the FNV-1a hash matches the JS
|
|
139
|
+
`stableCaseId` byte-for-byte, so `pytest` and `playwright` runs of
|
|
140
|
+
the same suite roll up to the same history on the platform.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
Apache-2.0.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kensho-pytest"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Kensho reporter for pytest — writes kensho-results/ so the Kensho CLI can generate a rich HTML report."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "KaizenReport" }]
|
|
13
|
+
keywords = ["pytest", "kensho", "kaizenreport", "test-report", "reporter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Framework :: Pytest",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pytest>=7.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/kaizenreport/kensho"
|
|
32
|
+
Source = "https://github.com/kaizenreport/kensho"
|
|
33
|
+
|
|
34
|
+
# Pytest plugin entry point — pytest auto-discovers this on install.
|
|
35
|
+
[project.entry-points.pytest11]
|
|
36
|
+
kensho = "kensho_pytest.plugin"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
kensho_pytest = ["py.typed"]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Public API for ``kensho-pytest``.
|
|
2
|
+
|
|
3
|
+
Most of the time you don't import anything — installing the package is
|
|
4
|
+
enough; pytest discovers the plugin via the ``pytest11`` entry point and
|
|
5
|
+
the reporter writes ``kensho-results/`` automatically.
|
|
6
|
+
|
|
7
|
+
The helpers exported here are for tests that want to add structured
|
|
8
|
+
metadata at runtime:
|
|
9
|
+
|
|
10
|
+
* :func:`step` — context manager, opens a Kensho step.
|
|
11
|
+
* :func:`attach` — register a file (screenshot, log, JSON dump, …).
|
|
12
|
+
* :func:`label` — attach an arbitrary string ``key=value`` to the case.
|
|
13
|
+
* :func:`link` — attach a hyperlink (Jira ticket, runbook, PR, …).
|
|
14
|
+
|
|
15
|
+
All four helpers are no-ops when called outside a running test (e.g.
|
|
16
|
+
during collection or in a non-pytest script), so it's safe to call them
|
|
17
|
+
from helper modules used from both prod and test code.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import time
|
|
23
|
+
import uuid
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, Iterator, Optional, Union
|
|
27
|
+
|
|
28
|
+
from . import _state
|
|
29
|
+
|
|
30
|
+
__all__ = ["step", "attach", "label", "link", "current_case_id"]
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextmanager
|
|
36
|
+
def step(title: str, *, action: Optional[str] = None) -> Iterator[Dict[str, Any]]:
|
|
37
|
+
"""Context manager that opens a Kensho step.
|
|
38
|
+
|
|
39
|
+
Steps can nest — the innermost ``with kensho.step(...)`` becomes the
|
|
40
|
+
parent of any further steps opened inside it. If the ``with`` block
|
|
41
|
+
raises, the step is marked ``fail`` and re-raised.
|
|
42
|
+
|
|
43
|
+
Example::
|
|
44
|
+
|
|
45
|
+
from kensho_pytest import step
|
|
46
|
+
|
|
47
|
+
def test_login(page):
|
|
48
|
+
with step("open the login page"):
|
|
49
|
+
page.goto("/login")
|
|
50
|
+
with step("submit credentials"):
|
|
51
|
+
page.fill("#user", "demo")
|
|
52
|
+
page.fill("#pwd", "demo")
|
|
53
|
+
page.click("text=Sign in")
|
|
54
|
+
|
|
55
|
+
Outside a pytest run the call is a no-op — the test code remains
|
|
56
|
+
safe to run as a plain script.
|
|
57
|
+
"""
|
|
58
|
+
scratch = _state.get_current()
|
|
59
|
+
if scratch is None:
|
|
60
|
+
# No active test — yield a dummy dict so callers can still write
|
|
61
|
+
# to it without surprising errors.
|
|
62
|
+
yield {}
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
started_perf = time.time()
|
|
66
|
+
started_iso = _iso_now()
|
|
67
|
+
step_obj: Dict[str, Any] = {
|
|
68
|
+
"id": "step_" + uuid.uuid4().hex[:10],
|
|
69
|
+
"title": str(title),
|
|
70
|
+
"status": "pass",
|
|
71
|
+
"startedAt": started_iso,
|
|
72
|
+
"duration": 0,
|
|
73
|
+
"_startedPerf": started_perf,
|
|
74
|
+
}
|
|
75
|
+
if action:
|
|
76
|
+
step_obj["action"] = str(action)
|
|
77
|
+
|
|
78
|
+
parent = scratch.step_stack[-1] if scratch.step_stack else None
|
|
79
|
+
if parent is not None:
|
|
80
|
+
parent.setdefault("children", []).append(step_obj)
|
|
81
|
+
else:
|
|
82
|
+
scratch.steps.append(step_obj)
|
|
83
|
+
scratch.step_stack.append(step_obj)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
yield step_obj
|
|
87
|
+
except Exception:
|
|
88
|
+
step_obj["status"] = "fail"
|
|
89
|
+
_close_step(step_obj)
|
|
90
|
+
# pop and re-raise — the test framework decides what to do.
|
|
91
|
+
if scratch.step_stack and scratch.step_stack[-1] is step_obj:
|
|
92
|
+
scratch.step_stack.pop()
|
|
93
|
+
raise
|
|
94
|
+
else:
|
|
95
|
+
_close_step(step_obj)
|
|
96
|
+
if scratch.step_stack and scratch.step_stack[-1] is step_obj:
|
|
97
|
+
scratch.step_stack.pop()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def attach(
|
|
101
|
+
path: Union[str, Path],
|
|
102
|
+
*,
|
|
103
|
+
kind: Optional[str] = None,
|
|
104
|
+
name: Optional[str] = None,
|
|
105
|
+
mime_type: Optional[str] = None,
|
|
106
|
+
) -> Optional[Dict[str, Any]]:
|
|
107
|
+
"""Copy a file into ``kensho-results/attachments/<caseId>/`` and register it.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
path:
|
|
112
|
+
Local path to the file to attach. Must exist; missing files are
|
|
113
|
+
skipped with a warning so a flaky screenshot can't break the run.
|
|
114
|
+
kind:
|
|
115
|
+
Optional override for ``attachment.kind``. One of ``screenshot``,
|
|
116
|
+
``video``, ``trace``, ``har``, ``text``, ``json``, ``html``,
|
|
117
|
+
``dom-snapshot``, ``log``. Inferred from the file extension when
|
|
118
|
+
omitted.
|
|
119
|
+
name:
|
|
120
|
+
Optional rename for the destination file. Defaults to the source
|
|
121
|
+
basename, prefixed with a short id to dodge collisions.
|
|
122
|
+
mime_type:
|
|
123
|
+
Optional MIME type override. Inferred from the file extension
|
|
124
|
+
when omitted.
|
|
125
|
+
|
|
126
|
+
Returns the registered attachment dict, or ``None`` if no test is
|
|
127
|
+
currently active (e.g. when called outside a pytest run).
|
|
128
|
+
"""
|
|
129
|
+
scratch = _state.get_current()
|
|
130
|
+
if scratch is None:
|
|
131
|
+
return None
|
|
132
|
+
plugin = _resolve_plugin()
|
|
133
|
+
if plugin is None:
|
|
134
|
+
return None
|
|
135
|
+
record = plugin.register_attachment(
|
|
136
|
+
scratch,
|
|
137
|
+
Path(str(path)),
|
|
138
|
+
kind=kind,
|
|
139
|
+
name=name,
|
|
140
|
+
mime_type=mime_type,
|
|
141
|
+
)
|
|
142
|
+
if record is None:
|
|
143
|
+
return None
|
|
144
|
+
# Anchor it on the innermost step if one is open, otherwise on the case.
|
|
145
|
+
if scratch.step_stack:
|
|
146
|
+
scratch.step_stack[-1].setdefault("attachments", []).append(record)
|
|
147
|
+
else:
|
|
148
|
+
scratch.attachments.append(record)
|
|
149
|
+
return record
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def label(key: str, value: str) -> None:
|
|
153
|
+
"""Set ``case.labels[key] = value`` for the currently-running test.
|
|
154
|
+
|
|
155
|
+
Useful for free-form metadata (``service``, ``team``, ``env``) that
|
|
156
|
+
doesn't fit the typed fields. No-op outside a test."""
|
|
157
|
+
scratch = _state.get_current()
|
|
158
|
+
if scratch is None or not key:
|
|
159
|
+
return
|
|
160
|
+
scratch.labels[str(key)] = str(value)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def link(
|
|
164
|
+
url: str,
|
|
165
|
+
*,
|
|
166
|
+
kind: Optional[str] = None,
|
|
167
|
+
label_text: Optional[str] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Attach a link to the currently-running case.
|
|
170
|
+
|
|
171
|
+
``kind`` is free-form by convention (``jira``, ``github``, ``runbook``…).
|
|
172
|
+
``label_text`` becomes ``link.label`` — the human-readable text. The
|
|
173
|
+
parameter is named ``label_text`` rather than ``label`` to avoid
|
|
174
|
+
shadowing the :func:`label` helper.
|
|
175
|
+
"""
|
|
176
|
+
scratch = _state.get_current()
|
|
177
|
+
if scratch is None or not url:
|
|
178
|
+
return
|
|
179
|
+
entry: Dict[str, str] = {"url": str(url)}
|
|
180
|
+
if kind:
|
|
181
|
+
entry["kind"] = str(kind)
|
|
182
|
+
if label_text:
|
|
183
|
+
entry["label"] = str(label_text)
|
|
184
|
+
scratch.links.append(entry)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def current_case_id() -> Optional[str]:
|
|
188
|
+
"""Return the stable id of the test currently being run, if any."""
|
|
189
|
+
scratch = _state.get_current()
|
|
190
|
+
return scratch.case_id if scratch is not None else None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# --------------------------------------------------------------------------- #
|
|
194
|
+
# Internals
|
|
195
|
+
# --------------------------------------------------------------------------- #
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _close_step(step_obj: Dict[str, Any]) -> None:
|
|
199
|
+
started_perf = step_obj.pop("_startedPerf", None)
|
|
200
|
+
if started_perf is None:
|
|
201
|
+
step_obj.setdefault("duration", 0)
|
|
202
|
+
return
|
|
203
|
+
step_obj["duration"] = max(0, int(round((time.time() - started_perf) * 1000)))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _iso_now() -> str:
|
|
207
|
+
import datetime as _dt
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
_dt.datetime.now(tz=_dt.timezone.utc)
|
|
211
|
+
.isoformat(timespec="milliseconds")
|
|
212
|
+
.replace("+00:00", "Z")
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _resolve_plugin() -> Any:
|
|
217
|
+
"""Return the active ``KenshoPlugin`` instance, or ``None`` if there is none.
|
|
218
|
+
|
|
219
|
+
The plugin publishes itself into ``_state.PLUGIN`` during
|
|
220
|
+
``pytest_configure`` so helper APIs can find it without importing
|
|
221
|
+
the heavy plugin module at startup time.
|
|
222
|
+
"""
|
|
223
|
+
return getattr(_state, "PLUGIN", None)
|