punit 1.12.2__tar.gz → 1.12.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.
Files changed (60) hide show
  1. {punit-1.12.2/src/pUnit.egg-info → punit-1.12.3}/PKG-INFO +1 -1
  2. {punit-1.12.2 → punit-1.12.3}/pyproject.toml +1 -1
  3. {punit-1.12.2 → punit-1.12.3/src/pUnit.egg-info}/PKG-INFO +1 -1
  4. {punit-1.12.2 → punit-1.12.3}/src/pUnit.egg-info/SOURCES.txt +1 -2
  5. {punit-1.12.2 → punit-1.12.3}/src/punit/__init__.py +4 -3
  6. {punit-1.12.2 → punit-1.12.3}/src/punit/cli.py +1 -1
  7. punit-1.12.2/src/punit/parallel.py → punit-1.12.3/src/punit/parallelism.py +230 -9
  8. {punit-1.12.2 → punit-1.12.3}/src/punit/runner.py +163 -6
  9. punit-1.12.2/src/punit/sequential.py +0 -63
  10. {punit-1.12.2 → punit-1.12.3}/LICENSE +0 -0
  11. {punit-1.12.2 → punit-1.12.3}/README.md +0 -0
  12. {punit-1.12.2 → punit-1.12.3}/setup.cfg +0 -0
  13. {punit-1.12.2 → punit-1.12.3}/src/pUnit.egg-info/dependency_links.txt +0 -0
  14. {punit-1.12.2 → punit-1.12.3}/src/pUnit.egg-info/entry_points.txt +0 -0
  15. {punit-1.12.2 → punit-1.12.3}/src/pUnit.egg-info/requires.txt +0 -0
  16. {punit-1.12.2 → punit-1.12.3}/src/pUnit.egg-info/top_level.txt +0 -0
  17. {punit-1.12.2 → punit-1.12.3}/src/punit/SKILL.md +0 -0
  18. {punit-1.12.2 → punit-1.12.3}/src/punit/TestResult.py +0 -0
  19. {punit-1.12.2 → punit-1.12.3}/src/punit/TextIOCapture.py +0 -0
  20. {punit-1.12.2 → punit-1.12.3}/src/punit/__main__.py +0 -0
  21. {punit-1.12.2 → punit-1.12.3}/src/punit/assertions/__init__.py +0 -0
  22. {punit-1.12.2 → punit-1.12.3}/src/punit/assertions/collections.py +0 -0
  23. {punit-1.12.2 → punit-1.12.3}/src/punit/assertions/exceptions.py +0 -0
  24. {punit-1.12.2 → punit-1.12.3}/src/punit/assertions/numeric.py +0 -0
  25. {punit-1.12.2 → punit-1.12.3}/src/punit/assertions/strings.py +0 -0
  26. {punit-1.12.2 → punit-1.12.3}/src/punit/conditions/__init__.py +0 -0
  27. {punit-1.12.2 → punit-1.12.3}/src/punit/conditions/skip.py +0 -0
  28. {punit-1.12.2 → punit-1.12.3}/src/punit/discovery/TestModuleDiscovery.py +0 -0
  29. {punit-1.12.2 → punit-1.12.3}/src/punit/discovery/__init__.py +0 -0
  30. {punit-1.12.2 → punit-1.12.3}/src/punit/facts/Fact.py +0 -0
  31. {punit-1.12.2 → punit-1.12.3}/src/punit/facts/FactManager.py +0 -0
  32. {punit-1.12.2 → punit-1.12.3}/src/punit/facts/__init__.py +0 -0
  33. {punit-1.12.2 → punit-1.12.3}/src/punit/filters/Filter.py +0 -0
  34. {punit-1.12.2 → punit-1.12.3}/src/punit/filters/FilterManager.py +0 -0
  35. {punit-1.12.2 → punit-1.12.3}/src/punit/filters/__init__.py +0 -0
  36. {punit-1.12.2 → punit-1.12.3}/src/punit/metadata/CallableMetadata.py +0 -0
  37. {punit-1.12.2 → punit-1.12.3}/src/punit/metadata/__init__.py +0 -0
  38. {punit-1.12.2 → punit-1.12.3}/src/punit/mocks/__init__.py +0 -0
  39. {punit-1.12.2 → punit-1.12.3}/src/punit/mocks/matcher.py +0 -0
  40. {punit-1.12.2 → punit-1.12.3}/src/punit/mocks/mock.py +0 -0
  41. {punit-1.12.2 → punit-1.12.3}/src/punit/mocks/patch.py +0 -0
  42. {punit-1.12.2 → punit-1.12.3}/src/punit/py.typed +0 -0
  43. {punit-1.12.2 → punit-1.12.3}/src/punit/reports/HtmlReportGenerator.py +0 -0
  44. {punit-1.12.2 → punit-1.12.3}/src/punit/reports/JUnitReportGenerator.py +0 -0
  45. {punit-1.12.2 → punit-1.12.3}/src/punit/reports/JsonReportGenerator.py +0 -0
  46. {punit-1.12.2 → punit-1.12.3}/src/punit/reports/__init__.py +0 -0
  47. {punit-1.12.2 → punit-1.12.3}/src/punit/results/__init__.py +0 -0
  48. {punit-1.12.2 → punit-1.12.3}/src/punit/results/fails.py +0 -0
  49. {punit-1.12.2 → punit-1.12.3}/src/punit/setups/Setup.py +0 -0
  50. {punit-1.12.2 → punit-1.12.3}/src/punit/setups/SetupManager.py +0 -0
  51. {punit-1.12.2 → punit-1.12.3}/src/punit/setups/__init__.py +0 -0
  52. {punit-1.12.2 → punit-1.12.3}/src/punit/teardowns/Teardown.py +0 -0
  53. {punit-1.12.2 → punit-1.12.3}/src/punit/teardowns/TeardownManager.py +0 -0
  54. {punit-1.12.2 → punit-1.12.3}/src/punit/teardowns/__init__.py +0 -0
  55. {punit-1.12.2 → punit-1.12.3}/src/punit/theories/Theory.py +0 -0
  56. {punit-1.12.2 → punit-1.12.3}/src/punit/theories/TheoryManager.py +0 -0
  57. {punit-1.12.2 → punit-1.12.3}/src/punit/theories/__init__.py +0 -0
  58. {punit-1.12.2 → punit-1.12.3}/src/punit/traits/Trait.py +0 -0
  59. {punit-1.12.2 → punit-1.12.3}/src/punit/traits/TraitManager.py +0 -0
  60. {punit-1.12.2 → punit-1.12.3}/src/punit/traits/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.12.2
