punit 1.5.3__tar.gz → 1.7.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.
- {punit-1.5.3/src/pUnit.egg-info → punit-1.7.3}/PKG-INFO +1 -1
- {punit-1.5.3 → punit-1.7.3}/pyproject.toml +1 -1
- {punit-1.5.3 → punit-1.7.3/src/pUnit.egg-info}/PKG-INFO +1 -1
- {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/SOURCES.txt +6 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/TestResult.py +24 -4
- {punit-1.5.3 → punit-1.7.3}/src/punit/__init__.py +6 -2
- {punit-1.5.3 → punit-1.7.3}/src/punit/__main__.py +39 -28
- {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/numeric.py +4 -2
- {punit-1.5.3 → punit-1.7.3}/src/punit/cli.py +34 -13
- {punit-1.5.3 → punit-1.7.3}/src/punit/facts/Fact.py +15 -5
- {punit-1.5.3 → punit-1.7.3}/src/punit/facts/FactManager.py +3 -1
- {punit-1.5.3 → punit-1.7.3}/src/punit/filters/Filter.py +13 -2
- {punit-1.5.3 → punit-1.7.3}/src/punit/filters/FilterManager.py +3 -1
- {punit-1.5.3 → punit-1.7.3}/src/punit/metadata/CallableMetadata.py +2 -1
- {punit-1.5.3 → punit-1.7.3}/src/punit/reports/JUnitReportGenerator.py +2 -2
- punit-1.7.3/src/punit/runner.py +242 -0
- punit-1.7.3/src/punit/setups/Setup.py +100 -0
- punit-1.7.3/src/punit/setups/SetupManager.py +69 -0
- punit-1.7.3/src/punit/setups/__init__.py +11 -0
- punit-1.7.3/src/punit/teardowns/Teardown.py +100 -0
- punit-1.7.3/src/punit/teardowns/TeardownManager.py +69 -0
- punit-1.7.3/src/punit/teardowns/__init__.py +11 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/theories/Theory.py +32 -5
- {punit-1.5.3 → punit-1.7.3}/src/punit/theories/TheoryManager.py +3 -1
- {punit-1.5.3 → punit-1.7.3}/src/punit/traits/TraitManager.py +3 -2
- punit-1.5.3/src/punit/runner.py +0 -124
- {punit-1.5.3 → punit-1.7.3}/LICENSE +0 -0
- {punit-1.5.3 → punit-1.7.3}/README.md +0 -0
- {punit-1.5.3 → punit-1.7.3}/setup.cfg +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/dependency_links.txt +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/entry_points.txt +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/requires.txt +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/top_level.txt +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/__init__.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/collections.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/exceptions.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/strings.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/discovery/TestModuleDiscovery.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/discovery/__init__.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/facts/__init__.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/filters/__init__.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/metadata/__init__.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/py.typed +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/reports/HtmlReportGenerator.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/reports/JsonReportGenerator.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/reports/__init__.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/theories/__init__.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/traits/Trait.py +0 -0
- {punit-1.5.3 → punit-1.7.3}/src/punit/traits/__init__.py +0 -0
|
@@ -32,6 +32,12 @@ src/punit/reports/HtmlReportGenerator.py
|
|
|
32
32
|
src/punit/reports/JUnitReportGenerator.py
|
|
33
33
|
src/punit/reports/JsonReportGenerator.py
|
|
34
34
|
src/punit/reports/__init__.py
|
|
35
|
+
src/punit/setups/Setup.py
|
|
36
|
+
src/punit/setups/SetupManager.py
|
|
37
|
+
src/punit/setups/__init__.py
|
|
38
|
+
src/punit/teardowns/Teardown.py
|
|
39
|
+
src/punit/teardowns/TeardownManager.py
|
|
40
|
+
src/punit/teardowns/__init__.py
|
|
35
41
|
src/punit/theories/Theory.py
|
|
36
42
|
src/punit/theories/TheoryManager.py
|
|
37
43
|
src/punit/theories/__init__.py
|
|
@@ -27,6 +27,22 @@ class TextIOCapture:
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class TestResult:
|
|
30
|
+
"""
|
|
31
|
+
Accumulates the outcome of a single test execution.
|
|
32
|
+
|
|
33
|
+
Example
|
|
34
|
+
-------
|
|
35
|
+
|
|
36
|
+
Create and populate a result manually:
|
|
37
|
+
|
|
38
|
+
.. code-block:: python
|
|
39
|
+
|
|
40
|
+
result = TestResult()
|
|
41
|
+
result.module_name = 'tests.example'
|
|
42
|
+
result.test_name = 'test_something'
|
|
43
|
+
result.is_success = True
|
|
44
|
+
|
|
45
|
+
"""
|
|
30
46
|
|
|
31
47
|
__class_name: Optional[str]
|
|
32
48
|
__exception: Exception | None
|
|
@@ -59,7 +75,11 @@ class TestResult:
|
|
|
59
75
|
|
|
60
76
|
@property
|
|
61
77
|
def class_name(self) -> Optional[str]:
|
|
62
|
-
return
|
|
78
|
+
return (
|
|
79
|
+
self.__class_name
|
|
80
|
+
if self.__class_name is not None and len(self.__class_name) > 0
|
|
81
|
+
else None
|
|
82
|
+
)
|
|
63
83
|
|
|
64
84
|
@class_name.setter
|
|
65
85
|
def class_name(self, value: Optional[str]) -> None:
|
|
@@ -171,15 +191,15 @@ class TestResult:
|
|
|
171
191
|
elif took >= 0.000000001:
|
|
172
192
|
return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
|
|
173
193
|
else:
|
|
174
|
-
return f'{(took*
|
|
194
|
+
return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'ns'
|
|
175
195
|
|
|
176
|
-
def
|
|
196
|
+
def capture_output(self, quiet: bool = False) -> None:
|
|
177
197
|
self.__stdout_capture = TextIOCapture(sys.stdout, quiet)
|
|
178
198
|
self.__stderr_capture = TextIOCapture(sys.stderr, quiet)
|
|
179
199
|
sys.stdout = self.__stdout_capture
|
|
180
200
|
sys.stderr = self.__stderr_capture
|
|
181
201
|
|
|
182
|
-
def
|
|
202
|
+
def release_output(self) -> None:
|
|
183
203
|
if self.__stdout_capture is not None and self.__stdout_capture.target is not None:
|
|
184
204
|
sys.stdout = self.__stdout_capture.target
|
|
185
205
|
if self.__stderr_capture is not None and self.__stderr_capture.target is not None:
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
from .assertions import collections, exceptions, numeric, strings
|
|
5
5
|
from .facts import fact
|
|
6
|
+
from .setups import Setup, setup
|
|
7
|
+
from .teardowns import Teardown, teardown
|
|
6
8
|
from .theories import theory, inlinedata
|
|
7
9
|
from .traits import trait
|
|
8
10
|
|
|
@@ -10,8 +12,8 @@ from .assertions.exceptions import raises
|
|
|
10
12
|
from .assertions.numeric import approx
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
__version__ = '1.
|
|
14
|
-
__commit__ = '
|
|
15
|
+
__version__ = '1.7.3'
|
|
16
|
+
__commit__ = 'a403690'
|
|
15
17
|
__all__ = [
|
|
16
18
|
'__version__', '__commit__',
|
|
17
19
|
'assertions',
|
|
@@ -21,6 +23,8 @@ __all__ = [
|
|
|
21
23
|
'strings',
|
|
22
24
|
'fact',
|
|
23
25
|
'metadata',
|
|
26
|
+
'setup', 'Setup',
|
|
27
|
+
'teardown', 'Teardown',
|
|
24
28
|
'theory', 'inlinedata',
|
|
25
29
|
'trait'
|
|
26
30
|
]
|
|
@@ -11,40 +11,42 @@ from .cli import CommandLineInterface
|
|
|
11
11
|
from .discovery import TestModuleDiscovery
|
|
12
12
|
from .reports import HtmlReportGenerator, JUnitReportGenerator, JsonReportGenerator
|
|
13
13
|
from .runner import TestRunner
|
|
14
|
+
from .setups.SetupManager import SetupManager
|
|
15
|
+
from .teardowns.TeardownManager import TeardownManager
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
async def async_main() -> None:
|
|
17
|
-
|
|
19
|
+
start_time = time.time()
|
|
18
20
|
cli = CommandLineInterface.parse()
|
|
19
21
|
if cli.help: # pragma: no cover
|
|
20
|
-
cli.
|
|
22
|
+
cli.print_help()
|
|
21
23
|
elif cli.verbose and not cli.quiet: # pragma: no cover
|
|
22
|
-
cli.
|
|
24
|
+
cli.print_summary()
|
|
23
25
|
elif not cli.quiet: # pragma: no cover
|
|
24
|
-
cli.
|
|
26
|
+
cli.print_version()
|
|
25
27
|
os.chdir(cli.workdir)
|
|
26
28
|
if cli.no_pathfix is not True:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if os.path.exists(
|
|
30
|
-
sys.path.append(
|
|
31
|
-
if
|
|
32
|
-
sys.path.append(
|
|
33
|
-
|
|
29
|
+
cwd_path = str(Path.cwd())
|
|
30
|
+
src_path = os.path.join(cwd_path, 'src')
|
|
31
|
+
if os.path.exists(src_path) and src_path not in sys.path:
|
|
32
|
+
sys.path.append(src_path)
|
|
33
|
+
if cwd_path not in sys.path:
|
|
34
|
+
sys.path.append(cwd_path)
|
|
35
|
+
test_module_discovery = TestModuleDiscovery(
|
|
34
36
|
os.path.join(cli.workdir, cli.test_package_name),
|
|
35
37
|
cli.includePatterns,
|
|
36
38
|
cli.excludePatterns,
|
|
37
39
|
cli)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
results = await
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
test_module_discovery.discover()
|
|
41
|
+
test_runner = TestRunner(cli.test_package_name, test_module_discovery.filenames, cli)
|
|
42
|
+
results = await test_runner.run()
|
|
43
|
+
total_time = time.time() - start_time
|
|
44
|
+
failure_count = 0
|
|
43
45
|
for result in results:
|
|
44
46
|
if not result.is_success:
|
|
45
|
-
|
|
47
|
+
failure_count += 1 # pragma: no cover
|
|
46
48
|
if not cli.quiet:
|
|
47
|
-
print(f'Total: {len(results)}, Failures: {
|
|
49
|
+
print(f'Total: {len(results)}, Failures: {failure_count}, Took: {total_time:.3f}s')
|
|
48
50
|
if cli.reportFormat is not None: # pragma: no cover
|
|
49
51
|
report: str = ''
|
|
50
52
|
match cli.reportFormat:
|
|
@@ -61,16 +63,25 @@ async def async_main() -> None:
|
|
|
61
63
|
with open(cli.outputFilename, 'wb') as file:
|
|
62
64
|
file.write(report.encode())
|
|
63
65
|
print(f'\n("{cli.reportFormat}" report written to: {cli.outputFilename})')
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
|
|
67
|
+
# not everyone runs python unbuffered as they should, so force a flush
|
|
68
|
+
sys.stdout.flush()
|
|
69
|
+
sys.stderr.flush()
|
|
70
|
+
try:
|
|
71
|
+
os.fsync(sys.stdout.fileno())
|
|
72
|
+
os.fsync(sys.stderr.fileno())
|
|
73
|
+
except (AttributeError, ValueError, OSError):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
if cli.no_exitcode is not True:
|
|
77
|
+
if failure_count:
|
|
78
|
+
# test failures trigger exit code 119 (for automation gating)
|
|
79
|
+
sys.exit(119)
|
|
80
|
+
if TeardownManager.instance().teardown_error_count > 0:
|
|
81
|
+
# teardown errors also trigger exit code 119 (for automation gating)
|
|
82
|
+
sys.exit(119)
|
|
83
|
+
if SetupManager.instance().setup_error_count > 0:
|
|
84
|
+
# setup errors also trigger exit code 119 (for automation gating)
|
|
74
85
|
sys.exit(119)
|
|
75
86
|
|
|
76
87
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
from decimal import Decimal
|
|
5
7
|
import math
|
|
6
8
|
from typing import cast
|
|
@@ -397,7 +399,7 @@ class ApproxRangeComparator:
|
|
|
397
399
|
self._abs_tol = abs_tol
|
|
398
400
|
self._exclusive = False
|
|
399
401
|
|
|
400
|
-
def inclusive(self, flag: bool = True) ->
|
|
402
|
+
def inclusive(self, flag: bool = True) -> ApproxRangeComparator:
|
|
401
403
|
"""
|
|
402
404
|
Return a new ``ApproxRangeComparator`` with toggled inclusivity.
|
|
403
405
|
|
|
@@ -617,7 +619,7 @@ class approx:
|
|
|
617
619
|
"""Return a ``StrictLessThanComparator`` for strictly ``< expected`` (one-sided tolerance below)."""
|
|
618
620
|
return StrictLessThanComparator(self._expected, self._rel_tol, self._abs_tol)
|
|
619
621
|
|
|
620
|
-
def in_range(self, min_val: float, max_val: float) ->
|
|
622
|
+
def in_range(self, min_val: float, max_val: float) -> ApproxRangeComparator:
|
|
621
623
|
"""Return a comparator for ``[min_val, max_val]`` range checks with directional tolerance."""
|
|
622
624
|
return ApproxRangeComparator(min_val, max_val, self._rel_tol, self._abs_tol)
|
|
623
625
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
import os
|
|
5
7
|
import sys
|
|
6
8
|
from typing import Optional
|
|
@@ -10,6 +12,20 @@ from .traits import Trait
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class CommandLineInterface:
|
|
15
|
+
"""
|
|
16
|
+
Command-line argument parser and display helper for pUnit.
|
|
17
|
+
|
|
18
|
+
Example
|
|
19
|
+
-------
|
|
20
|
+
|
|
21
|
+
Parse CLI arguments from ``sys.argv``:
|
|
22
|
+
|
|
23
|
+
.. code-block:: python
|
|
24
|
+
|
|
25
|
+
cli = CommandLineInterface.parse()
|
|
26
|
+
print(cli.test_package_name)
|
|
27
|
+
|
|
28
|
+
"""
|
|
13
29
|
|
|
14
30
|
__aliases: dict[str, str]
|
|
15
31
|
__excludePatterns: list[str]
|
|
@@ -46,7 +62,13 @@ class CommandLineInterface:
|
|
|
46
62
|
self.__workdir = os.path.curdir
|
|
47
63
|
self.__verbose = False
|
|
48
64
|
|
|
49
|
-
def __parse(self, argv: list[str]) ->
|
|
65
|
+
def __parse(self, argv: list[str]) -> CommandLineInterface:
|
|
66
|
+
"""
|
|
67
|
+
Parse command-line arguments using a finite-state machine.
|
|
68
|
+
|
|
69
|
+
:param argv: List of argument strings (typically ``sys.argv``).
|
|
70
|
+
:returns: ``self`` for method chaining.
|
|
71
|
+
"""
|
|
50
72
|
aliasName: str | None = None
|
|
51
73
|
extractFilter: bool = False
|
|
52
74
|
extractExcludePattern: bool = False
|
|
@@ -158,7 +180,7 @@ class CommandLineInterface:
|
|
|
158
180
|
FilterManager.instance().add(arg)
|
|
159
181
|
|
|
160
182
|
@property
|
|
161
|
-
def aliases(self) -> dict:
|
|
183
|
+
def aliases(self) -> dict[str, str]:
|
|
162
184
|
return self.__aliases
|
|
163
185
|
|
|
164
186
|
@property
|
|
@@ -211,16 +233,15 @@ class CommandLineInterface:
|
|
|
211
233
|
|
|
212
234
|
@property
|
|
213
235
|
def no_exitcode(self) -> bool:
|
|
214
|
-
return self.__no_exitcode
|
|
236
|
+
return self.__no_exitcode
|
|
215
237
|
|
|
216
238
|
@property
|
|
217
239
|
def no_pathfix(self) -> bool:
|
|
218
|
-
return self.__no_pathfix
|
|
240
|
+
return self.__no_pathfix
|
|
219
241
|
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
print("""
|
|
242
|
+
def print_help(self) -> None:
|
|
243
|
+
self.print_version()
|
|
244
|
+
print("""
|
|
224
245
|
Usage: python3 -m punit [-h|--help]
|
|
225
246
|
[-q|--quiet] [-v|--verbose]
|
|
226
247
|
[-z|--failfast]
|
|
@@ -279,10 +300,10 @@ Options:
|
|
|
279
300
|
write to FILENAME. In this case `--report` does not
|
|
280
301
|
suppress any program output.
|
|
281
302
|
""")
|
|
282
|
-
|
|
303
|
+
os._exit(0)
|
|
283
304
|
|
|
284
|
-
def
|
|
285
|
-
self.
|
|
305
|
+
def print_summary(self) -> None:
|
|
306
|
+
self.print_version()
|
|
286
307
|
print(f'Working Directory:\n\t{self.__workdir}')
|
|
287
308
|
print(f'Fail Fast: \n\t{"Yes" if self.__failfast else "No"}')
|
|
288
309
|
if len(self.__includePatterns) > 0:
|
|
@@ -295,7 +316,7 @@ Options:
|
|
|
295
316
|
print(f'\t{pattern}')
|
|
296
317
|
FilterManager.instance().print()
|
|
297
318
|
|
|
298
|
-
def
|
|
319
|
+
def print_version(self) -> None:
|
|
299
320
|
print(f'pUnit {__version__} ({__commit__})')
|
|
300
321
|
|
|
301
322
|
def validate(self) -> None:
|
|
@@ -314,7 +335,7 @@ Options:
|
|
|
314
335
|
self.__excludePatterns.append('/__*__')
|
|
315
336
|
|
|
316
337
|
@staticmethod
|
|
317
|
-
def parse(argv: list[str] = sys.argv) ->
|
|
338
|
+
def parse(argv: list[str] = sys.argv) -> CommandLineInterface:
|
|
318
339
|
result = CommandLineInterface().__parse(argv)
|
|
319
340
|
result.validate()
|
|
320
341
|
return result
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import inspect
|
|
5
5
|
from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, MethodType, ModuleType
|
|
6
|
-
from typing import Callable, Coroutine, Union, cast
|
|
6
|
+
from typing import Any, Callable, Coroutine, Union, cast
|
|
7
7
|
|
|
8
8
|
from ..metadata import CallableMetadata
|
|
9
9
|
|
|
@@ -24,13 +24,14 @@ class Fact:
|
|
|
24
24
|
def target(self) -> Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]:
|
|
25
25
|
return self.__target
|
|
26
26
|
|
|
27
|
-
async def execute(self, module: ModuleType) -> None:
|
|
27
|
+
async def execute(self, module: ModuleType) -> Any | None:
|
|
28
|
+
class_instance: Any | None = None
|
|
28
29
|
coro: Coroutine | None = None
|
|
29
30
|
if hasattr(self.__target, '__qualname__') and self.__target.__qualname__.find('.') > -1:
|
|
30
31
|
if isinstance(self.__target, staticmethod):
|
|
31
32
|
coro = self.__target()
|
|
32
33
|
else:
|
|
33
|
-
qnparts = self.__target.__qualname__.split('.')
|
|
34
|
+
qnparts = [p for p in self.__target.__qualname__.split('.') if p != '<locals>']
|
|
34
35
|
qntarget = module
|
|
35
36
|
for qnpart in qnparts[0:-1]:
|
|
36
37
|
qntarget = getattr(qntarget, qnpart)
|
|
@@ -38,17 +39,26 @@ class Fact:
|
|
|
38
39
|
coro = self.__target.__func__(qntarget)
|
|
39
40
|
else:
|
|
40
41
|
# every test execution gets a new instance of class
|
|
41
|
-
|
|
42
|
+
class_instance = cast(Any, cast(Callable, qntarget)())
|
|
43
|
+
coro = self.__target(class_instance)
|
|
42
44
|
else:
|
|
43
45
|
coro = self.__target()
|
|
44
46
|
if inspect.iscoroutine(coro):
|
|
45
47
|
await coro
|
|
48
|
+
return class_instance
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
def fact(target: Callable) -> Callable:
|
|
49
52
|
from .FactManager import FactManager
|
|
50
|
-
|
|
53
|
+
unwrapped = inspect.unwrap(target)
|
|
54
|
+
if not isinstance(unwrapped, (FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType)):
|
|
51
55
|
raise Exception('@fact can only be applied to functions and methods.')
|
|
56
|
+
if hasattr(unwrapped, '__punit_decorator'):
|
|
57
|
+
raise Exception(
|
|
58
|
+
f'@fact and {getattr(unwrapped, "__punit_decorator")} cannot decorate the same function. '
|
|
59
|
+
f'Function "{unwrapped.__name__}" has already been decorated.'
|
|
60
|
+
)
|
|
61
|
+
setattr(unwrapped, '__punit_decorator', '@fact')
|
|
52
62
|
fact: Fact = Fact(target)
|
|
53
63
|
FactManager.instance().put(fact)
|
|
54
64
|
return target
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
##
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
from typing import Optional
|
|
6
8
|
|
|
7
9
|
from ..filters.FilterManager import FilterManager
|
|
@@ -23,7 +25,7 @@ class FactManager:
|
|
|
23
25
|
self.__modules = {}
|
|
24
26
|
|
|
25
27
|
@staticmethod
|
|
26
|
-
def instance() ->
|
|
28
|
+
def instance() -> FactManager:
|
|
27
29
|
if FactManager.__instance is None:
|
|
28
30
|
FactManager.__instance = FactManager()
|
|
29
31
|
return FactManager.__instance
|
|
@@ -5,17 +5,28 @@ import re
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Filter:
|
|
8
|
+
"""
|
|
9
|
+
Wraps a glob-style filter pattern and compiles it to a case-insensitive regex.
|
|
10
|
+
|
|
11
|
+
Patterns use ``*`` for multi-character wildcards and ``?`` for single characters.
|
|
12
|
+
Prefix with ``!`` to mark the filter as an exclusion rule.
|
|
13
|
+
"""
|
|
8
14
|
|
|
9
15
|
__isExclude: bool
|
|
10
16
|
__pattern: str
|
|
11
|
-
__re: re.Pattern
|
|
17
|
+
__re: re.Pattern[str]
|
|
12
18
|
|
|
13
19
|
def __init__(self, pattern: str) -> None:
|
|
14
20
|
self.__isExclude = pattern.startswith('!')
|
|
15
21
|
self.__pattern = pattern
|
|
16
22
|
self.__re = self.__toRegex(pattern)
|
|
17
23
|
|
|
18
|
-
def __toRegex(self, pattern: str) -> re.Pattern:
|
|
24
|
+
def __toRegex(self, pattern: str) -> re.Pattern[str]:
|
|
25
|
+
"""Convert a glob-style pattern to a compiled regex.
|
|
26
|
+
|
|
27
|
+
Supports ``*`` (multi-char wildcard), ``?`` (single-char wildcard),
|
|
28
|
+
and ``!`` prefix (exclusion). Matching is case-insensitive.
|
|
29
|
+
"""
|
|
19
30
|
if self.__isExclude:
|
|
20
31
|
pattern = pattern[1:]
|
|
21
32
|
pattern = re.escape(pattern)\
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
import os
|
|
5
7
|
import select
|
|
6
8
|
import sys
|
|
@@ -22,7 +24,7 @@ class FilterManager:
|
|
|
22
24
|
return self.__filters
|
|
23
25
|
|
|
24
26
|
@staticmethod
|
|
25
|
-
def instance() ->
|
|
27
|
+
def instance() -> FilterManager:
|
|
26
28
|
instance = FilterManager.__instance
|
|
27
29
|
if instance is None:
|
|
28
30
|
instance = FilterManager()
|
|
@@ -14,7 +14,8 @@ class CallableMetadata:
|
|
|
14
14
|
|
|
15
15
|
def __init__(self, callable: Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]) -> None:
|
|
16
16
|
self.__callable = callable
|
|
17
|
-
|
|
17
|
+
parts = callable.__qualname__.replace(f'{callable.__module__}.', '').split('.')[0:-1]
|
|
18
|
+
self.__class_name = '.'.join(p for p in parts if p != '<locals>')
|
|
18
19
|
self.__module_name = callable.__module__
|
|
19
20
|
self.__name = callable.__name__
|
|
20
21
|
|
|
@@ -65,7 +65,7 @@ class JUnitTestSuite:
|
|
|
65
65
|
result: int = 0
|
|
66
66
|
if self.testCases is not None:
|
|
67
67
|
for testCase in self.testCases:
|
|
68
|
-
if testCase.disabled
|
|
68
|
+
if testCase.disabled is True:
|
|
69
69
|
result += 1
|
|
70
70
|
return result
|
|
71
71
|
|
|
@@ -187,7 +187,7 @@ class JUnitReportGenerator:
|
|
|
187
187
|
ele.text = testCase.stderr
|
|
188
188
|
testSuitesEle.attrib['disabled'] = str(totalDisabledCount)
|
|
189
189
|
testSuitesEle.attrib['errors'] = str(totalErrorCount)
|
|
190
|
-
testSuitesEle.attrib['failures'] = str(
|
|
190
|
+
testSuitesEle.attrib['failures'] = str(totalFailureCount)
|
|
191
191
|
testSuitesEle.attrib['tests'] = str(totalTestCount)
|
|
192
192
|
testSuitesEle.attrib['time'] = f'{totalTime:.6f}'.rstrip('0').rstrip('.')
|
|
193
193
|
et.indent(testSuitesEle, space=" ")
|