pytest-isolated 0.5.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_isolated-0.5.0/LICENSE +21 -0
- pytest_isolated-0.5.0/PKG-INFO +391 -0
- pytest_isolated-0.5.0/README.md +352 -0
- pytest_isolated-0.5.0/pyproject.toml +109 -0
- pytest_isolated-0.5.0/setup.cfg +4 -0
- pytest_isolated-0.5.0/src/pytest_isolated/__init__.py +3 -0
- pytest_isolated-0.5.0/src/pytest_isolated/config.py +183 -0
- pytest_isolated-0.5.0/src/pytest_isolated/execution.py +588 -0
- pytest_isolated-0.5.0/src/pytest_isolated/grouping.py +157 -0
- pytest_isolated-0.5.0/src/pytest_isolated/plugin.py +19 -0
- pytest_isolated-0.5.0/src/pytest_isolated/py.typed +0 -0
- pytest_isolated-0.5.0/src/pytest_isolated/reporting.py +210 -0
- pytest_isolated-0.5.0/src/pytest_isolated.egg-info/PKG-INFO +391 -0
- pytest_isolated-0.5.0/src/pytest_isolated.egg-info/SOURCES.txt +25 -0
- pytest_isolated-0.5.0/src/pytest_isolated.egg-info/dependency_links.txt +1 -0
- pytest_isolated-0.5.0/src/pytest_isolated.egg-info/entry_points.txt +2 -0
- pytest_isolated-0.5.0/src/pytest_isolated.egg-info/requires.txt +11 -0
- pytest_isolated-0.5.0/src/pytest_isolated.egg-info/top_level.txt +1 -0
- pytest_isolated-0.5.0/tests/test_app_isolation.py +65 -0
- pytest_isolated-0.5.0/tests/test_basic.py +113 -0
- pytest_isolated-0.5.0/tests/test_execution.py +364 -0
- pytest_isolated-0.5.0/tests/test_execution_units.py +435 -0
- pytest_isolated-0.5.0/tests/test_filtering.py +236 -0
- pytest_isolated-0.5.0/tests/test_grouping.py +577 -0
- pytest_isolated-0.5.0/tests/test_options.py +505 -0
- pytest_isolated-0.5.0/tests/test_plugin_compatibility.py +176 -0
- pytest_isolated-0.5.0/tests/test_reporting.py +285 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bryn Lloyd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-isolated
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Run marked pytest tests in grouped subprocesses (cross-platform).
|
|
5
|
+
Author: pytest-isolated contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dyollb/pytest-isolated
|
|
8
|
+
Project-URL: Repository, https://github.com/dyollb/pytest-isolated
|
|
9
|
+
Project-URL: Issues, https://github.com/dyollb/pytest-isolated/issues
|
|
10
|
+
Keywords: testing,cross-platform,pytest,process-isolation,pytest-plugin,subprocess,crash-handling,python-testing,test-isolation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: Pytest
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: pytest>=7.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy; extra == "dev"
|
|
34
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-forked; sys_platform != "win32" and extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# pytest-isolated
|
|
41
|
+
|
|
42
|
+
[](https://github.com/dyollb/pytest-isolated/actions/workflows/test.yml)
|
|
43
|
+
[](https://pypi.org/project/pytest-isolated/)
|
|
44
|
+
[](https://pypi.org/project/pytest-isolated/)
|
|
45
|
+
[](https://pepy.tech/project/pytest-isolated)
|
|
46
|
+
[](https://github.com/dyollb/pytest-isolated/blob/main/LICENSE)
|
|
47
|
+
[](https://github.com/dyollb/pytest-isolated)
|
|
48
|
+
|
|
49
|
+
A cross-platform pytest plugin that runs marked tests in isolated subprocesses with intelligent grouping.
|
|
50
|
+
|
|
51
|
+
## Why pytest-isolated?
|
|
52
|
+
|
|
53
|
+
Ever had a test that passes alone but fails in the suite? Or tests that mysteriously hang on CI? pytest-isolated solves this by running tests in completely isolated subprocesses.
|
|
54
|
+
|
|
55
|
+
**Common problems it solves:**
|
|
56
|
+
|
|
57
|
+
- ✅ Segfaults and crashes don't kill your entire test suite
|
|
58
|
+
- ✅ Tests modifying global state don't affect each other
|
|
59
|
+
- ✅ Hanging tests timeout without blocking other tests
|
|
60
|
+
- ✅ C extension crashes are contained
|
|
61
|
+
- ✅ Resource leaks are isolated per test
|
|
62
|
+
|
|
63
|
+
## Cheatsheet for pytest-forked users
|
|
64
|
+
|
|
65
|
+
This plugin is inspired by [pytest-forked](https://github.com/pytest-dev/pytest-forked). See [pytest-forked migration guide](docs/pytest-forked-migration.md) for a quick reference comparing features.
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
**1. Install:**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install pytest-isolated
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**2. Mark a flaky test:**
|
|
76
|
+
|
|
77
|
+
```py
|
|
78
|
+
# Example 1: Simple isolation
|
|
79
|
+
@pytest.mark.isolated
|
|
80
|
+
def test_with_clean_state():
|
|
81
|
+
import os
|
|
82
|
+
os.environ["DEBUG"] = "true"
|
|
83
|
+
# Other tests won't see this change
|
|
84
|
+
|
|
85
|
+
# Example 2: Crash protection
|
|
86
|
+
@pytest.mark.isolated
|
|
87
|
+
def test_that_crashes():
|
|
88
|
+
import ctypes
|
|
89
|
+
ctypes.string_at(0) # Crash is contained!
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**3. Run pytest normally:**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pytest # Crash is isolated, suite continues
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Features
|
|
99
|
+
|
|
100
|
+
- Run tests in fresh Python subprocesses to prevent state pollution
|
|
101
|
+
- Group related tests to run together in the same subprocess
|
|
102
|
+
- Handles crashes, timeouts, and setup/teardown failures
|
|
103
|
+
- Respects pytest's standard output capture settings (`-s`, `--capture`)
|
|
104
|
+
- Works with pytest reporters (JUnit XML, etc.)
|
|
105
|
+
- Configurable timeouts to prevent hanging subprocesses
|
|
106
|
+
- Cross-platform: Linux, macOS, Windows
|
|
107
|
+
|
|
108
|
+
## Basic Usage
|
|
109
|
+
|
|
110
|
+
Mark tests to run in isolated subprocesses:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import pytest
|
|
114
|
+
|
|
115
|
+
@pytest.mark.isolated
|
|
116
|
+
def test_isolated():
|
|
117
|
+
# Runs in a fresh subprocess
|
|
118
|
+
assert True
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Tests with the same group run together in one subprocess:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# Using keyword argument
|
|
125
|
+
@pytest.mark.isolated(group="mygroup")
|
|
126
|
+
def test_one():
|
|
127
|
+
shared_state.append(1)
|
|
128
|
+
|
|
129
|
+
@pytest.mark.isolated(group="mygroup")
|
|
130
|
+
def test_two():
|
|
131
|
+
# Sees state from test_one
|
|
132
|
+
assert len(shared_state) == 2
|
|
133
|
+
|
|
134
|
+
# Or using positional argument
|
|
135
|
+
@pytest.mark.isolated("mygroup")
|
|
136
|
+
def test_three():
|
|
137
|
+
shared_state.append(3)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Set timeout per test group:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
@pytest.mark.isolated(timeout=30)
|
|
144
|
+
def test_with_timeout():
|
|
145
|
+
# This group gets 30 second timeout (overrides global setting)
|
|
146
|
+
expensive_operation()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Note:** Tests without an explicit `group` parameter each run in their own unique subprocess for maximum isolation.
|
|
150
|
+
|
|
151
|
+
### Class and Module Markers
|
|
152
|
+
|
|
153
|
+
Apply to entire classes to share state between methods:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
@pytest.mark.isolated
|
|
157
|
+
class TestDatabase:
|
|
158
|
+
def test_setup(self):
|
|
159
|
+
self.db = create_database()
|
|
160
|
+
|
|
161
|
+
def test_query(self):
|
|
162
|
+
# Shares state with test_setup
|
|
163
|
+
result = self.db.query("SELECT 1")
|
|
164
|
+
assert result
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Apply to entire modules using `pytestmark`:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import pytest
|
|
171
|
+
|
|
172
|
+
pytestmark = pytest.mark.isolated
|
|
173
|
+
|
|
174
|
+
def test_one():
|
|
175
|
+
# Runs in isolated subprocess
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def test_two():
|
|
179
|
+
# Shares subprocess with test_one
|
|
180
|
+
pass
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Marker Precedence
|
|
184
|
+
|
|
185
|
+
When `@pytest.mark.isolated` appears at multiple scopes, the **closest marker wins** (following pytest's standard `get_closest_marker` convention):
|
|
186
|
+
|
|
187
|
+
1. **Explicit `group` always wins.** `@pytest.mark.isolated(group="name")` uses that group regardless of scope.
|
|
188
|
+
1. **Function > class > module.** A function-level `@pytest.mark.isolated` breaks out of any class or module group into its own subprocess.
|
|
189
|
+
1. **Class > module.** A class marker groups its methods together, even inside a module with `pytestmark`.
|
|
190
|
+
|
|
191
|
+
## Configuration
|
|
192
|
+
|
|
193
|
+
### Command Line
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Run all tests in isolation (even without @pytest.mark.isolated)
|
|
197
|
+
pytest --isolated
|
|
198
|
+
|
|
199
|
+
# Set isolated test timeout (seconds)
|
|
200
|
+
pytest --isolated-timeout=60
|
|
201
|
+
|
|
202
|
+
# Disable subprocess isolation for debugging
|
|
203
|
+
pytest --no-isolation
|
|
204
|
+
|
|
205
|
+
# Control output capture (standard pytest flags work with isolated tests)
|
|
206
|
+
pytest -s # Disable capture, show all output
|
|
207
|
+
pytest --capture=sys # Capture at sys.stdout/stderr level
|
|
208
|
+
|
|
209
|
+
# Combine with pytest debugger
|
|
210
|
+
pytest --no-isolation --pdb
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### pytest.ini / pyproject.toml
|
|
214
|
+
|
|
215
|
+
```ini
|
|
216
|
+
[pytest]
|
|
217
|
+
isolated_timeout = 300
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Or in `pyproject.toml`:
|
|
221
|
+
|
|
222
|
+
```toml
|
|
223
|
+
[tool.pytest.ini_options]
|
|
224
|
+
isolated_timeout = "300"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Real-World Use Cases
|
|
228
|
+
|
|
229
|
+
### 🌍 Isolating Global State
|
|
230
|
+
|
|
231
|
+
Prevent environment variable and configuration changes from leaking between tests:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
@pytest.mark.isolated
|
|
235
|
+
def test_modifies_environ():
|
|
236
|
+
import os
|
|
237
|
+
os.environ["MY_VAR"] = "value"
|
|
238
|
+
# Won't affect other tests
|
|
239
|
+
|
|
240
|
+
@pytest.mark.isolated
|
|
241
|
+
def test_clean_environ():
|
|
242
|
+
import os
|
|
243
|
+
assert "MY_VAR" not in os.environ # Fresh environment
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### 🔄 Testing Singletons
|
|
247
|
+
|
|
248
|
+
Group tests that share singleton state, or isolate them completely:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
@pytest.mark.isolated(group="singleton_tests")
|
|
252
|
+
def test_singleton_init():
|
|
253
|
+
from myapp import DatabaseConnection
|
|
254
|
+
db = DatabaseConnection.get_instance()
|
|
255
|
+
assert db is not None
|
|
256
|
+
|
|
257
|
+
@pytest.mark.isolated(group="singleton_tests")
|
|
258
|
+
def test_singleton_reuse():
|
|
259
|
+
db = DatabaseConnection.get_instance()
|
|
260
|
+
# Same instance as previous test in group
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### ⚙️ Testing Process Resources
|
|
264
|
+
|
|
265
|
+
Safely modify signal handlers and other process-level settings:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
@pytest.mark.isolated
|
|
269
|
+
def test_signal_handlers():
|
|
270
|
+
import signal
|
|
271
|
+
signal.signal(signal.SIGTERM, custom_handler)
|
|
272
|
+
# Won't interfere with pytest or other tests
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 💾 Testing Database Migrations
|
|
276
|
+
|
|
277
|
+
Group expensive operations while maintaining isolation from other tests:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
@pytest.mark.isolated(group="database")
|
|
281
|
+
class TestDatabase:
|
|
282
|
+
def test_migration(self):
|
|
283
|
+
db.migrate() # Expensive operation, runs once
|
|
284
|
+
|
|
285
|
+
def test_query(self):
|
|
286
|
+
# Reuses same DB from test_migration
|
|
287
|
+
result = db.query("SELECT 1")
|
|
288
|
+
assert result == [(1,)]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 🔧 Testing C Extensions
|
|
292
|
+
|
|
293
|
+
Isolate tests that might crash from C code:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
@pytest.mark.isolated
|
|
297
|
+
def test_numpy_operation():
|
|
298
|
+
import numpy as np
|
|
299
|
+
# If this segfaults, other tests still run
|
|
300
|
+
result = np.array([1, 2, 3])
|
|
301
|
+
assert len(result) == 3
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Output and Reporting
|
|
305
|
+
|
|
306
|
+
Failed tests automatically capture and display stdout/stderr:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
@pytest.mark.isolated
|
|
310
|
+
def test_failing():
|
|
311
|
+
print("Debug info")
|
|
312
|
+
assert False
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Works with standard pytest reporters:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
pytest --junitxml=report.xml --durations=10
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Limitations
|
|
322
|
+
|
|
323
|
+
**Fixtures**: Module/session fixtures run in each subprocess group. Cannot share fixture objects between parent and subprocess.
|
|
324
|
+
|
|
325
|
+
**Debugging**: Use `--no-isolation` to run all tests in the main process for easier debugging with `pdb` or IDE debuggers.
|
|
326
|
+
|
|
327
|
+
**Performance**: Subprocess creation adds ~100-500ms per group. Group related tests to minimize overhead. Only mark tests that need isolation.
|
|
328
|
+
|
|
329
|
+
### CLI Option Compatibility
|
|
330
|
+
|
|
331
|
+
pytest-isolated uses a forward-by-default (blacklist) model:
|
|
332
|
+
|
|
333
|
+
- Some options are blocked with a clear `UsageError` when isolation is active.
|
|
334
|
+
- Some options are supported but handled in the parent process (not forwarded).
|
|
335
|
+
- All remaining options (including custom plugin options) are forwarded to children.
|
|
336
|
+
|
|
337
|
+
See [Incompatible options reference](docs/incompatible-options.md) for the full list and workarounds.
|
|
338
|
+
|
|
339
|
+
## Advanced
|
|
340
|
+
|
|
341
|
+
### Coverage Integration
|
|
342
|
+
|
|
343
|
+
To collect coverage from isolated tests, enable subprocess tracking in `pyproject.toml`:
|
|
344
|
+
|
|
345
|
+
```toml
|
|
346
|
+
[tool.coverage.run]
|
|
347
|
+
parallel = true
|
|
348
|
+
concurrency = ["subprocess"]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
See the [coverage.py subprocess documentation](https://coverage.readthedocs.io/en/latest/subprocess.html) for details.
|
|
352
|
+
|
|
353
|
+
### Timeout Handling
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
pytest --isolated-timeout=30
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Timeout errors are clearly reported with the group name and timeout duration.
|
|
360
|
+
|
|
361
|
+
### Crash Detection
|
|
362
|
+
|
|
363
|
+
If a subprocess crashes, tests in that group are marked as failed with exit code information.
|
|
364
|
+
|
|
365
|
+
### Subprocess Detection
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
import os
|
|
369
|
+
|
|
370
|
+
if os.environ.get("PYTEST_RUNNING_IN_SUBPROCESS") == "1":
|
|
371
|
+
# Running in subprocess
|
|
372
|
+
pass
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Troubleshooting
|
|
376
|
+
|
|
377
|
+
**Tests timing out**: Increase timeout with `--isolated-timeout=600`
|
|
378
|
+
|
|
379
|
+
**Missing output**: Use `-s` or `--capture=no` to see output from passing tests, or `-v` for verbose output. pytest-isolated respects pytest's standard capture settings.
|
|
380
|
+
|
|
381
|
+
**Subprocess crashes**: Check for segfaults, OOM, or signal issues. Run with `-v` for details.
|
|
382
|
+
|
|
383
|
+
## Contributing
|
|
384
|
+
|
|
385
|
+
1. Install pre-commit: `pip install pre-commit && pre-commit install`
|
|
386
|
+
1. Run tests: `pytest tests/ -v`
|
|
387
|
+
1. Open an issue before submitting PRs for new features
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
MIT License - see LICENSE file for details.
|