pytest-timing 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.
- pytest_timing-0.1.0/.github/workflows/ci.yml +60 -0
- pytest_timing-0.1.0/.github/workflows/release.yml +17 -0
- pytest_timing-0.1.0/.gitignore +12 -0
- pytest_timing-0.1.0/CHANGELOG.md +10 -0
- pytest_timing-0.1.0/LICENSE +21 -0
- pytest_timing-0.1.0/PKG-INFO +161 -0
- pytest_timing-0.1.0/README.md +135 -0
- pytest_timing-0.1.0/docs/report.png +0 -0
- pytest_timing-0.1.0/examples/test_demo.py +41 -0
- pytest_timing-0.1.0/pyproject.toml +67 -0
- pytest_timing-0.1.0/src/pytest_timing/__init__.py +5 -0
- pytest_timing-0.1.0/src/pytest_timing/cli.py +150 -0
- pytest_timing-0.1.0/src/pytest_timing/collector.py +210 -0
- pytest_timing-0.1.0/src/pytest_timing/model.py +522 -0
- pytest_timing-0.1.0/src/pytest_timing/outputs.py +54 -0
- pytest_timing-0.1.0/src/pytest_timing/plugin.py +382 -0
- pytest_timing-0.1.0/src/pytest_timing/render/__init__.py +1 -0
- pytest_timing-0.1.0/src/pytest_timing/render/ascii.py +283 -0
- pytest_timing-0.1.0/src/pytest_timing/render/html.py +32 -0
- pytest_timing-0.1.0/src/pytest_timing/render/trace.py +126 -0
- pytest_timing-0.1.0/src/pytest_timing/static/__init__.py +0 -0
- pytest_timing-0.1.0/src/pytest_timing/static/report.html +653 -0
- pytest_timing-0.1.0/src/pytest_timing/xdist_compat.py +158 -0
- pytest_timing-0.1.0/tests/conftest.py +170 -0
- pytest_timing-0.1.0/tests/fixtures/legacy_complete_false.json +90 -0
- pytest_timing-0.1.0/tests/fixtures/legacy_complete_true.json +90 -0
- pytest_timing-0.1.0/tests/fixtures/legacy_no_flag.json +89 -0
- pytest_timing-0.1.0/tests/test_ascii.py +174 -0
- pytest_timing-0.1.0/tests/test_cli.py +182 -0
- pytest_timing-0.1.0/tests/test_collector.py +389 -0
- pytest_timing-0.1.0/tests/test_html.py +86 -0
- pytest_timing-0.1.0/tests/test_plugin.py +629 -0
- pytest_timing-0.1.0/tests/test_trace.py +27 -0
- pytest_timing-0.1.0/uv.lock +623 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: py${{ matrix.python }} / pytest ${{ matrix.pytest }}
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest]
|
|
16
|
+
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
pytest: ["latest"]
|
|
18
|
+
include:
|
|
19
|
+
- { os: ubuntu-latest, python: "3.10", pytest: "7.3" }
|
|
20
|
+
- { os: ubuntu-latest, python: "3.11", pytest: "7.4" }
|
|
21
|
+
- { os: ubuntu-latest, python: "3.12", pytest: "8.0" }
|
|
22
|
+
- { os: windows-latest, python: "3.12", pytest: "latest" }
|
|
23
|
+
- { os: macos-latest, python: "3.13", pytest: "latest" }
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: astral-sh/setup-uv@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python }}
|
|
29
|
+
- name: Install
|
|
30
|
+
run: |
|
|
31
|
+
uv sync --no-dev
|
|
32
|
+
uv pip install pytest-xdist pytest-rerunfailures hypothesis
|
|
33
|
+
if [ "${{ matrix.pytest }}" != "latest" ]; then uv pip install "pytest~=${{ matrix.pytest }}.0"; fi
|
|
34
|
+
shell: bash
|
|
35
|
+
- name: Test
|
|
36
|
+
run: uv run --no-sync pytest -v -p no:cacheprovider
|
|
37
|
+
|
|
38
|
+
no-xdist:
|
|
39
|
+
name: without pytest-xdist
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
- uses: astral-sh/setup-uv@v5
|
|
44
|
+
with:
|
|
45
|
+
python-version: "3.12"
|
|
46
|
+
- run: uv sync --no-dev
|
|
47
|
+
- run: uv pip install hypothesis
|
|
48
|
+
- run: uv run --no-sync pytest -v -p no:cacheprovider
|
|
49
|
+
|
|
50
|
+
lint:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: astral-sh/setup-uv@v5
|
|
55
|
+
with:
|
|
56
|
+
python-version: "3.12"
|
|
57
|
+
- run: uv sync
|
|
58
|
+
- run: uv run ruff check src tests
|
|
59
|
+
- run: uv run ruff format --check src tests
|
|
60
|
+
- run: uv run mypy
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
- run: uv build
|
|
17
|
+
- run: uv publish
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Output options are boolean flags (`--timing-json`) with separate `--timing-json-file PATH`
|
|
6
|
+
options, so an output flag can never swallow a test path.
|
|
7
|
+
- Runs record their termination (`finished`, `interrupted`, `aborted`, ...) explicitly.
|
|
8
|
+
- Initial release: controller-side timing capture with and without pytest-xdist,
|
|
9
|
+
ASCII Gantt chart in the terminal summary, self-contained HTML report, JSON output,
|
|
10
|
+
Chrome trace output, and a `pytest-timing` CLI with `render` and `merge` commands.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Messense Lv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
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,161 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pytest-timing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Record test timings under pytest-xdist and render cargo-style timing reports (ASCII Gantt and HTML).
|
|
5
|
+
Project-URL: Homepage, https://github.com/messense/pytest-timing
|
|
6
|
+
Author-email: messense <messense@icloud.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: gantt,profiling,pytest,timing,xdist
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Pytest
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: pytest>=7.3
|
|
23
|
+
Provides-Extra: xdist
|
|
24
|
+
Requires-Dist: pytest-xdist>=3.0; extra == 'xdist'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# pytest-timing
|
|
28
|
+
|
|
29
|
+
Record when every test ran, on which [pytest-xdist](https://github.com/pytest-dev/pytest-xdist)
|
|
30
|
+
worker, and for how long, then render the run as a timing report in the spirit of
|
|
31
|
+
`cargo build --timings`: an ASCII Gantt chart in the terminal and a self-contained
|
|
32
|
+
HTML report with worker lanes, a concurrency graph and a sortable table.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pytest -n 4 --timing --timing-html
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
================================ timing report =================================
|
|
40
|
+
pytest-timing: 129 tests (1 error, 1 failed), 4 workers, wall 1.37s, busy 95.4%
|
|
41
|
+
worker |-----------|-----------|------------|-----------|-----------|-------- busy%
|
|
42
|
+
gw0 ░░░░░░ ▄███████████████▄███████████████████████████████████████████ 93.6%
|
|
43
|
+
gw1 ░░░░░░░ ▄██████████████████████████████████████████████████X 94.1%
|
|
44
|
+
gw2 ░░░░░░░░▒▄███████████████████████████████████████████████████▄ 96.2%
|
|
45
|
+
gw3 ░░░░░░░░░▄████████████████████████████████▄███████████XXXXX 98.2%
|
|
46
|
+
0s 0.20s 0.40s 0.60s 0.80s 1.00s 1.37s
|
|
47
|
+
legend: ░ boot ▒ collect █ tests ▄ <50% busy X failure
|
|
48
|
+
|
|
49
|
+
slowest 3 tests (setup ░ / call █ / teardown ▒):
|
|
50
|
+
gw0 ████████████████████▒ 0.36s
|
|
51
|
+
test_big.py::test_slow[5]
|
|
52
|
+
gw2 ████████████████▒ 0.30s
|
|
53
|
+
test_big.py::test_slow[4]
|
|
54
|
+
gw1 ░░░░░░░░░░░░░▒ 0.21s
|
|
55
|
+
test_big.py::test_many[8]
|
|
56
|
+
HTML report written to pytest-timing.html
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Works with and without `-n`. Without xdist the run is a single `main` lane.
|
|
60
|
+
|
|
61
|
+
Each lane shows worker boot, collection, tests (`▄` marks a column that is under half busy,
|
|
62
|
+
so idle gaps stand out) and failures. The slowest tests are drawn on the same axis with
|
|
63
|
+
their setup / call / teardown split; note how the module-scoped fixture above lands in the
|
|
64
|
+
setup phase of the first test on each worker.
|
|
65
|
+
|
|
66
|
+
The HTML report has the same data with a zoomable lane Gantt, a concurrency graph, filters,
|
|
67
|
+
hover details, and a sortable table:
|
|
68
|
+
|
|
69
|
+

|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
pip install pytest-timing # plugin only
|
|
75
|
+
pip install "pytest-timing[xdist]" # with pytest-xdist
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Python 3.10+, pytest 7.3+ (the version that added wall-clock `start`/`stop` to test reports).
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
| Option | Effect |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `--timing` | Record timings and print the ASCII chart in the terminal summary. |
|
|
85
|
+
| `--timing-html` | Write a self-contained HTML report to `pytest-timing.html`. |
|
|
86
|
+
| `--timing-json` | Write the recorded run to `pytest-timing.json`. |
|
|
87
|
+
| `--timing-trace` | Write a Chrome trace file for [Perfetto](https://ui.perfetto.dev) to `pytest-timing.trace.json`. |
|
|
88
|
+
| `--timing-html-file PATH`, `--timing-json-file PATH`, `--timing-trace-file PATH` | Same, to an explicit path. |
|
|
89
|
+
| `--timing-top=N` | Rows in the slowest-tests section (default 10, `0` hides it). |
|
|
90
|
+
| `--timing-min=SECONDS` | Hide tests shorter than this from the slowest-tests section. |
|
|
91
|
+
| `--timing-ascii-style=unicode\|ascii` | Chart glyphs. |
|
|
92
|
+
| `--timing-width=N` | Override the terminal width for the chart. |
|
|
93
|
+
|
|
94
|
+
Any output option implies `--timing`. The output flags are plain booleans and the `-file`
|
|
95
|
+
options always take a path, so `pytest --timing-json test_x.py` runs exactly `test_x.py`.
|
|
96
|
+
|
|
97
|
+
The same settings are accepted as ini keys (`timing`, `timing_html`, `timing_json`,
|
|
98
|
+
`timing_trace` as paths or `true`, plus `timing_top`, `timing_min`, `timing_ascii_style`)
|
|
99
|
+
and environment variables (`PYTEST_TIMING=1`, `PYTEST_TIMING_HTML=path`, ...), so CI can
|
|
100
|
+
enable it without touching the command line.
|
|
101
|
+
|
|
102
|
+
Every JSON run records how the session ended (`finished`, `collect_only`, `interrupted`,
|
|
103
|
+
`aborted`, `internal_error`) with the reason pytest gave, and `complete` is derived from that.
|
|
104
|
+
|
|
105
|
+
### View the trace
|
|
106
|
+
|
|
107
|
+
`--timing-trace` writes a Chrome Trace Event file. Open it in
|
|
108
|
+
[Perfetto UI](https://ui.perfetto.dev) with "Open trace file", or in `chrome://tracing`.
|
|
109
|
+
Each worker is a track, every test is a bar, and the setup / call / teardown phases nest
|
|
110
|
+
underneath it. Zoom with W/A/S/D and select a range to aggregate durations.
|
|
111
|
+
|
|
112
|
+
### Re-render or merge saved runs
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
pytest-timing render pytest-timing.json --html report.html --ascii
|
|
116
|
+
pytest-timing merge shard1.json shard2.json -o all.json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`merge` places several runs (for example CI shards) on one shared time axis using their
|
|
120
|
+
absolute start times.
|
|
121
|
+
|
|
122
|
+
## How it works
|
|
123
|
+
|
|
124
|
+
Since pytest 7.3 every `TestReport` carries wall-clock `start` and `stop` timestamps. xdist
|
|
125
|
+
serialises those to the controller unchanged and attaches the worker to the report, so the
|
|
126
|
+
plugin only needs controller-side hooks: `pytest_runtest_logreport` for the setup / call /
|
|
127
|
+
teardown phases, plus xdist's node-ready, collection-finished and node-down hooks for the
|
|
128
|
+
worker lifecycle. Nothing runs inside the workers and nothing touches the execnet channel.
|
|
129
|
+
|
|
130
|
+
Each lane in the report shows boot (worker start-up until it is ready), collection, tests,
|
|
131
|
+
idle gaps and the point the worker shut down. Session-scoped fixture setup is attributed to
|
|
132
|
+
the first test's setup phase and its teardown to the last test's teardown phase, exactly as
|
|
133
|
+
pytest reports it.
|
|
134
|
+
|
|
135
|
+
## Overhead
|
|
136
|
+
|
|
137
|
+
Nothing runs inside the workers: the plugin only listens to the reports xdist already
|
|
138
|
+
sends to the controller, and each of the three phase reports per test costs a few
|
|
139
|
+
microseconds of bookkeeping. Rendering happens once, at the end of the session.
|
|
140
|
+
|
|
141
|
+
Measured on 5,000 trivial tests (a worst case, since the per-test cost is fixed while the
|
|
142
|
+
tests themselves take almost nothing), best of five runs:
|
|
143
|
+
|
|
144
|
+
| Configuration | Wall time | Overhead |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| single process, plugin disabled | 1.29 s | |
|
|
147
|
+
| single process, `--timing` | 1.38 s | +0.09 s |
|
|
148
|
+
| single process, `--timing` plus JSON, HTML and trace files | 1.45 s | +0.16 s |
|
|
149
|
+
| `-n 4`, plugin disabled | 1.18 s | |
|
|
150
|
+
| `-n 4`, `--timing` | 1.25 s | +0.07 s |
|
|
151
|
+
| `-n 4`, `--timing` plus all three files | 1.32 s | +0.14 s |
|
|
152
|
+
|
|
153
|
+
That is under 20 microseconds per test for recording, plus a fixed serialisation cost per
|
|
154
|
+
output file of roughly 10 ms per thousand tests. Output size is about 250 bytes per test
|
|
155
|
+
for the JSON and HTML files and 600 bytes for the trace. Memory held during the run is on
|
|
156
|
+
the same order as the JSON. The plugin registers nothing at all unless one of its options
|
|
157
|
+
is enabled.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# pytest-timing
|
|
2
|
+
|
|
3
|
+
Record when every test ran, on which [pytest-xdist](https://github.com/pytest-dev/pytest-xdist)
|
|
4
|
+
worker, and for how long, then render the run as a timing report in the spirit of
|
|
5
|
+
`cargo build --timings`: an ASCII Gantt chart in the terminal and a self-contained
|
|
6
|
+
HTML report with worker lanes, a concurrency graph and a sortable table.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pytest -n 4 --timing --timing-html
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
================================ timing report =================================
|
|
14
|
+
pytest-timing: 129 tests (1 error, 1 failed), 4 workers, wall 1.37s, busy 95.4%
|
|
15
|
+
worker |-----------|-----------|------------|-----------|-----------|-------- busy%
|
|
16
|
+
gw0 ░░░░░░ ▄███████████████▄███████████████████████████████████████████ 93.6%
|
|
17
|
+
gw1 ░░░░░░░ ▄██████████████████████████████████████████████████X 94.1%
|
|
18
|
+
gw2 ░░░░░░░░▒▄███████████████████████████████████████████████████▄ 96.2%
|
|
19
|
+
gw3 ░░░░░░░░░▄████████████████████████████████▄███████████XXXXX 98.2%
|
|
20
|
+
0s 0.20s 0.40s 0.60s 0.80s 1.00s 1.37s
|
|
21
|
+
legend: ░ boot ▒ collect █ tests ▄ <50% busy X failure
|
|
22
|
+
|
|
23
|
+
slowest 3 tests (setup ░ / call █ / teardown ▒):
|
|
24
|
+
gw0 ████████████████████▒ 0.36s
|
|
25
|
+
test_big.py::test_slow[5]
|
|
26
|
+
gw2 ████████████████▒ 0.30s
|
|
27
|
+
test_big.py::test_slow[4]
|
|
28
|
+
gw1 ░░░░░░░░░░░░░▒ 0.21s
|
|
29
|
+
test_big.py::test_many[8]
|
|
30
|
+
HTML report written to pytest-timing.html
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Works with and without `-n`. Without xdist the run is a single `main` lane.
|
|
34
|
+
|
|
35
|
+
Each lane shows worker boot, collection, tests (`▄` marks a column that is under half busy,
|
|
36
|
+
so idle gaps stand out) and failures. The slowest tests are drawn on the same axis with
|
|
37
|
+
their setup / call / teardown split; note how the module-scoped fixture above lands in the
|
|
38
|
+
setup phase of the first test on each worker.
|
|
39
|
+
|
|
40
|
+
The HTML report has the same data with a zoomable lane Gantt, a concurrency graph, filters,
|
|
41
|
+
hover details, and a sortable table:
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
pip install pytest-timing # plugin only
|
|
49
|
+
pip install "pytest-timing[xdist]" # with pytest-xdist
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Python 3.10+, pytest 7.3+ (the version that added wall-clock `start`/`stop` to test reports).
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
| Option | Effect |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `--timing` | Record timings and print the ASCII chart in the terminal summary. |
|
|
59
|
+
| `--timing-html` | Write a self-contained HTML report to `pytest-timing.html`. |
|
|
60
|
+
| `--timing-json` | Write the recorded run to `pytest-timing.json`. |
|
|
61
|
+
| `--timing-trace` | Write a Chrome trace file for [Perfetto](https://ui.perfetto.dev) to `pytest-timing.trace.json`. |
|
|
62
|
+
| `--timing-html-file PATH`, `--timing-json-file PATH`, `--timing-trace-file PATH` | Same, to an explicit path. |
|
|
63
|
+
| `--timing-top=N` | Rows in the slowest-tests section (default 10, `0` hides it). |
|
|
64
|
+
| `--timing-min=SECONDS` | Hide tests shorter than this from the slowest-tests section. |
|
|
65
|
+
| `--timing-ascii-style=unicode\|ascii` | Chart glyphs. |
|
|
66
|
+
| `--timing-width=N` | Override the terminal width for the chart. |
|
|
67
|
+
|
|
68
|
+
Any output option implies `--timing`. The output flags are plain booleans and the `-file`
|
|
69
|
+
options always take a path, so `pytest --timing-json test_x.py` runs exactly `test_x.py`.
|
|
70
|
+
|
|
71
|
+
The same settings are accepted as ini keys (`timing`, `timing_html`, `timing_json`,
|
|
72
|
+
`timing_trace` as paths or `true`, plus `timing_top`, `timing_min`, `timing_ascii_style`)
|
|
73
|
+
and environment variables (`PYTEST_TIMING=1`, `PYTEST_TIMING_HTML=path`, ...), so CI can
|
|
74
|
+
enable it without touching the command line.
|
|
75
|
+
|
|
76
|
+
Every JSON run records how the session ended (`finished`, `collect_only`, `interrupted`,
|
|
77
|
+
`aborted`, `internal_error`) with the reason pytest gave, and `complete` is derived from that.
|
|
78
|
+
|
|
79
|
+
### View the trace
|
|
80
|
+
|
|
81
|
+
`--timing-trace` writes a Chrome Trace Event file. Open it in
|
|
82
|
+
[Perfetto UI](https://ui.perfetto.dev) with "Open trace file", or in `chrome://tracing`.
|
|
83
|
+
Each worker is a track, every test is a bar, and the setup / call / teardown phases nest
|
|
84
|
+
underneath it. Zoom with W/A/S/D and select a range to aggregate durations.
|
|
85
|
+
|
|
86
|
+
### Re-render or merge saved runs
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
pytest-timing render pytest-timing.json --html report.html --ascii
|
|
90
|
+
pytest-timing merge shard1.json shard2.json -o all.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`merge` places several runs (for example CI shards) on one shared time axis using their
|
|
94
|
+
absolute start times.
|
|
95
|
+
|
|
96
|
+
## How it works
|
|
97
|
+
|
|
98
|
+
Since pytest 7.3 every `TestReport` carries wall-clock `start` and `stop` timestamps. xdist
|
|
99
|
+
serialises those to the controller unchanged and attaches the worker to the report, so the
|
|
100
|
+
plugin only needs controller-side hooks: `pytest_runtest_logreport` for the setup / call /
|
|
101
|
+
teardown phases, plus xdist's node-ready, collection-finished and node-down hooks for the
|
|
102
|
+
worker lifecycle. Nothing runs inside the workers and nothing touches the execnet channel.
|
|
103
|
+
|
|
104
|
+
Each lane in the report shows boot (worker start-up until it is ready), collection, tests,
|
|
105
|
+
idle gaps and the point the worker shut down. Session-scoped fixture setup is attributed to
|
|
106
|
+
the first test's setup phase and its teardown to the last test's teardown phase, exactly as
|
|
107
|
+
pytest reports it.
|
|
108
|
+
|
|
109
|
+
## Overhead
|
|
110
|
+
|
|
111
|
+
Nothing runs inside the workers: the plugin only listens to the reports xdist already
|
|
112
|
+
sends to the controller, and each of the three phase reports per test costs a few
|
|
113
|
+
microseconds of bookkeeping. Rendering happens once, at the end of the session.
|
|
114
|
+
|
|
115
|
+
Measured on 5,000 trivial tests (a worst case, since the per-test cost is fixed while the
|
|
116
|
+
tests themselves take almost nothing), best of five runs:
|
|
117
|
+
|
|
118
|
+
| Configuration | Wall time | Overhead |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| single process, plugin disabled | 1.29 s | |
|
|
121
|
+
| single process, `--timing` | 1.38 s | +0.09 s |
|
|
122
|
+
| single process, `--timing` plus JSON, HTML and trace files | 1.45 s | +0.16 s |
|
|
123
|
+
| `-n 4`, plugin disabled | 1.18 s | |
|
|
124
|
+
| `-n 4`, `--timing` | 1.25 s | +0.07 s |
|
|
125
|
+
| `-n 4`, `--timing` plus all three files | 1.32 s | +0.14 s |
|
|
126
|
+
|
|
127
|
+
That is under 20 microseconds per test for recording, plus a fixed serialisation cost per
|
|
128
|
+
output file of roughly 10 ms per thousand tests. Output size is about 250 bytes per test
|
|
129
|
+
for the JSON and HTML files and 600 bytes for the trace. Memory held during the run is on
|
|
130
|
+
the same order as the JSON. The plugin registers nothing at all unless one of its options
|
|
131
|
+
is enabled.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Small demo suite for trying pytest-timing. Run it with:
|
|
2
|
+
|
|
3
|
+
uv run pytest examples -n 3 --timing --timing-html
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(scope="module")
|
|
12
|
+
def database():
|
|
13
|
+
time.sleep(0.3) # expensive module fixture: shows up as setup on the first test per worker
|
|
14
|
+
yield
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client(database):
|
|
19
|
+
time.sleep(0.05)
|
|
20
|
+
yield
|
|
21
|
+
time.sleep(0.02)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.parametrize("i", range(12))
|
|
25
|
+
def test_api(client, i):
|
|
26
|
+
time.sleep(0.02 * (i % 4 + 1))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("i", range(3))
|
|
30
|
+
def test_slow(i):
|
|
31
|
+
time.sleep(0.2 + 0.1 * i)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_fails():
|
|
35
|
+
time.sleep(0.05)
|
|
36
|
+
assert 1 == 2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.skip(reason="demo")
|
|
40
|
+
def test_skipped():
|
|
41
|
+
pass
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-timing"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Record test timings under pytest-xdist and render cargo-style timing reports (ASCII Gantt and HTML)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "messense", email = "messense@icloud.com" }]
|
|
13
|
+
keywords = ["pytest", "xdist", "timing", "gantt", "profiling"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Framework :: Pytest",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
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
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Software Development :: Testing",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["pytest>=7.3"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
xdist = ["pytest-xdist>=3.0"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/messense/pytest-timing"
|
|
34
|
+
|
|
35
|
+
[project.entry-points.pytest11]
|
|
36
|
+
timing = "pytest_timing.plugin"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
pytest-timing = "pytest_timing.cli:main"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest-xdist>=3.0",
|
|
44
|
+
"pytest-rerunfailures>=12",
|
|
45
|
+
"hypothesis>=6",
|
|
46
|
+
"ruff>=0.6",
|
|
47
|
+
"mypy>=1.10",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/pytest_timing"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
addopts = "-p pytester"
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 100
|
|
59
|
+
target-version = "py310"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "UP", "B", "W"]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
strict = true
|
|
66
|
+
python_version = "3.10"
|
|
67
|
+
files = ["src"]
|