pUnit 1.2.0__py3-none-any.whl
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/TestResult.py +186 -0
- punit/__init__.py +21 -0
- punit/__main__.py +59 -0
- punit/assertions/__init__.py +11 -0
- punit/assertions/collections.py +76 -0
- punit/assertions/exceptions.py +37 -0
- punit/assertions/strings.py +55 -0
- punit/cli.py +294 -0
- punit/discovery/TestModuleDiscovery.py +113 -0
- punit/discovery/__init__.py +9 -0
- punit/facts/Fact.py +82 -0
- punit/facts/FactManager.py +94 -0
- punit/facts/__init__.py +11 -0
- punit/reports/HtmlReportGenerator.py +93 -0
- punit/reports/JUnitReportGenerator.py +196 -0
- punit/reports/__init__.py +5 -0
- punit/runner.py +118 -0
- punit/theories/Theory.py +100 -0
- punit/theories/TheoryManager.py +110 -0
- punit/theories/__init__.py +11 -0
- punit/traits/Trait.py +34 -0
- punit/traits/__init__.py +9 -0
- punit-1.2.0.data/data/bin/punit +7 -0
- punit-1.2.0.dist-info/METADATA +207 -0
- punit-1.2.0.dist-info/RECORD +28 -0
- punit-1.2.0.dist-info/WHEEL +5 -0
- punit-1.2.0.dist-info/licenses/LICENSE +21 -0
- punit-1.2.0.dist-info/top_level.txt +1 -0
punit/TestResult.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, TextIO, Optional, cast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TextIOCapture:
|
|
9
|
+
|
|
10
|
+
__quiet:bool
|
|
11
|
+
output:str|None = None
|
|
12
|
+
target:TextIO
|
|
13
|
+
|
|
14
|
+
def __init__(self, target:TextIO, quiet:bool = False) -> None:
|
|
15
|
+
self.__quiet = quiet
|
|
16
|
+
self.output = None
|
|
17
|
+
self.target = target
|
|
18
|
+
|
|
19
|
+
def write(self, text:str):
|
|
20
|
+
if self.output is None:
|
|
21
|
+
self.output = text
|
|
22
|
+
else:
|
|
23
|
+
self.output += text
|
|
24
|
+
if not self.__quiet:
|
|
25
|
+
self.target.write(text)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestResult:
|
|
29
|
+
|
|
30
|
+
__className:Optional[str]
|
|
31
|
+
__exception:Exception|None
|
|
32
|
+
__fileName:str|None
|
|
33
|
+
__hostName:str|None
|
|
34
|
+
__isSuccess:bool|None
|
|
35
|
+
__moduleName:str|None
|
|
36
|
+
__packageName:str|None
|
|
37
|
+
__properties:dict[str, Any]
|
|
38
|
+
__startTime:float|None
|
|
39
|
+
__stderrCapture:TextIOCapture|None
|
|
40
|
+
__stdoutCapture:TextIOCapture|None
|
|
41
|
+
__stopTime:float|None
|
|
42
|
+
__testName:str|None
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.__className = None
|
|
46
|
+
self.__exception = None
|
|
47
|
+
self.__fileName = None
|
|
48
|
+
self.__hostName = None
|
|
49
|
+
self.__isSuccess = None
|
|
50
|
+
self.__moduleName = None
|
|
51
|
+
self.__packageName = None
|
|
52
|
+
self.__properties = dict[str,Any]()
|
|
53
|
+
self.__startTime = None
|
|
54
|
+
self.__stderrCapture = None
|
|
55
|
+
self.__stdoutCapture = None
|
|
56
|
+
self.__stopTime = None
|
|
57
|
+
self.__testName = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def className(self) -> Optional[str]:
|
|
61
|
+
return self.__className
|
|
62
|
+
|
|
63
|
+
@className.setter
|
|
64
|
+
def className(self, value:Optional[str]) -> None:
|
|
65
|
+
self.__className = value
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def exception(self) -> Exception|None:
|
|
69
|
+
return self.__exception
|
|
70
|
+
|
|
71
|
+
@exception.setter
|
|
72
|
+
def exception(self, value:Exception) -> None:
|
|
73
|
+
self.__exception = value
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def fileName(self) -> str|None:
|
|
77
|
+
return self.__fileName
|
|
78
|
+
|
|
79
|
+
@fileName.setter
|
|
80
|
+
def fileName(self, value:str) -> None:
|
|
81
|
+
self.__fileName = value
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def hostName(self) -> str|None:
|
|
85
|
+
return self.__hostName
|
|
86
|
+
|
|
87
|
+
@hostName.setter
|
|
88
|
+
def hostName(self, value:str) -> None:
|
|
89
|
+
self.__hostName = value
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def isSuccess(self) -> bool:
|
|
93
|
+
return False if self.__isSuccess is None else self.__isSuccess
|
|
94
|
+
|
|
95
|
+
@isSuccess.setter
|
|
96
|
+
def isSuccess(self, value:bool) -> None:
|
|
97
|
+
self.__isSuccess = value
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def moduleName(self) -> str:
|
|
101
|
+
return cast(str,self.__moduleName)
|
|
102
|
+
|
|
103
|
+
@moduleName.setter
|
|
104
|
+
def moduleName(self, value:str) -> None:
|
|
105
|
+
self.__moduleName = value
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def packageName(self) -> str|None:
|
|
109
|
+
return self.__packageName
|
|
110
|
+
|
|
111
|
+
@packageName.setter
|
|
112
|
+
def packageName(self, value:str) -> None:
|
|
113
|
+
self.__packageName = value
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def properties(self) -> dict[str, Any]:
|
|
117
|
+
return self.__properties
|
|
118
|
+
|
|
119
|
+
@properties.setter
|
|
120
|
+
def properties(self, value:dict[str, str]) -> None:
|
|
121
|
+
self.__properties = value
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def startTime(self) -> float|None:
|
|
125
|
+
return self.__startTime
|
|
126
|
+
|
|
127
|
+
@startTime.setter
|
|
128
|
+
def startTime(self, value:float) -> None:
|
|
129
|
+
self.__startTime = value
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def stderr(self) -> str|None:
|
|
133
|
+
return None if self.__stderrCapture is None else self.__stderrCapture.output
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def stdout(self) -> str|None:
|
|
137
|
+
return None if self.__stdoutCapture is None else self.__stdoutCapture.output
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def stopTime(self) -> float|None:
|
|
141
|
+
return self.__stopTime
|
|
142
|
+
|
|
143
|
+
@stopTime.setter
|
|
144
|
+
def stopTime(self, value:float) -> None:
|
|
145
|
+
self.__stopTime = value
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def testName(self) -> str|None:
|
|
149
|
+
return self.__testName
|
|
150
|
+
|
|
151
|
+
@testName.setter
|
|
152
|
+
def testName(self, value:str) -> None:
|
|
153
|
+
self.__testName = value
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def took(self) -> float|None:
|
|
157
|
+
return None if self.__stopTime is None or self.__startTime is None else self.__stopTime - self.__startTime
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def tookPretty(self) -> str:
|
|
161
|
+
took = self.took
|
|
162
|
+
if took is None:
|
|
163
|
+
return 'N/A'
|
|
164
|
+
elif took >= 1:
|
|
165
|
+
return f'{took:.1f}'.rstrip('0').rstrip('.') + 's'
|
|
166
|
+
elif took >= 0.001:
|
|
167
|
+
return f'{(took*1000):.0f}ms'
|
|
168
|
+
elif took >= 0.000001:
|
|
169
|
+
return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
|
|
170
|
+
elif took >= 0.000000001:
|
|
171
|
+
return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
|
|
172
|
+
else:
|
|
173
|
+
return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def captureOutput(self, quiet:bool = False) -> None:
|
|
177
|
+
self.__stdoutCapture = TextIOCapture(sys.stdout, quiet)
|
|
178
|
+
self.__stderrCapture = TextIOCapture(sys.stderr, quiet)
|
|
179
|
+
sys.stdout = self.__stdoutCapture
|
|
180
|
+
sys.stderr = self.__stderrCapture
|
|
181
|
+
|
|
182
|
+
def releaseOutput(self) -> None:
|
|
183
|
+
if self.__stdoutCapture is not None and self.__stdoutCapture.target is not None:
|
|
184
|
+
sys.stdout = self.__stdoutCapture.target
|
|
185
|
+
if self.__stderrCapture is not None and self.__stderrCapture.target is not None:
|
|
186
|
+
sys.stderr = self.__stderrCapture.target
|
punit/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
__version__ = '1.2.0'
|
|
5
|
+
|
|
6
|
+
from .assertions import *
|
|
7
|
+
from .facts import *
|
|
8
|
+
from .theories import *
|
|
9
|
+
from .traits import *
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'assertions',
|
|
14
|
+
'collections', 'exceptions', 'strings',
|
|
15
|
+
'facts',
|
|
16
|
+
'fact',
|
|
17
|
+
'theories',
|
|
18
|
+
'theory', 'inlinedata',
|
|
19
|
+
'traits',
|
|
20
|
+
'trait'
|
|
21
|
+
]
|
punit/__main__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from .cli import CommandLineInterface
|
|
8
|
+
from .discovery import TestModuleDiscovery
|
|
9
|
+
from .reports import HtmlReportGenerator, JUnitReportGenerator
|
|
10
|
+
from .runner import TestRunner
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def async_main():
|
|
14
|
+
ts = time.time()
|
|
15
|
+
cli = CommandLineInterface.parse()
|
|
16
|
+
if cli.help:
|
|
17
|
+
cli.printHelp()
|
|
18
|
+
elif cli.verbose and not cli.quiet:
|
|
19
|
+
cli.printSummary()
|
|
20
|
+
elif not cli.quiet:
|
|
21
|
+
cli.printVersion()
|
|
22
|
+
os.chdir(cli.workdir)
|
|
23
|
+
testModuleDiscovery = TestModuleDiscovery(
|
|
24
|
+
os.path.join(cli.workdir, cli.testPackageName),
|
|
25
|
+
cli.includePatterns,
|
|
26
|
+
cli.excludePatterns,
|
|
27
|
+
cli)
|
|
28
|
+
testModuleDiscovery.discover()
|
|
29
|
+
testRunner = TestRunner(cli.testPackageName, testModuleDiscovery.filenames, cli)
|
|
30
|
+
results = await testRunner.run()
|
|
31
|
+
totalTime = time.time() - ts
|
|
32
|
+
failureCount = 0
|
|
33
|
+
for result in results:
|
|
34
|
+
if not result.isSuccess:
|
|
35
|
+
failureCount += 1
|
|
36
|
+
if not cli.quiet:
|
|
37
|
+
print(f'Total: {len(results)}, Failures: {failureCount}, Took: {totalTime:.3f}s')
|
|
38
|
+
if cli.reportFormat is not None:
|
|
39
|
+
report:str|None = None
|
|
40
|
+
match cli.reportFormat:
|
|
41
|
+
case 'html':
|
|
42
|
+
report = HtmlReportGenerator().generate(results)
|
|
43
|
+
case 'junit':
|
|
44
|
+
report = JUnitReportGenerator().generate(results)
|
|
45
|
+
if report is not None:
|
|
46
|
+
if cli.outputFilename is None:
|
|
47
|
+
print(report)
|
|
48
|
+
else:
|
|
49
|
+
with open(cli.outputFilename, 'wb') as file:
|
|
50
|
+
file.write(report.encode())
|
|
51
|
+
print(f'\n("{cli.reportFormat}" report written to: {cli.outputFilename})')
|
|
52
|
+
if failureCount > 0:
|
|
53
|
+
exit(119)
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
asyncio.run(async_main())
|
|
57
|
+
|
|
58
|
+
if (__name__ == '__main__'):
|
|
59
|
+
main()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Sequence, Optional, cast
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def areSame(a:Sequence[Any]|None, b:Sequence[Any]|None, sort:bool=False, sortFunction:Optional[Callable[[Any], Any]]=None) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Check if two sequences contain the same elements in the same order.
|
|
10
|
+
|
|
11
|
+
:param Sequence[Any]|None a: The sequence to check
|
|
12
|
+
:param Sequence[Any]|None b: The sequence to compare against
|
|
13
|
+
:param Optional[bool] sort: Sort sequences before performing comparisons.
|
|
14
|
+
:param Optional[Callable[[Any], Any]] sortFunction: Custom function to use when sorting.
|
|
15
|
+
:returns bool: True if the sequences contain the same elements in the same order, False otherwise.
|
|
16
|
+
"""
|
|
17
|
+
if a is b:
|
|
18
|
+
return True
|
|
19
|
+
elif a is None and b is not None:
|
|
20
|
+
return False
|
|
21
|
+
elif a is not None and b is None:
|
|
22
|
+
return False
|
|
23
|
+
elif a is not None and b is not None:
|
|
24
|
+
if len(a) != len(b):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
if sort:
|
|
28
|
+
if isinstance(a, dict) or isinstance(b, dict):
|
|
29
|
+
sortFunction = sortFunction if sortFunction is not None else lambda e: e[0]
|
|
30
|
+
a = sorted(cast(dict,a).items(), key=sortFunction)
|
|
31
|
+
b = sorted(cast(dict,b).items(), key=sortFunction)
|
|
32
|
+
else:
|
|
33
|
+
sortFunction = sortFunction if sortFunction is not None else lambda e: e
|
|
34
|
+
a = sorted(a, key=sortFunction)
|
|
35
|
+
b = sorted(b, key=sortFunction)
|
|
36
|
+
|
|
37
|
+
if isinstance(a, dict) or isinstance(b, dict):
|
|
38
|
+
for pairs in zip(cast(dict,a).items(), cast(dict,b).items()):
|
|
39
|
+
if not areSame(pairs[0], pairs[1]):
|
|
40
|
+
return False
|
|
41
|
+
else:
|
|
42
|
+
for pairs in zip(a, b):
|
|
43
|
+
if isinstance(pairs[0], dict) or isinstance(pairs[1], dict):
|
|
44
|
+
if not areSame(pairs[0], pairs[1]):
|
|
45
|
+
return False
|
|
46
|
+
elif pairs[0] != pairs[1]:
|
|
47
|
+
return False
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
def hasLength(sequence:Sequence[Any]|None, expected:int|None) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Check if a sequence has the expected number of elements.
|
|
53
|
+
|
|
54
|
+
:param Sequence[Any]|None sequence: The sequence to check
|
|
55
|
+
:param int|None expected: The expected number of elements
|
|
56
|
+
|
|
57
|
+
:returns bool: True if the sequence has exactly the expected number of elements, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
if sequence is None and (expected is None or expected == 0):
|
|
60
|
+
return True
|
|
61
|
+
elif sequence is None and (expected is not None and expected != 0):
|
|
62
|
+
return False
|
|
63
|
+
elif sequence is not None and expected is None:
|
|
64
|
+
return False
|
|
65
|
+
return sequence is not None and len(sequence) == expected
|
|
66
|
+
|
|
67
|
+
def isNoneOrEmpty(sequence:Sequence[Any]|None) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if a sequence is None or empty.
|
|
70
|
+
|
|
71
|
+
:param Sequence[Any]|None sequence: The sequence to check
|
|
72
|
+
:returns bool: True if the sequence is None or empty, False otherwise
|
|
73
|
+
"""
|
|
74
|
+
if sequence is None:
|
|
75
|
+
return True
|
|
76
|
+
return len(sequence) == 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Optional, cast, get_args
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def isNoneOrWhiteSpace(input:str):
|
|
8
|
+
return input is None or len(input.lstrip()) == 0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def isNoneOrEmpty(input:str):
|
|
12
|
+
return input is None or len(input) == 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class raises[TError:Exception]:
|
|
16
|
+
|
|
17
|
+
__action:Callable
|
|
18
|
+
__exact:bool
|
|
19
|
+
__expect:Optional[TError|type]
|
|
20
|
+
|
|
21
|
+
def __init__(self, action:Callable, *, exact:bool = False, expect:Optional[TError|type] = None) -> None:
|
|
22
|
+
self.__action = action
|
|
23
|
+
self.__exact = exact
|
|
24
|
+
self.__expect = expect
|
|
25
|
+
|
|
26
|
+
def __bool__(self) -> bool:
|
|
27
|
+
try:
|
|
28
|
+
self.__action()
|
|
29
|
+
except BaseException as ex:
|
|
30
|
+
expected = self.__expect
|
|
31
|
+
if expected is None and hasattr(self, '__orig_class__') is not None:
|
|
32
|
+
expected = get_args(getattr(self, '__orig_class__'))[0]
|
|
33
|
+
if self.__exact:
|
|
34
|
+
return type(ex) is expected
|
|
35
|
+
elif expected is not None:
|
|
36
|
+
return issubclass(type(ex), cast(Any,expected))
|
|
37
|
+
return False
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
def areSame(a:str|None, b:str|None) -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Check if two strings contain the same characters in the same order.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
a: The first string
|
|
12
|
+
b: The second string
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
True if the strings contain the same characters in the same order, False otherwise
|
|
16
|
+
"""
|
|
17
|
+
if a is None and b is not None:
|
|
18
|
+
return False
|
|
19
|
+
elif a is not None and b is None:
|
|
20
|
+
return False
|
|
21
|
+
elif a is not None and b is not None:
|
|
22
|
+
if len(a) != len(b):
|
|
23
|
+
return False
|
|
24
|
+
for i in range(len(a)):
|
|
25
|
+
if a[i] != b[i]:
|
|
26
|
+
return False
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
def isNoneOrEmpty(string:str|None) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Check if a string is None or empty.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
string: The string to check
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if the string is None or empty, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
if string is None:
|
|
40
|
+
return True
|
|
41
|
+
return len(string) == 0
|
|
42
|
+
|
|
43
|
+
def isNoneOrWhitespace(string:str|None) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Check if a string is None or whitespace.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
string: The string to check
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if the string is None or whitespace, False otherwise
|
|
52
|
+
"""
|
|
53
|
+
if string is None:
|
|
54
|
+
return True
|
|
55
|
+
return len(string.strip()) == 0
|