pytest-isolate 0.0.2__tar.gz → 0.0.4__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_isolate-0.0.2 → pytest_isolate-0.0.4}/.github/workflows/python-package.yml +2 -2
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/PKG-INFO +18 -8
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/README.md +14 -4
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/pyproject.toml +18 -17
- pytest_isolate-0.0.4/src/pytest_isolate/__about__.py +1 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/src/pytest_isolate/plugin.py +235 -56
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/tests/test_isolate.py +20 -4
- pytest_isolate-0.0.4/tox.ini +25 -0
- pytest_isolate-0.0.2/src/pytest_isolate/__about__.py +0 -1
- pytest_isolate-0.0.2/tox.ini +0 -25
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/.github/workflows/python-publish.yml +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/.gitignore +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/LICENSE.txt +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/src/pytest_isolate/__init__.py +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.4}/tests/conftest.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-isolate
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Run pytest tests in isolated subprocesses
|
|
4
5
|
Project-URL: Documentation, https://github.com/gilfree/pytest-isolate#readme
|
|
5
6
|
Project-URL: Issues, https://github.com/gilfree/pytest-isolate/issues
|
|
6
7
|
Project-URL: Source, https://github.com/gilfree/pytest-isolate
|
|
@@ -9,11 +10,10 @@ License-Expression: MIT
|
|
|
9
10
|
License-File: LICENSE.txt
|
|
10
11
|
Classifier: Development Status :: 4 - Beta
|
|
11
12
|
Classifier: Programming Language :: Python
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
19
|
Requires-Python: >=3.7
|
|
@@ -39,8 +39,10 @@ This pytest plugin was generated with Cookiecutter along with `@hackebrot`'s `co
|
|
|
39
39
|
* Captures stdout & stderr of crashing processes
|
|
40
40
|
* Add Timeout to a forked test
|
|
41
41
|
* Limit memory used by test
|
|
42
|
+
* Limit CPU time used by test
|
|
42
43
|
* Plays nice with pytest-xdist
|
|
43
44
|
* Shows warnings, even with xdist!
|
|
45
|
+
* *Create visual timeline of test execution (isolated or not)*
|
|
44
46
|
|
|
45
47
|
## Requirements
|
|
46
48
|
|
|
@@ -60,9 +62,16 @@ To run every test in its own **forked** subprocess.
|
|
|
60
62
|
|
|
61
63
|
Or:
|
|
62
64
|
|
|
63
|
-
pytest --isolate-timeout 10 --isolate-mem-limit 1000000
|
|
65
|
+
pytest --isolate-timeout 10 --isolate-mem-limit 1000000 --isolate-cpu-limit 10
|
|
64
66
|
|
|
65
|
-
To set a timeout to every test in addition to forking.
|
|
67
|
+
To set a timeout to every test in addition to forking, and limit to 10 cpu seconds.
|
|
68
|
+
|
|
69
|
+
Or:
|
|
70
|
+
|
|
71
|
+
pytest --timeline
|
|
72
|
+
|
|
73
|
+
With possible combination of the above, to generate a timeline of test execution. The
|
|
74
|
+
timeline can be viewed in chrome://tracing.
|
|
66
75
|
|
|
67
76
|
> Note:
|
|
68
77
|
>
|
|
@@ -75,10 +84,10 @@ they will take precedence. Uninstall them to use `pytest-isolate`.
|
|
|
75
84
|
|
|
76
85
|
Unlike `pytest-timeout`, timeout in `pytest-isolate` is implemented by forking the test to a separate subprocess, and setting timeout for that subprocess.
|
|
77
86
|
|
|
78
|
-
You can also use a mark to isolate or time limit the memory test:
|
|
87
|
+
You can also use a mark to isolate or time limit the memory and/or cpu usage test:
|
|
79
88
|
|
|
80
89
|
```python
|
|
81
|
-
@pytest.mark.isolate(timeout=10,mem_limit=10**6)
|
|
90
|
+
@pytest.mark.isolate(timeout=10, mem_limit=10**6, cpu_limit=10)
|
|
82
91
|
def test_something():
|
|
83
92
|
pass
|
|
84
93
|
```
|
|
@@ -89,6 +98,7 @@ The options can also be set in an pytest configuration file, e.g:
|
|
|
89
98
|
[tool.pytest.ini_options]
|
|
90
99
|
isolate_timeout=10
|
|
91
100
|
isolate_mem_limit=1000000
|
|
101
|
+
isolate_cpu_limit=10
|
|
92
102
|
```
|
|
93
103
|
|
|
94
104
|
## Contributing
|
|
@@ -15,8 +15,10 @@ This pytest plugin was generated with Cookiecutter along with `@hackebrot`'s `co
|
|
|
15
15
|
* Captures stdout & stderr of crashing processes
|
|
16
16
|
* Add Timeout to a forked test
|
|
17
17
|
* Limit memory used by test
|
|
18
|
+
* Limit CPU time used by test
|
|
18
19
|
* Plays nice with pytest-xdist
|
|
19
20
|
* Shows warnings, even with xdist!
|
|
21
|
+
* *Create visual timeline of test execution (isolated or not)*
|
|
20
22
|
|
|
21
23
|
## Requirements
|
|
22
24
|
|
|
@@ -36,9 +38,16 @@ To run every test in its own **forked** subprocess.
|
|
|
36
38
|
|
|
37
39
|
Or:
|
|
38
40
|
|
|
39
|
-
pytest --isolate-timeout 10 --isolate-mem-limit 1000000
|
|
41
|
+
pytest --isolate-timeout 10 --isolate-mem-limit 1000000 --isolate-cpu-limit 10
|
|
40
42
|
|
|
41
|
-
To set a timeout to every test in addition to forking.
|
|
43
|
+
To set a timeout to every test in addition to forking, and limit to 10 cpu seconds.
|
|
44
|
+
|
|
45
|
+
Or:
|
|
46
|
+
|
|
47
|
+
pytest --timeline
|
|
48
|
+
|
|
49
|
+
With possible combination of the above, to generate a timeline of test execution. The
|
|
50
|
+
timeline can be viewed in chrome://tracing.
|
|
42
51
|
|
|
43
52
|
> Note:
|
|
44
53
|
>
|
|
@@ -51,10 +60,10 @@ they will take precedence. Uninstall them to use `pytest-isolate`.
|
|
|
51
60
|
|
|
52
61
|
Unlike `pytest-timeout`, timeout in `pytest-isolate` is implemented by forking the test to a separate subprocess, and setting timeout for that subprocess.
|
|
53
62
|
|
|
54
|
-
You can also use a mark to isolate or time limit the memory test:
|
|
63
|
+
You can also use a mark to isolate or time limit the memory and/or cpu usage test:
|
|
55
64
|
|
|
56
65
|
```python
|
|
57
|
-
@pytest.mark.isolate(timeout=10,mem_limit=10**6)
|
|
66
|
+
@pytest.mark.isolate(timeout=10, mem_limit=10**6, cpu_limit=10)
|
|
58
67
|
def test_something():
|
|
59
68
|
pass
|
|
60
69
|
```
|
|
@@ -65,6 +74,7 @@ The options can also be set in an pytest configuration file, e.g:
|
|
|
65
74
|
[tool.pytest.ini_options]
|
|
66
75
|
isolate_timeout=10
|
|
67
76
|
isolate_mem_limit=1000000
|
|
77
|
+
isolate_cpu_limit=10
|
|
68
78
|
```
|
|
69
79
|
|
|
70
80
|
## Contributing
|
|
@@ -4,26 +4,23 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-isolate"
|
|
7
|
-
description = ''
|
|
7
|
+
description = 'Run pytest tests in isolated subprocesses '
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.7"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
keywords = []
|
|
12
|
-
authors = [
|
|
13
|
-
{ name = "gilfree", email = "gilfree@users.noreply.github.com" },
|
|
14
|
-
]
|
|
12
|
+
authors = [{ name = "gilfree", email = "gilfree@users.noreply.github.com" }]
|
|
15
13
|
classifiers = [
|
|
16
14
|
"Development Status :: 4 - Beta",
|
|
17
15
|
"Programming Language :: Python",
|
|
18
|
-
"Programming Language :: Python :: 3.7",
|
|
19
|
-
"Programming Language :: Python :: 3.8",
|
|
20
16
|
"Programming Language :: Python :: 3.9",
|
|
21
17
|
"Programming Language :: Python :: 3.10",
|
|
22
18
|
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
23
20
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
21
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
25
22
|
]
|
|
26
|
-
dependencies = ["dill","tblib","pytest"]
|
|
23
|
+
dependencies = ["dill", "tblib", "pytest"]
|
|
27
24
|
dynamic = ["version"]
|
|
28
25
|
|
|
29
26
|
[project.urls]
|
|
@@ -38,22 +35,26 @@ pytest_isolate = "pytest_isolate.plugin"
|
|
|
38
35
|
path = "src/pytest_isolate/__about__.py"
|
|
39
36
|
|
|
40
37
|
[tool.hatch.envs.default]
|
|
41
|
-
dependencies = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
]
|
|
38
|
+
dependencies = ["pytest", "pytest-cov", "numpy", "pytest-xdist"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
isolate_timeout = 2
|
|
45
43
|
|
|
46
44
|
[tool.ruff]
|
|
45
|
+
fix = true
|
|
46
|
+
|
|
47
|
+
# Assume Python 3.11.
|
|
48
|
+
target-version = "py311"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
47
51
|
# Enable Pyflakes `E` and `F` codes by default.
|
|
48
|
-
select = ["E", "F","I"]
|
|
52
|
+
select = ["E", "F", "I"]
|
|
49
53
|
ignore = []
|
|
50
|
-
fix=true
|
|
51
54
|
# Allow unused variables when underscore-prefixed.
|
|
52
55
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
53
56
|
|
|
54
|
-
# Assume Python 3.11.
|
|
55
|
-
target-version = "py311"
|
|
56
57
|
|
|
57
|
-
[tool.ruff.mccabe]
|
|
58
|
+
[tool.ruff.lint.mccabe]
|
|
58
59
|
# Unlike Flake8, default to a complexity level of 10.
|
|
59
|
-
max-complexity = 10
|
|
60
|
+
max-complexity = 10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.4"
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# This file is was originally based on pytest forked:
|
|
2
2
|
# see: https://github.com/pytest-dev/pytest-forked/blob/master/src/pytest_forked/__init__.py
|
|
3
|
-
# Since pytest forked is un unmaintained
|
|
4
|
-
#
|
|
3
|
+
# Since pytest forked is un unmaintained I decided to make my
|
|
4
|
+
# changes in a different project
|
|
5
5
|
# Please see README.md
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
import fcntl
|
|
9
9
|
import io
|
|
10
|
+
import json
|
|
10
11
|
import multiprocessing as mp
|
|
11
12
|
import os
|
|
12
13
|
import resource
|
|
13
14
|
import sys
|
|
14
15
|
import warnings
|
|
15
16
|
from contextlib import contextmanager
|
|
16
|
-
from datetime import datetime
|
|
17
17
|
from queue import Empty
|
|
18
18
|
from typing import Any, List, Optional, Tuple
|
|
19
19
|
|
|
@@ -28,6 +28,11 @@ try:
|
|
|
28
28
|
except ImportError:
|
|
29
29
|
pass
|
|
30
30
|
|
|
31
|
+
try:
|
|
32
|
+
import setproctitle # noqa F401
|
|
33
|
+
except ImportError:
|
|
34
|
+
pass
|
|
35
|
+
|
|
31
36
|
try:
|
|
32
37
|
from tblib import pickling_support
|
|
33
38
|
|
|
@@ -37,26 +42,33 @@ except ImportError:
|
|
|
37
42
|
|
|
38
43
|
|
|
39
44
|
def forked_subprocess(
|
|
40
|
-
target, args=(), timeout=None, memlimt=None
|
|
45
|
+
target, args=(), timeout=None, memlimt=None, cpulimit=None
|
|
41
46
|
) -> Tuple[Any, int, bool]:
|
|
42
47
|
sub = ForkedSubprocess()
|
|
43
|
-
exitcode, timed_out, result = sub.run_in_subprocess(
|
|
48
|
+
exitcode, timed_out, result = sub.run_in_subprocess(
|
|
49
|
+
timeout, memlimt, cpulimit, target, args
|
|
50
|
+
)
|
|
44
51
|
return result, exitcode, timed_out
|
|
45
52
|
|
|
46
53
|
|
|
47
54
|
@contextmanager
|
|
48
|
-
def limits(max_mem):
|
|
49
|
-
if not max_mem:
|
|
55
|
+
def limits(max_mem, max_cpu):
|
|
56
|
+
if not max_mem and not max_cpu:
|
|
50
57
|
yield
|
|
51
58
|
return
|
|
52
|
-
(
|
|
53
|
-
resource.
|
|
54
|
-
|
|
59
|
+
(soft_mem, hard_mem) = resource.getrlimit(resource.RLIMIT_DATA)
|
|
60
|
+
(soft_cpu, hard_cpu) = resource.getrlimit(resource.RLIMIT_CPU)
|
|
61
|
+
if max_mem:
|
|
62
|
+
resource.setrlimit(resource.RLIMIT_DATA, (max_mem, hard_mem))
|
|
63
|
+
if max_cpu:
|
|
64
|
+
resource.setrlimit(resource.RLIMIT_CPU, (max_cpu, hard_cpu))
|
|
55
65
|
try:
|
|
56
66
|
yield
|
|
57
67
|
finally:
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
if max_mem:
|
|
69
|
+
resource.setrlimit(resource.RLIMIT_DATA, (soft_mem, hard_mem))
|
|
70
|
+
if max_cpu:
|
|
71
|
+
resource.setrlimit(resource.RLIMIT_CPU, (soft_cpu, hard_cpu))
|
|
60
72
|
return
|
|
61
73
|
|
|
62
74
|
|
|
@@ -91,7 +103,7 @@ class ForkedSubprocess:
|
|
|
91
103
|
os.close(self.w_out)
|
|
92
104
|
os.close(self.w_err)
|
|
93
105
|
|
|
94
|
-
def run_in_subprocess(self, timeout, memlimit, target, args=()):
|
|
106
|
+
def run_in_subprocess(self, timeout, memlimit, cpulimit, target, args=()):
|
|
95
107
|
ctx = mp.get_context("fork")
|
|
96
108
|
q = ctx.Queue()
|
|
97
109
|
timed_out = None
|
|
@@ -99,9 +111,8 @@ class ForkedSubprocess:
|
|
|
99
111
|
def run_subprocess():
|
|
100
112
|
# Close the read fd, redirect output to write fd
|
|
101
113
|
self.child_redirect_streams()
|
|
102
|
-
|
|
103
114
|
try:
|
|
104
|
-
with limits(memlimit):
|
|
115
|
+
with limits(memlimit, cpulimit):
|
|
105
116
|
q.put(dill.dumps(target(*args)))
|
|
106
117
|
except BaseException as e:
|
|
107
118
|
q.put(dill.dumps(e))
|
|
@@ -183,12 +194,38 @@ def pytest_addoption(parser):
|
|
|
183
194
|
default=None,
|
|
184
195
|
help="Limit the memory usage of the test ",
|
|
185
196
|
)
|
|
197
|
+
group.addoption(
|
|
198
|
+
"--isolate-cpu-limit",
|
|
199
|
+
dest="isolate_cpu_limit",
|
|
200
|
+
default=None,
|
|
201
|
+
help="Limit the CPU usage of the test ",
|
|
202
|
+
)
|
|
186
203
|
parser.addini(
|
|
187
204
|
"isolate_timeout", "Default timeout for isolated tests", type="string"
|
|
188
205
|
)
|
|
189
206
|
parser.addini(
|
|
190
207
|
"isolate_mem_limit", "Default memory limit for isolated tests", type="string"
|
|
191
208
|
)
|
|
209
|
+
parser.addini(
|
|
210
|
+
"isolate_cpu_limit", "Default cpu limit for isolated tests", type="string"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
parser.addoption(
|
|
214
|
+
"--timeline",
|
|
215
|
+
dest="timeline",
|
|
216
|
+
action="store_true",
|
|
217
|
+
default=False,
|
|
218
|
+
help="Report test timelines",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
parser.addoption(
|
|
222
|
+
"--timeline-file",
|
|
223
|
+
dest="timeline_file",
|
|
224
|
+
default="./timeline.json",
|
|
225
|
+
help="Path to the timeline file",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
parser.addini("timeline_file", "timeline file", type="string")
|
|
192
229
|
|
|
193
230
|
|
|
194
231
|
@pytest.hookimpl(trylast=True)
|
|
@@ -208,8 +245,8 @@ def pytest_configure(config):
|
|
|
208
245
|
warnings.warn(
|
|
209
246
|
"Isolate is a replacement for pytest-timeout. "
|
|
210
247
|
"Pytest-timeout will take precedence while installed, "
|
|
211
|
-
"tests with timeout will not be isolated.
|
|
212
|
-
"the plugins"
|
|
248
|
+
"tests with timeout will not be isolated. "
|
|
249
|
+
"Please uninstall one of the plugins"
|
|
213
250
|
)
|
|
214
251
|
else:
|
|
215
252
|
config.addinivalue_line(
|
|
@@ -221,7 +258,6 @@ def pytest_configure(config):
|
|
|
221
258
|
# Taken from pytest 7.2:
|
|
222
259
|
@contextmanager
|
|
223
260
|
def catch_warnings(item: pytest.Item):
|
|
224
|
-
|
|
225
261
|
"""Context manager that catches warnings generated in the contained execution block.
|
|
226
262
|
|
|
227
263
|
``item`` can be None if we are not in the context of an item execution.
|
|
@@ -254,14 +290,25 @@ def catch_warnings(item: pytest.Item):
|
|
|
254
290
|
|
|
255
291
|
|
|
256
292
|
def run_subprocess(item: pytest.Item):
|
|
257
|
-
|
|
258
293
|
item.config.pluginmanager.unregister(name="capturemanager")
|
|
294
|
+
try:
|
|
295
|
+
if not os.getenv("PYTEST_ISOLATE_NO_SETPROCTITLE"):
|
|
296
|
+
import setproctitle # noqa F811
|
|
259
297
|
|
|
298
|
+
setproctitle.setproctitle(f"pytest {item.nodeid}")
|
|
299
|
+
except ImportError:
|
|
300
|
+
pass
|
|
260
301
|
try:
|
|
261
302
|
with catch_warnings(item) as warnings:
|
|
262
303
|
reports = runtestprotocol(item, log=False)
|
|
263
304
|
s_reports = []
|
|
264
305
|
for report in reports:
|
|
306
|
+
usage = resource.getrusage(resource.RUSAGE_SELF)
|
|
307
|
+
cpu_usage = usage.ru_utime + usage.ru_stime
|
|
308
|
+
usage = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
309
|
+
cpu_usage += usage.ru_utime + usage.ru_stime
|
|
310
|
+
report.user_properties.append(("cpu_usage", cpu_usage))
|
|
311
|
+
report.cpu_usage = cpu_usage
|
|
265
312
|
s_reports.append(
|
|
266
313
|
item.config.hook.pytest_report_to_serializable(
|
|
267
314
|
config=item.config, report=report
|
|
@@ -273,28 +320,29 @@ def run_subprocess(item: pytest.Item):
|
|
|
273
320
|
|
|
274
321
|
|
|
275
322
|
def run_in_subprocess(
|
|
276
|
-
item: pytest.Item,
|
|
323
|
+
item: pytest.Item,
|
|
324
|
+
timeout: Optional[float],
|
|
325
|
+
mem_limit: Optional[int],
|
|
326
|
+
cpu_limit: Optional[int],
|
|
277
327
|
) -> List[pytest.TestReport]:
|
|
278
|
-
|
|
279
|
-
cap
|
|
280
|
-
"capturemanager"
|
|
281
|
-
)
|
|
328
|
+
cap = item.config.pluginmanager.getplugin("capturemanager")
|
|
329
|
+
assert isinstance(cap, _pytest.capture.CaptureManager)
|
|
282
330
|
cap.resume_global_capture()
|
|
283
331
|
cap.activate_fixture()
|
|
284
|
-
current_time = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
285
|
-
print(f"{current_time}: Test lunched {item.nodeid} in subprocess ", file=sys.stderr)
|
|
286
332
|
result, exitcode, timed_out = forked_subprocess(
|
|
287
|
-
run_subprocess, (item,), timeout, mem_limit
|
|
333
|
+
run_subprocess, (item,), timeout, mem_limit, cpu_limit
|
|
288
334
|
)
|
|
289
335
|
cap.deactivate_fixture()
|
|
290
336
|
cap.suspend_global_capture(in_=False)
|
|
291
337
|
out, err = cap.read_global_capture()
|
|
292
338
|
|
|
339
|
+
warnings_captured = None
|
|
340
|
+
|
|
293
341
|
# The result might be an exception, that comes
|
|
294
342
|
# form current process
|
|
295
343
|
if result and not isinstance(result, BaseException):
|
|
296
344
|
# The result came from child process
|
|
297
|
-
result,
|
|
345
|
+
result, warnings_captured = result
|
|
298
346
|
|
|
299
347
|
if result and not isinstance(result, BaseException):
|
|
300
348
|
results: List[pytest.TestReport] = []
|
|
@@ -308,9 +356,9 @@ def run_in_subprocess(
|
|
|
308
356
|
results[-1].sections.append(("Captured stderr", err))
|
|
309
357
|
results[-1].sections.append(("Captured stdout", out))
|
|
310
358
|
|
|
311
|
-
if
|
|
359
|
+
if warnings_captured:
|
|
312
360
|
try:
|
|
313
|
-
for warning_message in
|
|
361
|
+
for warning_message in warnings_captured:
|
|
314
362
|
item.ihook.pytest_warning_recorded.call_historic(
|
|
315
363
|
kwargs=dict(
|
|
316
364
|
warning_message=warning_message,
|
|
@@ -343,10 +391,12 @@ def report_process_crash(
|
|
|
343
391
|
|
|
344
392
|
if timed_out is not None:
|
|
345
393
|
info += f"Timeout > {timed_out}"
|
|
394
|
+
elif exitcode == -24:
|
|
395
|
+
info += "SIGXCPU: CPU time limit exceeded"
|
|
346
396
|
elif exitcode != 0:
|
|
347
|
-
info += f"
|
|
397
|
+
info += f"Crashed with exit code {exitcode}"
|
|
348
398
|
else:
|
|
349
|
-
info += "Exited with no result,
|
|
399
|
+
info += "Exited with no result, memory limit exceeded (probably)"
|
|
350
400
|
|
|
351
401
|
call = runner.CallInfo.from_call(lambda: 0 / 0, "???")
|
|
352
402
|
call.excinfo = info
|
|
@@ -421,13 +471,17 @@ def pytest_runtest_protocol(item):
|
|
|
421
471
|
return
|
|
422
472
|
if item.config.pluginmanager.get_plugin("timeout"):
|
|
423
473
|
return
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
474
|
+
isolate, timeout, mem_limit, cpu_limit = get_isolation_options(item)
|
|
475
|
+
|
|
476
|
+
if (
|
|
477
|
+
isolate is not None
|
|
478
|
+
or mem_limit is not None
|
|
479
|
+
or timeout is not None
|
|
480
|
+
or cpu_limit is not None
|
|
481
|
+
):
|
|
428
482
|
ihook = item.ihook
|
|
429
483
|
ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
|
|
430
|
-
reports = run_in_subprocess(item, timeout, mem_limit)
|
|
484
|
+
reports = run_in_subprocess(item, timeout, mem_limit, cpu_limit)
|
|
431
485
|
|
|
432
486
|
for rep in reports:
|
|
433
487
|
ihook.pytest_runtest_logreport(report=rep)
|
|
@@ -436,22 +490,8 @@ def pytest_runtest_protocol(item):
|
|
|
436
490
|
return True
|
|
437
491
|
|
|
438
492
|
|
|
439
|
-
def
|
|
440
|
-
# Should we
|
|
441
|
-
forked = get_global_value(item, "forked")
|
|
442
|
-
forked_marker = get_marker(item, "forked")
|
|
443
|
-
isolate = get_global_value(item, "isolate")
|
|
444
|
-
isolate_marker = get_marker(item, "isolate")
|
|
445
|
-
if isolate_marker is not None:
|
|
446
|
-
isolate = isolate_marker
|
|
447
|
-
elif forked_marker is not None:
|
|
448
|
-
isolate = forked_marker
|
|
449
|
-
elif isolate is not None:
|
|
450
|
-
pass
|
|
451
|
-
else:
|
|
452
|
-
isolate = forked
|
|
453
|
-
|
|
454
|
-
# Should we timeout
|
|
493
|
+
def get_timeout(item):
|
|
494
|
+
# Should we timeout?
|
|
455
495
|
timeout = get_global_value(item, "timeout")
|
|
456
496
|
timeout_marker = get_marker(item, "timeout", "timeout", 0)
|
|
457
497
|
|
|
@@ -465,10 +505,15 @@ def get_isolation_options(item):
|
|
|
465
505
|
elif isolate_timeout is not None:
|
|
466
506
|
timeout = isolate_timeout
|
|
467
507
|
|
|
468
|
-
|
|
508
|
+
try:
|
|
469
509
|
timeout = float(timeout)
|
|
510
|
+
except (ValueError, TypeError):
|
|
511
|
+
timeout = None
|
|
512
|
+
return timeout
|
|
513
|
+
|
|
470
514
|
|
|
471
|
-
|
|
515
|
+
def get_memory_limit(item):
|
|
516
|
+
# Should we limit memory?
|
|
472
517
|
mem_limit = None
|
|
473
518
|
isolate_mem_limit_marker = get_marker(item, "isolate", "mem_limit", 1)
|
|
474
519
|
isolate_mem_limit = get_global_value(item, "isolate_mem_limit")
|
|
@@ -477,4 +522,138 @@ def get_isolation_options(item):
|
|
|
477
522
|
mem_limit = isolate_mem_limit_marker
|
|
478
523
|
else:
|
|
479
524
|
mem_limit = isolate_mem_limit
|
|
480
|
-
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
mem_limit = int(mem_limit)
|
|
528
|
+
if mem_limit < 1 or round(mem_limit) != mem_limit:
|
|
529
|
+
raise ValueError("Memory limit must be in whole bytes")
|
|
530
|
+
mem_limit = int(mem_limit)
|
|
531
|
+
except (ValueError, TypeError):
|
|
532
|
+
mem_limit = None
|
|
533
|
+
return mem_limit
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def get_cpu_limit(item):
|
|
537
|
+
# Should we limit cpu?
|
|
538
|
+
cpu_limit = None
|
|
539
|
+
isolate_cpu_limit_marker = get_marker(item, "isolate", "cpu_limit", 2)
|
|
540
|
+
isolate_cpu_limit = get_global_value(item, "isolate_cpu_limit")
|
|
541
|
+
|
|
542
|
+
if isolate_cpu_limit_marker is not None:
|
|
543
|
+
cpu_limit = isolate_cpu_limit_marker
|
|
544
|
+
else:
|
|
545
|
+
cpu_limit = isolate_cpu_limit
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
cpu_limit = float(cpu_limit)
|
|
549
|
+
if cpu_limit < 1 or round(cpu_limit) != cpu_limit:
|
|
550
|
+
raise ValueError("Cpu limit must be in whole seconds")
|
|
551
|
+
cpu_limit = int(cpu_limit)
|
|
552
|
+
except (ValueError, TypeError):
|
|
553
|
+
cpu_limit = None
|
|
554
|
+
return cpu_limit
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def get_isolation_options(item):
|
|
558
|
+
# Should we fork?
|
|
559
|
+
forked = get_global_value(item, "forked")
|
|
560
|
+
forked_marker = get_marker(item, "forked")
|
|
561
|
+
isolate = get_global_value(item, "isolate")
|
|
562
|
+
isolate_marker = get_marker(item, "isolate")
|
|
563
|
+
if isolate_marker is not None:
|
|
564
|
+
isolate = isolate_marker
|
|
565
|
+
elif forked_marker is not None:
|
|
566
|
+
isolate = forked_marker
|
|
567
|
+
elif isolate is not None:
|
|
568
|
+
pass
|
|
569
|
+
else:
|
|
570
|
+
isolate = forked
|
|
571
|
+
timeout = get_timeout(item)
|
|
572
|
+
mem_limit = get_memory_limit(item)
|
|
573
|
+
cpu_limit = get_cpu_limit(item)
|
|
574
|
+
|
|
575
|
+
return isolate, timeout, mem_limit, cpu_limit
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@pytest.hookimpl(tryfirst=True)
|
|
579
|
+
def pytest_terminal_summary(terminalreporter, exitstatus, config) -> None:
|
|
580
|
+
durations = terminalreporter.config.option.durations
|
|
581
|
+
durations_min = terminalreporter.config.option.durations_min
|
|
582
|
+
verbose = terminalreporter.config.getvalue("verbose")
|
|
583
|
+
tr = terminalreporter
|
|
584
|
+
if durations is not None:
|
|
585
|
+
dlist = []
|
|
586
|
+
for replist in tr.stats.values():
|
|
587
|
+
for rep in replist:
|
|
588
|
+
if hasattr(rep, "cpu_usage"):
|
|
589
|
+
dlist.append(rep)
|
|
590
|
+
if not dlist:
|
|
591
|
+
return
|
|
592
|
+
dlist.sort(key=lambda x: x.cpu_usage, reverse=True) # type: ignore[no-any-return]
|
|
593
|
+
if not durations:
|
|
594
|
+
tr.write_sep("=", "highest cpu_usage")
|
|
595
|
+
else:
|
|
596
|
+
tr.write_sep("=", "highest %s cpu_usage" % durations)
|
|
597
|
+
dlist = dlist[:durations]
|
|
598
|
+
|
|
599
|
+
for i, rep in enumerate(dlist):
|
|
600
|
+
if verbose < 2 and rep.cpu_usage < durations_min:
|
|
601
|
+
tr.write_line("")
|
|
602
|
+
tr.write_line(
|
|
603
|
+
"(%s cpu_usage < %gs hidden. Use -vv to show these durations.)"
|
|
604
|
+
% (len(dlist) - i, durations_min)
|
|
605
|
+
)
|
|
606
|
+
break
|
|
607
|
+
if rep.when == "teardown":
|
|
608
|
+
tr.write_line(f"{rep.cpu_usage:02.2f}s {rep.nodeid}")
|
|
609
|
+
|
|
610
|
+
if config.getoption("timeline"):
|
|
611
|
+
events = []
|
|
612
|
+
for replist in tr.stats.values():
|
|
613
|
+
rep: pytest.TestReport
|
|
614
|
+
|
|
615
|
+
for rep in replist:
|
|
616
|
+
if not isinstance(rep, pytest.TestReport) or rep.when != "call":
|
|
617
|
+
continue
|
|
618
|
+
props = dict(getattr(rep, "user_properties", ()))
|
|
619
|
+
events.extend(
|
|
620
|
+
create_event(
|
|
621
|
+
props.get("worker_id", "master"),
|
|
622
|
+
rep.nodeid,
|
|
623
|
+
rep.outcome,
|
|
624
|
+
rep.start,
|
|
625
|
+
rep.stop,
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
filename = config.getoption("timeline_file")
|
|
629
|
+
os.makedirs(os.path.dirname(os.path.abspath(filename)), exist_ok=True)
|
|
630
|
+
json.dump(events, open(filename, "w"))
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@pytest.hookimpl(tryfirst=False, hookwrapper=True)
|
|
634
|
+
def pytest_runtest_call(item: pytest.Item):
|
|
635
|
+
yield
|
|
636
|
+
item.user_properties.append(
|
|
637
|
+
("worker_id", os.getenv("PYTEST_XDIST_WORKER", "master"))
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def create_event(worker_name, test_name, category, start_time, end_time, **kwargs):
|
|
642
|
+
start_event = {
|
|
643
|
+
"name": test_name.rsplit("/")[-1],
|
|
644
|
+
"cat": category,
|
|
645
|
+
"ph": "B",
|
|
646
|
+
"pid": worker_name,
|
|
647
|
+
"tid": 0,
|
|
648
|
+
"ts": start_time * (1000**2),
|
|
649
|
+
"args": kwargs,
|
|
650
|
+
}
|
|
651
|
+
end_event = {
|
|
652
|
+
"name": test_name.rsplit("/")[-1],
|
|
653
|
+
"cat": "pipeline",
|
|
654
|
+
"ph": "E",
|
|
655
|
+
"pid": worker_name,
|
|
656
|
+
"tid": 0,
|
|
657
|
+
"ts": end_time * (1000**2),
|
|
658
|
+
}
|
|
659
|
+
return start_event, end_event
|
|
@@ -43,7 +43,7 @@ def test_bare_ok():
|
|
|
43
43
|
print(f"isolated in {os.getpid()}")
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
@pytest.mark.xfail
|
|
46
|
+
@pytest.mark.xfail(strict=True)
|
|
47
47
|
def test_bare_fail():
|
|
48
48
|
print("Hello")
|
|
49
49
|
print("Hello")
|
|
@@ -54,17 +54,33 @@ def test_bare_fail():
|
|
|
54
54
|
|
|
55
55
|
@pytest.mark.isolate(mem_limit=10**9)
|
|
56
56
|
def test_rss_limit_ok():
|
|
57
|
-
a = bytearray(10**
|
|
57
|
+
a = bytearray(10**5)
|
|
58
58
|
print(a[random.randint(0, len(a))])
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
@pytest.mark.xfail
|
|
62
|
-
@pytest.mark.isolate(mem_limit=10**
|
|
62
|
+
@pytest.mark.isolate(mem_limit=10**5)
|
|
63
63
|
def test_rss_limit_fail():
|
|
64
|
-
a = bytearray(10**
|
|
64
|
+
a = bytearray(10**6)
|
|
65
65
|
print(a[random.randint(0, len(a))])
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
@pytest.mark.isolate()
|
|
69
69
|
def test_warn():
|
|
70
70
|
warnings.warn("Boo")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.xfail
|
|
74
|
+
@pytest.mark.isolate(cpu_limit=1)
|
|
75
|
+
def test_isolate_cpu():
|
|
76
|
+
import numpy as np
|
|
77
|
+
|
|
78
|
+
# hog CPU:
|
|
79
|
+
x = 0
|
|
80
|
+
for i in range(100):
|
|
81
|
+
x += (np.random.rand(1000, 1000) @ np.random.rand(1000, 1000)).sum()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_slow():
|
|
85
|
+
sleep(0.1)
|
|
86
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# For more information about tox, see https://tox.readthedocs.io/en/latest/
|
|
2
|
+
[tox]
|
|
3
|
+
requires =
|
|
4
|
+
tox >=4
|
|
5
|
+
envlist = py{39,312}
|
|
6
|
+
isolated_build = True
|
|
7
|
+
|
|
8
|
+
[testenv]
|
|
9
|
+
deps =
|
|
10
|
+
pytest>6.0
|
|
11
|
+
numpy
|
|
12
|
+
pytest-xdist
|
|
13
|
+
pytest-sugar
|
|
14
|
+
|
|
15
|
+
commands = pytest --isolate --import-mode importlib {posargs:tests} --timeline
|
|
16
|
+
|
|
17
|
+
[testenv:lint]
|
|
18
|
+
skip_install = true
|
|
19
|
+
deps = ruff
|
|
20
|
+
commands = ruff check {postargs:.}
|
|
21
|
+
|
|
22
|
+
[gh]
|
|
23
|
+
python =
|
|
24
|
+
3.9 = py39
|
|
25
|
+
3.12 = py312
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.0.2'
|
pytest_isolate-0.0.2/tox.ini
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# For more information about tox, see https://tox.readthedocs.io/en/latest/
|
|
2
|
-
[tox]
|
|
3
|
-
envlist = py{37,311}
|
|
4
|
-
isolated_build = True
|
|
5
|
-
|
|
6
|
-
[tox:.package]
|
|
7
|
-
# note tox will use the same python version as under what tox is installed to package
|
|
8
|
-
# so unless this is python 3 you can require a given python version for the packaging
|
|
9
|
-
# environment via the basepython key
|
|
10
|
-
basepython = python3
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
[testenv]
|
|
14
|
-
deps = pytest>6.0
|
|
15
|
-
commands = pytest --isolate --import-mode importlib {posargs:tests}
|
|
16
|
-
|
|
17
|
-
[testenv:ruff]
|
|
18
|
-
skip_install = true
|
|
19
|
-
deps = ruff
|
|
20
|
-
commands = ruff .
|
|
21
|
-
|
|
22
|
-
[gh]
|
|
23
|
-
python =
|
|
24
|
-
3.7 = py37
|
|
25
|
-
3.11 = py311, ruff
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|