pytest-threadpool 0.2.0__tar.gz → 0.3.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 → pytest_threadpool-0.3.0}/ANALYSIS.md +1 -1
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/CONTRIBUTING.md +1 -1
- pytest_threadpool-0.2.0/README.md → pytest_threadpool-0.3.0/PKG-INFO +41 -4
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/PLAN.md +2 -1
- pytest_threadpool-0.2.0/PKG-INFO → pytest_threadpool-0.3.0/README.md +23 -21
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/TODO.md +0 -12
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/pyproject.toml +3 -2
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/_fixtures.py +68 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/_runner.py +160 -43
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_addfinalizer.py +49 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_chain.py +54 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_multiple_per_test.py +65 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_setup_parallel.py +51 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_with_shared_deps.py +61 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_with_tmp_path.py +45 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_with_xunit.py +55 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/fixture_func_yield_teardown.py +47 -0
- pytest_threadpool-0.3.0/tests/integration_tests/cases/xunit_method_teardown_runs.py +43 -0
- pytest_threadpool-0.3.0/tests/integration_tests/test_pytester_func_fixtures.py +69 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/.claude/settings.local.json +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/.github/workflows/ci.yml +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/.gitignore +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/CLAUDE.md +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/LICENSE +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/hooks/pre-commit +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/scripts/setup-dev +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/_api.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/_constants.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/_grouping.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/_markers.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/src/pytest_threadpool/plugin.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/_templates.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/class_single_method.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/class_thread_verification.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/edge_sigint.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/edge_system_exit.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/marks_custom.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/marks_standard.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/reporting_incremental.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/reporting_package_children.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_all_merged.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_module_children.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/scope_parameters.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/setup_all_fail.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/shared_counter.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/validate_threadpool.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/conftest.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_class.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_edge_cases.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_fixtures.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_marks.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_package.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_reporting.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_scopes.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_sequential.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_shared.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/integration_tests/test_pytester_xunit.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/unit_tests/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/unit_tests/test_unit_api.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/unit_tests/test_unit_fixtures.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/unit_tests/test_unit_grouping.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/unit_tests/test_unit_markers.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.0}/tests/unit_tests/test_unit_plugin.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
pytest-threadpool runs test bodies in parallel via
|
|
5
|
+
pytest-threadpool runs test bodies and function-scoped fixture setup in parallel via a thread pool while keeping shared fixtures (module/class/session scope) and teardown sequential. Function-scoped FixtureDefs are cloned per-item so each worker creates independent fixture instances. This analysis covers risks, potential bugs, and missing test coverage.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -107,7 +107,7 @@ src/pytest_threadpool/
|
|
|
107
107
|
_constants.py # ParallelScope and _GroupPrefix enums
|
|
108
108
|
_markers.py # MarkerResolver: marker introspection
|
|
109
109
|
_grouping.py # GroupKeyBuilder: parallel batch grouping
|
|
110
|
-
_fixtures.py # FixtureManager: finalizer save/restore
|
|
110
|
+
_fixtures.py # FixtureManager: fixture cloning, shared cache population, finalizer save/restore
|
|
111
111
|
_runner.py # ParallelRunner: parallel execution orchestration
|
|
112
112
|
plugin.py # pytest hook implementations (wiring only)
|
|
113
113
|
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-threadpool
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Parallel test execution for free-threaded Python builds
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: free-threaded,nogil,parallel,pytest,pytest parallel,testing,threadpool
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Framework :: Pytest
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: pytest>=9.0.2
|
|
16
|
+
Requires-Dist: selenium>=4.41.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
1
19
|
# pytest-threadpool
|
|
2
20
|
|
|
3
|
-
|
|
4
|
-
|
|
21
|
+

|
|
22
|
+

