pytest-threadpool 0.3.5__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.5 → pytest_threadpool-0.3.6}/CHANGELOG.md +9 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/PKG-INFO +1 -1
- 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.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_runner.py +35 -8
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_version.py +2 -2
- 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_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/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.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_fixtures.py +39 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_xunit.py +18 -0
- pytest_threadpool-0.3.5/examples/test_event_bus/event_bus.py +0 -85
- pytest_threadpool-0.3.5/examples/test_event_bus/test_event_bus.py +0 -112
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/.github/workflows/ci.yml +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/.github/workflows/publish.yml +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/.gitignore +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/.pre-commit-config.yaml +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/CONTRIBUTING.md +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/LICENSE +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/README.md +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/ROADMAP.md +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/codecov.yml +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/container.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/providers.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/services.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/test_factory.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/test_local.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/test_singleton.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_di/test_thread_local.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_event_bus/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_logging/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_logging/test_log_capture.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_queue/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_queue/conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_queue/test_queue.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_queue/user_pool.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_shared_state/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_shared_state/test_barrier.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_shared_state/test_counter.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/examples/test_shared_state/test_counter_async.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/pyproject.toml +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/scripts/setup-dev +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_api.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_constants.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_fixtures.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_grouping.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/_markers.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/plugin.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/src/pytest_threadpool/py.typed +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/_templates.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/capture_print_parallel.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/capture_two_groups.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/class_single_method.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/class_thread_verification.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/collected_count.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_first_item_setup_fail.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_maxfail_parallel.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_sigint.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_system_exit.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_teardown_exception.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_worker_exception.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/edge_worker_exception_conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_from_conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_from_conftest_conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_implicit_scope.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_mixed_parallel.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/marks_custom.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/marks_standard.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_incremental.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_package_children.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_single_file_params.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/reporting_stdout_during_parallel.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/runner_collect_only.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/runner_setup_only.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/runner_setup_show.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/runner_single_parallel_item.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/runner_single_worker.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_all_merged.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_children_on_function.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_mixed_fail_skip.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_module_children.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_parameters.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/scope_two_classes_mixed.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/setup_all_fail.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_counter.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_after_test_failure.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_parallel_timing.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_same_thread.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_xunit_function.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/teardown_xunit_method_thread.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/validate_threadpool.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_function_mixed_parallel.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_method_mixed_parallel.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/conftest.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_capture.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_class.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_edge_cases.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_func_fixtures.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_marks.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_package.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_parallel_teardown.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_reporting.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_scopes.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_sequential.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/integration_tests/test_pytester_shared.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/unit_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_api.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_fixtures.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_grouping.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_markers.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_plugin.py +0 -0
- {pytest_threadpool-0.3.5 → pytest_threadpool-0.3.6}/tests/unit_tests/test_unit_stream_proxy.py +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
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
|
+
|
|
3
12
|
## 0.3.5
|
|
4
13
|
|
|
5
14
|
### Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-threadpool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Parallel test execution for free-threaded Python builds
|
|
5
5
|
Project-URL: Homepage, https://github.com/pytest-threadpool/pytest-threadpool
|
|
6
6
|
Project-URL: Source, https://github.com/pytest-threadpool/pytest-threadpool
|
|
@@ -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]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Shared in-memory test double — concurrent access to a shared event bus.
|
|
2
|
+
|
|
3
|
+
This pattern is impossible with pytest-xdist: each subprocess has its own
|
|
4
|
+
memory space, so a plain Python object can't collect events from multiple
|
|
5
|
+
workers. You'd need Redis, a socket server, or a temp file to coordinate.
|
|
6
|
+
|
|
7
|
+
With pytest-threadpool, all tests share the same process. A class-scoped
|
|
8
|
+
fixture creates one EventBus instance, shared by all parallel methods —
|
|
9
|
+
proper lifecycle management with no global mutable state.
|
|
10
|
+
|
|
11
|
+
Patterns demonstrated:
|
|
12
|
+
- Class-scoped fixture providing a shared test double to parallel workers
|
|
13
|
+
- Concurrent access to a shared in-memory object from parallel threads
|
|
14
|
+
- Subscriber callbacks firing synchronously during publish
|
|
15
|
+
- Aggregate cross-test visibility via wait_for
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from examples.test_event_bus.event_bus import EventBus
|
|
23
|
+
|
|
24
|
+
_N_PUBLISHERS = 4
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="class")
|
|
28
|
+
def bus():
|
|
29
|
+
return EventBus()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parallelizable("children")
|
|
33
|
+
class TestEventBus:
|
|
34
|
+
"""All tests run in parallel, sharing one bus via a class-scoped fixture."""
|
|
35
|
+
|
|
36
|
+
def _publish_own_events(self, bus: EventBus, name: str) -> None:
|
|
37
|
+
"""Each test publishes its own uniquely-tagged events."""
|
|
38
|
+
bus.publish("work", {"source": name, "step": "start"})
|
|
39
|
+
bus.publish("work", {"source": name, "step": "done"})
|
|
40
|
+
|
|
41
|
+
def test_publish_and_verify_signup(self, bus):
|
|
42
|
+
"""Publish signup events and verify only own events are correct."""
|
|
43
|
+
bus.publish("user.created", {"user_id": "u1"})
|
|
44
|
+
bus.publish("email.queued", {"to": "a@test.com", "template": "welcome"})
|
|
45
|
+
|
|
46
|
+
own_emails = [e for e in bus.events("email.queued") if e["payload"]["to"] == "a@test.com"]
|
|
47
|
+
assert len(own_emails) == 1
|
|
48
|
+
assert own_emails[0]["payload"]["template"] == "welcome"
|
|
49
|
+
|
|
50
|
+
def test_publish_and_verify_purchase(self, bus):
|
|
51
|
+
"""Publish purchase events and verify own events landed."""
|
|
52
|
+
bus.publish("order.created", {"order_id": "o1", "user_id": "u2"})
|
|
53
|
+
bus.publish("payment.charged", {"order_id": "o1", "amount": 99})
|
|
54
|
+
|
|
55
|
+
own = [e for e in bus.events("payment.charged") if e["payload"]["order_id"] == "o1"]
|
|
56
|
+
assert len(own) == 1
|
|
57
|
+
assert own[0]["payload"]["amount"] == 99
|
|
58
|
+
|
|
59
|
+
def test_subscriber_callback_fires_on_publish(self):
|
|
60
|
+
"""Subscribe, publish, verify callback fired — all within one test.
|
|
61
|
+
|
|
62
|
+
Uses a dedicated EventBus instance so the subscriber doesn't leak
|
|
63
|
+
into the shared class-scoped bus.
|
|
64
|
+
"""
|
|
65
|
+
local_bus = EventBus()
|
|
66
|
+
received: list[dict] = []
|
|
67
|
+
|
|
68
|
+
def on_event(event):
|
|
69
|
+
received.append(event)
|
|
70
|
+
|
|
71
|
+
local_bus.subscribe("audit.log", on_event)
|
|
72
|
+
local_bus.publish("audit.log", {"action": "login", "user": "alice"})
|
|
73
|
+
local_bus.publish("audit.log", {"action": "logout", "user": "alice"})
|
|
74
|
+
|
|
75
|
+
actions = [e["payload"]["action"] for e in received]
|
|
76
|
+
assert actions == ["login", "logout"]
|
|
77
|
+
|
|
78
|
+
def test_wait_for_own_events(self, bus):
|
|
79
|
+
"""Publish from a background thread, wait_for in the test thread."""
|
|
80
|
+
topic = "async.work"
|
|
81
|
+
|
|
82
|
+
def background_publisher():
|
|
83
|
+
bus.publish(topic, {"step": 1})
|
|
84
|
+
bus.publish(topic, {"step": 2})
|
|
85
|
+
|
|
86
|
+
t = threading.Thread(target=background_publisher)
|
|
87
|
+
t.start()
|
|
88
|
+
|
|
89
|
+
events = bus.wait_for(2, topic=topic)
|
|
90
|
+
t.join(timeout=10)
|
|
91
|
+
|
|
92
|
+
steps = [e["payload"]["step"] for e in events]
|
|
93
|
+
assert 1 in steps
|
|
94
|
+
assert 2 in steps
|
|
95
|
+
|
|
96
|
+
# -- aggregate visibility: each parametrized worker publishes tagged events,
|
|
97
|
+
# then waits for all N_PUBLISHERS to have contributed --
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize("worker", range(_N_PUBLISHERS))
|
|
100
|
+
def test_publish_and_observe_aggregate(self, bus, worker):
|
|
101
|
+
"""Each worker publishes its own events, then waits to see all workers' events.
|
|
102
|
+
|
|
103
|
+
This proves cross-test visibility: events from parallel threads
|
|
104
|
+
are visible to every other thread via the shared bus.
|
|
105
|
+
"""
|
|
106
|
+
self._publish_own_events(bus, f"worker_{worker}")
|
|
107
|
+
|
|
108
|
+
all_events = bus.wait_for(_N_PUBLISHERS * 2, topic="work")
|
|
109
|
+
sources = {e["payload"]["source"] for e in all_events}
|
|
110
|
+
assert len(sources) == _N_PUBLISHERS
|
|
@@ -356,22 +356,34 @@ class ParallelRunner:
|
|
|
356
356
|
)
|
|
357
357
|
|
|
358
358
|
needs_sep = False
|
|
359
|
-
for group_key, items in groups:
|
|
359
|
+
for group_idx, (group_key, items) in enumerate(groups):
|
|
360
360
|
if session.shouldfail:
|
|
361
361
|
raise session.Failed(session.shouldfail)
|
|
362
362
|
if session.shouldstop:
|
|
363
363
|
raise session.Interrupted(session.shouldstop)
|
|
364
364
|
|
|
365
|
+
# First item of the next group — tells teardown_exact which
|
|
366
|
+
# session/module/class nodes to keep alive across groups.
|
|
367
|
+
next_group_first = None
|
|
368
|
+
for _, future_items in groups[group_idx + 1 :]:
|
|
369
|
+
if future_items:
|
|
370
|
+
next_group_first = future_items[0]
|
|
371
|
+
break
|
|
372
|
+
|
|
365
373
|
if group_key is None or len(items) <= 1 or self._nthreads <= 1:
|
|
366
374
|
for i, item in enumerate(items):
|
|
367
|
-
nextitem = items[i + 1] if i + 1 < len(items) else
|
|
375
|
+
nextitem = items[i + 1] if i + 1 < len(items) else next_group_first
|
|
368
376
|
if has_parallel:
|
|
369
377
|
self._run_sequential_nodeid(item, nextitem)
|
|
370
378
|
else:
|
|
371
379
|
self._run_sequential(item, nextitem)
|
|
372
380
|
needs_sep = bool(items)
|
|
373
381
|
else:
|
|
374
|
-
self._run_parallel(
|
|
382
|
+
self._run_parallel(
|
|
383
|
+
items,
|
|
384
|
+
after_sequential=needs_sep,
|
|
385
|
+
next_group_first=next_group_first,
|
|
386
|
+
)
|
|
375
387
|
needs_sep = True
|
|
376
388
|
|
|
377
389
|
return True
|
|
@@ -469,7 +481,7 @@ class ParallelRunner:
|
|
|
469
481
|
f.write(f"\n{item.nodeid} {color}{word}{reset}")
|
|
470
482
|
f.flush()
|
|
471
483
|
|
|
472
|
-
def _run_parallel(self, items, after_sequential: bool = False) -> None:
|
|
484
|
+
def _run_parallel(self, items, after_sequential: bool = False, next_group_first=None) -> None:
|
|
473
485
|
"""Run a group's tests with parallel fixture setup and calls.
|
|
474
486
|
|
|
475
487
|
Function-scoped FixtureDefs are cloned per-item so their setup can
|
|
@@ -525,7 +537,9 @@ class ParallelRunner:
|
|
|
525
537
|
ihook.pytest_runtest_logreport(report=setup_reports[item])
|
|
526
538
|
if setup_passed[item] and session.config.getoption("setupshow", False):
|
|
527
539
|
show_test_item(item)
|
|
528
|
-
self._teardown_all(
|
|
540
|
+
self._teardown_all(
|
|
541
|
+
items, per_item_fixture_fins, {}, saved_collector_fins, next_group_first
|
|
542
|
+
)
|
|
529
543
|
return
|
|
530
544
|
|
|
531
545
|
# Phase 1+2: fire hooks for all items, populate shared fixture caches,
|
|
@@ -832,13 +846,20 @@ class ParallelRunner:
|
|
|
832
846
|
session._setupstate.stack.pop(item) # pyright: ignore[reportPrivateUsage]
|
|
833
847
|
|
|
834
848
|
# Teardown reporting + collector teardown (always runs, even after interrupt)
|
|
835
|
-
self._teardown_all(
|
|
849
|
+
self._teardown_all(
|
|
850
|
+
items, per_item_fixture_fins, teardown_infos, saved_collector_fins, next_group_first
|
|
851
|
+
)
|
|
836
852
|
|
|
837
853
|
if interrupted:
|
|
838
854
|
raise KeyboardInterrupt
|
|
839
855
|
|
|
840
856
|
def _teardown_all(
|
|
841
|
-
self,
|
|
857
|
+
self,
|
|
858
|
+
items,
|
|
859
|
+
per_item_fixture_fins,
|
|
860
|
+
teardown_infos,
|
|
861
|
+
saved_collector_fins,
|
|
862
|
+
next_group_first=None,
|
|
842
863
|
) -> None:
|
|
843
864
|
"""Report teardown results, run any remaining finalizers, and tear down
|
|
844
865
|
collectors.
|
|
@@ -846,6 +867,10 @@ class ParallelRunner:
|
|
|
846
867
|
For items with pre-computed teardown_infos (from parallel workers),
|
|
847
868
|
only reporting happens here. For items without (setuponly mode),
|
|
848
869
|
finalizers from per_item_fixture_fins are executed sequentially.
|
|
870
|
+
|
|
871
|
+
next_group_first: first item of the next group (or None if last group).
|
|
872
|
+
Passed to teardown_exact so session/module/class-scoped fixtures that
|
|
873
|
+
are still needed by the next group are preserved rather than torn down.
|
|
849
874
|
"""
|
|
850
875
|
session = self._session
|
|
851
876
|
|
|
@@ -882,7 +907,9 @@ class ParallelRunner:
|
|
|
882
907
|
item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
|
|
883
908
|
|
|
884
909
|
# noinspection PyProtectedMember
|
|
885
|
-
|
|
910
|
+
# Pass next_group_first so session/module/class nodes needed by the
|
|
911
|
+
# next group stay in the stack with their cached fixtures intact.
|
|
912
|
+
session._setupstate.teardown_exact(nextitem=next_group_first) # pyright: ignore[reportPrivateUsage]
|
|
886
913
|
|
|
887
914
|
exceptions = []
|
|
888
915
|
for _node, fins in reversed(saved_collector_fins):
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.3.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 3,
|
|
31
|
+
__version__ = version = '0.3.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 6)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Class-scoped fixture: same object within a class, different across classes."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
_ids_a = []
|
|
8
|
+
_ids_b = []
|
|
9
|
+
lock = threading.Lock()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Resource:
|
|
13
|
+
"""Mutable object whose identity can be checked via id()."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(scope="class")
|
|
17
|
+
def class_resource():
|
|
18
|
+
return Resource()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parallelizable("children")
|
|
22
|
+
class TestGroupA:
|
|
23
|
+
def test_a1(self, class_resource):
|
|
24
|
+
with lock:
|
|
25
|
+
_ids_a.append(id(class_resource))
|
|
26
|
+
|
|
27
|
+
def test_a2(self, class_resource):
|
|
28
|
+
with lock:
|
|
29
|
+
_ids_a.append(id(class_resource))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parallelizable("children")
|
|
33
|
+
class TestGroupB:
|
|
34
|
+
def test_b1(self, class_resource):
|
|
35
|
+
with lock:
|
|
36
|
+
_ids_b.append(id(class_resource))
|
|
37
|
+
|
|
38
|
+
def test_b2(self, class_resource):
|
|
39
|
+
with lock:
|
|
40
|
+
_ids_b.append(id(class_resource))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_verify():
|
|
44
|
+
# Within each class, all tests see the same object
|
|
45
|
+
assert len(set(_ids_a)) == 1, f"class A fixture not shared: {set(_ids_a)}"
|
|
46
|
+
assert len(set(_ids_b)) == 1, f"class B fixture not shared: {set(_ids_b)}"
|
|
47
|
+
# Across classes, fixtures are different objects
|
|
48
|
+
assert _ids_a[0] != _ids_b[0], "class-scoped fixture leaked across classes"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Module-scoped fixture must be the same object across parallel groups and sequential tests."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
_ids = []
|
|
8
|
+
lock = threading.Lock()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Resource:
|
|
12
|
+
"""Mutable object whose identity can be checked via id()."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope="module")
|
|
16
|
+
def module_resource():
|
|
17
|
+
return Resource()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parallelizable("children")
|
|
21
|
+
class TestGroupA:
|
|
22
|
+
def test_a1(self, module_resource):
|
|
23
|
+
with lock:
|
|
24
|
+
_ids.append(id(module_resource))
|
|
25
|
+
|
|
26
|
+
def test_a2(self, module_resource):
|
|
27
|
+
with lock:
|
|
28
|
+
_ids.append(id(module_resource))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.not_parallelizable
|
|
32
|
+
def test_sequential_between(module_resource):
|
|
33
|
+
with lock:
|
|
34
|
+
_ids.append(id(module_resource))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parallelizable("children")
|
|
38
|
+
class TestGroupB:
|
|
39
|
+
def test_b1(self, module_resource):
|
|
40
|
+
with lock:
|
|
41
|
+
_ids.append(id(module_resource))
|
|
42
|
+
|
|
43
|
+
def test_b2(self, module_resource):
|
|
44
|
+
with lock:
|
|
45
|
+
_ids.append(id(module_resource))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_verify():
|
|
49
|
+
assert len(_ids) == 5, f"expected 5 recorded ids, got {len(_ids)}"
|
|
50
|
+
assert len(set(_ids)) == 1, f"module fixture created multiple objects: {set(_ids)}"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Package-scoped fixture shared across parallel groups in the same package.
|
|
2
|
+
|
|
3
|
+
This case provides source strings used by the integration test to
|
|
4
|
+
build a temporary package directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
CONFTEST_SRC = """\
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Resource:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope="package")
|
|
16
|
+
def pkg_resource():
|
|
17
|
+
return Resource()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
INIT_SRC = """\
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
pytestmark = pytest.mark.parallelizable("children")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
MOD_A_SRC = """\
|
|
27
|
+
import threading
|
|
28
|
+
|
|
29
|
+
lock = threading.Lock()
|
|
30
|
+
ids = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_a1(pkg_resource):
|
|
34
|
+
with lock:
|
|
35
|
+
ids.append(id(pkg_resource))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_a2(pkg_resource):
|
|
39
|
+
with lock:
|
|
40
|
+
ids.append(id(pkg_resource))
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
MOD_B_SRC = """\
|
|
44
|
+
import threading
|
|
45
|
+
|
|
46
|
+
lock = threading.Lock()
|
|
47
|
+
ids = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_b1(pkg_resource):
|
|
51
|
+
with lock:
|
|
52
|
+
ids.append(id(pkg_resource))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_b2(pkg_resource):
|
|
56
|
+
with lock:
|
|
57
|
+
ids.append(id(pkg_resource))
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
VERIFY_SRC = """\
|
|
61
|
+
from mypkg import test_mod_a, test_mod_b
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_verify():
|
|
65
|
+
all_ids = test_mod_a.ids + test_mod_b.ids
|
|
66
|
+
assert len(all_ids) == 4, f"expected 4 recorded ids, got {len(all_ids)}"
|
|
67
|
+
assert len(set(all_ids)) == 1, (
|
|
68
|
+
f"package fixture created multiple objects: {set(all_ids)}"
|
|
69
|
+
)
|
|
70
|
+
"""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Session-scoped fixture must be the same object across parallel groups and sequential tests."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
_ids = []
|
|
8
|
+
lock = threading.Lock()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Resource:
|
|
12
|
+
"""Mutable object whose identity can be checked via id()."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope="session")
|
|
16
|
+
def session_resource():
|
|
17
|
+
return Resource()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parallelizable("children")
|
|
21
|
+
class TestGroupA:
|
|
22
|
+
def test_a1(self, session_resource):
|
|
23
|
+
with lock:
|
|
24
|
+
_ids.append(id(session_resource))
|
|
25
|
+
|
|
26
|
+
def test_a2(self, session_resource):
|
|
27
|
+
with lock:
|
|
28
|
+
_ids.append(id(session_resource))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.not_parallelizable
|
|
32
|
+
def test_sequential_between(session_resource):
|
|
33
|
+
with lock:
|
|
34
|
+
_ids.append(id(session_resource))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parallelizable("children")
|
|
38
|
+
class TestGroupB:
|
|
39
|
+
def test_b1(self, session_resource):
|
|
40
|
+
with lock:
|
|
41
|
+
_ids.append(id(session_resource))
|
|
42
|
+
|
|
43
|
+
def test_b2(self, session_resource):
|
|
44
|
+
with lock:
|
|
45
|
+
_ids.append(id(session_resource))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_verify():
|
|
49
|
+
assert len(_ids) == 5, f"expected 5 recorded ids, got {len(_ids)}"
|
|
50
|
+
assert len(set(_ids)) == 1, f"session fixture created multiple objects: {set(_ids)}"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""setup_class/teardown_class: once per class, independent across parallel groups."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
lock = threading.Lock()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parallelizable("children")
|
|
12
|
+
class TestGroupA:
|
|
13
|
+
setup_ids: ClassVar[list] = []
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def setup_class(cls):
|
|
17
|
+
with lock:
|
|
18
|
+
cls.setup_ids.append(threading.current_thread().name)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def teardown_class(cls):
|
|
22
|
+
with lock:
|
|
23
|
+
cls.setup_ids.append("teardown_a")
|
|
24
|
+
|
|
25
|
+
def test_a1(self):
|
|
26
|
+
assert len([x for x in self.setup_ids if x != "teardown_a"]) == 1
|
|
27
|
+
|
|
28
|
+
def test_a2(self):
|
|
29
|
+
assert len([x for x in self.setup_ids if x != "teardown_a"]) == 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parallelizable("children")
|
|
33
|
+
class TestGroupB:
|
|
34
|
+
setup_ids: ClassVar[list] = []
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def setup_class(cls):
|
|
38
|
+
with lock:
|
|
39
|
+
cls.setup_ids.append(threading.current_thread().name)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def teardown_class(cls):
|
|
43
|
+
with lock:
|
|
44
|
+
cls.setup_ids.append("teardown_b")
|
|
45
|
+
|
|
46
|
+
def test_b1(self):
|
|
47
|
+
assert len([x for x in self.setup_ids if x != "teardown_b"]) == 1
|
|
48
|
+
|
|
49
|
+
def test_b2(self):
|
|
50
|
+
assert len([x for x in self.setup_ids if x != "teardown_b"]) == 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_verify():
|
|
54
|
+
# Each class had exactly one setup_class call
|
|
55
|
+
a_setups = [x for x in TestGroupA.setup_ids if x != "teardown_a"]
|
|
56
|
+
b_setups = [x for x in TestGroupB.setup_ids if x != "teardown_b"]
|
|
57
|
+
assert len(a_setups) == 1, f"setup_class ran {len(a_setups)} times for A"
|
|
58
|
+
assert len(b_setups) == 1, f"setup_class ran {len(b_setups)} times for B"
|
|
59
|
+
# Both teardowns ran
|
|
60
|
+
assert "teardown_a" in TestGroupA.setup_ids
|
|
61
|
+
assert "teardown_b" in TestGroupB.setup_ids
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""setup_method/teardown_method fire for every method across separate parallel classes."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
lock = threading.Lock()
|
|
8
|
+
all_setups: list = []
|
|
9
|
+
all_teardowns: list = []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parallelizable("children")
|
|
13
|
+
class TestGroupA:
|
|
14
|
+
barrier = threading.Barrier(2, timeout=10)
|
|
15
|
+
|
|
16
|
+
def setup_method(self, method):
|
|
17
|
+
with lock:
|
|
18
|
+
all_setups.append(f"A.{method.__name__}")
|
|
19
|
+
|
|
20
|
+
def teardown_method(self, method):
|
|
21
|
+
with lock:
|
|
22
|
+
all_teardowns.append(f"A.{method.__name__}")
|
|
23
|
+
|
|
24
|
+
def test_a1(self):
|
|
25
|
+
self.barrier.wait()
|
|
26
|
+
|
|
27
|
+
def test_a2(self):
|
|
28
|
+
self.barrier.wait()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parallelizable("children")
|
|
32
|
+
class TestGroupB:
|
|
33
|
+
barrier = threading.Barrier(2, timeout=10)
|
|
34
|
+
|
|
35
|
+
def setup_method(self, method):
|
|
36
|
+
with lock:
|
|
37
|
+
all_setups.append(f"B.{method.__name__}")
|
|
38
|
+
|
|
39
|
+
def teardown_method(self, method):
|
|
40
|
+
with lock:
|
|
41
|
+
all_teardowns.append(f"B.{method.__name__}")
|
|
42
|
+
|
|
43
|
+
def test_b1(self):
|
|
44
|
+
self.barrier.wait()
|
|
45
|
+
|
|
46
|
+
def test_b2(self):
|
|
47
|
+
self.barrier.wait()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_verify():
|
|
51
|
+
expected_setups = {"A.test_a1", "A.test_a2", "B.test_b1", "B.test_b2"}
|
|
52
|
+
assert expected_setups == set(all_setups), f"setup_method: {all_setups}"
|
|
53
|
+
assert expected_setups == set(all_teardowns), f"teardown_method: {all_teardowns}"
|