pytest-threadpool 0.3.4__tar.gz → 0.3.6__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.3.4 → pytest_threadpool-0.3.6}/.github/workflows/ci.yml +16 -2
- pytest_threadpool-0.3.6/.pre-commit-config.yaml +17 -0
- pytest_threadpool-0.3.6/CHANGELOG.md +28 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/CONTRIBUTING.md +0 -3
- pytest_threadpool-0.3.4/README.md → pytest_threadpool-0.3.6/PKG-INFO +87 -36
- pytest_threadpool-0.3.4/PKG-INFO → pytest_threadpool-0.3.6/README.md +64 -56
- pytest_threadpool-0.3.6/ROADMAP.md +83 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/codecov.yml +3 -0
- pytest_threadpool-0.3.6/examples/test_event_bus/event_bus.py +76 -0
- pytest_threadpool-0.3.6/examples/test_event_bus/test_event_bus.py +110 -0
- pytest_threadpool-0.3.6/examples/test_logging/test_log_capture.py +34 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_queue/user_pool.py +2 -2
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/pyproject.toml +38 -5
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/scripts/setup-dev +1 -1
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_grouping.py +9 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_markers.py +1 -4
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_runner.py +46 -8
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_version.py +2 -2
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/plugin.py +4 -1
- pytest_threadpool-0.3.6/tests/__init__.py +0 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/__init__.py +0 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/edge_first_item_setup_fail.py +32 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/edge_maxfail_parallel.py +24 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/edge_teardown_exception.py +32 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/edge_worker_exception.py +23 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/edge_worker_exception_conftest.py +14 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/fixture_class_across_groups.py +48 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/fixture_func_mixed_parallel.py +45 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/fixture_func_setup_parallel.py +48 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/fixture_module_across_groups.py +50 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/fixture_package_across_groups.py +70 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/fixture_session_across_groups.py +50 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/runner_collect_only.py +16 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/runner_setup_only.py +20 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/runner_setup_show.py +17 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/runner_single_parallel_item.py +23 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/runner_single_worker.py +25 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/scope_children_on_function.py +15 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/scope_mixed_fail_skip.py +38 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/scope_two_classes_mixed.py +48 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/xunit_function_mixed_parallel.py +60 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/xunit_method_mixed_parallel.py +49 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/xunit_setup_class_across_groups.py +61 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/xunit_setup_method_across_groups.py +53 -0
- pytest_threadpool-0.3.6/tests/integration_tests/cases/xunit_setup_module_across_groups.py +47 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/conftest.py +22 -1
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_edge_cases.py +145 -2
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_fixtures.py +39 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_func_fixtures.py +6 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_scopes.py +12 -0
- pytest_threadpool-0.3.6/tests/integration_tests/test_pytester_xunit.py +65 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_grouping.py +55 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_markers.py +70 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_plugin.py +21 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_stream_proxy.py +4 -4
- pytest_threadpool-0.3.4/examples/test_logging/test_file_logging.py +0 -30
- pytest_threadpool-0.3.4/examples/test_logging/test_log_capture.py +0 -66
- pytest_threadpool-0.3.4/examples/test_logging/test_print_capture.py +0 -44
- pytest_threadpool-0.3.4/hooks/pre-commit +0 -13
- pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -51
- pytest_threadpool-0.3.4/tests/integration_tests/test_pytester_xunit.py +0 -35
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/.github/workflows/publish.yml +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/.gitignore +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/LICENSE +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/__init__.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/__init__.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/conftest.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/container.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/providers.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/services.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/test_factory.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/test_local.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/test_singleton.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_di/test_thread_local.py +0 -0
- {pytest_threadpool-0.3.4/examples/test_logging → pytest_threadpool-0.3.6/examples/test_event_bus}/__init__.py +0 -0
- {pytest_threadpool-0.3.4/examples/test_queue → pytest_threadpool-0.3.6/examples/test_logging}/__init__.py +0 -0
- {pytest_threadpool-0.3.4/examples/test_shared_state → pytest_threadpool-0.3.6/examples/test_queue}/__init__.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_queue/conftest.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_queue/test_queue.py +0 -0
- {pytest_threadpool-0.3.4/tests → pytest_threadpool-0.3.6/examples/test_shared_state}/__init__.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_shared_state/test_barrier.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_shared_state/test_counter.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/examples/test_shared_state/test_counter_async.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/__init__.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_api.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_constants.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_fixtures.py +0 -0
- /pytest_threadpool-0.3.4/tests/integration_tests/cases/__init__.py → /pytest_threadpool-0.3.6/src/pytest_threadpool/py.typed +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/_templates.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/capture_print_parallel.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/capture_two_groups.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/class_single_method.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/class_thread_verification.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/collected_count.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_sigint.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_system_exit.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_from_conftest.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_from_conftest_conftest.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_implicit_scope.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/marks_custom.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/marks_standard.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_incremental.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_package_children.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_single_file_params.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_stdout_during_parallel.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_all_merged.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_module_children.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_parameters.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/setup_all_fail.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_counter.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_after_test_failure.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_parallel_timing.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_same_thread.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_xunit_function.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_xunit_method_thread.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/validate_threadpool.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_capture.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_class.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_marks.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_package.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_parallel_teardown.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_reporting.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_sequential.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_shared.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/unit_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_api.py +0 -0
- {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_fixtures.py +0 -0
|
@@ -34,9 +34,23 @@ jobs:
|
|
|
34
34
|
- uses: astral-sh/setup-uv@v6
|
|
35
35
|
- run: uv python install ${{ matrix.python }}
|
|
36
36
|
- run: uv sync --python ${{ matrix.python }} --group dev
|
|
37
|
-
-
|
|
37
|
+
- name: Run tests
|
|
38
|
+
id: tests
|
|
39
|
+
env:
|
|
40
|
+
COVERAGE_PROCESS_START: pyproject.toml
|
|
41
|
+
run: uv run --python ${{ matrix.python }} coverage run -m pytest -v --tb=short
|
|
42
|
+
- name: Generate coverage report
|
|
43
|
+
if: always() && steps.tests.outcome != 'cancelled'
|
|
44
|
+
run: |
|
|
45
|
+
uv run --python ${{ matrix.python }} coverage combine
|
|
46
|
+
uv run --python ${{ matrix.python }} coverage report --format=markdown >> "$GITHUB_STEP_SUMMARY"
|
|
47
|
+
uv run --python ${{ matrix.python }} coverage xml
|
|
48
|
+
sed -i 's|<source></source>|<source>.</source>|' coverage.xml
|
|
38
49
|
- name: Upload coverage reports to Codecov
|
|
39
|
-
if: matrix.python == '3.14t'
|
|
50
|
+
if: always() && matrix.python == '3.14t' && steps.tests.outcome != 'cancelled'
|
|
40
51
|
uses: codecov/codecov-action@v5
|
|
41
52
|
with:
|
|
42
53
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
54
|
+
files: coverage.xml
|
|
55
|
+
fail_ci_if_error: true
|
|
56
|
+
verbose: true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.15.6
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-format
|
|
6
|
+
args: [--check, src/, tests/]
|
|
7
|
+
- id: ruff
|
|
8
|
+
args: [src/, tests/]
|
|
9
|
+
|
|
10
|
+
- repo: local
|
|
11
|
+
hooks:
|
|
12
|
+
- id: pyright
|
|
13
|
+
name: pyright
|
|
14
|
+
entry: uv run pyright src/
|
|
15
|
+
language: system
|
|
16
|
+
types: [python]
|
|
17
|
+
pass_filenames: false
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.6
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- Fixed session, module, and class-scoped fixtures being torn down and
|
|
8
|
+
re-created between parallel groups instead of being resolved once and
|
|
9
|
+
shared for their entire scope. The same fix applies to xunit-style
|
|
10
|
+
`setup_module`/`setup_class` hooks.
|
|
11
|
+
|
|
12
|
+
## 0.3.5
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- Fixed crash (KeyError) when a worker thread throws an unexpected exception
|
|
17
|
+
outside the normal test lifecycle — failures are now reported gracefully
|
|
18
|
+
instead of taking down the entire test run.
|
|
19
|
+
- `--threadpool` with a non-numeric value (e.g. `--threadpool foo`) now raises
|
|
20
|
+
a clean `pytest.UsageError` instead of an unhandled `ValueError` traceback.
|
|
21
|
+
- `@parallelizable("children")` applied directly to a test function now emits
|
|
22
|
+
a warning explaining that functions have no children, instead of being
|
|
23
|
+
silently ignored.
|
|
24
|
+
|
|
25
|
+
### Cleanup
|
|
26
|
+
|
|
27
|
+
- Moved `ParallelScope` import to module top in `_markers.py`, removed
|
|
28
|
+
misleading circular-import comment.
|
|
@@ -127,9 +127,6 @@ src/pytest_threadpool/
|
|
|
127
127
|
_runner.py # ParallelRunner: parallel execution orchestration
|
|
128
128
|
plugin.py # pytest hook implementations (wiring only)
|
|
129
129
|
|
|
130
|
-
hooks/
|
|
131
|
-
pre-commit # Git pre-commit hook (ruff + pyright)
|
|
132
|
-
|
|
133
130
|
scripts/
|
|
134
131
|
setup-dev # One-command dev environment setup
|
|
135
132
|
```
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-threadpool
|
|
3
|
+
Version: 0.3.6
|
|
4
|
+
Summary: Parallel test execution for free-threaded Python builds
|
|
5
|
+
Project-URL: Homepage, https://github.com/pytest-threadpool/pytest-threadpool
|
|
6
|
+
Project-URL: Source, https://github.com/pytest-threadpool/pytest-threadpool
|
|
7
|
+
Project-URL: Issues, https://github.com/pytest-threadpool/pytest-threadpool/issues
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: free-threaded,nogil,parallel,pytest,pytest parallel,testing,threadpool
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: Pytest
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Requires-Dist: pytest<=9.0.2,>=9.0.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
1
24
|
# pytest-threadpool
|
|
2
25
|
|
|
3
26
|
[](https://pypi.org/project/pytest-threadpool/)
|
|
@@ -36,22 +59,28 @@ from pytest_threadpool import parallelizable, not_parallelizable
|
|
|
36
59
|
|
|
37
60
|
import pytest
|
|
38
61
|
|
|
39
|
-
|
|
62
|
+
|
|
63
|
+
@parallelizable("children") # all nested tests run in parallel
|
|
40
64
|
class TestMyFeature:
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
def test_a(self): ...
|
|
66
|
+
|
|
67
|
+
def test_b(self): ...
|
|
43
68
|
|
|
44
|
-
|
|
69
|
+
|
|
70
|
+
@parallelizable("parameters") # parametrized variants run in parallel
|
|
45
71
|
@pytest.mark.parametrize("x", [1, 2, 3])
|
|
46
72
|
def test_with_params(x): ...
|
|
47
73
|
|
|
48
|
-
|
|
74
|
+
|
|
75
|
+
@parallelizable("all") # children + parameters combined
|
|
49
76
|
class TestEverything:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
@pytest.mark.parametrize("n", [1, 2])
|
|
78
|
+
def test_param(self, n): ...
|
|
79
|
+
|
|
80
|
+
def test_plain(self): ...
|
|
53
81
|
|
|
54
|
-
|
|
82
|
+
|
|
83
|
+
@not_parallelizable # opt out of inherited parallelism
|
|
55
84
|
def test_must_be_sequential(): ...
|
|
56
85
|
```
|
|
57
86
|
|
|
@@ -72,8 +101,8 @@ state between concurrent fixture setups.
|
|
|
72
101
|
Shared fixtures (module, class, and session scope) are resolved once
|
|
73
102
|
sequentially before workers launch and served from cache to all items.
|
|
74
103
|
|
|
75
|
-
| Scope | Behavior
|
|
76
|
-
|
|
104
|
+
| Scope | Behavior |
|
|
105
|
+
|------------|---------------------------------------------------------|
|
|
77
106
|
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
78
107
|
| `class` | Resolved once, cached, shared across items |
|
|
79
108
|
| `module` | Resolved once, cached, shared across items |
|
|
@@ -103,33 +132,33 @@ import pytest
|
|
|
103
132
|
|
|
104
133
|
|
|
105
134
|
class SharedState:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
lock = threading.Lock() # not pickleable
|
|
136
|
+
event = threading.Event() # not pickleable
|
|
137
|
+
results = {}
|
|
109
138
|
|
|
110
139
|
|
|
111
140
|
@pytest.mark.parallelizable("children")
|
|
112
141
|
class TestGroupA:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
def test_a1(self):
|
|
143
|
+
with SharedState.lock:
|
|
144
|
+
SharedState.results["a1"] = True
|
|
116
145
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
def test_a2(self):
|
|
147
|
+
with SharedState.lock:
|
|
148
|
+
SharedState.results["a2"] = True
|
|
120
149
|
|
|
121
150
|
|
|
122
151
|
@pytest.mark.parallelizable("children")
|
|
123
152
|
class TestGroupB:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
def test_b1(self):
|
|
154
|
+
SharedState.event.set()
|
|
155
|
+
with SharedState.lock:
|
|
156
|
+
SharedState.results["b1"] = True
|
|
157
|
+
|
|
158
|
+
def test_b2(self):
|
|
159
|
+
assert SharedState.event.wait(timeout=10)
|
|
160
|
+
with SharedState.lock:
|
|
161
|
+
SharedState.results["b2"] = True
|
|
133
162
|
```
|
|
134
163
|
|
|
135
164
|
Objects like `threading.Lock`, `threading.Event`, `logging.Logger`, database
|
|
@@ -152,14 +181,28 @@ pytest
|
|
|
152
181
|
|
|
153
182
|
## Tested versions
|
|
154
183
|
|
|
155
|
-
| Component | Versions
|
|
156
|
-
|
|
184
|
+
| Component | Versions |
|
|
185
|
+
|-----------|---------------------------------------|
|
|
157
186
|
| Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
|
|
158
|
-
| pytest |
|
|
187
|
+
| pytest | 9.0.2 |
|
|
159
188
|
|
|
160
189
|
> **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
|
|
161
190
|
> for CPU-bound tests. I/O-bound tests still run concurrently.
|
|
162
191
|
|
|
192
|
+
## Examples
|
|
193
|
+
|
|
194
|
+
The [`examples/`](examples/) directory contains runnable usage patterns:
|
|
195
|
+
|
|
196
|
+
- **DI container** — dependency injection with Singleton, ThreadLocal, ContextLocal, and Factory scopes
|
|
197
|
+
- **Event bus** — shared in-memory test double with concurrent producers and aggregate verification
|
|
198
|
+
- **Parallel logging** — shared thread-safe log collector (caplog alternative)
|
|
199
|
+
- **Shared state** — barriers, atomic counters, and cross-group coordination
|
|
200
|
+
- **User pool** — custom thread pool with LIFO queue recycling
|
|
201
|
+
|
|
202
|
+
The [`tests/integration_tests/cases/`](tests/integration_tests/cases/) and
|
|
203
|
+
[`tests/integration_tests/`](tests/integration_tests/) directories are also
|
|
204
|
+
worth browsing for real-world grouping, fixture, and reporting scenarios.
|
|
205
|
+
|
|
163
206
|
## Known limitations
|
|
164
207
|
|
|
165
208
|
- **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
|
|
@@ -168,10 +211,18 @@ pytest
|
|
|
168
211
|
- **No plugin compatibility guarantees** — Interactions with other pytest
|
|
169
212
|
plugins (e.g. `pytest-xdist`, `pytest-timeout`, `pytest-randomly`) are
|
|
170
213
|
untested and may conflict.
|
|
171
|
-
- **No
|
|
172
|
-
|
|
173
|
-
|
|
214
|
+
- **No `capsys`/`capfd`/`caplog` in parallel** — These fixtures are not
|
|
215
|
+
thread-safe. `capsys`/`capfd` fail with "cannot use capsys and capsys at
|
|
216
|
+
the same time" when requested by parallel tests. `caplog` leaks records
|
|
217
|
+
across fixtures and its `at_level()` context managers race on the shared
|
|
218
|
+
root logger. Alternatives:
|
|
219
|
+
- **`print()`** — Worker output is suppressed by default (buffered by
|
|
220
|
+
thread-local stream proxies). Pass `-s` (`--capture=no`) to disable
|
|
221
|
+
suppression and see interleaved output.
|
|
222
|
+
- **Logging** — Use a per-test `FileHandler` writing to `tmp_path`, or
|
|
223
|
+
collect structured records in a shared thread-safe list (see
|
|
224
|
+
[`examples/test_logging/`](examples/test_logging/)).
|
|
174
225
|
|
|
175
226
|
## License
|
|
176
227
|
|
|
177
|
-
|
|
228
|
+
[Apache 2.0](LICENSE)
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pytest-threadpool
|
|
3
|
-
Version: 0.3.4
|
|
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: Programming Language :: Python :: 3.13
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
-
Classifier: Topic :: Software Development :: Testing
|
|
17
|
-
Requires-Python: >=3.13
|
|
18
|
-
Requires-Dist: pytest>=9.0.2
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
|
|
21
1
|
# pytest-threadpool
|
|
22
2
|
|
|
23
3
|
[](https://pypi.org/project/pytest-threadpool/)
|
|
@@ -56,22 +36,28 @@ from pytest_threadpool import parallelizable, not_parallelizable
|
|
|
56
36
|
|
|
57
37
|
import pytest
|
|
58
38
|
|
|
59
|
-
|
|
39
|
+
|
|
40
|
+
@parallelizable("children") # all nested tests run in parallel
|
|
60
41
|
class TestMyFeature:
|
|
61
|
-
|
|
62
|
-
|
|
42
|
+
def test_a(self): ...
|
|
43
|
+
|
|
44
|
+
def test_b(self): ...
|
|
63
45
|
|
|
64
|
-
|
|
46
|
+
|
|
47
|
+
@parallelizable("parameters") # parametrized variants run in parallel
|
|
65
48
|
@pytest.mark.parametrize("x", [1, 2, 3])
|
|
66
49
|
def test_with_params(x): ...
|
|
67
50
|
|
|
68
|
-
|
|
51
|
+
|
|
52
|
+
@parallelizable("all") # children + parameters combined
|
|
69
53
|
class TestEverything:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
@pytest.mark.parametrize("n", [1, 2])
|
|
55
|
+
def test_param(self, n): ...
|
|
56
|
+
|
|
57
|
+
def test_plain(self): ...
|
|
73
58
|
|
|
74
|
-
|
|
59
|
+
|
|
60
|
+
@not_parallelizable # opt out of inherited parallelism
|
|
75
61
|
def test_must_be_sequential(): ...
|
|
76
62
|
```
|
|
77
63
|
|
|
@@ -92,8 +78,8 @@ state between concurrent fixture setups.
|
|
|
92
78
|
Shared fixtures (module, class, and session scope) are resolved once
|
|
93
79
|
sequentially before workers launch and served from cache to all items.
|
|
94
80
|
|
|
95
|
-
| Scope | Behavior
|
|
96
|
-
|
|
81
|
+
| Scope | Behavior |
|
|
82
|
+
|------------|---------------------------------------------------------|
|
|
97
83
|
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
98
84
|
| `class` | Resolved once, cached, shared across items |
|
|
99
85
|
| `module` | Resolved once, cached, shared across items |
|
|
@@ -123,33 +109,33 @@ import pytest
|
|
|
123
109
|
|
|
124
110
|
|
|
125
111
|
class SharedState:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
112
|
+
lock = threading.Lock() # not pickleable
|
|
113
|
+
event = threading.Event() # not pickleable
|
|
114
|
+
results = {}
|
|
129
115
|
|
|
130
116
|
|
|
131
117
|
@pytest.mark.parallelizable("children")
|
|
132
118
|
class TestGroupA:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
119
|
+
def test_a1(self):
|
|
120
|
+
with SharedState.lock:
|
|
121
|
+
SharedState.results["a1"] = True
|
|
136
122
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
def test_a2(self):
|
|
124
|
+
with SharedState.lock:
|
|
125
|
+
SharedState.results["a2"] = True
|
|
140
126
|
|
|
141
127
|
|
|
142
128
|
@pytest.mark.parallelizable("children")
|
|
143
129
|
class TestGroupB:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
130
|
+
def test_b1(self):
|
|
131
|
+
SharedState.event.set()
|
|
132
|
+
with SharedState.lock:
|
|
133
|
+
SharedState.results["b1"] = True
|
|
134
|
+
|
|
135
|
+
def test_b2(self):
|
|
136
|
+
assert SharedState.event.wait(timeout=10)
|
|
137
|
+
with SharedState.lock:
|
|
138
|
+
SharedState.results["b2"] = True
|
|
153
139
|
```
|
|
154
140
|
|
|
155
141
|
Objects like `threading.Lock`, `threading.Event`, `logging.Logger`, database
|
|
@@ -172,14 +158,28 @@ pytest
|
|
|
172
158
|
|
|
173
159
|
## Tested versions
|
|
174
160
|
|
|
175
|
-
| Component | Versions
|
|
176
|
-
|
|
161
|
+
| Component | Versions |
|
|
162
|
+
|-----------|---------------------------------------|
|
|
177
163
|
| Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
|
|
178
|
-
| pytest |
|
|
164
|
+
| pytest | 9.0.2 |
|
|
179
165
|
|
|
180
166
|
> **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
|
|
181
167
|
> for CPU-bound tests. I/O-bound tests still run concurrently.
|
|
182
168
|
|
|
169
|
+
## Examples
|
|
170
|
+
|
|
171
|
+
The [`examples/`](examples/) directory contains runnable usage patterns:
|
|
172
|
+
|
|
173
|
+
- **DI container** — dependency injection with Singleton, ThreadLocal, ContextLocal, and Factory scopes
|
|
174
|
+
- **Event bus** — shared in-memory test double with concurrent producers and aggregate verification
|
|
175
|
+
- **Parallel logging** — shared thread-safe log collector (caplog alternative)
|
|
176
|
+
- **Shared state** — barriers, atomic counters, and cross-group coordination
|
|
177
|
+
- **User pool** — custom thread pool with LIFO queue recycling
|
|
178
|
+
|
|
179
|
+
The [`tests/integration_tests/cases/`](tests/integration_tests/cases/) and
|
|
180
|
+
[`tests/integration_tests/`](tests/integration_tests/) directories are also
|
|
181
|
+
worth browsing for real-world grouping, fixture, and reporting scenarios.
|
|
182
|
+
|
|
183
183
|
## Known limitations
|
|
184
184
|
|
|
185
185
|
- **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
|
|
@@ -188,10 +188,18 @@ pytest
|
|
|
188
188
|
- **No plugin compatibility guarantees** — Interactions with other pytest
|
|
189
189
|
plugins (e.g. `pytest-xdist`, `pytest-timeout`, `pytest-randomly`) are
|
|
190
190
|
untested and may conflict.
|
|
191
|
-
- **No
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
- **No `capsys`/`capfd`/`caplog` in parallel** — These fixtures are not
|
|
192
|
+
thread-safe. `capsys`/`capfd` fail with "cannot use capsys and capsys at
|
|
193
|
+
the same time" when requested by parallel tests. `caplog` leaks records
|
|
194
|
+
across fixtures and its `at_level()` context managers race on the shared
|
|
195
|
+
root logger. Alternatives:
|
|
196
|
+
- **`print()`** — Worker output is suppressed by default (buffered by
|
|
197
|
+
thread-local stream proxies). Pass `-s` (`--capture=no`) to disable
|
|
198
|
+
suppression and see interleaved output.
|
|
199
|
+
- **Logging** — Use a per-test `FileHandler` writing to `tmp_path`, or
|
|
200
|
+
collect structured records in a shared thread-safe list (see
|
|
201
|
+
[`examples/test_logging/`](examples/test_logging/)).
|
|
194
202
|
|
|
195
203
|
## License
|
|
196
204
|
|
|
197
|
-
|
|
205
|
+
[Apache 2.0](LICENSE)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
## Capture fixture support (capsys / capfd / caplog)
|
|
4
|
+
|
|
5
|
+
**Goal:** Make `capsys`, `capfd`, and `caplog` work transparently in parallel tests.
|
|
6
|
+
|
|
7
|
+
**Problem:** These fixtures assume exclusive ownership of global state (`sys.stdout`,
|
|
8
|
+
root logger) during a test's lifetime. When multiple tests run concurrently, output
|
|
9
|
+
interleaves and pytest raises "cannot use capsys and capsys at the same time".
|
|
10
|
+
`caplog` leaks records across tests, and `at_level()` context managers race on the
|
|
11
|
+
shared root logger.
|
|
12
|
+
|
|
13
|
+
**Approach:** Delayed replay — collect output per-worker during parallel execution,
|
|
14
|
+
then feed it back one item at a time during the sequential reporting phase.
|
|
15
|
+
|
|
16
|
+
- **capsys/capfd:** The `_ThreadLocalStream` proxy already buffers worker output
|
|
17
|
+
into per-thread `StringIO`. Instead of discarding on `deactivate()`, associate
|
|
18
|
+
each buffer with its test item and flush to the real stream before
|
|
19
|
+
`pytest_runtest_makereport` reads the capture.
|
|
20
|
+
|
|
21
|
+
- **caplog:** Install a per-thread `logging.Handler` that collects records into a
|
|
22
|
+
thread-local list. During sequential reporting, replay records into caplog's
|
|
23
|
+
handler so the fixture sees the correct records for each test.
|
|
24
|
+
|
|
25
|
+
**Edge cases to handle:**
|
|
26
|
+
- Nested captures (fixtures that call `capsys.readouterr()` mid-test)
|
|
27
|
+
- `caplog.at_level()` context managers across threads
|
|
28
|
+
- Fixtures that mix capsys with print output
|
|
29
|
+
- `capfd` (file descriptor level) vs `capsys` (sys.stdout level)
|
|
30
|
+
|
|
31
|
+
**Priority:** High — this is the most common friction point for adoption.
|
|
32
|
+
|
|
33
|
+
## Harden FixtureDef cloning
|
|
34
|
+
|
|
35
|
+
**Goal:** Make function-scoped fixture cloning more robust against pytest internals
|
|
36
|
+
changes and more transparent to developers.
|
|
37
|
+
|
|
38
|
+
**Current approach:** `clone_function_fixturedefs` uses `__new__` + `__dict__.update`
|
|
39
|
+
for a shallow copy, then resets `cached_result` and `_finalizers`. This works because
|
|
40
|
+
those are the only two fields mutated during fixture lifecycle — but it's fragile.
|
|
41
|
+
|
|
42
|
+
**Improvements:**
|
|
43
|
+
|
|
44
|
+
- **Deep-copy guard:** Validate at clone time that the only mutable fields are the
|
|
45
|
+
ones we reset. If a future pytest version adds mutable state to `FixtureDef`,
|
|
46
|
+
the clone should fail loudly (assertion or warning) rather than silently sharing
|
|
47
|
+
state across workers.
|
|
48
|
+
|
|
49
|
+
- **Document fixture chain isolation:** When fixture A (function-scoped) depends on
|
|
50
|
+
fixture B (also function-scoped), both get cloned independently. Chain resolution
|
|
51
|
+
works because `_arg2fixturedefs` is replaced as a whole, so `getfixturevalue("B")`
|
|
52
|
+
hits B's clone. This is correct but non-obvious — add inline comments explaining
|
|
53
|
+
the chain resolution path through the cloned map.
|
|
54
|
+
|
|
55
|
+
- **`object.__setattr__` on `_arg2fixturedefs`:** Currently bypasses the `Final`
|
|
56
|
+
typing annotation. If pytest ever enforces immutability at runtime (frozen
|
|
57
|
+
dataclass, `__slots__`), this breaks. Track whether upstream adds a setter or
|
|
58
|
+
public mutation API.
|
|
59
|
+
|
|
60
|
+
**Priority:** Medium — current implementation works, but one pytest release could
|
|
61
|
+
break it silently.
|
|
62
|
+
|
|
63
|
+
## Plugin compatibility testing
|
|
64
|
+
|
|
65
|
+
Validate and document interactions with commonly used pytest plugins:
|
|
66
|
+
|
|
67
|
+
- `pytest-randomly` — test reordering vs parallel group formation
|
|
68
|
+
- `pytest-timeout` — per-test timeouts in worker threads
|
|
69
|
+
- `pytest-repeat` — repeated test execution in parallel groups
|
|
70
|
+
- `pytest-cov` — coverage collection across worker threads
|
|
71
|
+
|
|
72
|
+
## Public pytest API migration
|
|
73
|
+
|
|
74
|
+
Track pytest releases for public equivalents of internal APIs currently used:
|
|
75
|
+
|
|
76
|
+
- `_pytest.fixtures.FixtureDef._finalizers`
|
|
77
|
+
- `_pytest.fixtures.FixtureDef.cached_result`
|
|
78
|
+
- `_pytest.setupplan.SetupState.stack`
|
|
79
|
+
- `_pytest.scope.Scope`
|
|
80
|
+
- `callspec._arg2scope`
|
|
81
|
+
|
|
82
|
+
Migrate to public APIs as they become available to reduce breakage risk
|
|
83
|
+
across pytest releases.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""In-memory event bus — thread-safe test double.
|
|
2
|
+
|
|
3
|
+
A minimal pub/sub bus that records all published events. Tests publish
|
|
4
|
+
events concurrently, then verify delivery, ordering, and deduplication
|
|
5
|
+
against the shared log.
|
|
6
|
+
|
|
7
|
+
With pytest-xdist this would require an external broker (Redis, RabbitMQ)
|
|
8
|
+
or a socket-based mock server since each worker is a separate process.
|
|
9
|
+
With pytest-threadpool it's a plain Python object protected by a Lock.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventBus:
|
|
16
|
+
"""Thread-safe in-memory event bus for testing.
|
|
17
|
+
|
|
18
|
+
Instance-based: each test scope gets its own bus via a fixture,
|
|
19
|
+
with proper setup/teardown handled by pytest's fixture lifecycle.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._lock = threading.Lock()
|
|
24
|
+
self._events: list[dict] = []
|
|
25
|
+
self._subscribers: dict[str, list] = {}
|
|
26
|
+
self._waiters: list[tuple[str | None, int, threading.Event]] = []
|
|
27
|
+
|
|
28
|
+
def publish(self, topic: str, payload: dict) -> None:
|
|
29
|
+
with self._lock:
|
|
30
|
+
event = {"topic": topic, "payload": payload, "thread": threading.current_thread().name}
|
|
31
|
+
self._events.append(event)
|
|
32
|
+
for callback in self._subscribers.get(topic, []):
|
|
33
|
+
callback(event)
|
|
34
|
+
self._check_waiters()
|
|
35
|
+
|
|
36
|
+
def subscribe(self, topic: str, callback) -> None:
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._subscribers.setdefault(topic, []).append(callback)
|
|
39
|
+
|
|
40
|
+
def events(self, topic: str | None = None) -> list[dict]:
|
|
41
|
+
with self._lock:
|
|
42
|
+
if topic is None:
|
|
43
|
+
return list(self._events)
|
|
44
|
+
return [e for e in self._events if e["topic"] == topic]
|
|
45
|
+
|
|
46
|
+
def wait_for(self, count: int, topic: str | None = None, timeout: float = 10) -> list[dict]:
|
|
47
|
+
"""Block until at least ``count`` events match, then return them."""
|
|
48
|
+
ready = threading.Event()
|
|
49
|
+
with self._lock:
|
|
50
|
+
matched = self._filter(topic)
|
|
51
|
+
if len(matched) >= count:
|
|
52
|
+
return matched
|
|
53
|
+
self._waiters.append((topic, count, ready))
|
|
54
|
+
if not ready.wait(timeout=timeout):
|
|
55
|
+
actual = len(self.events(topic))
|
|
56
|
+
label = f"topic={topic!r}" if topic else "all topics"
|
|
57
|
+
raise TimeoutError(
|
|
58
|
+
f"EventBus.wait_for: expected {count} events on {label}, "
|
|
59
|
+
f"got {actual} after {timeout}s"
|
|
60
|
+
)
|
|
61
|
+
return self.events(topic)
|
|
62
|
+
|
|
63
|
+
def _check_waiters(self) -> None:
|
|
64
|
+
"""Signal any waiters whose condition is met. Must hold _lock."""
|
|
65
|
+
remaining = []
|
|
66
|
+
for topic, count, event in self._waiters:
|
|
67
|
+
if len(self._filter(topic)) >= count:
|
|
68
|
+
event.set()
|
|
69
|
+
else:
|
|
70
|
+
remaining.append((topic, count, event))
|
|
71
|
+
self._waiters[:] = remaining
|
|
72
|
+
|
|
73
|
+
def _filter(self, topic: str | None) -> list[dict]:
|
|
74
|
+
if topic is None:
|
|
75
|
+
return list(self._events)
|
|
76
|
+
return [e for e in self._events if e["topic"] == topic]
|