|
|
5
23
|
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
6
24
|
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
7
25
|
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
@@ -9,8 +27,9 @@
|
|
|
9
27
|
|
|
10
28
|
**Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
|
|
11
29
|
|
|
12
|
-
Runs test *bodies* concurrently in a
|
|
13
|
-
|
|
30
|
+
Runs test *bodies* and function-scoped fixture setup concurrently in a thread
|
|
31
|
+
pool while keeping shared fixtures (module/class/session scope) and teardown
|
|
32
|
+
sequential.
|
|
14
33
|
|
|
15
34
|
## Installation
|
|
16
35
|
|
|
@@ -58,6 +77,24 @@ def test_must_be_sequential(): ...
|
|
|
58
77
|
| `parameters` | Parametrized variants of the same test run concurrently |
|
|
59
78
|
| `all` | Combines `children` + `parameters` |
|
|
60
79
|
|
|
80
|
+
## Fixture handling
|
|
81
|
+
|
|
82
|
+
Function-scoped fixtures are cloned per-item and set up in parallel alongside
|
|
83
|
+
test calls. Each worker gets independent fixture instances — no shared mutable
|
|
84
|
+
state between concurrent fixture setups.
|
|
85
|
+
|
|
86
|
+
Shared fixtures (module, class, and session scope) are resolved once
|
|
87
|
+
sequentially before workers launch and served from cache to all items.
|
|
88
|
+
|
|
89
|
+
| Scope | Behavior |
|
|
90
|
+
|------------|-------------------------------------------------|
|
|
91
|
+
| `function` | Cloned per-item, set up in parallel workers |
|
|
92
|
+
| `class` | Resolved once, cached, shared across items |
|
|
93
|
+
| `module` | Resolved once, cached, shared across items |
|
|
94
|
+
| `session` | Resolved once, cached, shared across items |
|
|
95
|
+
|
|
96
|
+
Teardown for all scopes runs sequentially after the parallel group completes.
|
|
97
|
+
|
|
61
98
|
## Marker levels
|
|
62
99
|
|
|
63
100
|
Markers can be applied at function, class, module (`pytestmark`), or
|
|
@@ -46,7 +46,8 @@ This is probably the single highest-value documentation addition.
|
|
|
46
46
|
You want one section that explicitly says:
|
|
47
47
|
Guarantees
|
|
48
48
|
test call phases may run concurrently
|
|
49
|
-
fixture setup
|
|
49
|
+
function-scoped fixture setup runs in parallel (cloned per-item)
|
|
50
|
+
shared fixture setup/teardown (module/class/session) remains sequential
|
|
50
51
|
unmarked tests remain sequential
|
|
51
52
|
not_parallelizable always wins
|
|
52
53
|
marker precedence rules are stable
|
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pytest-threadpool
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Parallel test execution for free-threaded Python builds
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Keywords: free-threaded,nogil,parallel,pytest,testing,threadpool
|
|
8
|
-
Classifier: Development Status :: 4 - Beta
|
|
9
|
-
Classifier: Framework :: Pytest
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Topic :: Software Development :: Testing
|
|
14
|
-
Requires-Python: >=3.13
|
|
15
|
-
Requires-Dist: pytest>=9.0.2
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
|
|
18
1
|
# pytest-threadpool
|
|
19
2
|
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+

|
|
4
|
+

