agapsys-tests 0.0.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.
- agapsys/tests/__init__.py +4 -0
- agapsys/tests/assertions.py +71 -0
- agapsys/tests/console.py +174 -0
- agapsys/tests/os.py +71 -0
- agapsys/tests/time.py +39 -0
- agapsys_tests-0.0.0.dist-info/METADATA +30 -0
- agapsys_tests-0.0.0.dist-info/RECORD +10 -0
- agapsys_tests-0.0.0.dist-info/WHEEL +5 -0
- agapsys_tests-0.0.0.dist-info/licenses/LICENSE +21 -0
- agapsys_tests-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
# Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
3
|
+
# Licensed under the MIT License.
|
|
4
|
+
|
|
5
|
+
# Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
|
|
8
|
+
from typing import Type, ContextManager
|
|
9
|
+
|
|
10
|
+
def raises(ex_type: Type[Exception] = Exception, msg: str | None = None, exact: bool = True) -> ContextManager:
|
|
11
|
+
"""
|
|
12
|
+
Returns a context manager expecting an exception is raised.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
ex_type (Type[Exception], optional):
|
|
16
|
+
Exception type to be raised. Defaults to `Exception`.
|
|
17
|
+
|
|
18
|
+
msg (str | None, optional):
|
|
19
|
+
Exception message that be detected or `None` is message is not
|
|
20
|
+
checked. Defaults to `None`.
|
|
21
|
+
|
|
22
|
+
exact (bool, optional):
|
|
23
|
+
When a message is specifed, defines if an exact match is required,
|
|
24
|
+
otherwise a a check if given messae is present in detected exception
|
|
25
|
+
message. Defaults to `True`.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
AssertionError:
|
|
29
|
+
If either an exception is not detected, its type does not match,
|
|
30
|
+
or exception message does not match with expected one.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ContextManager
|
|
34
|
+
"""
|
|
35
|
+
isinstance(ex_type, (type(None), type))
|
|
36
|
+
if ex_type is not None:
|
|
37
|
+
issubclass(ex_type, Exception)
|
|
38
|
+
isinstance(msg, (type(None), str))
|
|
39
|
+
isinstance(exact, bool)
|
|
40
|
+
|
|
41
|
+
class RaisesContext:
|
|
42
|
+
def __init__(self, ex_type: type | None, msg: str | None):
|
|
43
|
+
self.ex_type = ex_type
|
|
44
|
+
self.msg = msg
|
|
45
|
+
self.excinfo = None
|
|
46
|
+
|
|
47
|
+
def __enter__(self):
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
51
|
+
# No exception raised → fail
|
|
52
|
+
if exc_type is None:
|
|
53
|
+
raise AssertionError(f"No exception was raised")
|
|
54
|
+
|
|
55
|
+
# An exception was raised. Check type:
|
|
56
|
+
if self.ex_type is not None and not issubclass(exc_type, self.ex_type):
|
|
57
|
+
raise AssertionError(f"\nExpected type: {self.ex_type.__name__}\nGiven type: {exc_type.__name__}") from exc_value
|
|
58
|
+
|
|
59
|
+
# Correct exception, check message
|
|
60
|
+
if self.msg is not None:
|
|
61
|
+
ex_msg = str(exc_value)
|
|
62
|
+
if exact and msg != ex_msg:
|
|
63
|
+
raise AssertionError(f"\nExpected message: {repr(msg)}\nGiven message: {repr(ex_msg)}") from exc_value
|
|
64
|
+
if not exact and msg is not None and msg not in ex_msg:
|
|
65
|
+
raise AssertionError(f"\nNot found: {repr(msg)}\nIn: {repr(ex_msg)}") from exc_value
|
|
66
|
+
|
|
67
|
+
# store info and suppress it
|
|
68
|
+
self.excinfo = (exc_type, exc_value, traceback)
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
return RaisesContext(ex_type, msg)
|
agapsys/tests/console.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from pytest import fixture
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import io
|
|
9
|
+
|
|
10
|
+
class ConsoleTextOutputStream(io.StringIO):
|
|
11
|
+
"""Console text output stream. """
|
|
12
|
+
|
|
13
|
+
def __init__(self, original_stream) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.__original_stream = original_stream
|
|
16
|
+
|
|
17
|
+
def clear(self):
|
|
18
|
+
"""Clear stream data."""
|
|
19
|
+
self.truncate(0)
|
|
20
|
+
self.seek(0)
|
|
21
|
+
|
|
22
|
+
def dump(self):
|
|
23
|
+
"""
|
|
24
|
+
Dumps stored data into original (wrapped) stream.
|
|
25
|
+
"""
|
|
26
|
+
print(self.getvalue(), file=self.__original_stream)
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
return self.getvalue()
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return self.__str__()
|
|
33
|
+
|
|
34
|
+
def assert_lines(
|
|
35
|
+
self,
|
|
36
|
+
expected_lines: Sequence[str],
|
|
37
|
+
exact_lines: bool = False,
|
|
38
|
+
exact_len: bool = True
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Asserts contained data matches a given sequence of lines.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
expected_lines (Sequence[str]):
|
|
45
|
+
Expected line sequence.
|
|
46
|
+
|
|
47
|
+
exact_lines (bool, optional):
|
|
48
|
+
Defines if every line should match exactly. Defaults to False.
|
|
49
|
+
|
|
50
|
+
exact_len (bool, optional):
|
|
51
|
+
Defines if the number of contained lines matches with given
|
|
52
|
+
expected ones. Defaults to True.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
AssertionError: If test fails.
|
|
56
|
+
"""
|
|
57
|
+
out = self.getvalue().split('\n')
|
|
58
|
+
out = out[:-1] if out and out[-1] == "" else out
|
|
59
|
+
|
|
60
|
+
if exact_len:
|
|
61
|
+
match_length = (len(out) == len(expected_lines))
|
|
62
|
+
min_len = min(len(out), len(expected_lines))
|
|
63
|
+
|
|
64
|
+
for i in range(min_len):
|
|
65
|
+
if exact_lines:
|
|
66
|
+
assert expected_lines[i] == out[i], f"Difference at line {i + 1}:\nExpected: {repr(expected_lines[i])}\nGiven: {repr(out[i])}"
|
|
67
|
+
else:
|
|
68
|
+
assert expected_lines[i] in out[i], f"Substring not found at line {i + 1}:\nSearched: {repr(expected_lines[i])}\nBase: {repr(out[i])}"
|
|
69
|
+
|
|
70
|
+
if not match_length:
|
|
71
|
+
raise AssertionError(f"Unexpected line: {expected_lines[min_len] if len(expected_lines) > min_len else out[min_len]}")
|
|
72
|
+
|
|
73
|
+
self.clear()
|
|
74
|
+
|
|
75
|
+
class ConsoleTextInputStream(io.StringIO):
|
|
76
|
+
"""Console text input stream. """
|
|
77
|
+
def __init__(self, initial_value: str | None = "", newline: str | None = "\n"):
|
|
78
|
+
super().__init__(initial_value, newline)
|
|
79
|
+
|
|
80
|
+
def set_inputs(self, *inputs):
|
|
81
|
+
"""
|
|
82
|
+
Sets a sequence of inputs (for each input an implicit newline is added
|
|
83
|
+
automatically.
|
|
84
|
+
"""
|
|
85
|
+
self.seek(0)
|
|
86
|
+
for input in inputs:
|
|
87
|
+
self.write(f"{input}\n")
|
|
88
|
+
self.seek(0)
|
|
89
|
+
|
|
90
|
+
@fixture
|
|
91
|
+
def text_stdout():
|
|
92
|
+
"""
|
|
93
|
+
Replaces `sys.stdout` by a `ConsoleTextOutputStream upon every test.
|
|
94
|
+
|
|
95
|
+
Original stream is restored after test.
|
|
96
|
+
"""
|
|
97
|
+
old = sys.stdout
|
|
98
|
+
sys.stdout = ConsoleTextOutputStream(old)
|
|
99
|
+
try:
|
|
100
|
+
yield sys.stdout
|
|
101
|
+
finally:
|
|
102
|
+
sys.stdout = old
|
|
103
|
+
|
|
104
|
+
@fixture
|
|
105
|
+
def binary_stdout():
|
|
106
|
+
"""
|
|
107
|
+
Replaces `sys.stdout` by a byte buffer (`io.BytesIO`) upon every test.
|
|
108
|
+
|
|
109
|
+
Original stream is restored after test.
|
|
110
|
+
"""
|
|
111
|
+
old = sys.stdout
|
|
112
|
+
sys.stdout = io.BytesIO()
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
yield sys.stdout
|
|
116
|
+
finally:
|
|
117
|
+
sys.stdout = old
|
|
118
|
+
|
|
119
|
+
@fixture
|
|
120
|
+
def text_stderr():
|
|
121
|
+
"""
|
|
122
|
+
Replaces `sys.stderr` by a `ConsoleTextOutputStream upon every test.
|
|
123
|
+
|
|
124
|
+
Original stream is restored after test.
|
|
125
|
+
"""
|
|
126
|
+
old = sys.stderr
|
|
127
|
+
sys.stderr = ConsoleTextOutputStream(old)
|
|
128
|
+
try:
|
|
129
|
+
yield sys.stderr
|
|
130
|
+
finally:
|
|
131
|
+
sys.stderr = old
|
|
132
|
+
|
|
133
|
+
@fixture
|
|
134
|
+
def binary_stderr():
|
|
135
|
+
"""
|
|
136
|
+
Replaces `sys.stderr` by a byte buffer (`io.BytesIO`) upon every test.
|
|
137
|
+
|
|
138
|
+
Original stream is restored after test.
|
|
139
|
+
"""
|
|
140
|
+
old = sys.stderr
|
|
141
|
+
sys.stderr = io.BytesIO()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
yield sys.stderr
|
|
145
|
+
finally:
|
|
146
|
+
sys.stderr = old
|
|
147
|
+
|
|
148
|
+
@fixture
|
|
149
|
+
def text_stdin():
|
|
150
|
+
"""
|
|
151
|
+
Replaces `sys.stdin` by a `ConsoleTextInputStream upon every test.
|
|
152
|
+
|
|
153
|
+
Original stream is restored after test.
|
|
154
|
+
"""
|
|
155
|
+
old = sys.stdin
|
|
156
|
+
sys.stdin = ConsoleTextInputStream()
|
|
157
|
+
try:
|
|
158
|
+
yield sys.stdin
|
|
159
|
+
finally:
|
|
160
|
+
sys.stdin = old
|
|
161
|
+
|
|
162
|
+
@fixture
|
|
163
|
+
def binary_stdin():
|
|
164
|
+
"""
|
|
165
|
+
Replaces `sys.stdin` by a byte buffer (`io.BytesIO`) upon every test.
|
|
166
|
+
|
|
167
|
+
Original stream is restored after test.
|
|
168
|
+
"""
|
|
169
|
+
old = sys.stdin
|
|
170
|
+
sys.stdin = io.BytesIO()
|
|
171
|
+
try:
|
|
172
|
+
yield sys.stdin
|
|
173
|
+
finally:
|
|
174
|
+
sys.stdin = old
|
agapsys/tests/os.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from pytest import fixture
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
class Argv:
|
|
10
|
+
"""Wrapper for `sys.argv`."""
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Create a wrapper for `sys.argv`."""
|
|
13
|
+
self.__argv = sys.argv
|
|
14
|
+
self.set()
|
|
15
|
+
|
|
16
|
+
def set(self, *args: str, launcher: str = 'test'):
|
|
17
|
+
"""
|
|
18
|
+
Sets `sys.argv` args.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
*args (str):
|
|
22
|
+
Arguments for `sys.argv` starting at index 1.
|
|
23
|
+
|
|
24
|
+
launcher (str, optional):
|
|
25
|
+
Value for `sys.argv[0]`. Defaults to 'test'.
|
|
26
|
+
"""
|
|
27
|
+
assert isinstance(launcher, str) and launcher
|
|
28
|
+
self.__argv[:] = [launcher]
|
|
29
|
+
self.__argv.extend(args)
|
|
30
|
+
|
|
31
|
+
class Env:
|
|
32
|
+
"""Wrapper for `os.environ`."""
|
|
33
|
+
def __init__(self):
|
|
34
|
+
"""Create a wrapper for `os.environ`."""
|
|
35
|
+
self.__env = os.environ
|
|
36
|
+
|
|
37
|
+
def set(self, **vars: dict[str, str]):
|
|
38
|
+
"""
|
|
39
|
+
Sets `os.environ` variables.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
**vars (dict[str, str]):
|
|
43
|
+
Variables.
|
|
44
|
+
"""
|
|
45
|
+
assert isinstance(vars, dict)
|
|
46
|
+
self.__env.clear()
|
|
47
|
+
for k, v in vars.items():
|
|
48
|
+
assert isinstance(k, str) and isinstance(v, str)
|
|
49
|
+
self.__env[k] = v
|
|
50
|
+
|
|
51
|
+
@fixture
|
|
52
|
+
def env():
|
|
53
|
+
"""
|
|
54
|
+
Return test-specific environment.
|
|
55
|
+
"""
|
|
56
|
+
original = os.environ.copy()
|
|
57
|
+
env = Env()
|
|
58
|
+
try:
|
|
59
|
+
yield env
|
|
60
|
+
finally:
|
|
61
|
+
os.environ.clear()
|
|
62
|
+
os.environ.update(original)
|
|
63
|
+
|
|
64
|
+
@fixture
|
|
65
|
+
def argv():
|
|
66
|
+
backup = sys.argv.copy()
|
|
67
|
+
argv = Argv()
|
|
68
|
+
try:
|
|
69
|
+
yield argv
|
|
70
|
+
finally:
|
|
71
|
+
sys.argv = backup
|
agapsys/tests/time.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from pytest import fixture
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
class FakePerformanceCounter:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.__t = 0.0
|
|
11
|
+
self.__delay = None
|
|
12
|
+
|
|
13
|
+
def set(self, t: float):
|
|
14
|
+
assert isinstance(t, float)
|
|
15
|
+
self.__t = t
|
|
16
|
+
self.__delay = None
|
|
17
|
+
|
|
18
|
+
def delay(self, t: float):
|
|
19
|
+
assert isinstance(t, float) and t > 0.0
|
|
20
|
+
self.__delay = t
|
|
21
|
+
|
|
22
|
+
def __call__(self) -> float:
|
|
23
|
+
if self.__delay is not None:
|
|
24
|
+
t = self.__t
|
|
25
|
+
self.__t += self.__delay
|
|
26
|
+
self.__delay = None
|
|
27
|
+
return t
|
|
28
|
+
|
|
29
|
+
return self.__t
|
|
30
|
+
|
|
31
|
+
@fixture
|
|
32
|
+
def perf_counter():
|
|
33
|
+
backup = time.perf_counter
|
|
34
|
+
fake = FakePerformanceCounter()
|
|
35
|
+
time.perf_counter = fake
|
|
36
|
+
try:
|
|
37
|
+
yield fake
|
|
38
|
+
finally:
|
|
39
|
+
time.perf_counter = backup
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agapsys-tests
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Common test utilities
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pytest>=7.0
|
|
10
|
+
Requires-Dist: agapsys-utils<2.0.0,>=1.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: build; extra == "dev"
|
|
13
|
+
Requires-Dist: twine; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# agapsys-tests
|
|
17
|
+
|
|
18
|
+
This project provides testing general purpose utiltities that individually thet do not fit in a dedicated library.
|
|
19
|
+
|
|
20
|
+
## License
|
|
21
|
+
|
|
22
|
+
This project is distributed under MIT License. Please see the [LICENSE](LICENSE) file for details on copying and distribution.
|
|
23
|
+
|
|
24
|
+
## Basic usage
|
|
25
|
+
|
|
26
|
+
```py
|
|
27
|
+
import agapsys.tests as tests
|
|
28
|
+
|
|
29
|
+
# That's it... use the available modules
|
|
30
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
agapsys/tests/__init__.py,sha256=zasd3WW7rgjPISnSxs4_MYlTZuWUqCvpiNh2hBGkrxo,133
|
|
2
|
+
agapsys/tests/assertions.py,sha256=-nY0mhV2W2HvmhgoH2lraeQSoGoROY1nHRsQDkmjyas,2669
|
|
3
|
+
agapsys/tests/console.py,sha256=6DUXKR5jhrVBwGgYNILfj85IECfdGxuDzFZGe1V1n24,4486
|
|
4
|
+
agapsys/tests/os.py,sha256=q-_p8WvzegnSXNrmUK6ITrn1BuBlfnvfQH1AAOAyluU,1631
|
|
5
|
+
agapsys/tests/time.py,sha256=fLpw87fsZUHQsAi3I1II7BKW7mZrnRe8Va9buV_feoo,876
|
|
6
|
+
agapsys_tests-0.0.0.dist-info/licenses/LICENSE,sha256=yr_RkPiAq-n-SY5DlUgGF3NLyKJs7Fma2lNNbUmPSaI,1089
|
|
7
|
+
agapsys_tests-0.0.0.dist-info/METADATA,sha256=VjLXUAjNpIum9fE6LUZAxlJtocYkjMcu7BBGzdJk26c,764
|
|
8
|
+
agapsys_tests-0.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
agapsys_tests-0.0.0.dist-info/top_level.txt,sha256=jQtj8IXrsij7eYU7deXAYNMNiYvMvdhkfjud5AjBtOk,8
|
|
10
|
+
agapsys_tests-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agapsys
|