punit 0.0.27__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 +180 -0
- punit/__init__.py +8 -0
- punit/__main__.py +55 -0
- punit/assertions/__init__.py +7 -0
- punit/assertions/collections.py +18 -0
- punit/assertions/exceptions.py +33 -0
- punit/cli.py +261 -0
- punit/discovery/TestModuleDiscovery.py +104 -0
- punit/discovery/__init__.py +8 -0
- punit/facts/Fact.py +68 -0
- punit/facts/FactManager.py +50 -0
- punit/facts/__init__.py +5 -0
- punit/reports/HtmlReportGenerator.py +92 -0
- punit/reports/JUnitReportGenerator.py +185 -0
- punit/reports/__init__.py +5 -0
- punit/runner.py +118 -0
- punit/theories/Theory.py +84 -0
- punit/theories/TheoryManager.py +67 -0
- punit/theories/__init__.py +5 -0
- punit-0.0.27.dist-info/LICENSE +21 -0
- punit-0.0.27.dist-info/METADATA +174 -0
- punit-0.0.27.dist-info/RECORD +24 -0
- punit-0.0.27.dist-info/WHEEL +5 -0
- punit-0.0.27.dist-info/top_level.txt +1 -0
punit/TestResult.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from typing import TextIO
|
|
6
|
+
|
|
7
|
+
class TextIOCapture:
|
|
8
|
+
|
|
9
|
+
__quiet:bool
|
|
10
|
+
output:str = None
|
|
11
|
+
target:TextIO
|
|
12
|
+
|
|
13
|
+
def __init__(self, target:TextIO, quiet:bool = False) -> None:
|
|
14
|
+
self.__quiet = quiet
|
|
15
|
+
self.output = None
|
|
16
|
+
self.target = target
|
|
17
|
+
|
|
18
|
+
def write(self, text:str):
|
|
19
|
+
if self.output is None:
|
|
20
|
+
self.output = text
|
|
21
|
+
else:
|
|
22
|
+
self.output += text
|
|
23
|
+
if not self.__quiet:
|
|
24
|
+
self.target.write(text)
|
|
25
|
+
|
|
26
|
+
class TestResult:
|
|
27
|
+
|
|
28
|
+
__className:str
|
|
29
|
+
__exception:Exception
|
|
30
|
+
__fileName:str
|
|
31
|
+
__hostName:str
|
|
32
|
+
__isSuccess:bool
|
|
33
|
+
__moduleName:str
|
|
34
|
+
__packageName:str
|
|
35
|
+
__properties:dict[str, str]
|
|
36
|
+
__startTime:float
|
|
37
|
+
__stderrCapture:TextIOCapture
|
|
38
|
+
__stdoutCapture:TextIOCapture
|
|
39
|
+
__stopTime:float
|
|
40
|
+
__testName:str
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.__className = None
|
|
44
|
+
self.__exception = None
|
|
45
|
+
self.__fileName = None
|
|
46
|
+
self.__hostName = None
|
|
47
|
+
self.__isSuccess = None
|
|
48
|
+
self.__moduleName = None
|
|
49
|
+
self.__packageName = None
|
|
50
|
+
self.__properties = {}
|
|
51
|
+
self.__startTime = None
|
|
52
|
+
self.__stderrCapture = None
|
|
53
|
+
self.__stdoutCapture = None
|
|
54
|
+
self.__stopTime = None
|
|
55
|
+
self.__testName = None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def className(self) -> str:
|
|
59
|
+
return self.__className
|
|
60
|
+
|
|
61
|
+
@className.setter
|
|
62
|
+
def className(self, value:str) -> None:
|
|
63
|
+
self.__className = value
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def exception(self) -> Exception:
|
|
67
|
+
return self.__exception
|
|
68
|
+
|
|
69
|
+
@exception.setter
|
|
70
|
+
def exception(self, value:Exception) -> None:
|
|
71
|
+
self.__exception = value
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def fileName(self) -> str:
|
|
75
|
+
return self.__fileName
|
|
76
|
+
|
|
77
|
+
@fileName.setter
|
|
78
|
+
def fileName(self, value:str) -> None:
|
|
79
|
+
self.__fileName = value
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def hostName(self) -> str:
|
|
83
|
+
return self.__hostName
|
|
84
|
+
|
|
85
|
+
@hostName.setter
|
|
86
|
+
def hostName(self, value:str) -> None:
|
|
87
|
+
self.__hostName = value
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def isSuccess(self) -> bool:
|
|
91
|
+
return self.__isSuccess
|
|
92
|
+
|
|
93
|
+
@isSuccess.setter
|
|
94
|
+
def isSuccess(self, value:bool) -> None:
|
|
95
|
+
self.__isSuccess = value
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def moduleName(self) -> str:
|
|
99
|
+
return self.__moduleName
|
|
100
|
+
|
|
101
|
+
@moduleName.setter
|
|
102
|
+
def moduleName(self, value:str) -> None:
|
|
103
|
+
self.__moduleName = value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def packageName(self) -> str:
|
|
107
|
+
return self.__packageName
|
|
108
|
+
|
|
109
|
+
@packageName.setter
|
|
110
|
+
def packageName(self, value:str) -> None:
|
|
111
|
+
self.__packageName = value
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def properties(self) -> dict[str, str]:
|
|
115
|
+
return self.__properties
|
|
116
|
+
|
|
117
|
+
@properties.setter
|
|
118
|
+
def properties(self, value:dict[str, str]) -> None:
|
|
119
|
+
self.__properties = value
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def startTime(self) -> float:
|
|
123
|
+
return self.__startTime
|
|
124
|
+
|
|
125
|
+
@startTime.setter
|
|
126
|
+
def startTime(self, value:float) -> None:
|
|
127
|
+
self.__startTime = value
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def stderr(self) -> str:
|
|
131
|
+
return None if self.__stderrCapture is None else self.__stderrCapture.output
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def stdout(self) -> str:
|
|
135
|
+
return None if self.__stdoutCapture is None else self.__stdoutCapture.output
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def stopTime(self) -> float:
|
|
139
|
+
return self.__stopTime
|
|
140
|
+
|
|
141
|
+
@stopTime.setter
|
|
142
|
+
def stopTime(self, value:float) -> None:
|
|
143
|
+
self.__stopTime = value
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def testName(self) -> str:
|
|
147
|
+
return self.__testName
|
|
148
|
+
|
|
149
|
+
@testName.setter
|
|
150
|
+
def testName(self, value:str) -> None:
|
|
151
|
+
self.__testName = value
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def took(self) -> float:
|
|
155
|
+
return self.__stopTime - self.__startTime
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def tookPretty(self) -> str:
|
|
159
|
+
took = self.took
|
|
160
|
+
if took >= 1:
|
|
161
|
+
return f'{took:.1f}'.rstrip('0').rstrip('.') + 's'
|
|
162
|
+
elif took >= 0.001:
|
|
163
|
+
return f'{(took*1000):.0f}ms'
|
|
164
|
+
elif took >= 0.000001:
|
|
165
|
+
return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
|
|
166
|
+
elif took >= 0.000000001:
|
|
167
|
+
return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
|
|
168
|
+
else:
|
|
169
|
+
return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def captureOutput(self, quiet:bool = False) -> tuple[TextIO, TextIO]:
|
|
173
|
+
self.__stdoutCapture = TextIOCapture(sys.stdout, quiet)
|
|
174
|
+
self.__stderrCapture = TextIOCapture(sys.stderr, quiet)
|
|
175
|
+
sys.stdout = self.__stdoutCapture
|
|
176
|
+
sys.stderr = self.__stderrCapture
|
|
177
|
+
|
|
178
|
+
def releaseOutput(self) -> None:
|
|
179
|
+
sys.stdout = self.__stdoutCapture.target
|
|
180
|
+
sys.stderr = self.__stderrCapture.target
|
punit/__init__.py
ADDED
punit/__main__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from .cli import *
|
|
6
|
+
from .discovery import *
|
|
7
|
+
from .reports import HtmlReportGenerator, JUnitReportGenerator
|
|
8
|
+
from .runner import *
|
|
9
|
+
|
|
10
|
+
async def async_main():
|
|
11
|
+
ts = time.time()
|
|
12
|
+
cli = CommandLineInterface.parse()
|
|
13
|
+
if cli.help:
|
|
14
|
+
cli.printHelp()
|
|
15
|
+
elif cli.verbose and not cli.quiet:
|
|
16
|
+
cli.printSummary()
|
|
17
|
+
elif not cli.quiet:
|
|
18
|
+
cli.printVersion()
|
|
19
|
+
os.chdir(cli.workdir)
|
|
20
|
+
testModuleDiscovery = TestModuleDiscovery(
|
|
21
|
+
os.path.join(cli.workdir, cli.testPackageName),
|
|
22
|
+
cli.includePatterns,
|
|
23
|
+
cli.excludePatterns,
|
|
24
|
+
cli)
|
|
25
|
+
testModuleDiscovery.discover()
|
|
26
|
+
testRunner = TestRunner(cli.testPackageName, testModuleDiscovery.filenames, cli)
|
|
27
|
+
results = await testRunner.run()
|
|
28
|
+
totalTime = time.time() - ts
|
|
29
|
+
failureCount = 0
|
|
30
|
+
for result in results:
|
|
31
|
+
if not result.isSuccess:
|
|
32
|
+
failureCount += 1
|
|
33
|
+
if not cli.quiet:
|
|
34
|
+
print(f'Total: {len(results)}, Failures: {failureCount}, Took: {totalTime:.3f}s')
|
|
35
|
+
if cli.reportFormat is not None:
|
|
36
|
+
report:str = None
|
|
37
|
+
match cli.reportFormat:
|
|
38
|
+
case 'html':
|
|
39
|
+
report = HtmlReportGenerator().generate(results)
|
|
40
|
+
case 'junit':
|
|
41
|
+
report = JUnitReportGenerator().generate(results)
|
|
42
|
+
if cli.outputFilename is None:
|
|
43
|
+
print(report)
|
|
44
|
+
else:
|
|
45
|
+
with open(cli.outputFilename, 'wt') as file:
|
|
46
|
+
file.write(report)
|
|
47
|
+
print(f'\n("{cli.reportFormat}" report written to: {cli.outputFilename})')
|
|
48
|
+
if failureCount > 0:
|
|
49
|
+
exit(119)
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
asyncio.run(async_main())
|
|
53
|
+
|
|
54
|
+
if (__name__ == '__main__'):
|
|
55
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from typing import Any, Sequence
|
|
5
|
+
|
|
6
|
+
def areSame(a:Sequence[Any], b:Sequence[Any]) -> bool:
|
|
7
|
+
if a is b:
|
|
8
|
+
return True
|
|
9
|
+
elif a is None and b is not None:
|
|
10
|
+
return False
|
|
11
|
+
elif a is not None and b is None:
|
|
12
|
+
return False
|
|
13
|
+
elif len(a) != len(b):
|
|
14
|
+
return False
|
|
15
|
+
for i in range(len(a)):
|
|
16
|
+
if not a[i] == b[i]:
|
|
17
|
+
return False
|
|
18
|
+
return True
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from typing import Callable, get_args
|
|
5
|
+
|
|
6
|
+
def isNoneOrWhiteSpace(input:str):
|
|
7
|
+
return input is None or len(input.lstrip()) == 0
|
|
8
|
+
|
|
9
|
+
def isNoneOrEmpty(input:str):
|
|
10
|
+
return input is None or len(input) == 0
|
|
11
|
+
|
|
12
|
+
class raises[TError:Exception]:
|
|
13
|
+
|
|
14
|
+
__action:Callable
|
|
15
|
+
__exact:bool
|
|
16
|
+
__expect:TError
|
|
17
|
+
|
|
18
|
+
def __init__(self, action:Callable, *, exact:bool = False, expect:TError = None) -> None:
|
|
19
|
+
self.__action = action
|
|
20
|
+
self.__exact = exact
|
|
21
|
+
self.__expect = expect
|
|
22
|
+
|
|
23
|
+
def __bool__(self) -> bool:
|
|
24
|
+
try:
|
|
25
|
+
self.__action()
|
|
26
|
+
except BaseException as ex:
|
|
27
|
+
expected = self.__expect
|
|
28
|
+
if expected is None:
|
|
29
|
+
expected = get_args(self.__orig_class__)[0]
|
|
30
|
+
if self.__exact:
|
|
31
|
+
return type(ex) is expected
|
|
32
|
+
else:
|
|
33
|
+
return issubclass(type(ex), expected)
|
punit/cli.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import typing
|
|
7
|
+
from . import __version__
|
|
8
|
+
|
|
9
|
+
CommandLineInterface = typing.ForwardRef('CommandLineInterface')
|
|
10
|
+
|
|
11
|
+
class CommandLineInterface:
|
|
12
|
+
|
|
13
|
+
__excludePatterns:list[str]
|
|
14
|
+
__failfast:bool
|
|
15
|
+
__filterPattern:str
|
|
16
|
+
__help:bool
|
|
17
|
+
__includePatterns:list[str]
|
|
18
|
+
__no_default_patterns:bool
|
|
19
|
+
__outputFilename:str
|
|
20
|
+
__quiet:bool
|
|
21
|
+
__reportFormat:str
|
|
22
|
+
__testPackageName:str
|
|
23
|
+
__verbose:bool
|
|
24
|
+
__workdir:str
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.__aliases = {}
|
|
28
|
+
self.__excludePatterns = []
|
|
29
|
+
self.__failfast = False
|
|
30
|
+
self.__filterPattern = '*'
|
|
31
|
+
self.__help = False
|
|
32
|
+
self.__includePatterns = []
|
|
33
|
+
self.__no_default_patterns = False
|
|
34
|
+
self.__outputFilename = None
|
|
35
|
+
self.__quiet = False
|
|
36
|
+
self.__reportFormat = None
|
|
37
|
+
self.__testPackageName = 'tests'
|
|
38
|
+
self.__workdir = os.path.curdir
|
|
39
|
+
self.__verbose = False
|
|
40
|
+
|
|
41
|
+
def __parse(self, argv:list[str]) -> CommandLineInterface:
|
|
42
|
+
aliasName:str = None
|
|
43
|
+
extractFilterPattern:bool = False
|
|
44
|
+
extractExcludePattern:bool = False
|
|
45
|
+
extractIncludePattern:bool = False
|
|
46
|
+
extractAliasName:bool = False
|
|
47
|
+
extractAliasPath:bool = False
|
|
48
|
+
extractReportFormat:bool = False
|
|
49
|
+
extractOutputFilename:bool = False
|
|
50
|
+
for arg in argv:
|
|
51
|
+
if extractFilterPattern:
|
|
52
|
+
self.__filterPattern = arg
|
|
53
|
+
extractFilterPattern = False
|
|
54
|
+
continue
|
|
55
|
+
if extractIncludePattern:
|
|
56
|
+
self.__includePatterns.append(arg)
|
|
57
|
+
extractIncludePattern = False
|
|
58
|
+
continue
|
|
59
|
+
elif extractExcludePattern:
|
|
60
|
+
self.__excludePatterns.append(arg)
|
|
61
|
+
extractExcludePattern = False
|
|
62
|
+
continue
|
|
63
|
+
elif extractAliasName:
|
|
64
|
+
aliasName = arg
|
|
65
|
+
extractAliasName = False
|
|
66
|
+
extractAliasPath = True
|
|
67
|
+
continue
|
|
68
|
+
elif extractAliasPath:
|
|
69
|
+
extractAliasPath = False
|
|
70
|
+
self.__aliases[aliasName] = arg
|
|
71
|
+
continue
|
|
72
|
+
elif self.__workdir is None:
|
|
73
|
+
self.__workdir = arg
|
|
74
|
+
continue
|
|
75
|
+
elif self.__testPackageName is None:
|
|
76
|
+
self.__testPackageName = arg
|
|
77
|
+
continue
|
|
78
|
+
elif extractReportFormat:
|
|
79
|
+
extractReportFormat = False
|
|
80
|
+
self.__reportFormat = arg.lower()
|
|
81
|
+
match self.__reportFormat:
|
|
82
|
+
case 'html' | 'junit':
|
|
83
|
+
pass
|
|
84
|
+
case _:
|
|
85
|
+
print(f'Unsupported value "{arg}" for --report argument, aborting.')
|
|
86
|
+
print(f'(valid values are "json" and "junit")')
|
|
87
|
+
exit(4)
|
|
88
|
+
continue
|
|
89
|
+
elif extractOutputFilename:
|
|
90
|
+
extractOutputFilename = False
|
|
91
|
+
self.__outputFilename = arg
|
|
92
|
+
continue
|
|
93
|
+
match arg:
|
|
94
|
+
case '-h' | '--help':
|
|
95
|
+
self.__help = True
|
|
96
|
+
case '-a':
|
|
97
|
+
extractAliasName = True
|
|
98
|
+
case '-t' | '--filter':
|
|
99
|
+
extractFilterPattern = True
|
|
100
|
+
case '-e' | '--exclude':
|
|
101
|
+
extractExcludePattern = True
|
|
102
|
+
case '-f' | '--failfast':
|
|
103
|
+
self.__failfast = True
|
|
104
|
+
case '-p' | '--test-package':
|
|
105
|
+
self.__testPackageName = None
|
|
106
|
+
case '-i' | '--include':
|
|
107
|
+
extractIncludePattern = True
|
|
108
|
+
case '-q' | '--quiet':
|
|
109
|
+
self.__quiet = True
|
|
110
|
+
case '-w' | '--working-directory':
|
|
111
|
+
self.__workdir = None
|
|
112
|
+
case '-v' | '--verbose':
|
|
113
|
+
self.__verbose = True
|
|
114
|
+
case '-n' | '--no-default-patterns':
|
|
115
|
+
self.__no_default_patterns = True
|
|
116
|
+
case '-r' | '--report':
|
|
117
|
+
extractReportFormat = True
|
|
118
|
+
case '-o' | '--output':
|
|
119
|
+
extractOutputFilename = True
|
|
120
|
+
case _:
|
|
121
|
+
continue
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def aliases(self) -> bool:
|
|
126
|
+
return self.__aliases
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def failfast(self) -> bool:
|
|
130
|
+
return self.__failfast
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def filterPattern(self) -> str:
|
|
134
|
+
return self.__filterPattern
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def excludePatterns(self) -> list[str]:
|
|
138
|
+
return self.__excludePatterns
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def help(self) -> bool:
|
|
142
|
+
return self.__help
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def includePatterns(self) -> list[str]:
|
|
146
|
+
return self.__includePatterns
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def outputFilename(self) -> str|None:
|
|
150
|
+
return self.__outputFilename
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def quiet(self) -> bool:
|
|
154
|
+
return self.__quiet or (self.__reportFormat is not None and self.__outputFilename is None)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def reportFormat(self) -> str|None:
|
|
158
|
+
return self.__reportFormat
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def testPackageName(self) -> str:
|
|
162
|
+
return self.__testPackageName
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def verbose(self) -> bool:
|
|
166
|
+
return self.__verbose and self.__reportFormat is None
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def workdir(self) -> str:
|
|
170
|
+
return self.__workdir
|
|
171
|
+
|
|
172
|
+
def printHelp(self) -> None:
|
|
173
|
+
self.printVersion()
|
|
174
|
+
print(
|
|
175
|
+
"""
|
|
176
|
+
Usage: python3 -m punit [-h|--help]
|
|
177
|
+
[-q|--quiet] [-v|--verbose]
|
|
178
|
+
[-f|--failfast]
|
|
179
|
+
[-p|--test-package NAME]
|
|
180
|
+
[-i|--include PATTERN]
|
|
181
|
+
[-e|--exclude PATTERN]
|
|
182
|
+
[-t|--filter PATTERN]
|
|
183
|
+
[-w|--workdir DIRECTORY]
|
|
184
|
+
[-n|--no-default-patterns]
|
|
185
|
+
[-r|--report {junit|json}]
|
|
186
|
+
[-o|--output FILENAME]
|
|
187
|
+
|
|
188
|
+
Options:
|
|
189
|
+
-h, --help Show this help text and exit
|
|
190
|
+
-q, --quiet Quiet output
|
|
191
|
+
-v, --verbose Verbose output
|
|
192
|
+
-f, --failfast Stop on first failure or error
|
|
193
|
+
-p, --test-package NAME
|
|
194
|
+
Use NAME as the test package, all tests should
|
|
195
|
+
be locatable as modules in the named package.
|
|
196
|
+
Default: 'tests'
|
|
197
|
+
-i, --include PATTERN
|
|
198
|
+
Include any tests matching PATTERN
|
|
199
|
+
Default: '*.py'
|
|
200
|
+
-e, --exclude PATTERN
|
|
201
|
+
Exclude any tests matching PATTERN, overriding --include
|
|
202
|
+
Default: '__*__' (dunder files), '/.*/' (dot-directories)
|
|
203
|
+
-t, --filter
|
|
204
|
+
Only execute tests matching PATTERN
|
|
205
|
+
Default: '*'
|
|
206
|
+
-w, --working-directory DIRECTORY
|
|
207
|
+
Working directory (defaults to start directory)
|
|
208
|
+
-n, --no-default-patterns
|
|
209
|
+
Do not apply any default include/exclude patterns.
|
|
210
|
+
-r, --report {html|junit}
|
|
211
|
+
Generate a report to stdout using either an "html"
|
|
212
|
+
or "junit" format. When generating a report to stdout
|
|
213
|
+
all other output is suppressed, unless `--output`
|
|
214
|
+
is also specified.
|
|
215
|
+
-o, --output FILENAME
|
|
216
|
+
If `--report` is used, instead of writing to stdout
|
|
217
|
+
write to FILENAME. In this case `--report` does not
|
|
218
|
+
suppress any program output.
|
|
219
|
+
"""
|
|
220
|
+
)
|
|
221
|
+
exit(0)
|
|
222
|
+
|
|
223
|
+
def printSummary(self) -> None:
|
|
224
|
+
self.printVersion()
|
|
225
|
+
print(f'Working Directory:\n\t{self.__workdir}')
|
|
226
|
+
print(f'Fail Fast: \n\t{"Yes" if self.__failfast else "No"}')
|
|
227
|
+
if len(self.__includePatterns) > 0:
|
|
228
|
+
print('Include Patterns:')
|
|
229
|
+
for pattern in self.__includePatterns:
|
|
230
|
+
print(f'\t{pattern}')
|
|
231
|
+
if len(self.__excludePatterns) > 0:
|
|
232
|
+
print('Exclude Patterns:')
|
|
233
|
+
for pattern in self.__excludePatterns:
|
|
234
|
+
print(f'\t{pattern}')
|
|
235
|
+
print('Filter Pattern:')
|
|
236
|
+
print(f'\t{self.__filterPattern}')
|
|
237
|
+
def printVersion(self) -> None:
|
|
238
|
+
print(f'pUnit {__version__}')
|
|
239
|
+
|
|
240
|
+
def validate(self) -> None:
|
|
241
|
+
if self.__workdir is None or len(self.__workdir.lstrip()) == 0 or self.__workdir.startswith('-'):
|
|
242
|
+
print(f'Invalid working directory specified: {self.__workdir}')
|
|
243
|
+
exit(1)
|
|
244
|
+
elif not os.path.isdir(self.__workdir):
|
|
245
|
+
print(f'Working directory does not exist: {self.__workdir}')
|
|
246
|
+
exit(2)
|
|
247
|
+
self.__workdir = os.path.abspath(self.__workdir)
|
|
248
|
+
if not self.__no_default_patterns:
|
|
249
|
+
# if no other patterns specified, default to including all files found in the directory matching `testPackageName`
|
|
250
|
+
if len(self.__includePatterns) == 0:
|
|
251
|
+
self.__includePatterns.append(f'/{self.testPackageName}/*.py')
|
|
252
|
+
# always exclude dot-folders (.git, .venv, etc)
|
|
253
|
+
self.__excludePatterns.append('/.*')
|
|
254
|
+
# always exclude dunder files
|
|
255
|
+
self.__excludePatterns.append('/__*__')
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def parse(argv:list[str] = sys.argv) -> CommandLineInterface:
|
|
259
|
+
result = CommandLineInterface().__parse(argv)
|
|
260
|
+
result.validate()
|
|
261
|
+
return result
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from ..facts import FactManager
|
|
8
|
+
from ..theories import TheoryManager
|
|
9
|
+
from ..cli import CommandLineInterface
|
|
10
|
+
|
|
11
|
+
class TestModuleDiscovery:
|
|
12
|
+
|
|
13
|
+
__cli:CommandLineInterface
|
|
14
|
+
__excludePatterns:list[re.Pattern]
|
|
15
|
+
__filenames:list[str]
|
|
16
|
+
__filterPattern:re.Pattern
|
|
17
|
+
__includePatterns:list[re.Pattern]
|
|
18
|
+
__workdir:str
|
|
19
|
+
|
|
20
|
+
def __init__(self, workdir:str, includePatterns:list[str], excludePatterns:list[str], cli:CommandLineInterface):
|
|
21
|
+
self.__cli = cli
|
|
22
|
+
self.__excludePatterns = []
|
|
23
|
+
if excludePatterns is not None:
|
|
24
|
+
for pattern in excludePatterns:
|
|
25
|
+
self.__excludePatterns.append(
|
|
26
|
+
re.compile(
|
|
27
|
+
self.__convertPatternToRegex(pattern),
|
|
28
|
+
re.IGNORECASE))
|
|
29
|
+
self.__filenames = []
|
|
30
|
+
self.__filterPattern = re.compile(self.__convertPatternToRegex(cli.filterPattern))
|
|
31
|
+
self.__includePatterns = []
|
|
32
|
+
if includePatterns is not None:
|
|
33
|
+
for pattern in includePatterns:
|
|
34
|
+
self.__includePatterns.append(
|
|
35
|
+
re.compile(
|
|
36
|
+
self.__convertPatternToRegex(pattern),
|
|
37
|
+
re.IGNORECASE))
|
|
38
|
+
self.__workdir = workdir
|
|
39
|
+
|
|
40
|
+
def __convertPatternToRegex(self, pattern:str):
|
|
41
|
+
result = re.escape(pattern)\
|
|
42
|
+
.replace('\\\\', '/')\
|
|
43
|
+
.replace('\\*', r'.+')\
|
|
44
|
+
.replace('?', '.')
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
def __testAnyInclude(self, input:str) -> bool:
|
|
48
|
+
for pat in self.__includePatterns:
|
|
49
|
+
if len(pat.findall(input)) > 0:
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def __testAnyExclude(self, input:str) -> bool:
|
|
54
|
+
for pat in self.__excludePatterns:
|
|
55
|
+
if len(pat.findall(input)) > 0:
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def __walkDirectory(self, path:str) -> list[str]:
|
|
60
|
+
filenames = []
|
|
61
|
+
if os.path.isdir(path):
|
|
62
|
+
for dname, dlist, flist in os.walk(path, topdown=True):
|
|
63
|
+
if self.__cli.verbose:
|
|
64
|
+
print(f'.. discovery for: {dname}')
|
|
65
|
+
dname2 = dname.replace('\\', '/')
|
|
66
|
+
if self.__testAnyExclude(dname2):
|
|
67
|
+
# directory is excluded, prune all children
|
|
68
|
+
if self.__cli.verbose:
|
|
69
|
+
print(f'Excluded: {dname}')
|
|
70
|
+
while len(dlist) > 0:
|
|
71
|
+
del dlist[0]
|
|
72
|
+
while len(flist) > 0:
|
|
73
|
+
del flist[0]
|
|
74
|
+
# determine if any individual subdirs should be pruned
|
|
75
|
+
dlnames = dlist.copy()
|
|
76
|
+
for dlname in dlnames:
|
|
77
|
+
dlname2 = dlname.replace('\\', '/')
|
|
78
|
+
if self.__testAnyExclude(dlname2):
|
|
79
|
+
if self.__cli.verbose:
|
|
80
|
+
print(f'Excluded: {dlname}')
|
|
81
|
+
dlist.remove(dlname)
|
|
82
|
+
# determine if any individual files should be pruned
|
|
83
|
+
for fname in flist:
|
|
84
|
+
fname2 = os.path.join(dname, fname).replace('\\', '/')
|
|
85
|
+
if self.__testAnyInclude(fname2) and not self.__testAnyExclude(fname2):
|
|
86
|
+
if fname.endswith('.py'):
|
|
87
|
+
if self.__cli.verbose:
|
|
88
|
+
print(f'Included: {fname}')
|
|
89
|
+
filenames.append(fname2)
|
|
90
|
+
return filenames
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def filenames(self) -> list[str]:
|
|
94
|
+
return self.__filenames
|
|
95
|
+
|
|
96
|
+
def discover(self) -> list[str]:
|
|
97
|
+
FactManager.instance().filterPattern = self.__filterPattern
|
|
98
|
+
TheoryManager.instance().filterPattern = self.__filterPattern
|
|
99
|
+
if self.__cli.verbose:
|
|
100
|
+
print(f'.. starting test discovery')
|
|
101
|
+
self.__filenames = self.__walkDirectory(self.__workdir)
|
|
102
|
+
if self.__cli.verbose:
|
|
103
|
+
print('.. finished test discovery.')
|
|
104
|
+
return self.__filenames
|