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/cli.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .traits import Trait
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandLineInterface:
|
|
12
|
+
|
|
13
|
+
__aliases:dict[str,str]
|
|
14
|
+
__excludePatterns:list[str]
|
|
15
|
+
__excludeTraits:list[Trait]
|
|
16
|
+
__failfast:bool
|
|
17
|
+
__filterPattern:str
|
|
18
|
+
__help:bool
|
|
19
|
+
__includePatterns:list[str]
|
|
20
|
+
__includeTraits:list[Trait]
|
|
21
|
+
__no_default_patterns:bool
|
|
22
|
+
__outputFilename:Optional[str]
|
|
23
|
+
__quiet:bool
|
|
24
|
+
__reportFormat:Optional[str]
|
|
25
|
+
__testPackageName:str|None
|
|
26
|
+
__verbose:bool
|
|
27
|
+
__workdir:str|None
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.__aliases = dict[str,str]()
|
|
31
|
+
self.__excludePatterns = []
|
|
32
|
+
self.__excludeTraits = []
|
|
33
|
+
self.__failfast = False
|
|
34
|
+
self.__filterPattern = '*'
|
|
35
|
+
self.__help = False
|
|
36
|
+
self.__includePatterns = []
|
|
37
|
+
self.__includeTraits = []
|
|
38
|
+
self.__no_default_patterns = False
|
|
39
|
+
self.__outputFilename = None
|
|
40
|
+
self.__quiet = False
|
|
41
|
+
self.__reportFormat = None
|
|
42
|
+
self.__testPackageName = 'tests'
|
|
43
|
+
self.__workdir = os.path.curdir
|
|
44
|
+
self.__verbose = False
|
|
45
|
+
|
|
46
|
+
def __parse(self, argv:list[str]) -> 'CommandLineInterface':
|
|
47
|
+
aliasName:str|None = None
|
|
48
|
+
extractFilterPattern:bool = False
|
|
49
|
+
extractExcludePattern:bool = False
|
|
50
|
+
extractTrait:bool = False
|
|
51
|
+
extractIncludePattern:bool = False
|
|
52
|
+
extractAliasName:bool = False
|
|
53
|
+
extractAliasPath:bool = False
|
|
54
|
+
extractReportFormat:bool = False
|
|
55
|
+
extractOutputFilename:bool = False
|
|
56
|
+
for arg in argv:
|
|
57
|
+
if extractFilterPattern:
|
|
58
|
+
self.__filterPattern = arg
|
|
59
|
+
extractFilterPattern = False
|
|
60
|
+
continue
|
|
61
|
+
if extractIncludePattern:
|
|
62
|
+
self.__includePatterns.append(arg)
|
|
63
|
+
extractIncludePattern = False
|
|
64
|
+
continue
|
|
65
|
+
elif extractTrait:
|
|
66
|
+
isExclude = arg.startswith('!')
|
|
67
|
+
arg = arg.lstrip('!')
|
|
68
|
+
parts = arg.split('=')
|
|
69
|
+
trait = Trait(parts[0], parts[1]) if len(parts) == 2 else Trait(arg)
|
|
70
|
+
if isExclude:
|
|
71
|
+
self.__excludeTraits.append(trait)
|
|
72
|
+
else:
|
|
73
|
+
self.__includeTraits.append(trait)
|
|
74
|
+
extractTrait = False
|
|
75
|
+
continue
|
|
76
|
+
elif extractExcludePattern:
|
|
77
|
+
self.__excludePatterns.append(arg)
|
|
78
|
+
extractExcludePattern = False
|
|
79
|
+
continue
|
|
80
|
+
elif extractAliasName:
|
|
81
|
+
aliasName = arg
|
|
82
|
+
extractAliasName = False
|
|
83
|
+
extractAliasPath = True
|
|
84
|
+
continue
|
|
85
|
+
elif extractAliasPath and aliasName is not None:
|
|
86
|
+
extractAliasPath = False
|
|
87
|
+
self.__aliases[aliasName] = arg
|
|
88
|
+
continue
|
|
89
|
+
elif self.__workdir is None:
|
|
90
|
+
self.__workdir = arg
|
|
91
|
+
continue
|
|
92
|
+
elif self.__testPackageName is None:
|
|
93
|
+
self.__testPackageName = arg
|
|
94
|
+
continue
|
|
95
|
+
elif extractReportFormat:
|
|
96
|
+
extractReportFormat = False
|
|
97
|
+
self.__reportFormat = arg.lower()
|
|
98
|
+
match self.__reportFormat:
|
|
99
|
+
case 'html' | 'junit':
|
|
100
|
+
pass
|
|
101
|
+
case _:
|
|
102
|
+
print(f'Unsupported value "{arg}" for --report argument, aborting.')
|
|
103
|
+
print(f'(valid values are "json" and "junit")')
|
|
104
|
+
exit(4)
|
|
105
|
+
continue
|
|
106
|
+
elif extractOutputFilename:
|
|
107
|
+
extractOutputFilename = False
|
|
108
|
+
self.__outputFilename = arg
|
|
109
|
+
continue
|
|
110
|
+
match arg:
|
|
111
|
+
case '-h' | '--help':
|
|
112
|
+
self.__help = True
|
|
113
|
+
case '-a':
|
|
114
|
+
extractAliasName = True
|
|
115
|
+
case '-f' | '--filter':
|
|
116
|
+
extractFilterPattern = True
|
|
117
|
+
case '-e' | '--exclude':
|
|
118
|
+
extractExcludePattern = True
|
|
119
|
+
case '-z' | '--failfast':
|
|
120
|
+
self.__failfast = True
|
|
121
|
+
case '-p' | '--test-package':
|
|
122
|
+
self.__testPackageName = None
|
|
123
|
+
case '-i' | '--include':
|
|
124
|
+
extractIncludePattern = True
|
|
125
|
+
case '-q' | '--quiet':
|
|
126
|
+
self.__quiet = True
|
|
127
|
+
case '-w' | '--working-directory':
|
|
128
|
+
self.__workdir = None
|
|
129
|
+
case '-v' | '--verbose':
|
|
130
|
+
self.__verbose = True
|
|
131
|
+
case '-n' | '--no-default-patterns':
|
|
132
|
+
self.__no_default_patterns = True
|
|
133
|
+
case '-r' | '--report':
|
|
134
|
+
extractReportFormat = True
|
|
135
|
+
case '-o' | '--output':
|
|
136
|
+
extractOutputFilename = True
|
|
137
|
+
case '-t' | '--trait':
|
|
138
|
+
extractTrait = True
|
|
139
|
+
case _:
|
|
140
|
+
continue
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def aliases(self) -> dict:
|
|
145
|
+
return self.__aliases
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def failfast(self) -> bool:
|
|
149
|
+
return self.__failfast
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def filterPattern(self) -> str:
|
|
153
|
+
return self.__filterPattern
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def excludePatterns(self) -> list[str]:
|
|
157
|
+
return self.__excludePatterns
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def excludeTraits(self) -> list[Trait]:
|
|
161
|
+
return self.__excludeTraits
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def help(self) -> bool:
|
|
165
|
+
return self.__help
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def includePatterns(self) -> list[str]:
|
|
169
|
+
return self.__includePatterns
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def includeTraits(self) -> list[Trait]:
|
|
173
|
+
return self.__includeTraits
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def outputFilename(self) -> str|None:
|
|
177
|
+
return self.__outputFilename
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def quiet(self) -> bool:
|
|
181
|
+
return self.__quiet or (self.__reportFormat is not None and self.__outputFilename is None)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def reportFormat(self) -> str|None:
|
|
185
|
+
return self.__reportFormat
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def testPackageName(self) -> str:
|
|
189
|
+
return 'tests' if self.__testPackageName is None else self.__testPackageName
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def verbose(self) -> bool:
|
|
193
|
+
return self.__verbose and self.__reportFormat is None
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def workdir(self) -> str:
|
|
197
|
+
return os.path.curdir if self.__workdir is None else self.__workdir
|
|
198
|
+
|
|
199
|
+
def printHelp(self) -> None:
|
|
200
|
+
self.printVersion()
|
|
201
|
+
print(
|
|
202
|
+
"""
|
|
203
|
+
Usage: python3 -m punit [-h|--help]
|
|
204
|
+
[-q|--quiet] [-v|--verbose]
|
|
205
|
+
[-z|--failfast]
|
|
206
|
+
[-p|--test-package NAME]
|
|
207
|
+
[-i|--include PATTERN]
|
|
208
|
+
[-e|--exclude PATTERN]
|
|
209
|
+
[-f|--filter PATTERN]
|
|
210
|
+
[-t|--trait [!]NAME[=VALUE]]
|
|
211
|
+
[-w|--workdir DIRECTORY]
|
|
212
|
+
[-n|--no-default-patterns]
|
|
213
|
+
[-r|--report {junit|json}]
|
|
214
|
+
[-o|--output FILENAME]
|
|
215
|
+
|
|
216
|
+
Options:
|
|
217
|
+
-h, --help Show this help text and exit
|
|
218
|
+
-q, --quiet Quiet output
|
|
219
|
+
-v, --verbose Verbose output
|
|
220
|
+
-z, --failfast Stop on first failure or error
|
|
221
|
+
-p, --test-package NAME
|
|
222
|
+
Use NAME as the test package, all tests should
|
|
223
|
+
be locatable as modules in the named package.
|
|
224
|
+
Default: 'tests'
|
|
225
|
+
-i, --include PATTERN
|
|
226
|
+
Include any tests matching PATTERN
|
|
227
|
+
Default: '*.py'
|
|
228
|
+
-e, --exclude PATTERN
|
|
229
|
+
Exclude any tests matching PATTERN, overriding --include
|
|
230
|
+
Default: '__*__' (dunder files), '/.*/' (dot-directories)
|
|
231
|
+
-f, --filter
|
|
232
|
+
Only execute tests matching PATTERN
|
|
233
|
+
Default: '*'
|
|
234
|
+
-t, --trait [!]NAME[=VALUE]
|
|
235
|
+
Execute tests with the specified trait, negated by prefixing with '!'.
|
|
236
|
+
If VALUE is specified, matches tests with the trait having specified value.
|
|
237
|
+
If VALUE is not specified, matches any test with the trait having any value.
|
|
238
|
+
Default: No filtering based on traits.
|
|
239
|
+
-w, --working-directory DIRECTORY
|
|
240
|
+
Working directory (defaults to start directory)
|
|
241
|
+
-n, --no-default-patterns
|
|
242
|
+
Do not apply any default include/exclude patterns.
|
|
243
|
+
-r, --report {html|junit}
|
|
244
|
+
Generate a report to stdout using either an "html"
|
|
245
|
+
or "junit" format. When generating a report to stdout
|
|
246
|
+
all other output is suppressed, unless `--output`
|
|
247
|
+
is also specified.
|
|
248
|
+
-o, --output FILENAME
|
|
249
|
+
If `--report` is used, instead of writing to stdout
|
|
250
|
+
write to FILENAME. In this case `--report` does not
|
|
251
|
+
suppress any program output.
|
|
252
|
+
"""
|
|
253
|
+
)
|
|
254
|
+
exit(0)
|
|
255
|
+
|
|
256
|
+
def printSummary(self) -> None:
|
|
257
|
+
self.printVersion()
|
|
258
|
+
print(f'Working Directory:\n\t{self.__workdir}')
|
|
259
|
+
print(f'Fail Fast: \n\t{"Yes" if self.__failfast else "No"}')
|
|
260
|
+
if len(self.__includePatterns) > 0:
|
|
261
|
+
print('Include Patterns:')
|
|
262
|
+
for pattern in self.__includePatterns:
|
|
263
|
+
print(f'\t{pattern}')
|
|
264
|
+
if len(self.__excludePatterns) > 0:
|
|
265
|
+
print('Exclude Patterns:')
|
|
266
|
+
for pattern in self.__excludePatterns:
|
|
267
|
+
print(f'\t{pattern}')
|
|
268
|
+
print('Filter Pattern:')
|
|
269
|
+
print(f'\t{self.__filterPattern}')
|
|
270
|
+
def printVersion(self) -> None:
|
|
271
|
+
print(f'pUnit {__version__}')
|
|
272
|
+
|
|
273
|
+
def validate(self) -> None:
|
|
274
|
+
if self.__workdir is None or len(self.__workdir.lstrip()) == 0 or self.__workdir.startswith('-'):
|
|
275
|
+
print(f'Invalid working directory specified: {self.__workdir}')
|
|
276
|
+
exit(1)
|
|
277
|
+
elif not os.path.isdir(self.__workdir):
|
|
278
|
+
print(f'Working directory does not exist: {self.__workdir}')
|
|
279
|
+
exit(2)
|
|
280
|
+
self.__workdir = os.path.abspath(self.__workdir)
|
|
281
|
+
if not self.__no_default_patterns:
|
|
282
|
+
# if no other patterns specified, default to including all files found in the directory matching `testPackageName`
|
|
283
|
+
if len(self.__includePatterns) == 0:
|
|
284
|
+
self.__includePatterns.append(f'/{self.testPackageName}/*.py')
|
|
285
|
+
# always exclude dot-folders (.git, .venv, etc)
|
|
286
|
+
self.__excludePatterns.append('/.*')
|
|
287
|
+
# always exclude dunder files
|
|
288
|
+
self.__excludePatterns.append('/__*__')
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def parse(argv:list[str] = sys.argv) -> CommandLineInterface:
|
|
292
|
+
result = CommandLineInterface().__parse(argv)
|
|
293
|
+
result.validate()
|
|
294
|
+
return result
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 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
|
+
from ..traits import Trait
|
|
11
|
+
|
|
12
|
+
class TestModuleDiscovery:
|
|
13
|
+
|
|
14
|
+
__cli:CommandLineInterface
|
|
15
|
+
__excludePatterns:list[re.Pattern]
|
|
16
|
+
__excludeTraits:list[Trait]
|
|
17
|
+
__filenames:list[str]
|
|
18
|
+
__filterPattern:re.Pattern
|
|
19
|
+
__includePatterns:list[re.Pattern]
|
|
20
|
+
__includeTraits:list[Trait]
|
|
21
|
+
__workdir:str
|
|
22
|
+
|
|
23
|
+
def __init__(self, workdir:str, includePatterns:list[str], excludePatterns:list[str], cli:CommandLineInterface):
|
|
24
|
+
self.__cli = cli
|
|
25
|
+
self.__excludePatterns = []
|
|
26
|
+
if excludePatterns is not None:
|
|
27
|
+
for pattern in excludePatterns:
|
|
28
|
+
self.__excludePatterns.append(
|
|
29
|
+
re.compile(
|
|
30
|
+
self.__convertPatternToRegex(pattern),
|
|
31
|
+
re.IGNORECASE))
|
|
32
|
+
self.__filenames = []
|
|
33
|
+
self.__filterPattern = re.compile(self.__convertPatternToRegex(cli.filterPattern))
|
|
34
|
+
self.__includePatterns = []
|
|
35
|
+
self.__excludeTraits = cli.excludeTraits
|
|
36
|
+
self.__includeTraits = cli.includeTraits
|
|
37
|
+
if includePatterns is not None:
|
|
38
|
+
for pattern in includePatterns:
|
|
39
|
+
self.__includePatterns.append(
|
|
40
|
+
re.compile(
|
|
41
|
+
self.__convertPatternToRegex(pattern),
|
|
42
|
+
re.IGNORECASE))
|
|
43
|
+
self.__workdir = workdir
|
|
44
|
+
|
|
45
|
+
def __convertPatternToRegex(self, pattern:str):
|
|
46
|
+
result = re.escape(pattern)\
|
|
47
|
+
.replace('\\\\', '/')\
|
|
48
|
+
.replace('\\*', r'.+')\
|
|
49
|
+
.replace('?', '.')
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
def __testAnyInclude(self, input:str) -> bool:
|
|
53
|
+
for pat in self.__includePatterns:
|
|
54
|
+
if len(pat.findall(input)) > 0:
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def __testAnyExclude(self, input:str) -> bool:
|
|
59
|
+
for pat in self.__excludePatterns:
|
|
60
|
+
if len(pat.findall(input)) > 0:
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def __walkDirectory(self, path:str) -> list[str]:
|
|
65
|
+
filenames = []
|
|
66
|
+
if os.path.isdir(path):
|
|
67
|
+
for dname, dlist, flist in os.walk(path, topdown=True):
|
|
68
|
+
if self.__cli.verbose:
|
|
69
|
+
print(f'.. discovery for: {dname}')
|
|
70
|
+
dname2 = dname.replace('\\', '/')
|
|
71
|
+
if self.__testAnyExclude(dname2):
|
|
72
|
+
# directory is excluded, prune all children
|
|
73
|
+
if self.__cli.verbose:
|
|
74
|
+
print(f'Excluded: {dname}')
|
|
75
|
+
while len(dlist) > 0:
|
|
76
|
+
del dlist[0]
|
|
77
|
+
while len(flist) > 0:
|
|
78
|
+
del flist[0]
|
|
79
|
+
# determine if any individual subdirs should be pruned
|
|
80
|
+
dlnames = dlist.copy()
|
|
81
|
+
for dlname in dlnames:
|
|
82
|
+
dlname2 = dlname.replace('\\', '/')
|
|
83
|
+
if self.__testAnyExclude(dlname2):
|
|
84
|
+
if self.__cli.verbose:
|
|
85
|
+
print(f'Excluded: {dlname}')
|
|
86
|
+
dlist.remove(dlname)
|
|
87
|
+
# determine if any individual files should be pruned
|
|
88
|
+
for fname in flist:
|
|
89
|
+
fname2 = os.path.join(dname, fname).replace('\\', '/')
|
|
90
|
+
if self.__testAnyInclude(fname2) and not self.__testAnyExclude(fname2):
|
|
91
|
+
if fname.endswith('.py'):
|
|
92
|
+
if self.__cli.verbose:
|
|
93
|
+
print(f'Included: {fname}')
|
|
94
|
+
filenames.append(fname2)
|
|
95
|
+
return filenames
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def filenames(self) -> list[str]:
|
|
99
|
+
return self.__filenames
|
|
100
|
+
|
|
101
|
+
def discover(self) -> list[str]:
|
|
102
|
+
FactManager.instance().excludeTraits = self.__excludeTraits
|
|
103
|
+
FactManager.instance().filterPattern = self.__filterPattern
|
|
104
|
+
FactManager.instance().includeTraits = self.__includeTraits
|
|
105
|
+
TheoryManager.instance().excludeTraits = self.__excludeTraits
|
|
106
|
+
TheoryManager.instance().filterPattern = self.__filterPattern
|
|
107
|
+
TheoryManager.instance().includeTraits = self.__includeTraits
|
|
108
|
+
if self.__cli.verbose:
|
|
109
|
+
print(f'.. starting test discovery')
|
|
110
|
+
self.__filenames = self.__walkDirectory(self.__workdir)
|
|
111
|
+
if self.__cli.verbose:
|
|
112
|
+
print('.. finished test discovery.')
|
|
113
|
+
return self.__filenames
|
punit/facts/Fact.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from types import FunctionType, MethodType, ModuleType
|
|
6
|
+
from typing import Callable, Coroutine, Optional, cast
|
|
7
|
+
|
|
8
|
+
from ..traits.Trait import Trait
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Fact:
|
|
12
|
+
|
|
13
|
+
__className:Optional[str]
|
|
14
|
+
__moduleName:str
|
|
15
|
+
__target:FunctionType|MethodType
|
|
16
|
+
__testName:Optional[str]
|
|
17
|
+
__traits:list[Trait]
|
|
18
|
+
|
|
19
|
+
def __init__(self, moduleName:str, target:FunctionType|MethodType|Callable, className:Optional[str] = None, testName:Optional[str] = None):
|
|
20
|
+
self.__className = className
|
|
21
|
+
self.__moduleName = moduleName
|
|
22
|
+
self.__target = target
|
|
23
|
+
self.__testName = testName
|
|
24
|
+
self.__traits = []
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def className(self) -> Optional[str]:
|
|
28
|
+
return self.__className
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def moduleName(self) -> str:
|
|
32
|
+
return self.__moduleName
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def target(self) -> FunctionType|MethodType:
|
|
36
|
+
return self.__target
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def testName(self) -> str:
|
|
40
|
+
return self.__testName if self.__testName is not None else self.__target.__qualname__.split('.')[-1]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def filterName(self) -> str:
|
|
44
|
+
return f'{self.moduleName}/{"" if self.className is None or len(self.className) == 0 else f"{self.className}/"}{self.testName}'
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def traits(self) -> list[Trait]:
|
|
48
|
+
return self.__traits
|
|
49
|
+
|
|
50
|
+
async def execute(self, module:ModuleType) -> None:
|
|
51
|
+
coro:Coroutine|None = None
|
|
52
|
+
if hasattr(self.__target, '__qualname__') and self.__target.__qualname__.find('.') > -1:
|
|
53
|
+
qnparts = self.__target.__qualname__.split('.')
|
|
54
|
+
if isinstance(self.__target, staticmethod):
|
|
55
|
+
coro = self.__target()
|
|
56
|
+
self.__className = '.'.join(qnparts[0:-1])
|
|
57
|
+
self.__testName = qnparts[-1]
|
|
58
|
+
else:
|
|
59
|
+
qntarget = module
|
|
60
|
+
self.__className = '.'.join(qnparts[0:-1])
|
|
61
|
+
self.__testName = qnparts[-1]
|
|
62
|
+
for qnpart in qnparts[0:-1]:
|
|
63
|
+
qntarget = getattr(qntarget, qnpart)
|
|
64
|
+
if isinstance(self.__target, classmethod):
|
|
65
|
+
coro = self.__target.__func__(qntarget)
|
|
66
|
+
else:
|
|
67
|
+
coro = self.__target(cast(Callable,qntarget)())
|
|
68
|
+
else:
|
|
69
|
+
self.__className = None
|
|
70
|
+
self.__testName = self.__target.__name__
|
|
71
|
+
coro = self.__target()
|
|
72
|
+
if inspect.iscoroutine(coro):
|
|
73
|
+
await coro
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fact(target:Callable) -> Callable:
|
|
77
|
+
from .FactManager import FactManager
|
|
78
|
+
if (not inspect.isfunction(target)) and (not isinstance(target, classmethod)) and (not isinstance(target, staticmethod)):
|
|
79
|
+
raise Exception('@fact can only be applied to functions and methods.')
|
|
80
|
+
fact:Fact = Fact(target.__module__, target)
|
|
81
|
+
FactManager.instance().put(fact)
|
|
82
|
+
return target
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
##
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
from .Fact import Fact
|
|
9
|
+
from ..traits.Trait import Trait
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FactManager:
|
|
13
|
+
|
|
14
|
+
__excludeTraits:list[Trait]
|
|
15
|
+
__filterPattern:Optional[re.Pattern]
|
|
16
|
+
__instance:'FactManager'|None = None
|
|
17
|
+
__includeTraits:list[Trait]
|
|
18
|
+
__modules:dict[str, list[Fact]]
|
|
19
|
+
__traits:dict[Callable, list[Trait]]
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
if FactManager.__instance is not None:
|
|
23
|
+
raise Exception('Cannot create more than one instance of FactManager')
|
|
24
|
+
self.__filterPattern = None
|
|
25
|
+
self.__modules = {}
|
|
26
|
+
self.__traits = {}
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def instance() -> FactManager:
|
|
30
|
+
if FactManager.__instance is None:
|
|
31
|
+
FactManager.__instance = FactManager()
|
|
32
|
+
return FactManager.__instance
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def excludeTraits(self) -> list[Trait]:
|
|
36
|
+
return [] if self.__excludeTraits is None else self.__excludeTraits
|
|
37
|
+
|
|
38
|
+
@excludeTraits.setter
|
|
39
|
+
def excludeTraits(self, value:list[Trait]) -> None:
|
|
40
|
+
self.__excludeTraits = value
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def filterPattern(self) -> Optional[re.Pattern]:
|
|
44
|
+
return self.__filterPattern
|
|
45
|
+
|
|
46
|
+
@filterPattern.setter
|
|
47
|
+
def filterPattern(self, value:re.Pattern) -> None:
|
|
48
|
+
self.__filterPattern = value
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def includeTraits(self) -> list[Trait]:
|
|
52
|
+
return [] if self.__includeTraits is None else self.__includeTraits
|
|
53
|
+
|
|
54
|
+
@includeTraits.setter
|
|
55
|
+
def includeTraits(self, value:list[Trait]) -> None:
|
|
56
|
+
self.__includeTraits = value
|
|
57
|
+
|
|
58
|
+
def __excludeByTraits(self, fact:Fact) -> bool:
|
|
59
|
+
if self.__excludeTraits is not None and len(self.__excludeTraits) > 0:
|
|
60
|
+
for trait in self.__excludeTraits:
|
|
61
|
+
for L_trait in fact.traits:
|
|
62
|
+
if trait.name == L_trait.name and (trait.value is None or (trait.value == L_trait.value)):
|
|
63
|
+
return True
|
|
64
|
+
if self.__includeTraits is not None and len(self.__includeTraits) > 0:
|
|
65
|
+
for trait in self.__includeTraits:
|
|
66
|
+
for L_trait in fact.traits:
|
|
67
|
+
if trait.name == L_trait.name and (trait.value is None or (trait.value == L_trait.value)):
|
|
68
|
+
return False
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def get(self, moduleName:str) -> list[Fact]:
|
|
73
|
+
l = self.__modules.get(moduleName)
|
|
74
|
+
if l is None:
|
|
75
|
+
l = []
|
|
76
|
+
self.__modules[moduleName] = l
|
|
77
|
+
return l
|
|
78
|
+
|
|
79
|
+
def put(self, fact:Fact) -> None:
|
|
80
|
+
if self.__filterPattern is None or len(self.__filterPattern.findall(fact.filterName)) > 0:
|
|
81
|
+
l = self.get(fact.moduleName)
|
|
82
|
+
t = self.__traits.get(fact.target)
|
|
83
|
+
if t is not None:
|
|
84
|
+
for trait in t:
|
|
85
|
+
fact.traits.append(trait)
|
|
86
|
+
if not self.__excludeByTraits(fact):
|
|
87
|
+
l.append(fact)
|
|
88
|
+
|
|
89
|
+
def withTrait(self, target:Callable, trait:Trait) -> None:
|
|
90
|
+
t = self.__traits.get(target)
|
|
91
|
+
if t is None:
|
|
92
|
+
t = []
|
|
93
|
+
self.__traits[target] = t
|
|
94
|
+
t.append(trait)
|