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.
@@ -16,8 +16,8 @@ jobs:
16
16
  os:
17
17
  - Ubuntu
18
18
  py:
19
- - "3.11"
20
- - "3.7"
19
+ - "3.12"
20
+ - "3.9"
21
21
  steps:
22
22
  - name: Setup python for test ${{ matrix.py }}
23
23
  uses: actions/setup-python@v2
@@ -1,6 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pytest-isolate
3
- Version: 0.0.2
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
- "pytest",
43
- "pytest-cov",
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
- # I decided to make my changes in a different project.
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(timeout, memlimt, target, args)
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
- (soft, hard) = resource.getrlimit(resource.RLIMIT_DATA)
53
- resource.setrlimit(resource.RLIMIT_DATA, (max_mem, hard))
54
- (soft, hard) = resource.getrlimit(resource.RLIMIT_DATA)
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
- pass
59
- resource.setrlimit(resource.RLIMIT_DATA, (soft, hard))
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. please uninstall one of "
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, timeout: Optional[float], mem_limit: Optional[int]
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: _pytest.capture.CaptureManager = item.config.pluginmanager.getplugin(
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, warnings_caputred = 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 warnings_caputred:
359
+ if warnings_captured:
312
360
  try:
313
- for warning_message in warnings_caputred:
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"Running the test CRASHED with exit code {exitcode}"
397
+ info += f"Crashed with exit code {exitcode}"
348
398
  else:
349
- info += "Exited with no result, probably hit memory limit"
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
- isolate, timeout, mem_limit = get_isolation_options(item)
426
-
427
- if isolate is not None or mem_limit is not None or timeout is not None:
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 get_isolation_options(item):
440
- # Should we fork?
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
- if timeout and isinstance(timeout, str) and str.isnumeric(timeout):
508
+ try:
469
509
  timeout = float(timeout)
510
+ except (ValueError, TypeError):
511
+ timeout = None
512
+ return timeout
513
+
470
514
 
471
- # Should we memlimit?
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
- return isolate, timeout, mem_limit
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**7)
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**6)
62
+ @pytest.mark.isolate(mem_limit=10**5)
63
63
  def test_rss_limit_fail():
64
- a = bytearray(10**7)
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'
@@ -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