punit 0.0.27__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-0.0.27/LICENSE +21 -0
- punit-0.0.27/PKG-INFO +174 -0
- punit-0.0.27/README.md +155 -0
- punit-0.0.27/punit/TestResult.py +180 -0
- punit-0.0.27/punit/__init__.py +8 -0
- punit-0.0.27/punit/__main__.py +55 -0
- punit-0.0.27/punit/assertions/__init__.py +7 -0
- punit-0.0.27/punit/assertions/collections.py +18 -0
- punit-0.0.27/punit/assertions/exceptions.py +33 -0
- punit-0.0.27/punit/cli.py +261 -0
- punit-0.0.27/punit/discovery/TestModuleDiscovery.py +104 -0
- punit-0.0.27/punit/discovery/__init__.py +8 -0
- punit-0.0.27/punit/facts/Fact.py +68 -0
- punit-0.0.27/punit/facts/FactManager.py +50 -0
- punit-0.0.27/punit/facts/__init__.py +5 -0
- punit-0.0.27/punit/reports/HtmlReportGenerator.py +92 -0
- punit-0.0.27/punit/reports/JUnitReportGenerator.py +185 -0
- punit-0.0.27/punit/reports/__init__.py +5 -0
- punit-0.0.27/punit/runner.py +118 -0
- punit-0.0.27/punit/theories/Theory.py +84 -0
- punit-0.0.27/punit/theories/TheoryManager.py +67 -0
- punit-0.0.27/punit/theories/__init__.py +5 -0
- punit-0.0.27/punit.egg-info/PKG-INFO +174 -0
- punit-0.0.27/punit.egg-info/SOURCES.txt +26 -0
- punit-0.0.27/punit.egg-info/dependency_links.txt +1 -0
- punit-0.0.27/punit.egg-info/top_level.txt +4 -0
- punit-0.0.27/pyproject.toml +36 -0
- punit-0.0.27/setup.cfg +4 -0
punit-0.0.27/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Shaun Wilson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
punit-0.0.27/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: punit
|
|
3
|
+
Version: 0.0.27
|
|
4
|
+
Summary: A modernized unit-test framework for Python.
|
|
5
|
+
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
|
+
Project-URL: Documentation, https://punit.readthedocs.io/
|
|
7
|
+
Project-URL: Homepage, https://github.com/wilson0x4d/punit
|
|
8
|
+
Project-URL: Repository, https://github.com/wilson0x4d/punit.git
|
|
9
|
+
Keywords: test,unittest,unit-test
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
|
|
20
|
+
`pUnit` is a modernized unit-testing framework for Python, inspired by xUnit.
|
|
21
|
+
|
|
22
|
+
This README is only a high-level introduction to **pUnit**. For more detailed documentation, please view the official docs at [https://pUnit.readthedocs.io](https://pUnit.readthedocs.io).
|
|
23
|
+
|
|
24
|
+
## Command-Line Usage
|
|
25
|
+
|
|
26
|
+
Running pUnit with no arguments will perform test auto-discovery and execution:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python3 -m punit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
By default it will look for tests in the `tests/` directory. You can override this behavior by providing a `--test-package` argument:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python3 -m punit --test-package elsewhere
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In the above example, test discovery will instead occur in an `elsewhere/` directory.
|
|
39
|
+
|
|
40
|
+
### Report Generation
|
|
41
|
+
|
|
42
|
+
`pUnit` can generate "Test Results" reports. Currently supporting HTML and jUnit formats:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python3 -m punit --report html
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Reports can also be output to a file where they can be lifted as part of a CI/CD pipeline and stored as an artifact or used for other purposes:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python3 -m punit --report junit --output results.xml
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Syntax Help
|
|
55
|
+
|
|
56
|
+
There are more options available, passing a `--help` argument will print help text:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python3 -m punit --help
|
|
60
|
+
```
|
|
61
|
+
Outputs:
|
|
62
|
+
```plaintext
|
|
63
|
+
Usage: python3 -m punit [-h|--help]
|
|
64
|
+
[-q|--quiet] [-v|--verbose]
|
|
65
|
+
[-f|--failfast]
|
|
66
|
+
[-p|--test-package NAME]
|
|
67
|
+
[-i|--include PATTERN]
|
|
68
|
+
[-e|--exclude PATTERN]
|
|
69
|
+
[-t|--filter PATTERN]
|
|
70
|
+
[-w|--workdir DIRECTORY]
|
|
71
|
+
[-n|--no-default-patterns]
|
|
72
|
+
[-r|--report {junit|json}]
|
|
73
|
+
[-o|--output FILENAME]
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
-h, --help Show this help text and exit
|
|
77
|
+
-q, --quiet Quiet output
|
|
78
|
+
-v, --verbose Verbose output
|
|
79
|
+
-f, --failfast Stop on first failure or error
|
|
80
|
+
-p, --test-package NAME
|
|
81
|
+
Use NAME as the test package, all tests should
|
|
82
|
+
be locatable as modules in the named package.
|
|
83
|
+
Default: 'tests'
|
|
84
|
+
-i, --include PATTERN
|
|
85
|
+
Include any tests matching PATTERN
|
|
86
|
+
Default: '*.py'
|
|
87
|
+
-e, --exclude PATTERN
|
|
88
|
+
Exclude any tests matching PATTERN, overriding --include
|
|
89
|
+
Default: '__*__' (dunder files), '/.*/' (dot-directories)
|
|
90
|
+
-t, --filter PATTERN
|
|
91
|
+
Only execute tests matching PATTERN
|
|
92
|
+
Default: '*'
|
|
93
|
+
-w, --working-directory DIRECTORY
|
|
94
|
+
Working directory (defaults to start directory)
|
|
95
|
+
-n, --no-default-patterns
|
|
96
|
+
Do not apply any default include/exclude patterns.
|
|
97
|
+
-r, --report {html|junit}
|
|
98
|
+
Generate a report to stdout using either an "html"
|
|
99
|
+
or "junit" format. When generating a report to stdout
|
|
100
|
+
all other output is suppressed, unless `--output`
|
|
101
|
+
is also specified.
|
|
102
|
+
-o, --output FILENAME
|
|
103
|
+
If `--report` is used, instead of writing to stdout
|
|
104
|
+
write to FILENAME. In this case `--report` does not
|
|
105
|
+
suppress any program output.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Writing Tests
|
|
109
|
+
|
|
110
|
+
You can write tests as functions, class methods, instance methods, or static methods with all forms offering identical functionality. You can also utilize async/await syntax without any additional overhead.
|
|
111
|
+
|
|
112
|
+
**pUnit** is based upon the fundamental concepts of `Facts` and `Theories`. These are codified using decorators, aptly named `@fact` and `@theory`. Consider these examples:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
|
|
116
|
+
from punit import *
|
|
117
|
+
|
|
118
|
+
@fact
|
|
119
|
+
async def MyLibrary_WhenInitialized_TouchMustReturnTrue:
|
|
120
|
+
mylib = MyLibrary()
|
|
121
|
+
mylib.initialize()
|
|
122
|
+
await asyncio.sleep(1)
|
|
123
|
+
assert mylib.touch(), "Expecting touch() to return true, because initialize() was called."
|
|
124
|
+
|
|
125
|
+
class MyTestFixture:
|
|
126
|
+
|
|
127
|
+
def calc(number:int|None = None):
|
|
128
|
+
if number == None:
|
|
129
|
+
raise Exception('Invalid value "None".')
|
|
130
|
+
return number * number
|
|
131
|
+
|
|
132
|
+
@theory
|
|
133
|
+
@inlinedata(0, 0)
|
|
134
|
+
@inlinedata(1, 1)
|
|
135
|
+
@inlinedata(2, 4)
|
|
136
|
+
@inlinedata(3, 9)
|
|
137
|
+
@inlinedata(5, 25)
|
|
138
|
+
@inlinedata(8, 64)
|
|
139
|
+
def verifyCalcAssumptions(self, number:int, expected:int) -> None:
|
|
140
|
+
assert expected == self.calc(number)
|
|
141
|
+
|
|
142
|
+
@fact
|
|
143
|
+
def verifyCalcErrorCondition(self) -> None:
|
|
144
|
+
from punit.exceptions import raises
|
|
145
|
+
# assert errors are raised, or not
|
|
146
|
+
def calc_None():
|
|
147
|
+
self.calc(None)
|
|
148
|
+
def calc_1()
|
|
149
|
+
self.calc(1)
|
|
150
|
+
assert raises[Exception](calc_None)
|
|
151
|
+
assert not raises[Exception](calc_1)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Because `pUnit` is a decorative framework you are afforded the utmost freedom in how you structure and implement your tests.
|
|
155
|
+
|
|
156
|
+
Unlike other testing frameworks, the names you use for functions, classes, and methods is not relevant. There is no requirement to inherit classes from specific abstract/base classes for any particular functionality. There is no requirement that your tests be organized into modules with `__init__.py` files.
|
|
157
|
+
|
|
158
|
+
You will want to take particular note of the `--exclude` command-line parameter which allows you to restrict what `pUnit` will consider to be a valid test file. While the default behavior will fit 99% of use-cases, you _can_ exercise more control over the discovery process.
|
|
159
|
+
|
|
160
|
+
## Vision, Future, and LTS
|
|
161
|
+
|
|
162
|
+
The long-term vision is to provide both imperative and declarative syntaxes for testing while keeping `pUnit` as simple as possible in its implementation.
|
|
163
|
+
|
|
164
|
+
`pUnit` is a Python 3.12+ package and there are no plans to backport it to earlier versions of Python, however, user contributions to support backward compatibility _will be accepted_ when it makes sense.
|
|
165
|
+
|
|
166
|
+
As Python progresses so will `pUnit` and SEMVER rules will be respected to provide developers with assurance that a major version of `pUnit` is fit for a particular purpose, thus, if there is ever a breaking change in Python that requires a breaking change in `pUnit` you can expect `pUnit` versioning to reflect this.
|
|
167
|
+
|
|
168
|
+
In any situation where an undocumented feature may be used maintainers will actively keep watch on deprecation notices and removals, will clearly identify these dependencies in the docs, and most importantly will provide a LTS alternative to any undocumented feature or such features will not be included in `pUnit`.
|
|
169
|
+
|
|
170
|
+
With respect to long-term support, we will commit to maintaining major versions for 3 years from the date they were superceded by a new major version. This should align well with Python Core Development.
|
|
171
|
+
|
|
172
|
+
## Contact
|
|
173
|
+
|
|
174
|
+
You can reach me on [Discord](https://discordapp.com/users/307684202080501761) or [open an Issue on Github](https://github.com/wilson0x4d/punit/issues/new/choose).
|
punit-0.0.27/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
`pUnit` is a modernized unit-testing framework for Python, inspired by xUnit.
|
|
2
|
+
|
|
3
|
+
This README is only a high-level introduction to **pUnit**. For more detailed documentation, please view the official docs at [https://pUnit.readthedocs.io](https://pUnit.readthedocs.io).
|
|
4
|
+
|
|
5
|
+
## Command-Line Usage
|
|
6
|
+
|
|
7
|
+
Running pUnit with no arguments will perform test auto-discovery and execution:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python3 -m punit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
By default it will look for tests in the `tests/` directory. You can override this behavior by providing a `--test-package` argument:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python3 -m punit --test-package elsewhere
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
In the above example, test discovery will instead occur in an `elsewhere/` directory.
|
|
20
|
+
|
|
21
|
+
### Report Generation
|
|
22
|
+
|
|
23
|
+
`pUnit` can generate "Test Results" reports. Currently supporting HTML and jUnit formats:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python3 -m punit --report html
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Reports can also be output to a file where they can be lifted as part of a CI/CD pipeline and stored as an artifact or used for other purposes:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3 -m punit --report junit --output results.xml
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Syntax Help
|
|
36
|
+
|
|
37
|
+
There are more options available, passing a `--help` argument will print help text:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python3 -m punit --help
|
|
41
|
+
```
|
|
42
|
+
Outputs:
|
|
43
|
+
```plaintext
|
|
44
|
+
Usage: python3 -m punit [-h|--help]
|
|
45
|
+
[-q|--quiet] [-v|--verbose]
|
|
46
|
+
[-f|--failfast]
|
|
47
|
+
[-p|--test-package NAME]
|
|
48
|
+
[-i|--include PATTERN]
|
|
49
|
+
[-e|--exclude PATTERN]
|
|
50
|
+
[-t|--filter PATTERN]
|
|
51
|
+
[-w|--workdir DIRECTORY]
|
|
52
|
+
[-n|--no-default-patterns]
|
|
53
|
+
[-r|--report {junit|json}]
|
|
54
|
+
[-o|--output FILENAME]
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
-h, --help Show this help text and exit
|
|
58
|
+
-q, --quiet Quiet output
|
|
59
|
+
-v, --verbose Verbose output
|
|
60
|
+
-f, --failfast Stop on first failure or error
|
|
61
|
+
-p, --test-package NAME
|
|
62
|
+
Use NAME as the test package, all tests should
|
|
63
|
+
be locatable as modules in the named package.
|
|
64
|
+
Default: 'tests'
|
|
65
|
+
-i, --include PATTERN
|
|
66
|
+
Include any tests matching PATTERN
|
|
67
|
+
Default: '*.py'
|
|
68
|
+
-e, --exclude PATTERN
|
|
69
|
+
Exclude any tests matching PATTERN, overriding --include
|
|
70
|
+
Default: '__*__' (dunder files), '/.*/' (dot-directories)
|
|
71
|
+
-t, --filter PATTERN
|
|
72
|
+
Only execute tests matching PATTERN
|
|
73
|
+
Default: '*'
|
|
74
|
+
-w, --working-directory DIRECTORY
|
|
75
|
+
Working directory (defaults to start directory)
|
|
76
|
+
-n, --no-default-patterns
|
|
77
|
+
Do not apply any default include/exclude patterns.
|
|
78
|
+
-r, --report {html|junit}
|
|
79
|
+
Generate a report to stdout using either an "html"
|
|
80
|
+
or "junit" format. When generating a report to stdout
|
|
81
|
+
all other output is suppressed, unless `--output`
|
|
82
|
+
is also specified.
|
|
83
|
+
-o, --output FILENAME
|
|
84
|
+
If `--report` is used, instead of writing to stdout
|
|
85
|
+
write to FILENAME. In this case `--report` does not
|
|
86
|
+
suppress any program output.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Writing Tests
|
|
90
|
+
|
|
91
|
+
You can write tests as functions, class methods, instance methods, or static methods with all forms offering identical functionality. You can also utilize async/await syntax without any additional overhead.
|
|
92
|
+
|
|
93
|
+
**pUnit** is based upon the fundamental concepts of `Facts` and `Theories`. These are codified using decorators, aptly named `@fact` and `@theory`. Consider these examples:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
|
|
97
|
+
from punit import *
|
|
98
|
+
|
|
99
|
+
@fact
|
|
100
|
+
async def MyLibrary_WhenInitialized_TouchMustReturnTrue:
|
|
101
|
+
mylib = MyLibrary()
|
|
102
|
+
mylib.initialize()
|
|
103
|
+
await asyncio.sleep(1)
|
|
104
|
+
assert mylib.touch(), "Expecting touch() to return true, because initialize() was called."
|
|
105
|
+
|
|
106
|
+
class MyTestFixture:
|
|
107
|
+
|
|
108
|
+
def calc(number:int|None = None):
|
|
109
|
+
if number == None:
|
|
110
|
+
raise Exception('Invalid value "None".')
|
|
111
|
+
return number * number
|
|
112
|
+
|
|
113
|
+
@theory
|
|
114
|
+
@inlinedata(0, 0)
|
|
115
|
+
@inlinedata(1, 1)
|
|
116
|
+
@inlinedata(2, 4)
|
|
117
|
+
@inlinedata(3, 9)
|
|
118
|
+
@inlinedata(5, 25)
|
|
119
|
+
@inlinedata(8, 64)
|
|
120
|
+
def verifyCalcAssumptions(self, number:int, expected:int) -> None:
|
|
121
|
+
assert expected == self.calc(number)
|
|
122
|
+
|
|
123
|
+
@fact
|
|
124
|
+
def verifyCalcErrorCondition(self) -> None:
|
|
125
|
+
from punit.exceptions import raises
|
|
126
|
+
# assert errors are raised, or not
|
|
127
|
+
def calc_None():
|
|
128
|
+
self.calc(None)
|
|
129
|
+
def calc_1()
|
|
130
|
+
self.calc(1)
|
|
131
|
+
assert raises[Exception](calc_None)
|
|
132
|
+
assert not raises[Exception](calc_1)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Because `pUnit` is a decorative framework you are afforded the utmost freedom in how you structure and implement your tests.
|
|
136
|
+
|
|
137
|
+
Unlike other testing frameworks, the names you use for functions, classes, and methods is not relevant. There is no requirement to inherit classes from specific abstract/base classes for any particular functionality. There is no requirement that your tests be organized into modules with `__init__.py` files.
|
|
138
|
+
|
|
139
|
+
You will want to take particular note of the `--exclude` command-line parameter which allows you to restrict what `pUnit` will consider to be a valid test file. While the default behavior will fit 99% of use-cases, you _can_ exercise more control over the discovery process.
|
|
140
|
+
|
|
141
|
+
## Vision, Future, and LTS
|
|
142
|
+
|
|
143
|
+
The long-term vision is to provide both imperative and declarative syntaxes for testing while keeping `pUnit` as simple as possible in its implementation.
|
|
144
|
+
|
|
145
|
+
`pUnit` is a Python 3.12+ package and there are no plans to backport it to earlier versions of Python, however, user contributions to support backward compatibility _will be accepted_ when it makes sense.
|
|
146
|
+
|
|
147
|
+
As Python progresses so will `pUnit` and SEMVER rules will be respected to provide developers with assurance that a major version of `pUnit` is fit for a particular purpose, thus, if there is ever a breaking change in Python that requires a breaking change in `pUnit` you can expect `pUnit` versioning to reflect this.
|
|
148
|
+
|
|
149
|
+
In any situation where an undocumented feature may be used maintainers will actively keep watch on deprecation notices and removals, will clearly identify these dependencies in the docs, and most importantly will provide a LTS alternative to any undocumented feature or such features will not be included in `pUnit`.
|
|
150
|
+
|
|
151
|
+
With respect to long-term support, we will commit to maintaining major versions for 3 years from the date they were superceded by a new major version. This should align well with Python Core Development.
|
|
152
|
+
|
|
153
|
+
## Contact
|
|
154
|
+
|
|
155
|
+
You can reach me on [Discord](https://discordapp.com/users/307684202080501761) or [open an Issue on Github](https://github.com/wilson0x4d/punit/issues/new/choose).
|
|
@@ -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
|
|
@@ -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)
|