pytest-boorst 0.1.0a5__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ """Optional, experimental acceleration for pytest."""
@@ -0,0 +1,207 @@
1
+ """Reuse stock directory reports for a narrow, session-local file plan."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import inspect
7
+ from collections import Counter
8
+ from pathlib import Path
9
+
10
+ from _pytest.main import Dir, Session
11
+ from _pytest.python import Package
12
+
13
+ MIN_FILES = 32
14
+ _METHODS = (Session.collect, Session._collect_one_node, Dir.collect, Package.collect)
15
+ _METHOD_HASHES = (
16
+ "059b4635fb9e56688b817887ea750616b10738cb6d29571f5b7721f401dc32d6",
17
+ "41c0b9cb38ec89c9157f5df1c6d6af9b3986f45de538c02366c5887e63065225",
18
+ "f314ca1c43aa632c04b5570c9b273419aa848c3dcf9275aa58d6091a8bd36848",
19
+ "2dc823bfc8facccd8af8e0b2c61363202832efd0feee92d7ebacc8c70e9f1ff8",
20
+ )
21
+ _HOOKS = (
22
+ "pytest_collect_directory",
23
+ "pytest_collect_file",
24
+ "pytest_pycollect_makemodule",
25
+ "pytest_ignore_collect",
26
+ "pytest_collectstart",
27
+ "pytest_make_collect_report",
28
+ "pytest_collectreport",
29
+ )
30
+ _HOOK_HASHES = {
31
+ "_pytest.python.pytest_pycollect_makemodule": "942b7cdbbcf74ea989b5b373f8429c1b0570f26c31c3d084330e0c3f6fe83f15",
32
+ "_pytest.main.Session.pytest_collectstart": "bcd2d6b3082c4a77f602a5453a9e551669491ef8a505ee9f8efab75710de15ac",
33
+ "_pytest.main.Session.pytest_runtest_logreport": "d3003ff2a48eccec1ec963a0786c4619b1775d95e78dd081a31ccbbe0d5e7473",
34
+ "_pytest.main.pytest_collect_directory": "681454321faaf96bc7fd65e5eceb0e55b4f4825e63e4bc7c9a4ec971f850d0a6",
35
+ "_pytest.python.pytest_collect_directory": "7d337767579e97d39cf47b6abe040675a80a967bf39e27e1aa309c3fc4c7372a",
36
+ "_pytest.python.pytest_collect_file": "8812fc7a1632f71df0f2aaea1e61b011e8293b41166135b9e8561088a7ff22f3",
37
+ "_pytest.doctest.pytest_collect_file": "674f6e18c5e7e3596fbadd8c77f6f6e78f409fa931e48e3bc113b340ed0596ba",
38
+ "_pytest.main.pytest_ignore_collect": "7329f1c31d9a480829163bd7d1a3083b3ffe434553edbc2c3ff7a3636b41cf08",
39
+ "_pytest.runner.pytest_make_collect_report": "dd5756e160df76070b1cdbabf033ce39b5a5392320106eb42409b2bd82022d08",
40
+ "_pytest.capture.CaptureManager.pytest_make_collect_report": "d2f4f10b82e12a841c41bd07310902cd727c044ac502611f99afd6b8f81de122",
41
+ "_pytest.fixtures.FixtureManager.pytest_make_collect_report": "29629dfaf02a19320d591003af61b89743f472fb2e370dc23067ee71f0b786d3",
42
+ "_pytest.cacheprovider.LFPlugin.pytest_collectreport": "52a840fb38a24163aa35e4adea902fe4e1206c19d4b8b51a2984b27c71129bc9",
43
+ "_pytest.terminal.TerminalReporter.pytest_collectreport": "1e4d932bbc21fa9f4b68e25bdbd0cf5dce6ff8d6c508a19463083dc532e8e946",
44
+ }
45
+
46
+
47
+ def _hash(function):
48
+ return hashlib.sha256(inspect.getsource(function).encode()).hexdigest()
49
+
50
+
51
+ def _hooks(config):
52
+ return tuple(
53
+ (
54
+ impl.function,
55
+ getattr(impl.function, "__code__", None),
56
+ impl.wrapper,
57
+ impl.hookwrapper,
58
+ )
59
+ for name in _HOOKS
60
+ for impl in getattr(config.hook, name).get_hookimpls()
61
+ )
62
+
63
+
64
+ def _options(config):
65
+ return (
66
+ tuple(config.args),
67
+ tuple(config.getini("python_files")),
68
+ tuple(config.option.ignore or ()),
69
+ tuple(config.option.ignore_glob or ()),
70
+ config.option.collect_in_virtualenv,
71
+ getattr(config.option, "doctestmodules", False),
72
+ tuple(getattr(config.option, "doctestglob", ()) or ()),
73
+ tuple(
74
+ getattr(config.option, name, False)
75
+ for name in ("pyargs", "keepduplicates", "lf", "failedfirst", "debug")
76
+ ),
77
+ )
78
+
79
+
80
+ def install(session, state):
81
+ config = session.config
82
+ state.update(discovery_status="fallback", directory_reuses=0)
83
+ # ponytail: only large sibling groups; small groups keep stock discovery.
84
+ if len(config.args) < MIN_FILES or any(
85
+ getattr(config.option, name, False)
86
+ for name in (
87
+ "pyargs",
88
+ "keepduplicates",
89
+ "lf",
90
+ "failedfirst",
91
+ "doctestmodules",
92
+ "debug",
93
+ )
94
+ ):
95
+ return
96
+ try:
97
+ paths = [Path(arg).absolute() for arg in config.args]
98
+ groups = Counter(path.parent for path in paths)
99
+ parents = {parent for parent, count in groups.items() if count >= MIN_FILES}
100
+ # Overlapping parents rebuild child collectors and change fixture visibility.
101
+ if not parents or any(
102
+ parent in groups for child in groups for parent in child.parents
103
+ ):
104
+ return
105
+ if len(set(paths)) != len(paths) or any(
106
+ "::" in arg
107
+ or path.suffix != ".py"
108
+ or path.resolve() != path
109
+ or not path.is_file()
110
+ for arg, path in zip(config.args, paths, strict=True)
111
+ ):
112
+ return
113
+ if (
114
+ (Session.collect, Session._collect_one_node, Dir.collect, Package.collect)
115
+ != _METHODS
116
+ or "_collect_one_node" in session.__dict__
117
+ or tuple(map(_hash, _METHODS)) != _METHOD_HASHES
118
+ ):
119
+ return
120
+ hooks = _hooks(config)
121
+ for function, _, _, _ in hooks:
122
+ name = f"{function.__module__}.{function.__qualname__}"
123
+ if hasattr(function, "__wrapped__") or _hash(function) != _HOOK_HASHES.get(
124
+ name
125
+ ):
126
+ return
127
+ options = _options(config)
128
+ plugins = frozenset(config.pluginmanager.get_plugins())
129
+ codes = tuple(function.__code__ for function in _METHODS)
130
+ except (OSError, TypeError, AttributeError, ValueError):
131
+ return
132
+ original = session._collect_one_node
133
+ cached = {}
134
+ validated_epoch = None
135
+
136
+ def accelerated(node, handle_dupes=True):
137
+ nonlocal validated_epoch
138
+ if (
139
+ state["discovery_status"] != "active"
140
+ or Session._collect_one_node is not _METHODS[1]
141
+ ):
142
+ state["discovery_status"] = "fallback"
143
+ restore()
144
+ return session._collect_one_node(node, handle_dupes)
145
+ if handle_dupes or type(node) not in (Dir, Package) or node.path not in parents:
146
+ return original(node, handle_dupes)
147
+ if session._collection_cache is not validated_epoch:
148
+ cached.clear()
149
+ parts = session._initial_parts
150
+ if len(parts) != len(paths) or any(
151
+ part.path != path
152
+ or part.parts
153
+ or part.parametrization
154
+ or part.module_name
155
+ for part, path in zip(parts, paths, strict=True)
156
+ ):
157
+ state["discovery_status"] = "fallback"
158
+ validated_epoch = session._collection_cache
159
+ if (
160
+ state["discovery_status"] != "active"
161
+ or _hooks(config) != hooks
162
+ or _options(config) != options
163
+ or frozenset(config.pluginmanager.get_plugins()) != plugins
164
+ or tuple(function.__code__ for function in _METHODS) != codes
165
+ or (
166
+ Session.collect,
167
+ Session._collect_one_node,
168
+ Dir.collect,
169
+ Package.collect,
170
+ )
171
+ != _METHODS
172
+ or "collect" in node.__dict__
173
+ ):
174
+ cached.clear()
175
+ state["discovery_status"] = "fallback"
176
+ restore()
177
+ return session._collect_one_node(node, handle_dupes)
178
+ try:
179
+ stat = node.path.stat()
180
+ current = (stat.st_dev, stat.st_ino, stat.st_mtime_ns, stat.st_ctime_ns)
181
+ except OSError:
182
+ cached.pop(node, None)
183
+ return original(node, handle_dupes)
184
+ entry = cached.get(node)
185
+ if (
186
+ entry is not None
187
+ and session._collection_cache is entry[1]
188
+ and current == entry[2]
189
+ ):
190
+ node.ihook.pytest_collectstart(collector=node)
191
+ state["directory_reuses"] += 1
192
+ # This is still a new file argument, not a duplicate argument.
193
+ return entry[0], False
194
+ report, duplicate = original(node, handle_dupes)
195
+ if report.passed:
196
+ cached[node] = (report, session._collection_cache, current)
197
+ else:
198
+ cached.pop(node, None)
199
+ return report, duplicate
200
+
201
+ def restore():
202
+ if session.__dict__.get("_collect_one_node") is accelerated:
203
+ del session._collect_one_node
204
+
205
+ config.add_cleanup(restore)
206
+ session._collect_one_node = accelerated
207
+ state["discovery_status"] = "active"
pytest_boorst/_ids.py ADDED
@@ -0,0 +1,29 @@
1
+ """Python comparison implementation for the parameter ID benchmark."""
2
+
3
+ from collections import Counter, defaultdict
4
+
5
+
6
+ def unique_ids(ids: list[str]) -> list[str]:
7
+ """Match pytest suffixing while maintaining the live ID counts."""
8
+ result = list(ids)
9
+ counts = Counter(ids)
10
+ if len(counts) == len(ids):
11
+ return result
12
+ occupied = counts.copy()
13
+ suffixes: dict[str, int] = defaultdict(int)
14
+ for index, value in enumerate(ids):
15
+ if counts[value] < 2:
16
+ continue
17
+ separator = "_" if value and value[-1].isdigit() else ""
18
+ suffix = suffixes[value]
19
+ candidate = f"{value}{separator}{suffix}"
20
+ while candidate in occupied:
21
+ suffix += 1
22
+ candidate = f"{value}{separator}{suffix}"
23
+ occupied[value] -= 1
24
+ if not occupied[value]:
25
+ del occupied[value]
26
+ occupied[candidate] += 1
27
+ result[index] = candidate
28
+ suffixes[value] = suffix + 1
29
+ return result
@@ -0,0 +1,161 @@
1
+ """Opt-in adapters for verified pytest implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import importlib
7
+ import inspect
8
+ import os
9
+ import sys
10
+ import sysconfig
11
+
12
+ import pytest
13
+
14
+ try:
15
+ from _pytest.python import IdMaker
16
+ except ImportError:
17
+ IdMaker = None
18
+
19
+ STATE_KEY = pytest.StashKey[dict]() if hasattr(pytest, "StashKey") else None
20
+ MIN_IDS = 64
21
+ _ORIGINAL = getattr(IdMaker, "make_unique_parameterset_ids", None)
22
+ _SOURCE_SHA256 = "6baee487481ac9da41d36fee0c8870f093aab91b9f507434d5c89620ddf588b8"
23
+
24
+
25
+ class _ResolvedIds:
26
+ """Let the original method consume IDs without repeating user callbacks."""
27
+
28
+ def __init__(self, maker, ids, strict=None):
29
+ self.maker = maker
30
+ self.ids = ids
31
+ self.strict = strict
32
+
33
+ def _resolve_ids(self):
34
+ return iter(self.ids)
35
+
36
+ def _strict_parametrization_ids_enabled(self):
37
+ if self.strict is not None:
38
+ return self.strict
39
+ return self.maker._strict_parametrization_ids_enabled()
40
+
41
+ def __getattr__(self, name):
42
+ return getattr(self.maker, name)
43
+
44
+
45
+ def pytest_configure(config):
46
+ if STATE_KEY is None:
47
+ return
48
+ state = {
49
+ "status": "disabled",
50
+ "reason": "set PYTEST_BOORST=1 to enable the alpha",
51
+ "native_calls": 0,
52
+ "native_ids": 0,
53
+ "fallback_calls": 0,
54
+ }
55
+ config.stash[STATE_KEY] = state
56
+ if os.environ.get("PYTEST_BOORST") != "1":
57
+ return
58
+ state.update(status="fallback", reason="unsupported interpreter or pytest version")
59
+ if (
60
+ pytest.__version__ not in {"9.0.2", "9.0.3", "9.1.1"}
61
+ or sys.implementation.name != "cpython"
62
+ or not (3, 10) <= sys.version_info[:2] <= (3, 14)
63
+ or sysconfig.get_config_var("Py_GIL_DISABLED")
64
+ ):
65
+ return
66
+ state["reason"] = "pytest ID method was changed or cannot be verified"
67
+ if (
68
+ IdMaker is None
69
+ or _ORIGINAL is None
70
+ or getattr(IdMaker, "make_unique_parameterset_ids", None) is not _ORIGINAL
71
+ or not inspect.isfunction(_ORIGINAL)
72
+ or hasattr(_ORIGINAL, "__wrapped__")
73
+ or _ORIGINAL.__module__ != "_pytest.python"
74
+ or _ORIGINAL.__qualname__ != "IdMaker.make_unique_parameterset_ids"
75
+ ):
76
+ return
77
+ try:
78
+ source_hash = hashlib.sha256(inspect.getsource(_ORIGINAL).encode()).hexdigest()
79
+ except (OSError, TypeError):
80
+ return
81
+ if source_hash != _SOURCE_SHA256:
82
+ return
83
+ state["reason"] = "native extension unavailable"
84
+ try:
85
+ native = importlib.import_module("pytest_boorst._native")
86
+ except (ImportError, OSError):
87
+ return
88
+ unique_ids = native.unique_ids
89
+
90
+ def accelerated(maker):
91
+ if maker.config is not config:
92
+ return _ORIGINAL(maker)
93
+ # ponytail: small inputs use pytest; revisit the cutoff with real workloads.
94
+ if len(maker.parametersets) < MIN_IDS:
95
+ state["fallback_calls"] += 1
96
+ return _ORIGINAL(maker)
97
+ ids = list(maker._resolve_ids())
98
+ if not all(type(value) is str and value.isascii() for value in ids):
99
+ state["fallback_calls"] += 1
100
+ return _ORIGINAL(_ResolvedIds(maker, ids))
101
+ if len(ids) == len(set(ids)):
102
+ state["fallback_calls"] += 1
103
+ return ids
104
+ if maker._strict_parametrization_ids_enabled():
105
+ state["fallback_calls"] += 1
106
+ return _ORIGINAL(_ResolvedIds(maker, ids, strict=True))
107
+ result = unique_ids(ids)
108
+ state["native_calls"] += 1
109
+ state["native_ids"] += len(ids)
110
+ return result
111
+
112
+ def restore():
113
+ if IdMaker.make_unique_parameterset_ids is accelerated:
114
+ IdMaker.make_unique_parameterset_ids = _ORIGINAL
115
+
116
+ config.add_cleanup(restore)
117
+ IdMaker.make_unique_parameterset_ids = accelerated
118
+ state.update(status="active", reason="ASCII parameter ID deduplication")
119
+
120
+
121
+ def pytest_report_header(config):
122
+ if STATE_KEY is None:
123
+ return
124
+ state = config.stash[STATE_KEY]
125
+ if state["status"] != "disabled":
126
+ return f"boorst: {state['status']} ({state['reason']})"
127
+
128
+
129
+ @pytest.hookimpl(trylast=True)
130
+ def pytest_sessionstart(session):
131
+ if (
132
+ STATE_KEY is None
133
+ or os.environ.get("PYTEST_BOORST") != "1"
134
+ or pytest.__version__ != "9.1.1"
135
+ or sys.platform == "win32"
136
+ or os.environ.get("PYTEST_DEBUG")
137
+ or sys.implementation.name != "cpython"
138
+ or not (3, 10) <= sys.version_info[:2] <= (3, 14)
139
+ or sysconfig.get_config_var("Py_GIL_DISABLED")
140
+ ):
141
+ return
142
+ from ._discovery import install
143
+
144
+ install(session, session.config.stash[STATE_KEY])
145
+
146
+
147
+ def pytest_terminal_summary(terminalreporter, config):
148
+ if STATE_KEY is None:
149
+ return
150
+ state = config.stash[STATE_KEY]
151
+ if state["status"] == "active":
152
+ terminalreporter.write_line(
153
+ f"boorst: {state['native_calls']} native ID batches "
154
+ f"({state['native_ids']} IDs), {state['fallback_calls']} stock batches"
155
+ )
156
+ elif state["status"] == "fallback":
157
+ terminalreporter.write_line(f"boorst: fallback ({state['reason']})")
158
+ if state.get("directory_reuses"):
159
+ terminalreporter.write_line(
160
+ f"boorst: {state['directory_reuses']} directory reports reused"
161
+ )
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-boorst
3
+ Version: 0.1.0a5
4
+ Summary: Experimental Rust-powered acceleration for pytest
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Framework :: Pytest
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Rust
11
+ Requires-Dist: pytest
12
+ Requires-Python: >=3.10
13
+ Project-URL: Repository, https://github.com/dnikolayev/pytest-boorst
14
+ Project-URL: Issues, https://github.com/dnikolayev/pytest-boorst/issues
15
+ Description-Content-Type: text/markdown
16
+
17
+ # pytest-boorst
18
+
19
+ Experimental Rust-powered acceleration for pytest.
20
+
21
+ This alpha accelerates duplicate parameter-ID bookkeeping and repeated directory
22
+ discovery while pytest keeps control of test collection, fixtures, execution, and
23
+ reporting. Acceleration is opt-in and guarded by a tested pytest version and
24
+ implementation fingerprint. See the [measured results](#performance-evidence).
25
+
26
+ ## Try the alpha
27
+
28
+ With uv installed, run this command from your project with its test dependencies
29
+ already installed in its virtual environment. It works in POSIX shells and PowerShell:
30
+
31
+ ```sh
32
+ uv run --no-sync https://raw.githubusercontent.com/dnikolayev/pytest-boorst/v0.1.0a5/scripts/try.py
33
+ ```
34
+
35
+ Append your usual pytest arguments, such as `-q tests/`. The launcher installs the
36
+ pinned GitHub release using a compatible wheel and enables Boorst for that pytest
37
+ process. It preserves the existing Python, pytest, dependencies, project configuration,
38
+ and lockfile. An already-installed matching alpha needs no wheel download or reinstallation.
39
+
40
+ Native wheels cover macOS Intel/ARM64, Windows x86-64/x86/ARM64, and Linux
41
+ glibc/musl on x86-64/x86/ARM64. See the [platform matrix](CONTRIBUTING.md#platform-wheels).
42
+ Other platforms can install the universal Python wheel: it retains the guarded
43
+ directory optimization on supported Linux/macOS environments and uses stock IDs.
44
+
45
+ Boorst remains disabled for ordinary pytest runs unless `PYTEST_BOORST=1`. Use
46
+ `-p no:boorst` to prevent plugin loading; uninstalling the package restores ordinary
47
+ pytest behavior. The launcher respects plugin-autoload settings.
48
+
49
+ You can also install the pinned alpha from PyPI with `uv pip install pytest-boorst==0.1.0a5`.
50
+ [GitHub releases](https://github.com/dnikolayev/pytest-boorst/releases) trigger the full CI
51
+ matrix, then publish its tested distributions to PyPI using Trusted Publishing.
52
+ Release assets contain the same wheels, source distribution and SHA-256 manifest.
53
+
54
+ ## Compatibility boundary
55
+
56
+ The parameter-ID adapter currently accelerates **pytest 9.0.2, 9.0.3, and 9.1.1 on CPython
57
+ 3.10–3.14 with the GIL enabled**. It checks the original method's source fingerprint
58
+ before changing it and restores its owned patch at session cleanup. Other pytest versions,
59
+ modified methods, and missing or unloadable native extensions use stock parameter IDs.
60
+ Installation does not require changing an already-installed pytest version.
61
+ On older releases without pytest's Stash API, the plugin falls back quietly.
62
+
63
+ Only duplicate batches of at least 64 resolved, exact ASCII strings enter Rust.
64
+ Small batches, unique IDs, Unicode, string subclasses, hidden IDs, and strict-ID
65
+ errors use pytest behavior. Pytest still resolves custom IDs and calls user hooks
66
+ exactly once. Unexpected native computation errors propagate rather than rerunning
67
+ callbacks or hiding a bug.
68
+
69
+ On **pytest 9.1.1, CPython 3.10–3.14, Linux and macOS**, directory discovery can
70
+ reuse successful reports for directories with at least 32 distinct `.py` files
71
+ passed as plain arguments. Groups may be interleaved across several directories;
72
+ smaller groups retain stock discovery. Selected parent directories must not
73
+ contain one another. This Python optimization avoids recreating every
74
+ sibling collector for each requested file. It needs no native extension and does
75
+ not change the arguments, import unrequested tests, or replace file collection.
76
+
77
+ Directory reuse requires verified stock directory methods and discovery/report
78
+ hooks. Custom discovery/report hooks, modified methods, selectors, duplicate paths, symlinks,
79
+ overlapping parent directories, doctest-module mode, last-failed/failed-first modes, and debug
80
+ tracing use stock discovery. Changed hooks, plugins, or collection options stop
81
+ reuse; changed directory metadata invalidates the report. Windows currently uses
82
+ stock discovery. Plugins inspecting discarded collectors or pytest's private
83
+ collection-cache layout are outside this experimental compatibility boundary.
84
+
85
+ This uses a narrowly guarded private pytest method. The tested compatibility
86
+ boundary does not establish compatibility with every possible third-party plugin
87
+ or modification of pytest internals. Tests cover ordered IDs, phase outcomes,
88
+ coverage, asyncio, xdist, callbacks, errors, and repeated invocation.
89
+
90
+ On releases with the Stash API, the header reports activation or fallback.
91
+ The summary reports native ID batches, stock batches, and reused directory reports.
92
+ Counters are per process; xdist workers collect their own batches, so the
93
+ controller can report zero native calls.
94
+
95
+ ## Development
96
+
97
+ Install uv and Rust, then:
98
+
99
+ ```sh
100
+ uv sync --locked --python 3.14
101
+ uv run --no-sync maturin develop --release --locked
102
+ uv run --no-sync ruff check .
103
+ uv run --no-sync ruff format --check .
104
+ uv run --no-sync pytest
105
+ cargo fmt --check
106
+ cargo clippy --locked --all-targets -- -D warnings
107
+ cargo test --locked --lib
108
+ ```
109
+
110
+ `uv.lock` and `Cargo.lock` pin the development environments. Maturin builds the
111
+ PyO3 extension. GitHub CI installs built wheels and tests Python 3.10–3.14 on Linux,
112
+ plus macOS and Windows smoke coverage, and rebuilds the source distribution.
113
+ The Linux Python 3.12 wheel lane also runs the complete suite and installed-wheel
114
+ smoke checks with pytest 9.0.2 and 9.0.3, and verifies stock fallback on pytest 8.4.2.
115
+ Normal wheel-installation checks preserve pytest 6.2.5 and 7.4.4 in separate
116
+ environments and compare fallback IDs and phase outcomes against stock pytest.
117
+ An independent benchmark job retains raw timings and CPU/memory observations.
118
+
119
+ ## Performance evidence
120
+
121
+ Measured on Ubuntu 24.04, CPython 3.14.8, pytest 9.1.1 and Boorst 0.1.0a2.
122
+ Times are medians of three fresh **full pytest runs**, including startup,
123
+ collection, fixtures and execution. Percentages use unrounded medians.
124
+
125
+ | Synthetic workload | Stock pytest | Boorst enabled | Time change |
126
+ | --- | ---: | ---: | ---: |
127
+ | 10,000 duplicate parameter IDs | 8.491 s | 6.685 s | 21.3% less |
128
+ | 10,000 colliding numeric-suffix IDs | 8.291 s | 6.495 s | 21.7% less |
129
+ | 384 explicit sibling test files | 7.755 s | 1.003 s | 87.1% less |
130
+ | 10,000 unique IDs (control) | 6.526 s | 6.511 s | 0.2% less |
131
+ | 8 IDs (control) | 0.244 s | 0.248 s | 1.7% more |
132
+
133
+ | Optimization | Why it helps |
134
+ | --- | --- |
135
+ | Duplicate-ID bookkeeping | Avoids rebuilding the used-ID set for every collision; PyO3 accelerates the bounded helper. |
136
+ | Directory-report reuse | Reuses a successful sibling-collector listing across file arguments, avoiding repeated scans and collector construction. This optimization uses Python. |
137
+
138
+ Most ID savings come from the algorithm: the optimized Python comparison took
139
+ 6.739 s for duplicate IDs, versus 6.685 s with Rust. The isolated helper took
140
+ 6.40 ms in Python and 4.34 ms in Rust; helper timings describe only that component.
141
+ Unique and small controls made zero native calls. Ordinary suites can see little
142
+ benefit or overhead; these synthetic results do not predict overall project CI.
143
+
144
+ Both harnesses verify exact ordered node IDs, setup/call/teardown outcomes and
145
+ exit codes. The ID harness checks parity before timing; the discovery harness
146
+ includes identical receipt recording during timing and disables bytecode writes
147
+ for both modes. CI requires at least 10% duplicate-workload improvement and 5%
148
+ directory-workload improvement. Separate compatibility tests cover callbacks,
149
+ fixtures, errors, coverage, asyncio, xdist and fallback behavior. Multi-directory
150
+ checks include interleaved groups, directory-local fixtures, syntax errors,
151
+ independent directory invalidation and stock fallback for overlapping groups.
152
+
153
+ Sources: [ID measurements](benchmarks/results/alpha2-python314-ids-ci.json),
154
+ [directory measurements](benchmarks/results/alpha2-python314-discovery-ci.json), and the
155
+ [successful CI run](https://github.com/dnikolayev/pytest-boorst/actions/runs/37696156274).
156
+
157
+ Alpha 4 also covers independent sibling groups. A macOS ARM64 / CPython
158
+ 3.14.7 / pytest 9.1.1 trial with the universal wheel measured three fresh full runs:
159
+
160
+ | Additional synthetic workload | Stock pytest | Boorst enabled | Time change |
161
+ | --- | ---: | ---: | ---: |
162
+ | 384 files across four directories | 1.963 s | 0.973 s | 50.4% less |
163
+
164
+ [Raw measurements](benchmarks/results/alpha4-multiple-directories-local.json)
165
+ record exact ordered-ID, phase and exit-code parity, with 380 directory reuses and
166
+ zero native ID calls per run. This measures the Python discovery optimization.
167
+ Alpha 3 uses stock discovery for this multi-directory plan. CI repeats the four-directory
168
+ workload on Python 3.14.8 and retains its own raw receipt.
169
+
170
+ The benchmark job uses CPython 3.14.8. Existing projects can keep their own
171
+ Python version; the trial launcher uses their existing environment.
172
+
173
+ Reproduce with Python 3.14 and a release build:
174
+
175
+ ```sh
176
+ uv sync --locked --python 3.14
177
+ uv run --no-sync maturin develop --release --locked
178
+ uv run --no-sync python benchmarks/run.py --size 10000 --repeats 3
179
+ uv run --no-sync python benchmarks/discovery.py --repeats 3
180
+ uv run --no-sync python benchmarks/discovery.py --directories 4 --repeats 3 --output benchmark-results-multiple-directories.json
181
+ ```
182
+
183
+ See [benchmark methodology](benchmarks/README.md) for detailed compatibility
184
+ controls and earlier public-project measurements.
@@ -0,0 +1,9 @@
1
+ pytest_boorst/__init__.py,sha256=V_9FjAHepAeKdccveW3tuEEzkFB_odn7XmVJK6DcYdQ,54
2
+ pytest_boorst/_discovery.py,sha256=FjLRRrk7xvU6i0MPbJ6zKW0ro6hK40ys4cYt-XX0Mb4,8310
3
+ pytest_boorst/_ids.py,sha256=FHVWuqeqzUg4QMmxHSOt5IGN3AFtj9-34Rp0FpXuhoo,984
4
+ pytest_boorst/plugin.py,sha256=4Exm5NJ0Tzd-nahVgnCDyYIkaPOPS79s-l2l2lLolu4,5284
5
+ pytest_boorst-0.1.0a5.dist-info/licenses/LICENSE,sha256=C3b0bJzAQcmo6MKZY7wMWxSBm8wFhozZCQQhF3trN4Y,3521
6
+ pytest_boorst-0.1.0a5.dist-info/WHEEL,sha256=WKk67IZPzp9uSkBO-iWoZzrqp9-AwH7FE9EB8EoJbf4,81
7
+ pytest_boorst-0.1.0a5.dist-info/entry_points.txt,sha256=C-OM76ixrlFiPIKKmGlQ8BBQVTFx90B4N63BsbkUgx4,42
8
+ pytest_boorst-0.1.0a5.dist-info/METADATA,sha256=Lk-KQLNpKvVYkqwjgZM1-v52UlEdAW1pZ5dev1cDnXU,9794
9
+ pytest_boorst-0.1.0a5.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.12
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [pytest11]
2
+ boorst = pytest_boorst.plugin
3
+
@@ -0,0 +1,70 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pytest-boorst contributors
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.
22
+
23
+ Third-party notices
24
+ ===================
25
+
26
+ PyO3 provides the native Python bindings and is used under its MIT license:
27
+ https://github.com/PyO3/pyo3
28
+
29
+ Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining a copy
32
+ of this software and associated documentation files (the "Software"), to deal
33
+ in the Software without restriction, including without limitation the rights
34
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35
+ copies of the Software, and to permit persons to whom the Software is
36
+ furnished to do so, subject to the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be included in all
39
+ copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47
+ SOFTWARE.
48
+
49
+ Parameter ID suffix semantics follow pytest, which is distributed under MIT:
50
+ https://github.com/pytest-dev/pytest
51
+
52
+ Copyright (c) 2004 Holger Krekel and others
53
+
54
+ Permission is hereby granted, free of charge, to any person obtaining a copy
55
+ of this software and associated documentation files (the "Software"), to deal
56
+ in the Software without restriction, including without limitation the rights
57
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
58
+ copies of the Software, and to permit persons to whom the Software is
59
+ furnished to do so, subject to the following conditions:
60
+
61
+ The above copyright notice and this permission notice shall be included in all
62
+ copies or substantial portions of the Software.
63
+
64
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
65
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
66
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
67
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
68
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
69
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
70
+ SOFTWARE.