pytest-isolate 0.0.2__tar.gz → 0.0.3__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.3}/PKG-INFO +9 -8
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/README.md +5 -4
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/pyproject.toml +17 -15
- pytest_isolate-0.0.3/src/pytest_isolate/__about__.py +1 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/src/pytest_isolate/plugin.py +167 -56
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/tests/test_isolate.py +14 -4
- pytest_isolate-0.0.3/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.3}/.github/workflows/python-package.yml +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/.github/workflows/python-publish.yml +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/.gitignore +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/LICENSE.txt +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/src/pytest_isolate/__init__.py +0 -0
- {pytest_isolate-0.0.2 → pytest_isolate-0.0.3}/tests/conftest.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: pytest-isolate
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
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
|
|
7
8
|
Author-email: gilfree <gilfree@users.noreply.github.com>
|
|
8
|
-
License
|
|
9
|
-
License-File: LICENSE.txt
|
|
9
|
+
License: MIT
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.7
|
|
@@ -60,9 +60,9 @@ To run every test in its own **forked** subprocess.
|
|
|
60
60
|
|
|
61
61
|
Or:
|
|
62
62
|
|
|
63
|
-
pytest --isolate-timeout 10 --isolate-mem-limit 1000000
|
|
63
|
+
pytest --isolate-timeout 10 --isolate-mem-limit 1000000 --isolate-cpu-limit 10
|
|
64
64
|
|
|
65
|
-
To set a timeout to every test in addition to forking.
|
|
65
|
+
To set a timeout to every test in addition to forking, and limit to 10 cpu seconds.
|
|
66
66
|
|
|
67
67
|
> Note:
|
|
68
68
|
>
|
|
@@ -75,10 +75,10 @@ they will take precedence. Uninstall them to use `pytest-isolate`.
|
|
|
75
75
|
|
|
76
76
|
Unlike `pytest-timeout`, timeout in `pytest-isolate` is implemented by forking the test to a separate subprocess, and setting timeout for that subprocess.
|
|
77
77
|
|
|
78
|
-
You can also use a mark to isolate or time limit the memory test:
|
|
78
|
+
You can also use a mark to isolate or time limit the memory and/or cpu usage test:
|
|
79
79
|
|
|
80
80
|
```python
|
|
81
|
-
@pytest.mark.isolate(timeout=10,mem_limit=10**6)
|
|
81
|
+
@pytest.mark.isolate(timeout=10, mem_limit=10**6, cpu_limit=10)
|
|
82
82
|
def test_something():
|
|
83
83
|
pass
|
|
84
84
|
```
|
|
@@ -89,6 +89,7 @@ The options can also be set in an pytest configuration file, e.g:
|
|
|
89
89
|
[tool.pytest.ini_options]
|
|
90
90
|
isolate_timeout=10
|
|
91
91
|
isolate_mem_limit=1000000
|
|
92
|
+
isolate_cpu_limit=10
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
## Contributing
|
|
@@ -36,9 +36,9 @@ To run every test in its own **forked** subprocess.
|
|
|
36
36
|
|
|
37
37
|
Or:
|
|
38
38
|
|
|
39
|
-
pytest --isolate-timeout 10 --isolate-mem-limit 1000000
|
|
39
|
+
pytest --isolate-timeout 10 --isolate-mem-limit 1000000 --isolate-cpu-limit 10
|
|
40
40
|
|
|
41
|
-
To set a timeout to every test in addition to forking.
|
|
41
|
+
To set a timeout to every test in addition to forking, and limit to 10 cpu seconds.
|
|
42
42
|
|
|
43
43
|
> Note:
|
|
44
44
|
>
|
|
@@ -51,10 +51,10 @@ they will take precedence. Uninstall them to use `pytest-isolate`.
|
|
|
51
51
|
|
|
52
52
|
Unlike `pytest-timeout`, timeout in `pytest-isolate` is implemented by forking the test to a separate subprocess, and setting timeout for that subprocess.
|
|
53
53
|
|
|
54
|
-
You can also use a mark to isolate or time limit the memory test:
|
|
54
|
+
You can also use a mark to isolate or time limit the memory and/or cpu usage test:
|
|
55
55
|
|
|
56
56
|
```python
|
|
57
|
-
@pytest.mark.isolate(timeout=10,mem_limit=10**6)
|
|
57
|
+
@pytest.mark.isolate(timeout=10, mem_limit=10**6, cpu_limit=10)
|
|
58
58
|
def test_something():
|
|
59
59
|
pass
|
|
60
60
|
```
|
|
@@ -65,6 +65,7 @@ The options can also be set in an pytest configuration file, e.g:
|
|
|
65
65
|
[tool.pytest.ini_options]
|
|
66
66
|
isolate_timeout=10
|
|
67
67
|
isolate_mem_limit=1000000
|
|
68
|
+
isolate_cpu_limit=10
|
|
68
69
|
```
|
|
69
70
|
|
|
70
71
|
## Contributing
|
|
@@ -4,14 +4,12 @@ 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",
|
|
@@ -23,7 +21,7 @@ classifiers = [
|
|
|
23
21
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
22
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
25
23
|
]
|
|
26
|
-
dependencies = ["dill","tblib","pytest"]
|
|
24
|
+
dependencies = ["dill", "tblib", "pytest"]
|
|
27
25
|
dynamic = ["version"]
|
|
28
26
|
|
|
29
27
|
[project.urls]
|
|
@@ -38,22 +36,26 @@ pytest_isolate = "pytest_isolate.plugin"
|
|
|
38
36
|
path = "src/pytest_isolate/__about__.py"
|
|
39
37
|
|
|
40
38
|
[tool.hatch.envs.default]
|
|
41
|
-
dependencies = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
]
|
|
39
|
+
dependencies = ["pytest", "pytest-cov", "numpy", "pytest-xdist"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
isolate_timeout = 2
|
|
45
44
|
|
|
46
45
|
[tool.ruff]
|
|
46
|
+
fix = true
|
|
47
|
+
|
|
48
|
+
# Assume Python 3.11.
|
|
49
|
+
target-version = "py311"
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
47
52
|
# Enable Pyflakes `E` and `F` codes by default.
|
|
48
|
-
select = ["E", "F","I"]
|
|
53
|
+
select = ["E", "F", "I"]
|
|
49
54
|
ignore = []
|
|
50
|
-
fix=true
|
|
51
55
|
# Allow unused variables when underscore-prefixed.
|
|
52
56
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
53
57
|
|
|
54
|
-
# Assume Python 3.11.
|
|
55
|
-
target-version = "py311"
|
|
56
58
|
|
|
57
|
-
[tool.ruff.mccabe]
|
|
59
|
+
[tool.ruff.lint.mccabe]
|
|
58
60
|
# Unlike Flake8, default to a complexity level of 10.
|
|
59
|
-
max-complexity = 10
|
|
61
|
+
max-complexity = 10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.3'
|
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
|
@@ -13,7 +13,6 @@ import resource
|
|
|
13
13
|
import sys
|
|
14
14
|
import warnings
|
|
15
15
|
from contextlib import contextmanager
|
|
16
|
-
from datetime import datetime
|
|
17
16
|
from queue import Empty
|
|
18
17
|
from typing import Any, List, Optional, Tuple
|
|
19
18
|
|
|
@@ -28,6 +27,11 @@ try:
|
|
|
28
27
|
except ImportError:
|
|
29
28
|
pass
|
|
30
29
|
|
|
30
|
+
try:
|
|
31
|
+
import setproctitle # noqa F401
|
|
32
|
+
except ImportError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
31
35
|
try:
|
|
32
36
|
from tblib import pickling_support
|
|
33
37
|
|
|
@@ -37,26 +41,33 @@ except ImportError:
|
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
def forked_subprocess(
|
|
40
|
-
target, args=(), timeout=None, memlimt=None
|
|
44
|
+
target, args=(), timeout=None, memlimt=None, cpulimit=None
|
|
41
45
|
) -> Tuple[Any, int, bool]:
|
|
42
46
|
sub = ForkedSubprocess()
|
|
43
|
-
exitcode, timed_out, result = sub.run_in_subprocess(
|
|
47
|
+
exitcode, timed_out, result = sub.run_in_subprocess(
|
|
48
|
+
timeout, memlimt, cpulimit, target, args
|
|
49
|
+
)
|
|
44
50
|
return result, exitcode, timed_out
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
@contextmanager
|
|
48
|
-
def limits(max_mem):
|
|
49
|
-
if not max_mem:
|
|
54
|
+
def limits(max_mem, max_cpu):
|
|
55
|
+
if not max_mem and not max_cpu:
|
|
50
56
|
yield
|
|
51
57
|
return
|
|
52
|
-
(
|
|
53
|
-
resource.
|
|
54
|
-
|
|
58
|
+
(soft_mem, hard_mem) = resource.getrlimit(resource.RLIMIT_DATA)
|
|
59
|
+
(soft_cpu, hard_cpu) = resource.getrlimit(resource.RLIMIT_CPU)
|
|
60
|
+
if max_mem:
|
|
61
|
+
resource.setrlimit(resource.RLIMIT_DATA, (max_mem, hard_mem))
|
|
62
|
+
if max_cpu:
|
|
63
|
+
resource.setrlimit(resource.RLIMIT_CPU, (max_cpu, hard_cpu))
|
|
55
64
|
try:
|
|
56
65
|
yield
|
|
57
66
|
finally:
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
if max_mem:
|
|
68
|
+
resource.setrlimit(resource.RLIMIT_DATA, (soft_mem, hard_mem))
|
|
69
|
+
if max_cpu:
|
|
70
|
+
resource.setrlimit(resource.RLIMIT_CPU, (soft_cpu, hard_cpu))
|
|
60
71
|
return
|
|
61
72
|
|
|
62
73
|
|
|
@@ -91,7 +102,7 @@ class ForkedSubprocess:
|
|
|
91
102
|
os.close(self.w_out)
|
|
92
103
|
os.close(self.w_err)
|
|
93
104
|
|
|
94
|
-
def run_in_subprocess(self, timeout, memlimit, target, args=()):
|
|
105
|
+
def run_in_subprocess(self, timeout, memlimit, cpulimit, target, args=()):
|
|
95
106
|
ctx = mp.get_context("fork")
|
|
96
107
|
q = ctx.Queue()
|
|
97
108
|
timed_out = None
|
|
@@ -99,9 +110,8 @@ class ForkedSubprocess:
|
|
|
99
110
|
def run_subprocess():
|
|
100
111
|
# Close the read fd, redirect output to write fd
|
|
101
112
|
self.child_redirect_streams()
|
|
102
|
-
|
|
103
113
|
try:
|
|
104
|
-
with limits(memlimit):
|
|
114
|
+
with limits(memlimit, cpulimit):
|
|
105
115
|
q.put(dill.dumps(target(*args)))
|
|
106
116
|
except BaseException as e:
|
|
107
117
|
q.put(dill.dumps(e))
|
|
@@ -183,12 +193,21 @@ def pytest_addoption(parser):
|
|
|
183
193
|
default=None,
|
|
184
194
|
help="Limit the memory usage of the test ",
|
|
185
195
|
)
|
|
196
|
+
group.addoption(
|
|
197
|
+
"--isolate-cpu-limit",
|
|
198
|
+
dest="isolate_cpu_limit",
|
|
199
|
+
default=None,
|
|
200
|
+
help="Limit the CPU usage of the test ",
|
|
201
|
+
)
|
|
186
202
|
parser.addini(
|
|
187
203
|
"isolate_timeout", "Default timeout for isolated tests", type="string"
|
|
188
204
|
)
|
|
189
205
|
parser.addini(
|
|
190
206
|
"isolate_mem_limit", "Default memory limit for isolated tests", type="string"
|
|
191
207
|
)
|
|
208
|
+
parser.addini(
|
|
209
|
+
"isolate_cpu_limit", "Default cpu limit for isolated tests", type="string"
|
|
210
|
+
)
|
|
192
211
|
|
|
193
212
|
|
|
194
213
|
@pytest.hookimpl(trylast=True)
|
|
@@ -208,8 +227,8 @@ def pytest_configure(config):
|
|
|
208
227
|
warnings.warn(
|
|
209
228
|
"Isolate is a replacement for pytest-timeout. "
|
|
210
229
|
"Pytest-timeout will take precedence while installed, "
|
|
211
|
-
"tests with timeout will not be isolated.
|
|
212
|
-
"the plugins"
|
|
230
|
+
"tests with timeout will not be isolated. "
|
|
231
|
+
"Please uninstall one of the plugins"
|
|
213
232
|
)
|
|
214
233
|
else:
|
|
215
234
|
config.addinivalue_line(
|
|
@@ -221,7 +240,6 @@ def pytest_configure(config):
|
|
|
221
240
|
# Taken from pytest 7.2:
|
|
222
241
|
@contextmanager
|
|
223
242
|
def catch_warnings(item: pytest.Item):
|
|
224
|
-
|
|
225
243
|
"""Context manager that catches warnings generated in the contained execution block.
|
|
226
244
|
|
|
227
245
|
``item`` can be None if we are not in the context of an item execution.
|
|
@@ -254,14 +272,25 @@ def catch_warnings(item: pytest.Item):
|
|
|
254
272
|
|
|
255
273
|
|
|
256
274
|
def run_subprocess(item: pytest.Item):
|
|
257
|
-
|
|
258
275
|
item.config.pluginmanager.unregister(name="capturemanager")
|
|
276
|
+
try:
|
|
277
|
+
if not os.getenv("PYTEST_ISOLATE_NO_SETPROCTITLE"):
|
|
278
|
+
import setproctitle # noqa F811
|
|
259
279
|
|
|
280
|
+
setproctitle.setproctitle(f"pytest {item.nodeid}")
|
|
281
|
+
except ImportError:
|
|
282
|
+
pass
|
|
260
283
|
try:
|
|
261
284
|
with catch_warnings(item) as warnings:
|
|
262
285
|
reports = runtestprotocol(item, log=False)
|
|
263
286
|
s_reports = []
|
|
264
287
|
for report in reports:
|
|
288
|
+
usage = resource.getrusage(resource.RUSAGE_SELF)
|
|
289
|
+
cpu_usage = usage.ru_utime + usage.ru_stime
|
|
290
|
+
usage = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
291
|
+
cpu_usage += usage.ru_utime + usage.ru_stime
|
|
292
|
+
report.user_properties.append(("cpu_usage", cpu_usage))
|
|
293
|
+
report.cpu_usage = cpu_usage
|
|
265
294
|
s_reports.append(
|
|
266
295
|
item.config.hook.pytest_report_to_serializable(
|
|
267
296
|
config=item.config, report=report
|
|
@@ -273,28 +302,29 @@ def run_subprocess(item: pytest.Item):
|
|
|
273
302
|
|
|
274
303
|
|
|
275
304
|
def run_in_subprocess(
|
|
276
|
-
item: pytest.Item,
|
|
305
|
+
item: pytest.Item,
|
|
306
|
+
timeout: Optional[float],
|
|
307
|
+
mem_limit: Optional[int],
|
|
308
|
+
cpu_limit: Optional[int],
|
|
277
309
|
) -> List[pytest.TestReport]:
|
|
278
|
-
|
|
279
|
-
cap
|
|
280
|
-
"capturemanager"
|
|
281
|
-
)
|
|
310
|
+
cap = item.config.pluginmanager.getplugin("capturemanager")
|
|
311
|
+
assert isinstance(cap, _pytest.capture.CaptureManager)
|
|
282
312
|
cap.resume_global_capture()
|
|
283
313
|
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
314
|
result, exitcode, timed_out = forked_subprocess(
|
|
287
|
-
run_subprocess, (item,), timeout, mem_limit
|
|
315
|
+
run_subprocess, (item,), timeout, mem_limit, cpu_limit
|
|
288
316
|
)
|
|
289
317
|
cap.deactivate_fixture()
|
|
290
318
|
cap.suspend_global_capture(in_=False)
|
|
291
319
|
out, err = cap.read_global_capture()
|
|
292
320
|
|
|
321
|
+
warnings_captured = None
|
|
322
|
+
|
|
293
323
|
# The result might be an exception, that comes
|
|
294
324
|
# form current process
|
|
295
325
|
if result and not isinstance(result, BaseException):
|
|
296
326
|
# The result came from child process
|
|
297
|
-
result,
|
|
327
|
+
result, warnings_captured = result
|
|
298
328
|
|
|
299
329
|
if result and not isinstance(result, BaseException):
|
|
300
330
|
results: List[pytest.TestReport] = []
|
|
@@ -308,9 +338,9 @@ def run_in_subprocess(
|
|
|
308
338
|
results[-1].sections.append(("Captured stderr", err))
|
|
309
339
|
results[-1].sections.append(("Captured stdout", out))
|
|
310
340
|
|
|
311
|
-
if
|
|
341
|
+
if warnings_captured:
|
|
312
342
|
try:
|
|
313
|
-
for warning_message in
|
|
343
|
+
for warning_message in warnings_captured:
|
|
314
344
|
item.ihook.pytest_warning_recorded.call_historic(
|
|
315
345
|
kwargs=dict(
|
|
316
346
|
warning_message=warning_message,
|
|
@@ -343,10 +373,12 @@ def report_process_crash(
|
|
|
343
373
|
|
|
344
374
|
if timed_out is not None:
|
|
345
375
|
info += f"Timeout > {timed_out}"
|
|
376
|
+
elif exitcode == -24:
|
|
377
|
+
info += "SIGXCPU: CPU time limit exceeded"
|
|
346
378
|
elif exitcode != 0:
|
|
347
|
-
info += f"
|
|
379
|
+
info += f"Crashed with exit code {exitcode}"
|
|
348
380
|
else:
|
|
349
|
-
info += "Exited with no result,
|
|
381
|
+
info += "Exited with no result, memory limit exceeded (probably)"
|
|
350
382
|
|
|
351
383
|
call = runner.CallInfo.from_call(lambda: 0 / 0, "???")
|
|
352
384
|
call.excinfo = info
|
|
@@ -421,13 +453,17 @@ def pytest_runtest_protocol(item):
|
|
|
421
453
|
return
|
|
422
454
|
if item.config.pluginmanager.get_plugin("timeout"):
|
|
423
455
|
return
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
456
|
+
isolate, timeout, mem_limit, cpu_limit = get_isolation_options(item)
|
|
457
|
+
|
|
458
|
+
if (
|
|
459
|
+
isolate is not None
|
|
460
|
+
or mem_limit is not None
|
|
461
|
+
or timeout is not None
|
|
462
|
+
or cpu_limit is not None
|
|
463
|
+
):
|
|
428
464
|
ihook = item.ihook
|
|
429
465
|
ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
|
|
430
|
-
reports = run_in_subprocess(item, timeout, mem_limit)
|
|
466
|
+
reports = run_in_subprocess(item, timeout, mem_limit, cpu_limit)
|
|
431
467
|
|
|
432
468
|
for rep in reports:
|
|
433
469
|
ihook.pytest_runtest_logreport(report=rep)
|
|
@@ -436,22 +472,8 @@ def pytest_runtest_protocol(item):
|
|
|
436
472
|
return True
|
|
437
473
|
|
|
438
474
|
|
|
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
|
|
475
|
+
def get_timeout(item):
|
|
476
|
+
# Should we timeout?
|
|
455
477
|
timeout = get_global_value(item, "timeout")
|
|
456
478
|
timeout_marker = get_marker(item, "timeout", "timeout", 0)
|
|
457
479
|
|
|
@@ -465,10 +487,15 @@ def get_isolation_options(item):
|
|
|
465
487
|
elif isolate_timeout is not None:
|
|
466
488
|
timeout = isolate_timeout
|
|
467
489
|
|
|
468
|
-
|
|
490
|
+
try:
|
|
469
491
|
timeout = float(timeout)
|
|
492
|
+
except (ValueError, TypeError):
|
|
493
|
+
timeout = None
|
|
494
|
+
return timeout
|
|
495
|
+
|
|
470
496
|
|
|
471
|
-
|
|
497
|
+
def get_memory_limit(item):
|
|
498
|
+
# Should we limit memory?
|
|
472
499
|
mem_limit = None
|
|
473
500
|
isolate_mem_limit_marker = get_marker(item, "isolate", "mem_limit", 1)
|
|
474
501
|
isolate_mem_limit = get_global_value(item, "isolate_mem_limit")
|
|
@@ -477,4 +504,88 @@ def get_isolation_options(item):
|
|
|
477
504
|
mem_limit = isolate_mem_limit_marker
|
|
478
505
|
else:
|
|
479
506
|
mem_limit = isolate_mem_limit
|
|
480
|
-
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
mem_limit = int(mem_limit)
|
|
510
|
+
if mem_limit < 1 or round(mem_limit) != mem_limit:
|
|
511
|
+
raise ValueError("Memory limit must be in whole bytes")
|
|
512
|
+
mem_limit = int(mem_limit)
|
|
513
|
+
except (ValueError, TypeError):
|
|
514
|
+
mem_limit = None
|
|
515
|
+
return mem_limit
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def get_cpu_limit(item):
|
|
519
|
+
# Should we limit cpu?
|
|
520
|
+
cpu_limit = None
|
|
521
|
+
isolate_cpu_limit_marker = get_marker(item, "isolate", "cpu_limit", 2)
|
|
522
|
+
isolate_cpu_limit = get_global_value(item, "isolate_cpu_limit")
|
|
523
|
+
|
|
524
|
+
if isolate_cpu_limit_marker is not None:
|
|
525
|
+
cpu_limit = isolate_cpu_limit_marker
|
|
526
|
+
else:
|
|
527
|
+
cpu_limit = isolate_cpu_limit
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
cpu_limit = float(cpu_limit)
|
|
531
|
+
if cpu_limit < 1 or round(cpu_limit) != cpu_limit:
|
|
532
|
+
raise ValueError("Cpu limit must be in whole seconds")
|
|
533
|
+
cpu_limit = int(cpu_limit)
|
|
534
|
+
except (ValueError, TypeError):
|
|
535
|
+
cpu_limit = None
|
|
536
|
+
return cpu_limit
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def get_isolation_options(item):
|
|
540
|
+
# Should we fork?
|
|
541
|
+
forked = get_global_value(item, "forked")
|
|
542
|
+
forked_marker = get_marker(item, "forked")
|
|
543
|
+
isolate = get_global_value(item, "isolate")
|
|
544
|
+
isolate_marker = get_marker(item, "isolate")
|
|
545
|
+
if isolate_marker is not None:
|
|
546
|
+
isolate = isolate_marker
|
|
547
|
+
elif forked_marker is not None:
|
|
548
|
+
isolate = forked_marker
|
|
549
|
+
elif isolate is not None:
|
|
550
|
+
pass
|
|
551
|
+
else:
|
|
552
|
+
isolate = forked
|
|
553
|
+
timeout = get_timeout(item)
|
|
554
|
+
mem_limit = get_memory_limit(item)
|
|
555
|
+
cpu_limit = get_cpu_limit(item)
|
|
556
|
+
|
|
557
|
+
return isolate, timeout, mem_limit, cpu_limit
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@pytest.hookimpl(tryfirst=True)
|
|
561
|
+
def pytest_terminal_summary(terminalreporter) -> None:
|
|
562
|
+
durations = terminalreporter.config.option.durations
|
|
563
|
+
durations_min = terminalreporter.config.option.durations_min
|
|
564
|
+
verbose = terminalreporter.config.getvalue("verbose")
|
|
565
|
+
if durations is None:
|
|
566
|
+
return
|
|
567
|
+
tr = terminalreporter
|
|
568
|
+
dlist = []
|
|
569
|
+
for replist in tr.stats.values():
|
|
570
|
+
for rep in replist:
|
|
571
|
+
if hasattr(rep, "cpu_usage"):
|
|
572
|
+
dlist.append(rep)
|
|
573
|
+
if not dlist:
|
|
574
|
+
return
|
|
575
|
+
dlist.sort(key=lambda x: x.cpu_usage, reverse=True) # type: ignore[no-any-return]
|
|
576
|
+
if not durations:
|
|
577
|
+
tr.write_sep("=", "highest cpu_usage")
|
|
578
|
+
else:
|
|
579
|
+
tr.write_sep("=", "highest %s cpu_usage" % durations)
|
|
580
|
+
dlist = dlist[:durations]
|
|
581
|
+
|
|
582
|
+
for i, rep in enumerate(dlist):
|
|
583
|
+
if verbose < 2 and rep.cpu_usage < durations_min:
|
|
584
|
+
tr.write_line("")
|
|
585
|
+
tr.write_line(
|
|
586
|
+
"(%s cpu_usage < %gs hidden. Use -vv to show these durations.)"
|
|
587
|
+
% (len(dlist) - i, durations_min)
|
|
588
|
+
)
|
|
589
|
+
break
|
|
590
|
+
if rep.when == "teardown":
|
|
591
|
+
tr.write_line(f"{rep.cpu_usage:02.2f}s {rep.nodeid}")
|
|
@@ -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,27 @@ 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
|
+
# hog CPU:
|
|
78
|
+
x=0
|
|
79
|
+
for i in range(100):
|
|
80
|
+
x+=(np.random.rand(1000,1000)@np.random.rand(1000,1000)).sum()
|
|
@@ -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{37,311}
|
|
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}
|
|
16
|
+
|
|
17
|
+
[testenv:lint]
|
|
18
|
+
skip_install = true
|
|
19
|
+
deps = ruff
|
|
20
|
+
commands = ruff check {postargs:.}
|
|
21
|
+
|
|
22
|
+
[gh]
|
|
23
|
+
python =
|
|
24
|
+
3.7 = py37
|
|
25
|
+
3.11 = py311
|
|
@@ -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
|
|
File without changes
|