|
|
22
5
|
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
23
6
|
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
24
7
|
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
@@ -26,8 +9,9 @@ Description-Content-Type: text/markdown
|
|
|
26
9
|
|
|
27
10
|
**Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
|
|
28
11
|
|
|
29
|
-
Runs test *bodies* concurrently in a
|
|
30
|
-
|
|
12
|
+
Runs test *bodies* and function-scoped fixture setup concurrently in a thread
|
|
13
|
+
pool while keeping shared fixtures (module/class/session scope) and teardown
|
|
14
|
+
sequential.
|
|
31
15
|
|
|
32
16
|
## Installation
|
|
33
17
|
|
|
@@ -75,6 +59,24 @@ def test_must_be_sequential(): ...
|
|
|
75
59
|
| `parameters` | Parametrized variants of the same test run concurrently |
|
|
76
60
|
| `all` | Combines `children` + `parameters` |
|
|
77
61
|
|
|
62
|
+
## Fixture handling
|
|
63
|
+
|
|
64
|
+
Function-scoped fixtures are cloned per-item and set up in parallel alongside
|
|
65
|
+
test calls. Each worker gets independent fixture instances — no shared mutable
|
|
66
|
+
state between concurrent fixture setups.
|
|
67
|
+
|
|
68
|
+
Shared fixtures (module, class, and session scope) are resolved once
|
|
69
|
+
sequentially before workers launch and served from cache to all items.
|
|
70
|
+
|
|
71
|
+
| Scope | Behavior |
|
|
72
|
+
|------------|-------------------------------------------------|
|
|
73
|
+
| `function` | Cloned per-item, set up in parallel workers |
|
|
74
|
+
| `class` | Resolved once, cached, shared across items |
|
|
75
|
+
| `module` | Resolved once, cached, shared across items |
|
|
76
|
+
| `session` | Resolved once, cached, shared across items |
|
|
77
|
+
|
|
78
|
+
Teardown for all scopes runs sequentially after the parallel group completes.
|
|
79
|
+
|
|
78
80
|
## Marker levels
|
|
79
81
|
|
|
80
82
|
Markers can be applied at function, class, module (`pytestmark`), or
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
# TODO — Road to Beta and Beyond
|
|
2
2
|
|
|
3
|
-
## Beta Requirements
|
|
4
|
-
|
|
5
|
-
### CI/CD
|
|
6
|
-
- [x] Add GitHub Actions running tests on Python 3.13t
|
|
7
|
-
- [x] Validate at least one pytest version in CI
|
|
8
|
-
|
|
9
|
-
### Documentation
|
|
10
|
-
- [x] Add "Known Limitations" section to README (private API fragility, no plugin compat guarantees, no captured stdout in parallel)
|
|
11
|
-
- [x] Add pytest version upper bound or document tested versions
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
3
|
## 1.0 Requirements
|
|
16
4
|
|
|
17
5
|
### Phase 1 — Trust (Highest ROI)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytest-threadpool"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Parallel test execution for free-threaded Python builds"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
keywords = ["pytest", "parallel", "free-threaded", "nogil", "testing", "threadpool"]
|
|
8
|
+
keywords = ["pytest", "parallel", "free-threaded", "nogil", "testing", "threadpool", "pytest parallel"]
|
|
9
9
|
classifiers = [
|
|
10
10
|
"Framework :: Pytest",
|
|
11
11
|
"Development Status :: 4 - Beta",
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"pytest>=9.0.2",
|
|
19
|
+
"selenium>=4.41.0",
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
[project.entry-points.pytest11]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Fixture finalizer save/restore helpers for parallel execution."""
|
|
2
2
|
|
|
3
|
+
from _pytest.fixtures import FixtureDef
|
|
3
4
|
from _pytest.scope import Scope
|
|
4
5
|
|
|
5
6
|
|
|
@@ -12,6 +13,40 @@ class FixtureManager:
|
|
|
12
13
|
runner.py and fixtures.py use.
|
|
13
14
|
"""
|
|
14
15
|
|
|
16
|
+
@staticmethod
|
|
17
|
+
def clone_function_fixturedefs(item) -> None:
|
|
18
|
+
"""Clone function-scoped FixtureDefs so this item has its own copies.
|
|
19
|
+
|
|
20
|
+
Shared (module/class/session) FixtureDefs are kept as-is — their
|
|
21
|
+
cached values are read-only after the first item's setup.
|
|
22
|
+
Function-scoped FixtureDefs get independent copies with fresh
|
|
23
|
+
cached_result and _finalizers, allowing concurrent fixture setup
|
|
24
|
+
across items without racing on shared singleton state.
|
|
25
|
+
"""
|
|
26
|
+
# noinspection PyProtectedMember
|
|
27
|
+
# request._arg2fixturedefs, fixturedef._scope: no public API;
|
|
28
|
+
# mirrors pytest's own TopRequest/FixtureDef internals.
|
|
29
|
+
request = getattr(item, "_request", None)
|
|
30
|
+
if not request or not hasattr(request, "_arg2fixturedefs"):
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
new_arg2fds = {}
|
|
34
|
+
for argname, fds in request._arg2fixturedefs.items(): # pyright: ignore[reportPrivateUsage]
|
|
35
|
+
new_fds = []
|
|
36
|
+
for fd in fds:
|
|
37
|
+
if fd._scope is Scope.Function: # pyright: ignore[reportPrivateUsage]
|
|
38
|
+
clone = FixtureDef.__new__(FixtureDef)
|
|
39
|
+
clone.__dict__.update(fd.__dict__)
|
|
40
|
+
clone.cached_result = None
|
|
41
|
+
clone._finalizers = [] # pyright: ignore[reportPrivateUsage, reportAttributeAccessIssue]
|
|
42
|
+
new_fds.append(clone)
|
|
43
|
+
else:
|
|
44
|
+
new_fds.append(fd)
|
|
45
|
+
new_arg2fds[argname] = new_fds
|
|
46
|
+
|
|
47
|
+
# _arg2fixturedefs is typed Final but not enforced at runtime
|
|
48
|
+
object.__setattr__(request, "_arg2fixturedefs", new_arg2fds)
|
|
49
|
+
|
|
15
50
|
@staticmethod
|
|
16
51
|
def save_and_clear_function_fixtures(item) -> list:
|
|
17
52
|
"""After an item's setup, save its function-scoped fixture finalizers
|
|
@@ -58,6 +93,39 @@ class FixtureManager:
|
|
|
58
93
|
if fixturedef._scope is Scope.Function and fixturedef.cached_result is not None: # pyright: ignore[reportPrivateUsage]
|
|
59
94
|
fixturedef.cached_result = None
|
|
60
95
|
|
|
96
|
+
@staticmethod
|
|
97
|
+
def populate_shared_fixtures(item) -> None:
|
|
98
|
+
"""Resolve only non-function-scoped fixtures for the item.
|
|
99
|
+
|
|
100
|
+
Populates shared fixture caches (module/class/session scope) by
|
|
101
|
+
calling getfixturevalue for each non-function-scoped fixture.
|
|
102
|
+
Function-scoped fixtures are skipped — they will be created later
|
|
103
|
+
from cloned FixtureDefs in parallel workers.
|
|
104
|
+
|
|
105
|
+
Must be called inside a setup hook context (item in setupstate)
|
|
106
|
+
so that addfinalizer works for the resolved fixtures.
|
|
107
|
+
"""
|
|
108
|
+
# noinspection PyProtectedMember
|
|
109
|
+
# request._arg2fixturedefs, fixturedef._scope: no public API;
|
|
110
|
+
# mirrors pytest's own TopRequest/FixtureDef internals.
|
|
111
|
+
request = getattr(item, "_request", None)
|
|
112
|
+
if not request or not hasattr(request, "_arg2fixturedefs"):
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
for argname in item.fixturenames:
|
|
116
|
+
if argname in item.funcargs:
|
|
117
|
+
continue
|
|
118
|
+
fds = request._arg2fixturedefs.get(argname, []) # pyright: ignore[reportPrivateUsage]
|
|
119
|
+
if fds and fds[-1]._scope is not Scope.Function: # pyright: ignore[reportPrivateUsage]
|
|
120
|
+
value = request.getfixturevalue(argname)
|
|
121
|
+
item.funcargs[argname] = value
|
|
122
|
+
# Eagerly initialize lazy state on shared fixture values.
|
|
123
|
+
# TmpPathFactory.getbasetemp() lazily creates the basetemp
|
|
124
|
+
# directory; without this, parallel workers would race on
|
|
125
|
+
# the first tmp_path_factory.mktemp() call.
|
|
126
|
+
if hasattr(value, "getbasetemp"):
|
|
127
|
+
value.getbasetemp()
|
|
128
|
+
|
|
61
129
|
@staticmethod
|
|
62
130
|
def save_collector_finalizers(session, next_item) -> list:
|
|
63
131
|
"""Save finalizers from stack nodes that would be torn down when
|
|
@@ -339,12 +339,17 @@ class ParallelRunner:
|
|
|
339
339
|
ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
|
|
340
340
|
|
|
341
341
|
def _run_parallel(self, items) -> None:
|
|
342
|
-
"""Run a group's tests with parallel
|
|
342
|
+
"""Run a group's tests with parallel fixture setup and calls.
|
|
343
343
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
344
|
+
Function-scoped FixtureDefs are cloned per-item so their setup can
|
|
345
|
+
run concurrently alongside the test call. Shared fixtures
|
|
346
|
+
(module/class/session scope) are set up once via the first item,
|
|
347
|
+
then served from cache to all workers.
|
|
348
|
+
|
|
349
|
+
1. Sequential: set up first item (populates shared fixture caches).
|
|
350
|
+
2. Sequential: prepare remaining items (_initrequest + clone FixtureDefs).
|
|
351
|
+
3. Parallel: workers run setup (cloned fixtures) + call per item.
|
|
352
|
+
4. Sequential: report, teardown.
|
|
348
353
|
"""
|
|
349
354
|
session = self._session
|
|
350
355
|
setup_passed = {}
|
|
@@ -352,12 +357,12 @@ class ParallelRunner:
|
|
|
352
357
|
per_item_fixture_fins = {}
|
|
353
358
|
saved_collector_fins = []
|
|
354
359
|
|
|
355
|
-
# Phase 1: sequential setup (silent)
|
|
356
360
|
# noinspection PyProtectedMember
|
|
357
361
|
# item._request/_initrequest and session._setupstate: no public API
|
|
358
362
|
# for request lifecycle or setup state management. Mirrors pytest's
|
|
359
363
|
# own runner.py (_pytest.runner.runtestprotocol / SetupState).
|
|
360
|
-
|
|
364
|
+
def _setup_first_item(item):
|
|
365
|
+
"""Full sequential setup for the first item (caches shared fixtures)."""
|
|
361
366
|
if hasattr(item, "_request") and not item._request: # pyright: ignore[reportPrivateUsage]
|
|
362
367
|
item._initrequest() # pyright: ignore[reportPrivateUsage]
|
|
363
368
|
|
|
@@ -381,6 +386,8 @@ class ParallelRunner:
|
|
|
381
386
|
session._setupstate.stack.pop(item) # pyright: ignore[reportPrivateUsage]
|
|
382
387
|
|
|
383
388
|
if session.config.getoption("setuponly", False):
|
|
389
|
+
for item in items:
|
|
390
|
+
_setup_first_item(item)
|
|
384
391
|
for item in items:
|
|
385
392
|
ihook = item.ihook
|
|
386
393
|
ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
|
|
@@ -390,29 +397,123 @@ class ParallelRunner:
|
|
|
390
397
|
self._teardown_all(items, per_item_fixture_fins, saved_collector_fins)
|
|
391
398
|
return
|
|
392
399
|
|
|
393
|
-
# Phase 2:
|
|
394
|
-
|
|
395
|
-
|
|
400
|
+
# Phase 1+2: fire hooks for all items, populate shared fixture caches,
|
|
401
|
+
# and clone function-scoped FixtureDefs.
|
|
402
|
+
#
|
|
403
|
+
# The first item that passes hook evaluation resolves shared fixtures
|
|
404
|
+
# (module/class/session scope) to populate FixtureDef caches. All
|
|
405
|
+
# subsequent items get cache hits for shared fixtures. Function-scoped
|
|
406
|
+
# fixtures are NOT created here — they are deferred to parallel workers.
|
|
407
|
+
#
|
|
408
|
+
# noinspection PyProtectedMember
|
|
409
|
+
parallel_items = []
|
|
410
|
+
shared_populated = False
|
|
411
|
+
|
|
412
|
+
for item in items:
|
|
413
|
+
if hasattr(item, "_request") and not item._request: # pyright: ignore[reportPrivateUsage]
|
|
414
|
+
item._initrequest() # pyright: ignore[reportPrivateUsage]
|
|
415
|
+
|
|
416
|
+
# Handle collector transitions (needed for PKG_CHILDREN groups
|
|
417
|
+
# that span multiple modules).
|
|
418
|
+
needed = set(item.listchain())
|
|
419
|
+
if any(node not in needed for node in session._setupstate.stack): # pyright: ignore[reportPrivateUsage]
|
|
420
|
+
saved_collector_fins.extend(
|
|
421
|
+
FixtureManager.save_collector_finalizers(session, item)
|
|
422
|
+
)
|
|
423
|
+
session._setupstate.teardown_exact(nextitem=item) # pyright: ignore[reportPrivateUsage]
|
|
424
|
+
|
|
425
|
+
# Fire setup hooks with a custom setup function:
|
|
426
|
+
# - First eligible item: resolve shared fixtures only (populates caches)
|
|
427
|
+
# - Remaining items: no-op (shared caches already populated)
|
|
428
|
+
# This evaluates skip/xfail markers and initializes capture/logging
|
|
429
|
+
# without creating function-scoped fixtures.
|
|
430
|
+
original_setup = item.setup
|
|
431
|
+
if not shared_populated:
|
|
432
|
+
item.setup = lambda i=item: FixtureManager.populate_shared_fixtures(i)
|
|
433
|
+
else:
|
|
434
|
+
item.setup = lambda: None
|
|
435
|
+
try:
|
|
436
|
+
hook_rep = call_and_report(item, "setup", log=False)
|
|
437
|
+
finally:
|
|
438
|
+
item.setup = original_setup
|
|
439
|
+
|
|
440
|
+
# Pop item from setupstate (pushed by _setupstate.setup inside the
|
|
441
|
+
# hook) so the next item's hook doesn't fail the stack assertion.
|
|
442
|
+
if item in session._setupstate.stack: # pyright: ignore[reportPrivateUsage]
|
|
443
|
+
session._setupstate.stack.pop(item) # pyright: ignore[reportPrivateUsage]
|
|
396
444
|
|
|
445
|
+
if not hook_rep.passed:
|
|
446
|
+
# Hooks raised (skip, skipif, xfail NOTRUN, etc.)
|
|
447
|
+
setup_reports[item] = hook_rep
|
|
448
|
+
setup_passed[item] = False
|
|
449
|
+
continue
|
|
450
|
+
|
|
451
|
+
if not shared_populated:
|
|
452
|
+
shared_populated = True
|
|
453
|
+
|
|
454
|
+
FixtureManager.clone_function_fixturedefs(item)
|
|
455
|
+
parallel_items.append(item)
|
|
456
|
+
|
|
457
|
+
# Push all parallel items to setupstate stack so addfinalizer
|
|
458
|
+
# assertions pass during fixture resolution in worker threads.
|
|
459
|
+
# Done after the hook loop so items don't interfere with each
|
|
460
|
+
# other's _setupstate.setup() assertions.
|
|
461
|
+
for item in parallel_items:
|
|
462
|
+
session._setupstate.stack[item] = ([item.teardown], None) # pyright: ignore[reportPrivateUsage]
|
|
463
|
+
|
|
464
|
+
# Phase 3: parallel setup + call
|
|
465
|
+
workers = min(self._nthreads, len(parallel_items) or 1)
|
|
397
466
|
call_results = {}
|
|
398
467
|
reported = set()
|
|
399
468
|
live = _LiveReporter(session, items)
|
|
400
469
|
live.pre_print()
|
|
401
470
|
|
|
402
|
-
|
|
471
|
+
cancelled = threading.Event()
|
|
472
|
+
|
|
473
|
+
def _do_setup_and_call(test_item):
|
|
474
|
+
"""Setup + call worker (for items with cloned FixtureDefs).
|
|
475
|
+
|
|
476
|
+
Fixture setup uses cloned function-scoped FixtureDefs so each
|
|
477
|
+
worker creates independent fixture instances without racing
|
|
478
|
+
on shared FixtureDef state. Shared fixtures are already cached
|
|
479
|
+
from the first item's sequential setup, so their FixtureDef.execute()
|
|
480
|
+
is a cache-hit read.
|
|
481
|
+
|
|
482
|
+
addfinalizer is redirected to a per-item list to avoid the
|
|
483
|
+
setupstate stack assertion (the item IS in the stack, but
|
|
484
|
+
list.append on the stack entry is also safe on free-threaded Python;
|
|
485
|
+
the redirect simply avoids coupling to setupstate internals).
|
|
486
|
+
"""
|
|
403
487
|
if cancelled.is_set():
|
|
404
|
-
return test_item,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
488
|
+
return test_item, None, None, []
|
|
489
|
+
|
|
490
|
+
# Redirect node.addfinalizer to per-item list
|
|
491
|
+
original_addfinalizer = test_item.addfinalizer
|
|
492
|
+
node_fins = []
|
|
493
|
+
test_item.addfinalizer = lambda fin: node_fins.append(fin)
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
setup_info = CallInfo.from_call(lambda: test_item.setup(), when="setup")
|
|
497
|
+
finally:
|
|
498
|
+
test_item.addfinalizer = original_addfinalizer
|
|
499
|
+
|
|
500
|
+
if setup_info.excinfo is None:
|
|
501
|
+
fixture_fins = FixtureManager.save_and_clear_function_fixtures(test_item)
|
|
502
|
+
if not cancelled.is_set():
|
|
503
|
+
live.mark_running(test_item)
|
|
504
|
+
call_info = CallInfo.from_call(lambda: test_item.runtest(), when="call")
|
|
505
|
+
if not cancelled.is_set():
|
|
506
|
+
live.mark_call_done(test_item, call_info.excinfo)
|
|
507
|
+
return test_item, setup_info, call_info, fixture_fins
|
|
508
|
+
return test_item, setup_info, None, fixture_fins
|
|
509
|
+
|
|
510
|
+
FixtureManager.clear_function_fixture_caches(test_item)
|
|
511
|
+
return test_item, setup_info, None, []
|
|
410
512
|
|
|
411
513
|
def _report_item(item):
|
|
412
514
|
"""Report a single item — live cursor mode or plain fallback."""
|
|
413
515
|
ihook = item.ihook
|
|
414
516
|
|
|
415
|
-
# Build the call report first (needed for a live dot letter)
|
|
416
517
|
call_rep = None
|
|
417
518
|
if setup_passed[item] and item in call_results:
|
|
418
519
|
call_info = call_results[item]
|
|
@@ -420,10 +521,6 @@ class ParallelRunner:
|
|
|
420
521
|
|
|
421
522
|
report = call_rep or setup_reports[item]
|
|
422
523
|
|
|
423
|
-
# Suppress terminal reporter output, fire hooks for stats only.
|
|
424
|
-
# try/finally ensures restore() runs even on KeyboardInterrupt,
|
|
425
|
-
# otherwise the terminal writer stays suppressed and pytest's
|
|
426
|
-
# interrupt traceback is silently lost.
|
|
427
524
|
live.suppress()
|
|
428
525
|
try:
|
|
429
526
|
ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
|
|
@@ -440,8 +537,7 @@ class ParallelRunner:
|
|
|
440
537
|
reported.add(item)
|
|
441
538
|
|
|
442
539
|
interrupted = False
|
|
443
|
-
|
|
444
|
-
if workers > 1 and len(callable_items) > 1:
|
|
540
|
+
if workers > 1 and len(parallel_items) > 1:
|
|
445
541
|
work_queue = queue.SimpleQueue()
|
|
446
542
|
result_queue = queue.SimpleQueue()
|
|
447
543
|
|
|
@@ -451,16 +547,21 @@ class ParallelRunner:
|
|
|
451
547
|
if work_item is None or cancelled.is_set():
|
|
452
548
|
return
|
|
453
549
|
try:
|
|
454
|
-
|
|
550
|
+
item, setup_info, call_info, fixture_fins = _do_setup_and_call(work_item)
|
|
551
|
+
per_item_fixture_fins[item] = fixture_fins
|
|
552
|
+
if setup_info is not None:
|
|
553
|
+
setup_rep = item.ihook.pytest_runtest_makereport(
|
|
554
|
+
item=item, call=setup_info
|
|
555
|
+
)
|
|
556
|
+
setup_reports[item] = setup_rep
|
|
557
|
+
setup_passed[item] = setup_info.excinfo is None
|
|
558
|
+
result_queue.put((item, call_info))
|
|
455
559
|
except BaseException as exc:
|
|
456
|
-
# Ensure the result queue always gets an entry so the
|
|
457
|
-
# main thread never blocks forever on .get().
|
|
458
560
|
call_info = CallInfo.from_call(
|
|
459
561
|
lambda e=exc: (_ for _ in ()).throw(e),
|
|
460
562
|
when="call",
|
|
461
563
|
)
|
|
462
|
-
|
|
463
|
-
result_queue.put(result)
|
|
564
|
+
result_queue.put((work_item, call_info))
|
|
464
565
|
|
|
465
566
|
threads = []
|
|
466
567
|
for _ in range(workers):
|
|
@@ -468,36 +569,46 @@ class ParallelRunner:
|
|
|
468
569
|
t.start()
|
|
469
570
|
threads.append(t)
|
|
470
571
|
|
|
471
|
-
|
|
472
|
-
|
|
572
|
+
# Submit all parallel items (setup + call)
|
|
573
|
+
for item in parallel_items:
|
|
574
|
+
work_queue.put(item)
|
|
473
575
|
|
|
474
576
|
try:
|
|
475
|
-
|
|
476
|
-
while
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
577
|
+
collected = 0
|
|
578
|
+
while collected < len(parallel_items):
|
|
579
|
+
finished_item, call_info = result_queue.get()
|
|
580
|
+
if call_info is not None:
|
|
581
|
+
call_results[finished_item] = call_info
|
|
582
|
+
_report_item(finished_item)
|
|
583
|
+
collected += 1
|
|
481
584
|
except KeyboardInterrupt:
|
|
482
585
|
interrupted = True
|
|
483
586
|
cancelled.set()
|
|
484
587
|
|
|
485
|
-
# Signal workers to stop (best-effort; they're daemon threads)
|
|
486
588
|
for _ in range(workers):
|
|
487
589
|
work_queue.put(None)
|
|
488
590
|
if not interrupted:
|
|
489
591
|
for t in threads:
|
|
490
592
|
t.join()
|
|
491
593
|
else:
|
|
594
|
+
# Single worker fallback: sequential setup + call
|
|
492
595
|
try:
|
|
493
|
-
for item in
|
|
494
|
-
|
|
495
|
-
|
|
596
|
+
for item in parallel_items:
|
|
597
|
+
item, setup_info, call_info, fixture_fins = _do_setup_and_call(item)
|
|
598
|
+
per_item_fixture_fins[item] = fixture_fins
|
|
599
|
+
if setup_info is not None:
|
|
600
|
+
setup_rep = item.ihook.pytest_runtest_makereport(
|
|
601
|
+
item=item, call=setup_info
|
|
602
|
+
)
|
|
603
|
+
setup_reports[item] = setup_rep
|
|
604
|
+
setup_passed[item] = setup_info.excinfo is None
|
|
605
|
+
if call_info is not None:
|
|
606
|
+
call_results[item] = call_info
|
|
496
607
|
_report_item(item)
|
|
497
608
|
except KeyboardInterrupt:
|
|
498
609
|
interrupted = True
|
|
499
610
|
|
|
500
|
-
#
|
|
611
|
+
# Report any remaining items (setup failures, stragglers)
|
|
501
612
|
if not interrupted:
|
|
502
613
|
for item in items:
|
|
503
614
|
if item not in reported:
|
|
@@ -505,7 +616,13 @@ class ParallelRunner:
|
|
|
505
616
|
|
|
506
617
|
live.finish()
|
|
507
618
|
|
|
508
|
-
#
|
|
619
|
+
# Pop parallel items from setupstate stack
|
|
620
|
+
# noinspection PyProtectedMember
|
|
621
|
+
for item in parallel_items:
|
|
622
|
+
if item in session._setupstate.stack: # pyright: ignore[reportPrivateUsage]
|
|
623
|
+
session._setupstate.stack.pop(item) # pyright: ignore[reportPrivateUsage]
|
|
624
|
+
|
|
625
|
+
# Teardown (always runs, even after interrupt)
|
|
509
626
|
self._teardown_all(items, per_item_fixture_fins, saved_collector_fins)
|
|
510
627
|
|
|
511
628
|
if interrupted:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Function-scoped fixture using request.addfinalizer (non-yield style)."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestState:
|
|
10
|
+
setup_log: ClassVar[list] = []
|
|
11
|
+
finalizer_log: ClassVar[list] = []
|
|
12
|
+
lock: ClassVar[threading.Lock] = threading.Lock()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def managed_resource(request):
|
|
17
|
+
name = request.node.name
|
|
18
|
+
with TestState.lock:
|
|
19
|
+
TestState.setup_log.append(name)
|
|
20
|
+
|
|
21
|
+
def cleanup():
|
|
22
|
+
with TestState.lock:
|
|
23
|
+
TestState.finalizer_log.append(name)
|
|
24
|
+
|
|
25
|
+
request.addfinalizer(cleanup) # noqa: PT021 — intentionally testing addfinalizer API
|
|
26
|
+
return f"resource_{name}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parallelizable("children")
|
|
30
|
+
class TestAddfinalizer:
|
|
31
|
+
barrier = threading.Barrier(3, timeout=10)
|
|
32
|
+
|
|
33
|
+
def test_a(self, managed_resource):
|
|
34
|
+
assert managed_resource == "resource_test_a"
|
|
35
|
+
self.barrier.wait()
|
|
36
|
+
|
|
37
|
+
def test_b(self, managed_resource):
|
|
38
|
+
assert managed_resource == "resource_test_b"
|
|
39
|
+
self.barrier.wait()
|
|
40
|
+
|
|
41
|
+
def test_c(self, managed_resource):
|
|
42
|
+
assert managed_resource == "resource_test_c"
|
|
43
|
+
self.barrier.wait()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_verify():
|
|
47
|
+
expected = {"test_a", "test_b", "test_c"}
|
|
48
|
+
assert expected == set(TestState.setup_log), f"setups: {TestState.setup_log}"
|
|
49
|
+
assert expected == set(TestState.finalizer_log), f"finalizers: {TestState.finalizer_log}"
|