3
+ Version: 1.12.3
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pUnit"
3
- version = "1.12.2"
3
+ version = "1.12.3"
4
4
  description = "A modernized unit-test framework for Python."
5
5
  keywords = ["test", "unittest", "unit-test", "xUnit", "nUnit", "pytest"]
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.12.2
3
+ Version: 1.12.3
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License-Expression: MIT
@@ -13,10 +13,9 @@ src/punit/TextIOCapture.py
13
13
  src/punit/__init__.py
14
14
  src/punit/__main__.py
15
15
  src/punit/cli.py
16
- src/punit/parallel.py
16
+ src/punit/parallelism.py
17
17
  src/punit/py.typed
18
18
  src/punit/runner.py
19
- src/punit/sequential.py
20
19
  src/punit/assertions/__init__.py
21
20
  src/punit/assertions/collections.py
22
21
  src/punit/assertions/exceptions.py
@@ -14,12 +14,12 @@ from .conditions import skip
14
14
  from .teardowns import Teardown, teardown
15
15
  from .theories import theory, inlinedata
16
16
  from .traits import trait
17
- from .sequential import sequential
17
+ from .parallelism import parallel, sequential
18
18
 
19
19
  from . import mocks
20
20
 
21
- __version__ = '1.12.2'
22
- __commit__ = '3afa951'
21
+ __version__ = '1.12.3'
22
+ __commit__ = 'f1990b5'
23
23
  __all__ = [
24
24
  '__version__', '__commit__',
25
25
  'assertions',
@@ -36,4 +36,5 @@ __all__ = [
36
36
  'fails',
37
37
  'skip',
38
38
  'sequential',
39
+ 'parallel',
39
40
  ]
@@ -205,7 +205,7 @@ class CommandLineInterface:
205
205
  return self.__aliases
206
206
 
207
207
  @property
208
- def parallel(self) -> int | None:
208
+ def parallelism(self) -> int | None:
209
209
  return self.__parallelism
210
210
 
211
211
  @property
@@ -2,14 +2,90 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
4
  """
5
- Parallel test execution infrastructure for pUnit.
5
+ Parallelism for pUnit.
6
6
 
7
- Provides a pool of worker threads, each with its own asyncio event loop,
8
- for executing tests in parallel.
7
+ pUnit provides three mechanisms for controlling test execution parallelism:
8
+ the **``--parallel [THREADS]``** CLI flag, the **``@parallel``** decorator,
9
+ and the **``@sequential``** decorator.
10
+
11
+ CLI flag
12
+ --------
13
+ The ``--parallel [THREADS]`` option enables multi-threaded parallel execution.
14
+ Each worker thread runs its own asyncio event loop for isolation.
15
+
16
+ * ``--parallel`` (bare flag, no number) runs tests using
17
+ ``cpu_count // 2`` workers.
18
+ * ``--parallel N`` runs tests using *N* workers.
19
+ * No flag means tests run sequentially, one after another.
20
+
21
+ When parallel mode is active the runner processes each test file in four
22
+ batches: parallel facts, parallel theories, sequential facts, then
23
+ sequential theories.
24
+
25
+ @parallel decorator
26
+ -------------------
27
+ Marking a fact or theory with ``@parallel`` triggers **auto-enable** mode.
28
+ When pUnit scans the test package and finds at least one ``@parallel``
29
+ decorated test (and no ``--parallel`` flag was given on the CLI):
30
+
31
+ * Parallel execution is enabled automatically with
32
+ ``cpu_count // 2`` workers.
33
+ * **Only** ``@parallel``-marked tests run in the concurrent batch.
34
+ * All other tests are treated as sequential (as if they carried ``@sequential``).
35
+
36
+ Applying ``@parallel`` to a **class** marks every method of that class.
37
+
38
+ ``@parallel`` is a no-op when ``--parallel`` is specified on the CLI; the
39
+ CLI flag takes full priority.
40
+
41
+ @sequential decorator
42
+ ---------------------
43
+ Marking a fact or theory with ``@sequential`` forces it to run as a
44
+ sequential test **after** all parralel tests for that file have
45
+ completed. This lets you co-run certain tests one-at-a-time inside
46
+ an otherwise parallel test suite (for example, tests that share mutable
47
+ state).
48
+
49
+ When no ``@parallel`` decorator is present and ``--parallel`` is not used
50
+ on the CLI, all tests run sequentially (the default).
51
+
52
+ Examples
53
+ --------
54
+
55
+ Enable parallel mode for the entire test package via CLI::
56
+
57
+ punit tests/ --parallel 4
58
+
59
+ Run only selected tests in parallel -- no CLI flag needed::
60
+
61
+ from punit import fact, parallel
62
+
63
+ @fact
64
+ @parallel
65
+ def test_network_io():
66
+ ...
67
+
68
+ @fact
69
+ def test_database_setup():
70
+ # Runs sequentially alongside the parallel tests
71
+ ...
72
+
73
+ Mix parallel and sequential tests inside a parallel suite::
74
+
75
+ from punit import fact, parallel, sequential
76
+
77
+ @parallel
78
+ class SlowTests:
79
+ @fact
80
+ def test_heavy_computation(self):
81
+ ...
82
+
83
+ @fact
84
+ @sequential
85
+ def test_shared_file_handle(self):
86
+ # Runs one-at-a-time after all other SlowTests finish
87
+ ...
9
88
 
10
- Each test is wrapped in a coroutine that creates a ``TestResult``, runs
11
- setup -> test -> teardown, and yields the populated result. The main thread
12
- dispatches these coroutines to worker threads and collects the results.
13
89
  """
14
90
 
15
91
  from __future__ import annotations
@@ -20,7 +96,7 @@ import queue
20
96
  import threading
21
97
  import time
22
98
  from types import ModuleType
23
- from typing import Any
99
+ from typing import Any, Callable
24
100
 
25
101
  from .TestResult import TestResult
26
102
 
@@ -239,7 +315,7 @@ async def _execute_fact(
239
315
 
240
316
  async def _execute_theory(
241
317
  theory: Any,
242
- data: tuple,
318
+ data: tuple[Any, ...],
243
319
  module: ModuleType,
244
320
  module_report_name: str,
245
321
  filename: str,
@@ -330,6 +406,151 @@ async def _execute_theory(
330
406
  return result
331
407
 
332
408
 
409
+ def sequential(target: Callable[..., Any] | type) -> Callable[..., Any]:
410
+ """Mark a test function, method, or class for **sequential** execution.
411
+
412
+ When pUnit is started with ``--parallel THREADS`` (where *THREADS* > 1),
413
+ tests decorated without ``@sequential`` run in parallel up to *N* at
414
+ any given point. ``@sequential`` tests are **not** added to the
415
+ concurrent dispatch queue -- after all peers at the same scope finish
416
+ concurrently, the sequential tests run one after another in their
417
+ definition order.
418
+
419
+ When applied to a **class**, all methods of the class are automatically
420
+ marked as sequential.
421
+
422
+ The decorator returns the original ``target`` unchanged; it installs the
423
+ ``__punit_sequential`` marker attribute on the unwrapped function (or the
424
+ class object itself for class targets).
425
+
426
+ Parameters
427
+ ----------
428
+ target : Callable | type
429
+ The test function, method, or class to mark.
430
+
431
+ Returns
432
+ -------
433
+ Callable
434
+ The original, undecorated target.
435
+
436
+ Example
437
+ -------
438
+
439
+ .. code-block:: python
440
+
441
+ from punit import fact, sequential
442
+
443
+ @fact
444
+ @sequential
445
+ def my_test():
446
+ assert True
447
+
448
+ class MyTestCase:
449
+ @fact
450
+ @sequential
451
+ def test_one(self):
452
+ assert True
453
+
454
+ # Apply @sequential to an entire class -- all methods run sequentially
455
+ @sequential
456
+ class SequentialTestCase:
457
+ @fact
458
+ def test_one(self):
459
+ assert True
460
+
461
+ @fact
462
+ def test_two(self):
463
+ assert True
464
+
465
+ """
466
+ import inspect
467
+
468
+ if isinstance(target, type):
469
+ setattr(target, "__punit_sequential", True)
470
+ for name, method in inspect.getmembers(target, predicate=inspect.isfunction):
471
+ setattr(method, "__punit_sequential", True)
472
+ return target
473
+
474
+ unwrapped = inspect.unwrap(target)
475
+ setattr(unwrapped, "__punit_sequential", True)
476
+ return target
477
+
478
+
479
+ def parallel(target: Callable[..., Any]) -> Callable[..., Any]:
480
+ """Mark a test function, method, or class for **parallel** execution.
481
+
482
+ When pUnit detects any ``@parallel``-decorated tests in a test package at
483
+ runtime, it automatically triggers (multi-threaded) parallel execution for
484
+ the entire test run -- no ``--parallel`` CLI flag is required.
485
+
486
+ When ``@parallel`` triggers auto-enable:
487
+
488
+ * Only tests marked with ``@parallel`` run in the parallel batch
489
+ * Tests not marked with ``@parallel`` are treated as sequential
490
+ (as if decorated with ``@sequential``)
491
+ * The thread pool is initialized with ``cpu_count // 2`` workers
492
+ * This auto-enable is superseded by ``--parallel`` on the CLI
493
+
494
+ When ``--parallel`` is specified on the CLI, this decorator has no effect;
495
+ the existing ``--parallel`` behavior takes priority and all
496
+ non-``@sequential`` tests run in parallel as before.
497
+
498
+ When applied to a class, all methods of the class are automatically marked
499
+ as parallel.
500
+
501
+ The decorator returns the original ``target`` unchanged; it installs the
502
+ ``__punit_parallel`` marker attribute on the unwrapped function (or the
503
+ class object itself for class targets).
504
+
505
+ Parameters
506
+ ----------
507
+ target : Callable | type
508
+ The test function, method, or class to mark.
509
+
510
+ Returns
511
+ -------
512
+ Callable
513
+ The original, undecorated target.
514
+
515
+ Example
516
+ -------
517
+
518
+ .. code-block:: python
519
+
520
+ from punit import fact, parallel
521
+
522
+ @fact
523
+ @parallel
524
+ def my_parallel_test():
525
+ assert True
526
+
527
+ class ParallelTestCase:
528
+ @fact
529
+ @parallel
530
+ def test_one(self):
531
+ assert True
532
+
533
+ @fact
534
+ def test_sequential(self):
535
+ # Runs sequentially (not in parallel batch)
536
+ assert True
537
+
538
+ """
539
+ import inspect
540
+
541
+ if isinstance(target, type):
542
+ setattr(target, "__punit_parallel", True)
543
+ for name, method in inspect.getmembers(target, predicate=inspect.isfunction):
544
+ setattr(method, "__punit_parallel", True)
545
+ return target
546
+
547
+ unwrapped = inspect.unwrap(target)
548
+ setattr(unwrapped, "__punit_parallel", True)
549
+ return target
550
+
551
+
333
552
  __all__ = [
334
- 'ThreadPool'
553
+ 'ThreadPool',
554
+ 'parallel',
555
+ 'sequential'
335
556
  ]
@@ -13,7 +13,7 @@ from typing import Any, Callable, Optional
13
13
 
14
14
  from .TextIOCapture import setup_global_text_io, _PERSISTENT_STDOUT, _PERSISTENT_STDERR
15
15
  from .cli import CommandLineInterface
16
- from .parallel import ThreadPool, _execute_fact, _execute_theory
16
+ from .parallelism import ThreadPool, _execute_fact, _execute_theory
17
17
  from .facts.FactManager import FactManager
18
18
  from .setups.SetupManager import SetupManager
19
19
  from .teardowns.TeardownManager import TeardownManager
@@ -43,19 +43,25 @@ def _is_sequential(target: Any) -> bool:
43
43
  return getattr(unwrapped, '__punit_sequential', None) is True
44
44
 
45
45
 
46
+ def _is_parallel(target: Any) -> bool:
47
+ """Return True if *target* is marked with ``@parallel``."""
48
+ unwrapped = inspect.unwrap(target)
49
+ return getattr(unwrapped, '__punit_parallel', None) is True
50
+
51
+
46
52
  def _get_parallelism(cli: 'CommandLineInterface') -> int:
47
53
  """Resolve the parallelism level from CLI.
48
54
 
49
- Returns ``-1`` when parallel mode is disabled (use sequential flow).
55
+ Returns ``-1`` when parallelism is disabled (use sequential flow).
50
56
  Returns the worker count when enabled; ``0`` maps to ``cpu_count // 2``.
51
57
  """
52
- mode = cli.parallel
53
- if mode is None:
58
+ parallelism = cli.parallelism
59
+ if parallelism is None:
54
60
  return -1
55
- if mode <= 0:
61
+ if parallelism <= 0:
56
62
  cores = os.cpu_count() or 1
57
63
  return max(1, cores // 2)
58
- return mode
64
+ return parallelism
59
65
 
60
66
 
61
67
  class TestRunner:
@@ -167,6 +173,22 @@ class TestRunner:
167
173
  if self.__cli.verbose and (not test_result.is_success) and test_result.exception is not None:
168
174
  print(f'Test File:\n {test_result.file_name}\nError:\n {test_result.exception}\n Traceback:\n{"".join(traceback.format_tb(test_result.exception.__traceback__))}')
169
175
 
176
+ def __detect_parallel_tests(self) -> bool:
177
+ """Return True if any registered fact or theory is marked with ``@parallel``."""
178
+ fm = FactManager.instance()
179
+ modules = getattr(fm, '_FactManager__modules', {})
180
+ for facts in modules.values():
181
+ for fact in facts:
182
+ if _is_parallel(fact.target):
183
+ return True
184
+ tm = TheoryManager.instance()
185
+ modules = getattr(tm, '_TheoryManager__modules', {})
186
+ for theories in modules.values():
187
+ for theory in theories:
188
+ if _is_parallel(theory.target):
189
+ return True
190
+ return False
191
+
170
192
  async def __run_facts(self, host_name: str, test_module: ModuleType, filename: str, module_report_name: str, results: list[TestResult]) -> None:
171
193
  """Run all facts for a test module and return their results."""
172
194
  facts = FactManager.instance().get(test_module.__name__)
@@ -288,6 +310,14 @@ class TestRunner:
288
310
  if parallelism > 0:
289
311
  return await self._run_parallel(results, test_package_path)
290
312
 
313
+ # Auto-enable parallel mode when @parallel-decorated tests are detected
314
+ has_parallel_tests = self.__detect_parallel_tests()
315
+ if has_parallel_tests:
316
+ cores = os.cpu_count() or 1
317
+ parallelism = max(1, cores // 2)
318
+ await self._run_parallel_parallel_only(results, test_package_path, parallelism)
319
+ return results
320
+
291
321
  for filename in self.__filenames:
292
322
  ts = time.time()
293
323
  if not self.__test_package_name:
@@ -333,6 +363,82 @@ class TestRunner:
333
363
  # Parallel execution
334
364
  # ------------------------------------------------------------------
335
365
 
366
+ async def _run_parallel_parallel_only(
367
+ self,
368
+ results: list[TestResult],
369
+ test_package_path: str,
370
+ parallelism: int,
371
+ ) -> None:
372
+ """Execute ``@parallel``-decorated tests in a worker pool, rest sequentially.
373
+
374
+ This is used when ``@parallel`` decorators are detected at runtime and
375
+ no ``--parallel`` CLI flag was provided: only tests marked with
376
+ ``@parallel`` go into the parallel batch, everything else runs one-by-one.
377
+ """
378
+ try:
379
+ pool = ThreadPool(parallelism)
380
+ with pool:
381
+ module_report_name = ''
382
+ for filename in self.__filenames:
383
+ try:
384
+ if not self.__test_package_name:
385
+ module_import_name = filename
386
+ else:
387
+ module_import_name = filename.replace(test_package_path, '').replace('/', '.')
388
+ if module_import_name.endswith('.py'):
389
+ module_import_name = module_import_name[:-3]
390
+ module_report_name = module_import_name.lstrip('.')
391
+ if not self.__test_package_name:
392
+ test_module = importlib.import_module(module_import_name)
393
+ else:
394
+ test_module = importlib.import_module(module_import_name, self.__test_package_name)
395
+ host_name = socket.gethostname()
396
+
397
+ await self._run_parallel_facts_parallel_only(host_name, test_module, filename, module_report_name, results, pool)
398
+ if self.__cli.failfast and any(e.is_success is False for e in results):
399
+ break
400
+
401
+ await self._run_parallel_theories_parallel_only(host_name, test_module, filename, module_report_name, results, pool)
402
+ if self.__cli.failfast and any(e.is_success is False for e in results):
403
+ break
404
+
405
+ facts = FactManager.instance().get(test_module.__name__)
406
+ for fact in facts:
407
+ if _is_parallel(fact.target):
408
+ continue
409
+ test_result = await _execute_fact(fact, test_module, module_report_name, filename, host_name, self.__test_package_name)
410
+ results.append(test_result)
411
+ self.print_test_result(test_result)
412
+ if self.__cli.failfast and not test_result.is_success:
413
+ return
414
+
415
+ theories = TheoryManager.instance().get(test_module.__name__)
416
+ for theory in theories:
417
+ if _is_parallel(theory.target):
418
+ continue
419
+ for data in theory.datas:
420
+ test_result = await _execute_theory(theory, data, test_module, module_report_name, filename, host_name, self.__test_package_name)
421
+ results.append(test_result)
422
+ self.print_test_result(test_result)
423
+ if self.__cli.failfast and not test_result.is_success:
424
+ return
425
+
426
+ except Exception as ex:
427
+ result = TestResult()
428
+ result.class_name = '*'
429
+ result.module_name = module_report_name
430
+ result.test_name = '*'
431
+ result.start_time = time.time()
432
+ result.stop_time = time.time()
433
+ result.is_success = False
434
+ result.exception = ex
435
+ results.append(result)
436
+ self.print_test_result(result)
437
+ if self.__cli.failfast:
438
+ break
439
+ except Exception:
440
+ pass
441
+
336
442
  async def _run_parallel(self, results: list[TestResult], test_package_path: str) -> list[TestResult]:
337
443
  """Execute all tests with a worker pool (each worker has its own event loop)."""
338
444
  parallelism = _get_parallelism(self.__cli)
@@ -474,3 +580,54 @@ class TestRunner:
474
580
  self.print_test_result(test_result)
475
581
  if self.__cli.failfast and not test_result.is_success:
476
582
  return
583
+
584
+ async def _run_parallel_facts_parallel_only(
585
+ self,
586
+ host_name: str,
587
+ test_module: ModuleType,
588
+ filename: str,
589
+ module_report_name: str,
590
+ results: list[TestResult],
591
+ pool: ThreadPool
592
+ ) -> None:
593
+ """Run @parallel facts in parallel (auto-enable mode)."""
594
+ facts = FactManager.instance().get(test_module.__name__)
595
+ coros: list[Any] = []
596
+ for fact in facts:
597
+ if not _is_parallel(fact.target):
598
+ continue
599
+ coro = _execute_fact(fact, test_module, module_report_name, filename, host_name, self.__test_package_name)
600
+ coros.append(coro)
601
+ if coros:
602
+ test_results = await asyncio.gather(*coros)
603
+ for test_result in test_results:
604
+ results.append(test_result)
605
+ self.print_test_result(test_result)
606
+ if self.__cli.failfast and not test_result.is_success:
607
+ return
608
+
609
+ async def _run_parallel_theories_parallel_only(
610
+ self,
611
+ host_name: str,
612
+ test_module: ModuleType,
613
+ filename: str,
614
+ module_report_name: str,
615
+ results: list[TestResult],
616
+ pool: ThreadPool
617
+ ) -> None:
618
+ """Run @parallel theory data-points in parallel (auto-enable mode)."""
619
+ theories = TheoryManager.instance().get(test_module.__name__)
620
+ coros: list[Any] = []
621
+ for theory in theories:
622
+ if not _is_parallel(theory.target):
623
+ continue
624
+ for data in theory.datas:
625
+ coro = _execute_theory(theory, data, test_module, module_report_name, filename, host_name, self.__test_package_name)
626
+ coros.append(coro)
627
+ if coros:
628
+ test_results = await asyncio.gather(*coros)
629
+ for test_result in test_results:
630
+ results.append(test_result)
631
+ self.print_test_result(test_result)
632
+ if self.__cli.failfast and not test_result.is_success:
633
+ return
@@ -1,63 +0,0 @@
1
- # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
- # SPDX-License-Identifier: MIT
3
-
4
- """
5
- ``@sequential`` decorator for pUnit.
6
-
7
- When running tests with ``--parallel``, any test decorated with
8
- ``@sequential`` is deferred to a sequential pass that runs after all
9
- parallel peers at the same scope have finished.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- from typing import Any, Callable
15
-
16
-
17
- def sequential(target: Callable[..., Any]) -> Callable[..., Any]:
18
- """Mark a test function or method for **sequential** (sequential) execution.
19
-
20
- When pUnit is started with ``--parralel THREADS`` (where *THREADS* > 1),
21
- tests decorated without ``@sequential`` run in parallel up to *N* at
22
- any given point. ``@sequential`` tests are **not** added to the
23
- concurrent dispatch queue -- after all peers at the same scope finish
24
- concurrently, the sequential tests run one after another in their
25
- definition order.
26
-
27
- The decorator returns the original ``target`` unchanged; it installs the
28
- ``__punit_sequential`` marker attribute on the unwrapped function.
29
-
30
- Parameters
31
- ----------
32
- target : Callable
33
- The test function or method to mark.
34
-
35
- Returns
36
- -------
37
- Callable
38
- The original, undecorated target.
39
-
40
- Example
41
- -------
42
-
43
- .. code-block:: python
44
-
45
- from punit import fact, sequential
46
-
47
- @fact
48
- @sequential
49
- def my_test():
50
- assert True
51
-
52
- class MyTestCase:
53
- @fact
54
- @sequential
55
- def test_one(self):
56
- assert True
57
-
58
- """
59
- import inspect
60
-
61
- unwrapped = inspect.unwrap(target)
62
- setattr(unwrapped, "__punit_sequential", True)
63
- return target
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes