pytest-threadpool 0.2.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_threadpool-0.2.0/.claude/settings.local.json +30 -0
- pytest_threadpool-0.2.0/.github/workflows/ci.yml +36 -0
- pytest_threadpool-0.2.0/.gitignore +165 -0
- pytest_threadpool-0.2.0/ANALYSIS.md +142 -0
- pytest_threadpool-0.2.0/CLAUDE.md +0 -0
- pytest_threadpool-0.2.0/CONTRIBUTING.md +164 -0
- pytest_threadpool-0.2.0/LICENSE +201 -0
- pytest_threadpool-0.2.0/PKG-INFO +168 -0
- pytest_threadpool-0.2.0/PLAN.md +222 -0
- pytest_threadpool-0.2.0/README.md +151 -0
- pytest_threadpool-0.2.0/TODO.md +44 -0
- pytest_threadpool-0.2.0/hooks/pre-commit +13 -0
- pytest_threadpool-0.2.0/pyproject.toml +77 -0
- pytest_threadpool-0.2.0/scripts/setup-dev +15 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/__init__.py +6 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/_api.py +23 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/_constants.py +30 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/_fixtures.py +79 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/_grouping.py +106 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/_markers.py +145 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/_runner.py +558 -0
- pytest_threadpool-0.2.0/src/pytest_threadpool/plugin.py +76 -0
- pytest_threadpool-0.2.0/tests/__init__.py +0 -0
- pytest_threadpool-0.2.0/tests/integration_tests/__init__.py +3 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/__init__.py +0 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/_templates.py +35 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/class_barrier_concurrency.py +19 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/class_single_method.py +9 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/class_thread_verification.py +23 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_cross_module_group.py +57 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_keyboard_interrupt.py +15 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_nested_threads.py +39 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_sigint.py +29 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_sigint_many.py +72 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_system_exit.py +15 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_autouse_function.py +41 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_class_scoped_once.py +29 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_class_yield.py +26 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_function_scoped.py +30 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_interdependent_finalizers.py +38 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_multiple_scopes.py +26 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_parameterized.py +16 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_teardown_exception.py +36 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_yield_cleanup.py +26 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/marks_custom.py +26 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/marks_standard.py +29 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/parallel_only_skip.py +12 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_cross_module.py +15 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_incremental.py +24 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_incremental_conftest.py +29 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_package_children.py +43 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_all_merged.py +20 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_all_not_parallelizable.py +34 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_children_separate_params.py +18 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_dynamic_parametrize.py +19 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_method_overrides_class.py +26 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_module_children.py +23 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_not_parallelizable_function.py +18 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_not_parallelizable_method.py +28 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_parameters.py +14 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/sequential_bare_functions.py +29 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/sequential_unmarked_class.py +22 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/setup_all_fail.py +20 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/setup_mixed_pass_fail.py +22 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_counter.py +30 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +48 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_dict_mutation.py +31 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_non_pickleable.py +37 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_two_phase_barrier.py +28 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/validate_threadpool.py +12 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/validate_threadpool_conftest.py +16 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_class_setup.py +29 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_combined_setup.py +29 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_function_setup.py +27 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_method_setup.py +27 -0
- pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_module_setup.py +19 -0
- pytest_threadpool-0.2.0/tests/integration_tests/conftest.py +155 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_class.py +25 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_edge_cases.py +190 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_fixtures.py +63 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_marks.py +74 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_package.py +156 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_reporting.py +308 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_scopes.py +61 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_sequential.py +17 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_shared.py +35 -0
- pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_xunit.py +35 -0
- pytest_threadpool-0.2.0/tests/unit_tests/__init__.py +3 -0
- pytest_threadpool-0.2.0/tests/unit_tests/test_unit_api.py +32 -0
- pytest_threadpool-0.2.0/tests/unit_tests/test_unit_fixtures.py +181 -0
- pytest_threadpool-0.2.0/tests/unit_tests/test_unit_grouping.py +270 -0
- pytest_threadpool-0.2.0/tests/unit_tests/test_unit_markers.py +403 -0
- pytest_threadpool-0.2.0/tests/unit_tests/test_unit_plugin.py +40 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(PYTHON_GIL=0 .venv/bin/pytest:*)",
|
|
5
|
+
"Bash(.venv/bin/pytest:*)",
|
|
6
|
+
"Bash(python3:*)",
|
|
7
|
+
"Bash(uv run:*)",
|
|
8
|
+
"Bash(PYTHON_GIL=0 uv run:*)",
|
|
9
|
+
"Bash(do PYTHON_GIL=0:*)",
|
|
10
|
+
"Bash(python:*)",
|
|
11
|
+
"Bash(ls:*)",
|
|
12
|
+
"Bash(pip install:*)",
|
|
13
|
+
"Bash(pip show:*)",
|
|
14
|
+
"Bash(PYTHONPATH=/home/work/repos/pytest-freethreaded-example/src python:*)",
|
|
15
|
+
"Bash(/home/work/repos/pytest-freethreaded-example/.venv/bin/pip install:*)",
|
|
16
|
+
"Bash(uv pip:*)",
|
|
17
|
+
"Bash(grep:*)",
|
|
18
|
+
"Bash(wc:*)",
|
|
19
|
+
"Bash(git:*)",
|
|
20
|
+
"Read(//tmp/**)",
|
|
21
|
+
"Bash(mkdir -p test_debug)",
|
|
22
|
+
"Bash(cp /home/work/repos/pytest-freethreaded-example/tests/cases/setup_mixed_pass_fail.py /tmp/test_debug/test_case.py)",
|
|
23
|
+
"Bash(ruff check:*)",
|
|
24
|
+
"Bash(cat:*)",
|
|
25
|
+
"Bash(uv sync:*)",
|
|
26
|
+
"Bash(find:*)",
|
|
27
|
+
"Bash(find src:*)"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v6
|
|
15
|
+
- run: uv sync --python 3.14t --group dev
|
|
16
|
+
- run: uv run ruff format --check src/ tests/
|
|
17
|
+
- run: uv run ruff check src/ tests/
|
|
18
|
+
- run: uv run pyright src/
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
python: ["3.13t", "3.14t", "3.15t"]
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: astral-sh/setup-uv@v6
|
|
29
|
+
- run: uv python install ${{ matrix.python }}
|
|
30
|
+
- run: uv sync --python ${{ matrix.python }} --group dev
|
|
31
|
+
- run: uv run --python ${{ matrix.python }} pytest -v --tb=short --cov --cov-branch --cov-report=xml
|
|
32
|
+
- name: Upload coverage reports to Codecov
|
|
33
|
+
if: matrix.python == '3.14t'
|
|
34
|
+
uses: codecov/codecov-action@v5
|
|
35
|
+
with:
|
|
36
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
### Python template
|
|
2
|
+
# Byte-compiled / optimized / DLL files
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*$py.class
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# poetry
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
103
|
+
#poetry.lock
|
|
104
|
+
|
|
105
|
+
# pdm
|
|
106
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
107
|
+
#pdm.lock
|
|
108
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
109
|
+
# in version control.
|
|
110
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
111
|
+
.pdm.toml
|
|
112
|
+
.pdm-python
|
|
113
|
+
.pdm-build/
|
|
114
|
+
|
|
115
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
116
|
+
__pypackages__/
|
|
117
|
+
|
|
118
|
+
# Celery stuff
|
|
119
|
+
celerybeat-schedule
|
|
120
|
+
celerybeat.pid
|
|
121
|
+
|
|
122
|
+
# SageMath parsed files
|
|
123
|
+
*.sage.py
|
|
124
|
+
|
|
125
|
+
# Environments
|
|
126
|
+
.env
|
|
127
|
+
.venv
|
|
128
|
+
env/
|
|
129
|
+
venv/
|
|
130
|
+
ENV/
|
|
131
|
+
env.bak/
|
|
132
|
+
venv.bak/
|
|
133
|
+
|
|
134
|
+
# Spyder project settings
|
|
135
|
+
.spyderproject
|
|
136
|
+
.spyproject
|
|
137
|
+
|
|
138
|
+
# Rope project settings
|
|
139
|
+
.ropeproject
|
|
140
|
+
|
|
141
|
+
# mkdocs documentation
|
|
142
|
+
/site
|
|
143
|
+
|
|
144
|
+
# mypy
|
|
145
|
+
.mypy_cache/
|
|
146
|
+
.dmypy.json
|
|
147
|
+
dmypy.json
|
|
148
|
+
|
|
149
|
+
# Pyre type checker
|
|
150
|
+
.pyre/
|
|
151
|
+
|
|
152
|
+
# pytype static type analyzer
|
|
153
|
+
.pytype/
|
|
154
|
+
|
|
155
|
+
# Cython debug symbols
|
|
156
|
+
cython_debug/
|
|
157
|
+
|
|
158
|
+
# PyCharm
|
|
159
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
160
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
161
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
162
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
163
|
+
.idea/
|
|
164
|
+
uv.lock
|
|
165
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# pytest-threadpool: Risk, Bug & Test Gap Analysis
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
pytest-threadpool runs test bodies in parallel via `ThreadPoolExecutor` while keeping all pytest internals (fixtures, setup/teardown, reporting) sequential. This analysis covers risks, potential bugs, and missing test coverage.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Potential Bugs
|
|
10
|
+
|
|
11
|
+
### 1. ~~Empty `callable_items` after setup failures~~ [RESOLVED — Not a bug]
|
|
12
|
+
|
|
13
|
+
**Debunked.** When `callable_items` is empty, `workers = 1` (line 365). The guard on line 409 (`if workers > 1 and len(callable_items) > 1`) prevents `ThreadPoolExecutor` creation — execution falls through to the `else` branch (lines 427–431), which iterates over the empty list harmlessly. Phase 3 then correctly reports setup failures via `_report_item`. No unnecessary thread pool is created and no misleading output is produced.
|
|
14
|
+
|
|
15
|
+
### 2. ~~`_do_call` exception swallowing~~ [RESOLVED — Not a bug]
|
|
16
|
+
|
|
17
|
+
**Debunked.** When `future.exception()` is not None (lines 416–422), the exception is wrapped in a `CallInfo` via `CallInfo.from_call()` and stored in `call_results[item]`. Then `_report_item(item)` is called immediately (line 426), which builds a report via `pytest_runtest_makereport` and fires `pytest_runtest_logreport` — properly recording it as a test failure in pytest's statistics. Exceptions are not swallowed; they follow the standard reporting path.
|
|
18
|
+
|
|
19
|
+
### 3. ~~Fixture finalizer ordering after restore~~ [RESOLVED — Not a bug]
|
|
20
|
+
|
|
21
|
+
**Debunked.** `save_and_clear_function_fixtures()` uses `saved.extend(fixturedef._finalizers)` iterating over `request._fixture_defs.values()`. Since Python 3.7+ dicts preserve insertion order (matching fixture resolution/setup order), and `_teardown_all` runs `for fn in reversed(fns)`, the result is correct LIFO ordering: later-resolved fixtures' finalizers run first, and within each fixture, finalizers run in reverse registration order — matching pytest's normal teardown semantics.
|
|
22
|
+
|
|
23
|
+
### 4. ~~`sys.modules` iteration in `MarkerResolver.package_scope()`~~ [RESOLVED — Not a bug]
|
|
24
|
+
|
|
25
|
+
**Debunked.** The code uses `sys.modules.get(pkg)` (line 76 of `_markers.py`) — a single-key dict lookup, not iteration. The analysis incorrectly claims the code "iterates `sys.modules`." Furthermore, `package_scope()` is only called during `GroupKeyBuilder.group_key()`, which runs during the sequential collection/grouping phase before any parallel execution begins. No thread-safety concern exists.
|
|
26
|
+
|
|
27
|
+
### 5. ~~`_initrequest()` side effects~~ [RESOLVED — Not a bug, duplicate of Risk #1]
|
|
28
|
+
|
|
29
|
+
**Debunked as a standalone bug.** The `_initrequest()` call mirrors pytest's own `runtestprotocol` in `_pytest/runner.py`. It's the standard way to initialize the request lifecycle for an item. The concern about private API stability is valid but is already covered under Risk #1 (heavy reliance on pytest private API). This is a maintenance risk, not a bug.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Risks
|
|
34
|
+
|
|
35
|
+
### 1. Heavy reliance on pytest private API (HIGH)
|
|
36
|
+
|
|
37
|
+
The plugin accesses numerous protected members:
|
|
38
|
+
- `item._request` / `item._initrequest()`
|
|
39
|
+
- `request._fixture_defs`
|
|
40
|
+
- `session._setupstate.stack`
|
|
41
|
+
- `fixturedef._scope`, `._finalizers`, `.cached_result`
|
|
42
|
+
- `TerminalReporter._tw`
|
|
43
|
+
|
|
44
|
+
**Impact**: Any pytest minor release could break the plugin silently. No compatibility layer or version detection exists beyond `pytest >= 9.0.2`.
|
|
45
|
+
|
|
46
|
+
### 2. No free-threaded runtime validation (MEDIUM)
|
|
47
|
+
|
|
48
|
+
The plugin doesn't verify that it's running on free-threaded Python (`PYTHON_GIL=0` or 3.14t+). If run on a GIL-enabled build, tests will execute but without true parallelism, potentially masking concurrency bugs in user code that only manifest under real parallelism.
|
|
49
|
+
|
|
50
|
+
### 3. Single lock for all terminal output (MEDIUM)
|
|
51
|
+
|
|
52
|
+
`_LiveReporter` uses one `threading.Lock()` for state updates. The lock protects `_item_state` mutations but doesn't protect the actual terminal write operations in `_render()`. If `_render()` is called from the main thread while a worker thread calls `mark_done()`, the render could read partially-updated state.
|
|
53
|
+
|
|
54
|
+
### 4. ANSI escape sequence fragility (LOW)
|
|
55
|
+
|
|
56
|
+
The live reporter uses raw ANSI cursor movement (`\033[F`, `\033[K`) for in-place updates. This assumes:
|
|
57
|
+
- Terminal supports ANSI sequences
|
|
58
|
+
- No other output is written to stdout during rendering
|
|
59
|
+
- Line count calculations are accurate
|
|
60
|
+
|
|
61
|
+
Broken assumptions produce garbled terminal output.
|
|
62
|
+
|
|
63
|
+
### 5. Hardcoded barrier timeouts in tests (LOW)
|
|
64
|
+
|
|
65
|
+
Test cases use `threading.Barrier(N, timeout=10)`. On slow CI or high-contention environments, this could cause flaky test failures that look like concurrency bugs but are just timeout issues.
|
|
66
|
+
|
|
67
|
+
### 6. ~~No signal/interrupt handling during parallel phase~~ [RESOLVED]
|
|
68
|
+
|
|
69
|
+
Replaced ThreadPoolExecutor with a daemon-thread worker pool. On SIGINT, pending work items are abandoned (daemon threads exit with the process), fixture teardown still runs, and KeyboardInterrupt is re-raised for pytest's handler.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Missing Tests
|
|
74
|
+
|
|
75
|
+
### Error & Edge Cases
|
|
76
|
+
- [x] Test body raises `SystemExit` or `KeyboardInterrupt` during parallel execution — tested, both caught and reported as failures
|
|
77
|
+
- [x] All tests in a parallel group fail during setup (empty `callable_items`) — tested + fixed cached error leak
|
|
78
|
+
- [x] Mixed setup pass/fail within a single parallel group — tested + fixed cached error leak
|
|
79
|
+
- [ ] Non-pickleable objects in test state during parallel batches
|
|
80
|
+
- [x] SIGINT during parallel execution phase — fixed with daemon-thread pool, exits promptly
|
|
81
|
+
- [ ] Test that modifies `sys.modules` during parallel execution
|
|
82
|
+
|
|
83
|
+
### Fixture Scenarios
|
|
84
|
+
- [x] Fixtures with interdependent finalizers (finalizer A must run before finalizer B) — tested, LIFO ordering correct
|
|
85
|
+
- [x] `autouse` fixtures with function scope in parallel groups — tested, works correctly
|
|
86
|
+
- [x] Fixture teardown that raises exceptions in parallel groups — tested + fixed to run all finalizers
|
|
87
|
+
- [ ] Complex fixture parameter combinations where scope changes mid-group
|
|
88
|
+
- [ ] `yield` fixtures with cleanup that depends on execution order
|
|
89
|
+
|
|
90
|
+
### Scope & Grouping
|
|
91
|
+
- [ ] Deeply nested packages (3+ levels) with mixed marker inheritance
|
|
92
|
+
- [x] Dynamic parametrize (e.g., `pytest_generate_tests`) with parallel markers — tested + fixed grouping bug
|
|
93
|
+
- [x] Groups where all items have `not_parallelizable` — verify no thread pool created — tested, runs on MainThread
|
|
94
|
+
- [ ] Very large groups (100+ tests) to verify thread pool scaling
|
|
95
|
+
|
|
96
|
+
### Reporting
|
|
97
|
+
- [ ] Mixed verbosity flags (`-v`, `-vv`, `-q`) with parallel output
|
|
98
|
+
- [ ] `--tb=long` / `--tb=short` with parallel test failures
|
|
99
|
+
- [ ] ~~Captured stdout/stderr from parallel tests~~ — capsys is process-global, incompatible with parallel execution by design
|
|
100
|
+
- [ ] `--no-header` and other output-modifying flags
|
|
101
|
+
- [ ] Plugin interaction with `pytest-xdist` (should error or warn)
|
|
102
|
+
|
|
103
|
+
### Compatibility
|
|
104
|
+
- [ ] Python 3.13t with explicit `PYTHON_GIL=0`
|
|
105
|
+
- [ ] pytest version matrix (9.0.2, 9.1.x, 9.2.x, 10.x)
|
|
106
|
+
- [ ] Interaction with common plugins (`pytest-cov`, `pytest-timeout`, `pytest-randomly`)
|
|
107
|
+
|
|
108
|
+
### Concurrency Stress
|
|
109
|
+
- [ ] High thread count (16, 32, 64 threads) with many small tests
|
|
110
|
+
- [x] Tests that themselves spawn threads (nested parallelism) — tested, works correctly
|
|
111
|
+
- [ ] Tests with heavy I/O (file writes to same directory) in parallel
|
|
112
|
+
- [ ] Memory pressure: many parallel tests allocating large objects
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Summary
|
|
117
|
+
|
|
118
|
+
| Category | Count | Status |
|
|
119
|
+
|----------|-------|--------|
|
|
120
|
+
| Potential bugs | 5 | **All 5 debunked** — none were actual bugs |
|
|
121
|
+
| Risks | 6 | 1 high, 3 medium, 2 low (unchanged) |
|
|
122
|
+
| Missing test areas | 25+ | Mixed (unchanged) |
|
|
123
|
+
|
|
124
|
+
All five "potential bugs" were investigated and found to be incorrect or misleading claims. The code handles each scenario correctly. The highest-priority item remains the **dependency on pytest private API** (Risk #1). The second priority is adding **error/edge-case tests** for parallel execution failures.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
┌─────┬───────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────┐
|
|
128
|
+
│ # │ Risk │ Real? │ Fixable? │
|
|
129
|
+
├─────┼───────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤
|
|
130
|
+
│ 1 │ Private API reliance │ True — inherent to approach │ No code fix possible — it's a design tradeoff │
|
|
131
|
+
├─────┼───────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤
|
|
132
|
+
│ 2 │ No free-threaded runtime validation │ True — but by design │ Feature request, not a bug │
|
|
133
|
+
├─────┼───────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤
|
|
134
|
+
│ 3 │ Single lock / unprotected terminal writes │ False — _render() doesn't exist; both mark_running and mark_done hold the lock when writing │ N/A │
|
|
135
|
+
├─────┼───────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤
|
|
136
|
+
│ 4 │ ANSI escape fragility │ True — already mitigated by plain-mode fallback │ Low severity, no fix needed │
|
|
137
|
+
├─────┼───────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤
|
|
138
|
+
│ 5 │ Hardcoded barrier timeouts │ True — test-only concern │ Low severity, no production code affected │
|
|
139
|
+
├─────┼───────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤
|
|
140
|
+
│ 6 │ No SIGINT handling │ True — ThreadPoolExecutor provides basic cleanup but Phase 4 teardown is skipped │ Feature request, not a bug │
|
|
141
|
+
└─────┴───────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────┘
|
|
142
|
+
|
|
File without changes
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Contributing to pytest-threadpool
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone <repo-url>
|
|
7
|
+
cd pytest-threadpool
|
|
8
|
+
./scripts/setup-dev # defaults to python 3.14t
|
|
9
|
+
./scripts/setup-dev 3.13t # or specify a version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This installs a free-threaded Python via uv, syncs all dependencies (including
|
|
13
|
+
dev tools), and sets up a pre-commit hook that runs `ruff format`, `ruff check`,
|
|
14
|
+
and `pyright` before each commit.
|
|
15
|
+
|
|
16
|
+
> Python 3.13t and 3.14t ship with the GIL disabled by default.
|
|
17
|
+
|
|
18
|
+
## Architecture rules
|
|
19
|
+
|
|
20
|
+
### No module-level executable code
|
|
21
|
+
|
|
22
|
+
All modules define classes, functions, constants, and imports only. No mutable
|
|
23
|
+
state, no object construction, no side effects at import time.
|
|
24
|
+
|
|
25
|
+
`__init__.py` files contain only markers and imports — never barriers, dicts,
|
|
26
|
+
lists, or other state.
|
|
27
|
+
|
|
28
|
+
The single exception is `plugin.py`, which defines bare functions that pytest
|
|
29
|
+
discovers as hooks.
|
|
30
|
+
|
|
31
|
+
### One module, one class
|
|
32
|
+
|
|
33
|
+
Each module contains a single primary class. A second class is only allowed
|
|
34
|
+
when it is tightly coupled to the first (e.g., two enums in the same domain).
|
|
35
|
+
If a class grows unrelated responsibilities, split it into a new module.
|
|
36
|
+
|
|
37
|
+
### Shared state lives in classes
|
|
38
|
+
|
|
39
|
+
Any mutable state needed across tests or across a test run must be held as
|
|
40
|
+
class attributes or instance attributes — never as module-level variables.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# Good
|
|
44
|
+
class TestState:
|
|
45
|
+
log = {}
|
|
46
|
+
barrier = threading.Barrier(3, timeout=10)
|
|
47
|
+
|
|
48
|
+
# Bad
|
|
49
|
+
_log = {}
|
|
50
|
+
_barrier = threading.Barrier(3, timeout=10)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Access on need, not on import
|
|
54
|
+
|
|
55
|
+
State should be created and accessed at the point of use. Importing a module
|
|
56
|
+
should not trigger construction of barriers, connections, or other resources.
|
|
57
|
+
|
|
58
|
+
### Parallelism tests use `ftdir` fixture
|
|
59
|
+
|
|
60
|
+
Tests that verify parallel execution (barriers, concurrent writes, thread counts)
|
|
61
|
+
must use the `ftdir` fixture (defined in `conftest.py`) and `run_pytest`. Unlike
|
|
62
|
+
`pytester`, `ftdir` is thread-safe — it uses `tmp_path` instead of `os.chdir()`,
|
|
63
|
+
so tests can run in parallel via `--threadpool` on the outer suite itself.
|
|
64
|
+
|
|
65
|
+
Each test writes files into its own `tmp_path` directory and runs pytest as a
|
|
66
|
+
subprocess with the exact thread count needed. Use explicit thread counts for
|
|
67
|
+
barrier-based tests (not `auto`) to avoid deadlocks on low-core machines.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Good — isolated subprocess via ftdir, explicit thread count
|
|
71
|
+
def test_children_run_concurrently(self, ftdir):
|
|
72
|
+
ftdir.makepyfile("""
|
|
73
|
+
import threading, pytest
|
|
74
|
+
@pytest.mark.parallelizable("children")
|
|
75
|
+
class TestBarrier:
|
|
76
|
+
barrier = threading.Barrier(2, timeout=10)
|
|
77
|
+
def test_a(self): self.barrier.wait()
|
|
78
|
+
def test_b(self): self.barrier.wait()
|
|
79
|
+
""")
|
|
80
|
+
result = ftdir.run_pytest("--threadpool", "2")
|
|
81
|
+
result.assert_outcomes(passed=2)
|
|
82
|
+
|
|
83
|
+
# Bad — depends on outer runner flags
|
|
84
|
+
class TestBarrier:
|
|
85
|
+
barrier = threading.Barrier(2, timeout=10)
|
|
86
|
+
def test_a(self): self.barrier.wait()
|
|
87
|
+
def test_b(self): self.barrier.wait()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Constants over strings
|
|
91
|
+
|
|
92
|
+
Use `ParallelScope` enum and `_constants` module instead of bare string
|
|
93
|
+
literals. Any string that appears in more than one location belongs in
|
|
94
|
+
`_constants.py`. Enums must use `StrEnum` (not `str, Enum`).
|
|
95
|
+
|
|
96
|
+
### Plugin hooks are wiring only
|
|
97
|
+
|
|
98
|
+
`plugin.py` hook functions delegate to classes immediately. No business logic
|
|
99
|
+
in hook functions — keep them thin.
|
|
100
|
+
|
|
101
|
+
## Project structure
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
src/pytest_threadpool/
|
|
105
|
+
__init__.py # Re-exports only
|
|
106
|
+
_api.py # Public API: parallelizable, not_parallelizable
|
|
107
|
+
_constants.py # ParallelScope and _GroupPrefix enums
|
|
108
|
+
_markers.py # MarkerResolver: marker introspection
|
|
109
|
+
_grouping.py # GroupKeyBuilder: parallel batch grouping
|
|
110
|
+
_fixtures.py # FixtureManager: finalizer save/restore
|
|
111
|
+
_runner.py # ParallelRunner: parallel execution orchestration
|
|
112
|
+
plugin.py # pytest hook implementations (wiring only)
|
|
113
|
+
|
|
114
|
+
hooks/
|
|
115
|
+
pre-commit # Git pre-commit hook (ruff + pyright)
|
|
116
|
+
|
|
117
|
+
scripts/
|
|
118
|
+
setup-dev # One-command dev environment setup
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Commit message style
|
|
122
|
+
|
|
123
|
+
Every commit message starts with one or more tags in brackets:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
[Tag] Short description
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Tags
|
|
130
|
+
|
|
131
|
+
| Tag | Scope |
|
|
132
|
+
|-----|-------|
|
|
133
|
+
| `[Runner]` | Core parallel execution engine (`_runner.py`, `_fixtures.py`) |
|
|
134
|
+
| `[Markers]` | Marker resolution and grouping (`_markers.py`, `_grouping.py`, `_constants.py`) |
|
|
135
|
+
| `[Report]` | Terminal reporting and display (`_LiveReporter`) |
|
|
136
|
+
| `[Plugin]` | Plugin hooks and CLI options (`plugin.py`) |
|
|
137
|
+
| `[API]` | Public API changes (`_api.py`, `__init__.py`) |
|
|
138
|
+
| `[Test]` | Test additions or changes only |
|
|
139
|
+
| `[Tooling]` | Ruff, pyright, pre-commit, CI, scripts |
|
|
140
|
+
| `[Docs]` | README, CONTRIBUTING, docstrings |
|
|
141
|
+
| `[Refactor]` | Internal restructuring, no behavior change |
|
|
142
|
+
| `[Fix]` | Bug fix — must include issue number if fixing a known issue |
|
|
143
|
+
|
|
144
|
+
### Combining tags
|
|
145
|
+
|
|
146
|
+
Tags can be combined. The fix tag always comes first and includes the issue
|
|
147
|
+
number when one exists:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
[Fix #42][Runner] Prevent deadlock when setup fails mid-group
|
|
151
|
+
[Fix][Report] Correct progress count after skipped tests
|
|
152
|
+
[Test][Markers] Add coverage for nested package marker inheritance
|
|
153
|
+
[Tooling] Add ruff PIE and RET rule sets
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Running tests
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Run all tests in parallel (each test spawns its own subprocess)
|
|
160
|
+
pytest --threadpool auto
|
|
161
|
+
|
|
162
|
+
# Run sequentially
|
|
163
|
+
pytest -v
|
|
164
|
+
```
|