pytest-isolated 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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/__init__.py +1 -1
- pytest_isolated/plugin.py +43 -23
- pytest_isolated/py.typed +0 -0
- {pytest_isolated-0.1.0.dist-info → pytest_isolated-0.2.0.dist-info}/METADATA +41 -36
- pytest_isolated-0.2.0.dist-info/RECORD +9 -0
- pytest_isolated-0.1.0.dist-info/RECORD +0 -8
- {pytest_isolated-0.1.0.dist-info → pytest_isolated-0.2.0.dist-info}/WHEEL +0 -0
- {pytest_isolated-0.1.0.dist-info → pytest_isolated-0.2.0.dist-info}/entry_points.txt +0 -0
- {pytest_isolated-0.1.0.dist-info → pytest_isolated-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_isolated-0.1.0.dist-info → pytest_isolated-0.2.0.dist-info}/top_level.txt +0 -0
pytest_isolated/__init__.py
CHANGED
pytest_isolated/plugin.py
CHANGED
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
8
|
import tempfile
|
|
9
|
+
import time
|
|
9
10
|
from collections import OrderedDict
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any
|
|
@@ -21,27 +22,27 @@ SUBPROC_REPORT_PATH = "PYTEST_SUBPROCESS_REPORT_PATH"
|
|
|
21
22
|
|
|
22
23
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
23
24
|
"""Add configuration options for subprocess isolation."""
|
|
24
|
-
group = parser.getgroup("
|
|
25
|
+
group = parser.getgroup("isolated")
|
|
25
26
|
group.addoption(
|
|
26
|
-
"--
|
|
27
|
+
"--isolated-timeout",
|
|
27
28
|
type=int,
|
|
28
29
|
default=None,
|
|
29
|
-
help="Timeout in seconds for
|
|
30
|
+
help="Timeout in seconds for isolated test groups (default: 300)",
|
|
30
31
|
)
|
|
31
32
|
group.addoption(
|
|
32
|
-
"--no-
|
|
33
|
+
"--no-isolation",
|
|
33
34
|
action="store_true",
|
|
34
35
|
default=False,
|
|
35
36
|
help="Disable subprocess isolation (for debugging)",
|
|
36
37
|
)
|
|
37
38
|
parser.addini(
|
|
38
|
-
"
|
|
39
|
+
"isolated_timeout",
|
|
39
40
|
type="string",
|
|
40
41
|
default="300",
|
|
41
|
-
help="Default timeout in seconds for
|
|
42
|
+
help="Default timeout in seconds for isolated test groups",
|
|
42
43
|
)
|
|
43
44
|
parser.addini(
|
|
44
|
-
"
|
|
45
|
+
"isolated_capture_passed",
|
|
45
46
|
type="bool",
|
|
46
47
|
default=False,
|
|
47
48
|
help="Capture output for passed tests (default: False)",
|
|
@@ -51,8 +52,9 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
51
52
|
def pytest_configure(config: pytest.Config) -> None:
|
|
52
53
|
config.addinivalue_line(
|
|
53
54
|
"markers",
|
|
54
|
-
"
|
|
55
|
-
"tests with the same group run together in
|
|
55
|
+
"isolated(group=None, timeout=None): run this test in a grouped "
|
|
56
|
+
"fresh Python subprocess; tests with the same group run together in "
|
|
57
|
+
"one subprocess. timeout (seconds) overrides global --isolated-timeout.",
|
|
56
58
|
)
|
|
57
59
|
|
|
58
60
|
|
|
@@ -100,17 +102,18 @@ def pytest_collection_modifyitems(
|
|
|
100
102
|
if os.environ.get(SUBPROC_ENV) == "1":
|
|
101
103
|
return # child should not do grouping
|
|
102
104
|
|
|
103
|
-
# If --no-
|
|
104
|
-
if config.getoption("
|
|
105
|
+
# If --no-isolation is set, treat all tests as normal (no subprocess isolation)
|
|
106
|
+
if config.getoption("no_isolation", False):
|
|
105
107
|
config._subprocess_groups = OrderedDict() # type: ignore[attr-defined]
|
|
106
108
|
config._subprocess_normal_items = items # type: ignore[attr-defined]
|
|
107
109
|
return
|
|
108
110
|
|
|
109
111
|
groups: OrderedDict[str, list[pytest.Item]] = OrderedDict()
|
|
112
|
+
group_timeouts: dict[str, int | None] = {} # Track timeout per group
|
|
110
113
|
normal: list[pytest.Item] = []
|
|
111
114
|
|
|
112
115
|
for item in items:
|
|
113
|
-
m = item.get_closest_marker("
|
|
116
|
+
m = item.get_closest_marker("isolated")
|
|
114
117
|
if not m:
|
|
115
118
|
normal.append(item)
|
|
116
119
|
continue
|
|
@@ -119,9 +122,16 @@ def pytest_collection_modifyitems(
|
|
|
119
122
|
# Default grouping to module path (so you don't accidentally group everything)
|
|
120
123
|
if group is None:
|
|
121
124
|
group = item.nodeid.split("::")[0]
|
|
122
|
-
|
|
125
|
+
|
|
126
|
+
# Store group-specific timeout (first marker wins)
|
|
127
|
+
group_key = str(group)
|
|
128
|
+
if group_key not in group_timeouts:
|
|
129
|
+
group_timeouts[group_key] = m.kwargs.get("timeout")
|
|
130
|
+
|
|
131
|
+
groups.setdefault(group_key, []).append(item)
|
|
123
132
|
|
|
124
133
|
config._subprocess_groups = groups # type: ignore[attr-defined]
|
|
134
|
+
config._subprocess_group_timeouts = group_timeouts # type: ignore[attr-defined]
|
|
125
135
|
config._subprocess_normal_items = normal # type: ignore[attr-defined]
|
|
126
136
|
|
|
127
137
|
|
|
@@ -143,17 +153,20 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
|
|
|
143
153
|
groups: OrderedDict[str, list[pytest.Item]] = getattr(
|
|
144
154
|
config, "_subprocess_groups", OrderedDict()
|
|
145
155
|
)
|
|
156
|
+
group_timeouts: dict[str, int | None] = getattr(
|
|
157
|
+
config, "_subprocess_group_timeouts", {}
|
|
158
|
+
)
|
|
146
159
|
normal_items: list[pytest.Item] = getattr(
|
|
147
160
|
config, "_subprocess_normal_items", session.items
|
|
148
161
|
)
|
|
149
162
|
|
|
150
|
-
# Get timeout configuration
|
|
151
|
-
timeout_opt = config.getoption("
|
|
152
|
-
timeout_ini = config.getini("
|
|
153
|
-
|
|
163
|
+
# Get default timeout configuration
|
|
164
|
+
timeout_opt = config.getoption("isolated_timeout", None)
|
|
165
|
+
timeout_ini = config.getini("isolated_timeout")
|
|
166
|
+
default_timeout = timeout_opt or (int(timeout_ini) if timeout_ini else 300)
|
|
154
167
|
|
|
155
168
|
# Get capture configuration
|
|
156
|
-
capture_passed = config.getini("
|
|
169
|
+
capture_passed = config.getini("isolated_capture_passed")
|
|
157
170
|
|
|
158
171
|
def emit_report(
|
|
159
172
|
item: pytest.Item,
|
|
@@ -205,6 +218,9 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
|
|
|
205
218
|
for group_name, group_items in groups.items():
|
|
206
219
|
nodeids = [it.nodeid for it in group_items]
|
|
207
220
|
|
|
221
|
+
# Get timeout for this group (marker timeout > global timeout)
|
|
222
|
+
group_timeout = group_timeouts.get(group_name) or default_timeout
|
|
223
|
+
|
|
208
224
|
# file where the child will append JSONL records
|
|
209
225
|
with tempfile.NamedTemporaryFile(
|
|
210
226
|
prefix="pytest-subproc-", suffix=".jsonl", delete=False
|
|
@@ -215,12 +231,13 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
|
|
|
215
231
|
env[SUBPROC_ENV] = "1"
|
|
216
232
|
env[SUBPROC_REPORT_PATH] = report_path
|
|
217
233
|
|
|
218
|
-
# Run pytest in subprocess with timeout
|
|
234
|
+
# Run pytest in subprocess with timeout, tracking execution time
|
|
219
235
|
cmd = [sys.executable, "-m", "pytest", *nodeids]
|
|
236
|
+
start_time = time.time()
|
|
220
237
|
|
|
221
238
|
try:
|
|
222
239
|
proc = subprocess.run(
|
|
223
|
-
cmd, env=env, timeout=
|
|
240
|
+
cmd, env=env, timeout=group_timeout, capture_output=False, check=False
|
|
224
241
|
)
|
|
225
242
|
returncode = proc.returncode
|
|
226
243
|
timed_out = False
|
|
@@ -228,6 +245,8 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
|
|
|
228
245
|
returncode = -1
|
|
229
246
|
timed_out = True
|
|
230
247
|
|
|
248
|
+
execution_time = time.time() - start_time
|
|
249
|
+
|
|
231
250
|
# Gather results from JSONL file
|
|
232
251
|
results: dict[str, dict[str, Any]] = {}
|
|
233
252
|
report_file = Path(report_path)
|
|
@@ -250,9 +269,10 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
|
|
|
250
269
|
# Handle timeout or crash
|
|
251
270
|
if timed_out:
|
|
252
271
|
msg = (
|
|
253
|
-
f"Subprocess group={group_name!r} timed out after {
|
|
254
|
-
f"seconds
|
|
255
|
-
f"
|
|
272
|
+
f"Subprocess group={group_name!r} timed out after {group_timeout} "
|
|
273
|
+
f"seconds (execution time: {execution_time:.2f}s). "
|
|
274
|
+
f"Increase timeout with --isolated-timeout, isolated_timeout ini, "
|
|
275
|
+
f"or @pytest.mark.isolated(timeout=N)."
|
|
256
276
|
)
|
|
257
277
|
for it in group_items:
|
|
258
278
|
emit_report(it, "call", "failed", longrepr=msg)
|
pytest_isolated/py.typed
ADDED
|
File without changes
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-isolated
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Run marked pytest tests in grouped subprocesses (cross-platform).
|
|
5
5
|
Author: pytest-isolated contributors
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
8
8
|
Classifier: Framework :: Pytest
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
10
|
Classifier: Operating System :: OS Independent
|
|
12
11
|
Classifier: Programming Language :: Python :: 3
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -27,6 +26,9 @@ Dynamic: license-file
|
|
|
27
26
|
|
|
28
27
|
# pytest-isolated
|
|
29
28
|
|
|
29
|
+
[](https://github.com/dyollb/pytest-isolated/actions/workflows/test.yml)
|
|
30
|
+
[](https://pypi.org/project/pytest-isolated/)
|
|
31
|
+
|
|
30
32
|
A pytest plugin that runs marked tests in isolated subprocesses with intelligent grouping.
|
|
31
33
|
|
|
32
34
|
## Features
|
|
@@ -52,7 +54,7 @@ Mark tests to run in isolated subprocesses:
|
|
|
52
54
|
```python
|
|
53
55
|
import pytest
|
|
54
56
|
|
|
55
|
-
@pytest.mark.
|
|
57
|
+
@pytest.mark.isolated
|
|
56
58
|
def test_isolated():
|
|
57
59
|
# Runs in a fresh subprocess
|
|
58
60
|
assert True
|
|
@@ -61,16 +63,25 @@ def test_isolated():
|
|
|
61
63
|
Tests with the same group run together in one subprocess:
|
|
62
64
|
|
|
63
65
|
```python
|
|
64
|
-
@pytest.mark.
|
|
66
|
+
@pytest.mark.isolated(group="mygroup")
|
|
65
67
|
def test_one():
|
|
66
68
|
shared_state.append(1)
|
|
67
69
|
|
|
68
|
-
@pytest.mark.
|
|
70
|
+
@pytest.mark.isolated(group="mygroup")
|
|
69
71
|
def test_two():
|
|
70
72
|
# Sees state from test_one
|
|
71
73
|
assert len(shared_state) == 2
|
|
72
74
|
```
|
|
73
75
|
|
|
76
|
+
Set timeout per test group:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
@pytest.mark.isolated(timeout=30)
|
|
80
|
+
def test_with_timeout():
|
|
81
|
+
# This group gets 30 second timeout (overrides global setting)
|
|
82
|
+
expensive_operation()
|
|
83
|
+
```
|
|
84
|
+
|
|
74
85
|
Tests without an explicit group are automatically grouped by module.
|
|
75
86
|
|
|
76
87
|
## Configuration
|
|
@@ -78,30 +89,30 @@ Tests without an explicit group are automatically grouped by module.
|
|
|
78
89
|
### Command Line
|
|
79
90
|
|
|
80
91
|
```bash
|
|
81
|
-
# Set
|
|
82
|
-
pytest --
|
|
92
|
+
# Set isolated test timeout (seconds)
|
|
93
|
+
pytest --isolated-timeout=60
|
|
83
94
|
|
|
84
95
|
# Disable subprocess isolation for debugging
|
|
85
|
-
pytest --no-
|
|
96
|
+
pytest --no-isolation
|
|
86
97
|
|
|
87
98
|
# Combine with pytest debugger
|
|
88
|
-
pytest --no-
|
|
99
|
+
pytest --no-isolation --pdb
|
|
89
100
|
```
|
|
90
101
|
|
|
91
102
|
### pytest.ini / pyproject.toml
|
|
92
103
|
|
|
93
104
|
```ini
|
|
94
105
|
[pytest]
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
isolated_timeout = 300
|
|
107
|
+
isolated_capture_passed = false
|
|
97
108
|
```
|
|
98
109
|
|
|
99
110
|
Or in `pyproject.toml`:
|
|
100
111
|
|
|
101
112
|
```toml
|
|
102
113
|
[tool.pytest.ini_options]
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
isolated_timeout = "300"
|
|
115
|
+
isolated_capture_passed = false
|
|
105
116
|
```
|
|
106
117
|
|
|
107
118
|
## Use Cases
|
|
@@ -109,13 +120,13 @@ subprocess_capture_passed = false
|
|
|
109
120
|
### Testing Global State
|
|
110
121
|
|
|
111
122
|
```python
|
|
112
|
-
@pytest.mark.
|
|
123
|
+
@pytest.mark.isolated
|
|
113
124
|
def test_modifies_environ():
|
|
114
125
|
import os
|
|
115
126
|
os.environ["MY_VAR"] = "value"
|
|
116
127
|
# Won't affect other tests
|
|
117
128
|
|
|
118
|
-
@pytest.mark.
|
|
129
|
+
@pytest.mark.isolated
|
|
119
130
|
def test_clean_environ():
|
|
120
131
|
import os
|
|
121
132
|
assert "MY_VAR" not in os.environ
|
|
@@ -124,13 +135,13 @@ def test_clean_environ():
|
|
|
124
135
|
### Testing Singletons
|
|
125
136
|
|
|
126
137
|
```python
|
|
127
|
-
@pytest.mark.
|
|
138
|
+
@pytest.mark.isolated(group="singleton_tests")
|
|
128
139
|
def test_singleton_init():
|
|
129
140
|
from myapp import DatabaseConnection
|
|
130
141
|
db = DatabaseConnection.get_instance()
|
|
131
142
|
assert db is not None
|
|
132
143
|
|
|
133
|
-
@pytest.mark.
|
|
144
|
+
@pytest.mark.isolated(group="singleton_tests")
|
|
134
145
|
def test_singleton_reuse():
|
|
135
146
|
db = DatabaseConnection.get_instance()
|
|
136
147
|
# Same instance as previous test in group
|
|
@@ -139,7 +150,7 @@ def test_singleton_reuse():
|
|
|
139
150
|
### Testing Process Resources
|
|
140
151
|
|
|
141
152
|
```python
|
|
142
|
-
@pytest.mark.
|
|
153
|
+
@pytest.mark.isolated
|
|
143
154
|
def test_signal_handlers():
|
|
144
155
|
import signal
|
|
145
156
|
signal.signal(signal.SIGTERM, custom_handler)
|
|
@@ -151,7 +162,7 @@ def test_signal_handlers():
|
|
|
151
162
|
Failed tests automatically capture and display stdout/stderr:
|
|
152
163
|
|
|
153
164
|
```python
|
|
154
|
-
@pytest.mark.
|
|
165
|
+
@pytest.mark.isolated
|
|
155
166
|
def test_failing():
|
|
156
167
|
print("Debug info")
|
|
157
168
|
assert False
|
|
@@ -167,7 +178,7 @@ pytest --junitxml=report.xml --durations=10
|
|
|
167
178
|
|
|
168
179
|
**Fixtures**: Module/session fixtures run in each subprocess group. Cannot share fixture objects between parent and subprocess.
|
|
169
180
|
|
|
170
|
-
**Debugging**: Use `--no-
|
|
181
|
+
**Debugging**: Use `--no-isolation` to run all tests in the main process for easier debugging with `pdb` or IDE debuggers.
|
|
171
182
|
|
|
172
183
|
**Performance**: Subprocess creation adds ~100-500ms per group. Group related tests to minimize overhead.
|
|
173
184
|
|
|
@@ -176,7 +187,7 @@ pytest --junitxml=report.xml --durations=10
|
|
|
176
187
|
### Timeout Handling
|
|
177
188
|
|
|
178
189
|
```bash
|
|
179
|
-
pytest --
|
|
190
|
+
pytest --isolated-timeout=30
|
|
180
191
|
```
|
|
181
192
|
|
|
182
193
|
Timeout errors are clearly reported with the group name and timeout duration.
|
|
@@ -197,24 +208,18 @@ if os.environ.get("PYTEST_RUNNING_IN_SUBPROCESS") == "1":
|
|
|
197
208
|
|
|
198
209
|
## Troubleshooting
|
|
199
210
|
|
|
200
|
-
**Tests timing out**: Increase timeout with `--
|
|
211
|
+
**Tests timing out**: Increase timeout with `--isolated-timeout=600`
|
|
201
212
|
|
|
202
|
-
**Missing output**: Enable capture for passed tests with `
|
|
213
|
+
**Missing output**: Enable capture for passed tests with `isolated_capture_passed = true`
|
|
203
214
|
|
|
204
215
|
**Subprocess crashes**: Check for segfaults, OOM, or signal issues. Run with `-v` for details.
|
|
205
216
|
|
|
206
|
-
##
|
|
207
|
-
|
|
208
|
-
MIT License - see LICENSE file for details.
|
|
217
|
+
## Contributing
|
|
209
218
|
|
|
210
|
-
|
|
219
|
+
1. Install pre-commit: `pip install pre-commit && pre-commit install`
|
|
220
|
+
1. Run tests: `pytest tests/ -v`
|
|
221
|
+
1. Open an issue before submitting PRs for new features
|
|
211
222
|
|
|
212
|
-
|
|
223
|
+
## License
|
|
213
224
|
|
|
214
|
-
-
|
|
215
|
-
- Process isolation with subprocess marker
|
|
216
|
-
- Smart grouping by module or explicit group names
|
|
217
|
-
- Timeout support
|
|
218
|
-
- Complete test phase capture (setup/call/teardown)
|
|
219
|
-
- JUnit XML and standard reporter integration
|
|
220
|
-
- Comprehensive error handling and reporting
|
|
225
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pytest_isolated/__init__.py,sha256=PN_IEdfxCUz6vL1vf0Ka9CGmCq9ppFk33fVGirSVtMc,89
|
|
2
|
+
pytest_isolated/plugin.py,sha256=0AKRTWmdLaiwZdwRicRd3TMLrIeisGBMlFqCDOnJRX0,12206
|
|
3
|
+
pytest_isolated/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pytest_isolated-0.2.0.dist-info/licenses/LICENSE,sha256=WECJyowi685PZSnKcA4Tqs7jukfzbnk7iMPLnm_q4JI,1067
|
|
5
|
+
pytest_isolated-0.2.0.dist-info/METADATA,sha256=q_E02kvtbnt1QgQc-ATrghLcJka7Y_2Zx50tmatVGCM,5384
|
|
6
|
+
pytest_isolated-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
pytest_isolated-0.2.0.dist-info/entry_points.txt,sha256=HgRNPjIGoPBF1pkhma4UtaSwhpOVB8oZRZ0L1FcZXgk,45
|
|
8
|
+
pytest_isolated-0.2.0.dist-info/top_level.txt,sha256=FAtpozhvI-YaiFoZMepi9JAm6e87mW-TM1Ovu5xLOxg,16
|
|
9
|
+
pytest_isolated-0.2.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pytest_isolated/__init__.py,sha256=iqPlqv1sT1c10XcWOeYbJyRyIzeQk0Sa-OhOS14IMfg,89
|
|
2
|
-
pytest_isolated/plugin.py,sha256=lLNjY5Jm5SmllySvVPRCF87fpQTnRx_RMtloKeM1m9U,11296
|
|
3
|
-
pytest_isolated-0.1.0.dist-info/licenses/LICENSE,sha256=WECJyowi685PZSnKcA4Tqs7jukfzbnk7iMPLnm_q4JI,1067
|
|
4
|
-
pytest_isolated-0.1.0.dist-info/METADATA,sha256=qfVoG6VGZCb-Dj5Wl12sGQH7WO-U2Gj4KcGV2H_FXzU,5131
|
|
5
|
-
pytest_isolated-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
pytest_isolated-0.1.0.dist-info/entry_points.txt,sha256=HgRNPjIGoPBF1pkhma4UtaSwhpOVB8oZRZ0L1FcZXgk,45
|
|
7
|
-
pytest_isolated-0.1.0.dist-info/top_level.txt,sha256=FAtpozhvI-YaiFoZMepi9JAm6e87mW-TM1Ovu5xLOxg,16
|
|
8
|
-
pytest_isolated-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|