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.
Files changed (93) hide show
  1. pytest_threadpool-0.2.0/.claude/settings.local.json +30 -0
  2. pytest_threadpool-0.2.0/.github/workflows/ci.yml +36 -0
  3. pytest_threadpool-0.2.0/.gitignore +165 -0
  4. pytest_threadpool-0.2.0/ANALYSIS.md +142 -0
  5. pytest_threadpool-0.2.0/CLAUDE.md +0 -0
  6. pytest_threadpool-0.2.0/CONTRIBUTING.md +164 -0
  7. pytest_threadpool-0.2.0/LICENSE +201 -0
  8. pytest_threadpool-0.2.0/PKG-INFO +168 -0
  9. pytest_threadpool-0.2.0/PLAN.md +222 -0
  10. pytest_threadpool-0.2.0/README.md +151 -0
  11. pytest_threadpool-0.2.0/TODO.md +44 -0
  12. pytest_threadpool-0.2.0/hooks/pre-commit +13 -0
  13. pytest_threadpool-0.2.0/pyproject.toml +77 -0
  14. pytest_threadpool-0.2.0/scripts/setup-dev +15 -0
  15. pytest_threadpool-0.2.0/src/pytest_threadpool/__init__.py +6 -0
  16. pytest_threadpool-0.2.0/src/pytest_threadpool/_api.py +23 -0
  17. pytest_threadpool-0.2.0/src/pytest_threadpool/_constants.py +30 -0
  18. pytest_threadpool-0.2.0/src/pytest_threadpool/_fixtures.py +79 -0
  19. pytest_threadpool-0.2.0/src/pytest_threadpool/_grouping.py +106 -0
  20. pytest_threadpool-0.2.0/src/pytest_threadpool/_markers.py +145 -0
  21. pytest_threadpool-0.2.0/src/pytest_threadpool/_runner.py +558 -0
  22. pytest_threadpool-0.2.0/src/pytest_threadpool/plugin.py +76 -0
  23. pytest_threadpool-0.2.0/tests/__init__.py +0 -0
  24. pytest_threadpool-0.2.0/tests/integration_tests/__init__.py +3 -0
  25. pytest_threadpool-0.2.0/tests/integration_tests/cases/__init__.py +0 -0
  26. pytest_threadpool-0.2.0/tests/integration_tests/cases/_templates.py +35 -0
  27. pytest_threadpool-0.2.0/tests/integration_tests/cases/class_barrier_concurrency.py +19 -0
  28. pytest_threadpool-0.2.0/tests/integration_tests/cases/class_single_method.py +9 -0
  29. pytest_threadpool-0.2.0/tests/integration_tests/cases/class_thread_verification.py +23 -0
  30. pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_cross_module_group.py +57 -0
  31. pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_keyboard_interrupt.py +15 -0
  32. pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_nested_threads.py +39 -0
  33. pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_sigint.py +29 -0
  34. pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_sigint_many.py +72 -0
  35. pytest_threadpool-0.2.0/tests/integration_tests/cases/edge_system_exit.py +15 -0
  36. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_autouse_function.py +41 -0
  37. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_class_scoped_once.py +29 -0
  38. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_class_yield.py +26 -0
  39. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_function_scoped.py +30 -0
  40. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_interdependent_finalizers.py +38 -0
  41. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_multiple_scopes.py +26 -0
  42. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_parameterized.py +16 -0
  43. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_teardown_exception.py +36 -0
  44. pytest_threadpool-0.2.0/tests/integration_tests/cases/fixture_yield_cleanup.py +26 -0
  45. pytest_threadpool-0.2.0/tests/integration_tests/cases/marks_custom.py +26 -0
  46. pytest_threadpool-0.2.0/tests/integration_tests/cases/marks_standard.py +29 -0
  47. pytest_threadpool-0.2.0/tests/integration_tests/cases/parallel_only_skip.py +12 -0
  48. pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_cross_module.py +15 -0
  49. pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_incremental.py +24 -0
  50. pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_incremental_conftest.py +29 -0
  51. pytest_threadpool-0.2.0/tests/integration_tests/cases/reporting_package_children.py +43 -0
  52. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_all_merged.py +20 -0
  53. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_all_not_parallelizable.py +34 -0
  54. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_children_separate_params.py +18 -0
  55. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_dynamic_parametrize.py +19 -0
  56. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_method_overrides_class.py +26 -0
  57. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_module_children.py +23 -0
  58. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_not_parallelizable_function.py +18 -0
  59. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_not_parallelizable_method.py +28 -0
  60. pytest_threadpool-0.2.0/tests/integration_tests/cases/scope_parameters.py +14 -0
  61. pytest_threadpool-0.2.0/tests/integration_tests/cases/sequential_bare_functions.py +29 -0
  62. pytest_threadpool-0.2.0/tests/integration_tests/cases/sequential_unmarked_class.py +22 -0
  63. pytest_threadpool-0.2.0/tests/integration_tests/cases/setup_all_fail.py +20 -0
  64. pytest_threadpool-0.2.0/tests/integration_tests/cases/setup_mixed_pass_fail.py +22 -0
  65. pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_counter.py +30 -0
  66. pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +48 -0
  67. pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_dict_mutation.py +31 -0
  68. pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_non_pickleable.py +37 -0
  69. pytest_threadpool-0.2.0/tests/integration_tests/cases/shared_two_phase_barrier.py +28 -0
  70. pytest_threadpool-0.2.0/tests/integration_tests/cases/validate_threadpool.py +12 -0
  71. pytest_threadpool-0.2.0/tests/integration_tests/cases/validate_threadpool_conftest.py +16 -0
  72. pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_class_setup.py +29 -0
  73. pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_combined_setup.py +29 -0
  74. pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_function_setup.py +27 -0
  75. pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_method_setup.py +27 -0
  76. pytest_threadpool-0.2.0/tests/integration_tests/cases/xunit_module_setup.py +19 -0
  77. pytest_threadpool-0.2.0/tests/integration_tests/conftest.py +155 -0
  78. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_class.py +25 -0
  79. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_edge_cases.py +190 -0
  80. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_fixtures.py +63 -0
  81. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_marks.py +74 -0
  82. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_package.py +156 -0
  83. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_reporting.py +308 -0
  84. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_scopes.py +61 -0
  85. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_sequential.py +17 -0
  86. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_shared.py +35 -0
  87. pytest_threadpool-0.2.0/tests/integration_tests/test_pytester_xunit.py +35 -0
  88. pytest_threadpool-0.2.0/tests/unit_tests/__init__.py +3 -0
  89. pytest_threadpool-0.2.0/tests/unit_tests/test_unit_api.py +32 -0
  90. pytest_threadpool-0.2.0/tests/unit_tests/test_unit_fixtures.py +181 -0
  91. pytest_threadpool-0.2.0/tests/unit_tests/test_unit_grouping.py +270 -0
  92. pytest_threadpool-0.2.0/tests/unit_tests/test_unit_markers.py +403 -0
  93. 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
+ ```