pytest-regtest 2.0.1__tar.gz → 2.0.3__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.
- {pytest-regtest-2.0.1/pytest_regtest/pytest_regtest.egg-info → pytest-regtest-2.0.3}/PKG-INFO +61 -54
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/README.md +59 -53
- pytest-regtest-2.0.3/pyproject.toml +19 -0
- pytest-regtest-2.0.3/pytest_regtest/__init__.py +2 -0
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3/pytest_regtest/pytest_regtest.egg-info}/PKG-INFO +61 -54
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/pytest_regtest/pytest_regtest.egg-info/SOURCES.txt +2 -0
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/pytest_regtest/pytest_regtest.egg-info/requires.txt +1 -0
- pytest-regtest-2.0.3/pytest_regtest/pytest_regtest.egg-info/top_level.txt +2 -0
- pytest-regtest-2.0.3/pytest_regtest/pytest_regtest.py +380 -0
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/setup.cfg +4 -5
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/tests/test_plugin.py +81 -0
- pytest-regtest-2.0.1/pyproject.toml +0 -2
- pytest-regtest-2.0.1/pytest_regtest/pytest_regtest.egg-info/top_level.txt +0 -1
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/LICENSE.txt +0 -0
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/MANIFEST.in +0 -0
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/pytest_regtest/pytest_regtest.egg-info/dependency_links.txt +0 -0
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/pytest_regtest/pytest_regtest.egg-info/entry_points.txt +0 -0
- {pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/pytest_regtest/pytest_regtest.egg-info/not-zip-safe +0 -0
{pytest-regtest-2.0.1/pytest_regtest/pytest_regtest.egg-info → pytest-regtest-2.0.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytest-regtest
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: "pytest plugin for snapshot regression testing"
|
|
5
5
|
Author: Uwe Schmitt
|
|
6
6
|
Author-email: uwe.schmitt@id.ethz.ch
|
|
@@ -20,27 +20,30 @@ Provides-Extra: dev
|
|
|
20
20
|
Requires-Dist: twine; extra == "dev"
|
|
21
21
|
Requires-Dist: build; extra == "dev"
|
|
22
22
|
Requires-Dist: wheel; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
24
|
|
|
24
|
-
#
|
|
25
|
+
# pytest-regtest
|
|
25
26
|
|
|
26
27
|
## About
|
|
27
28
|
|
|
28
29
|
`pytest-regtest` is a plugin for [pytest](https://pytest.org) to implement
|
|
29
|
-
|
|
30
|
+
**regression testing**.
|
|
30
31
|
|
|
31
32
|
Unlike [functional testing](https://en.wikipedia.org/wiki/Functional_testing),
|
|
32
|
-
regression testing
|
|
33
|
-
|
|
33
|
+
[regression testing](https://en.wikipedia.org/wiki/Regression_testing)
|
|
34
|
+
testing does not test whether the software produces the correct
|
|
35
|
+
results, but whether it behaves as it did before changes were introduced.
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
More specifically, `pytest-regtest` provides **snapshot testing**, which
|
|
38
|
+
implements regression testing by recording the textual output of a test
|
|
39
|
+
function and comparing this recorded output to a reference output.
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
comparing this recorded output to a reference output.
|
|
41
|
+
**Regression testing** is a common technique to implement basic testing
|
|
42
|
+
before refactoring legacy code that lacks a test suite.
|
|
41
43
|
|
|
42
|
-
Snapshot testing can be used to implement tests for complex outcomes, such
|
|
43
|
-
recording textual database dumps or the results of a scientific analysis
|
|
44
|
+
Snapshot testing can also be used to implement tests for complex outcomes, such
|
|
45
|
+
as recording textual database dumps or the results of a scientific analysis
|
|
46
|
+
routine.
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
## Installation
|
|
@@ -51,14 +54,13 @@ To install and activate this plugin execute:
|
|
|
51
54
|
|
|
52
55
|
## Basic Usage
|
|
53
56
|
|
|
54
|
-
*pytest-regtest* plugin provides multiple fixtures.
|
|
55
57
|
|
|
56
58
|
### Write a test
|
|
57
59
|
|
|
60
|
+
*pytest-regtest* plugin provides multiple fixtures.
|
|
58
61
|
To record output, use the fixture *regtest* that works like a file handle:
|
|
59
62
|
|
|
60
|
-
```
|
|
61
|
-
|
|
63
|
+
```py
|
|
62
64
|
def test_squares_up_to_ten(regtest):
|
|
63
65
|
|
|
64
66
|
result = [i*i for i in range(10)]
|
|
@@ -68,7 +70,6 @@ def test_squares_up_to_ten(regtest):
|
|
|
68
70
|
|
|
69
71
|
# alternative method to record output:
|
|
70
72
|
regtest.write("done")
|
|
71
|
-
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
You can also use the `regtest_all` fixture. This enables all output to stdout to be
|
|
@@ -82,7 +83,7 @@ recorded output for this test function so far and thus the test will
|
|
|
82
83
|
fail with a message including a diff:
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
```
|
|
86
|
+
```bash
|
|
86
87
|
$ pytest test_demo.py
|
|
87
88
|
========================= test session starts ==========================
|
|
88
89
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -118,7 +119,7 @@ This is a diff of the current output `is` to a previously recorded output
|
|
|
118
119
|
To record the current output, we run *pytest* with the *--reset-regtest*
|
|
119
120
|
flag:
|
|
120
121
|
|
|
121
|
-
```
|
|
122
|
+
```bash
|
|
122
123
|
$ py.test -v --regtest-reset test_demo.py
|
|
123
124
|
========================= test session starts ==========================
|
|
124
125
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -143,7 +144,7 @@ Don't forget to commit this folder to your version control system!
|
|
|
143
144
|
|
|
144
145
|
When we run the test again, it succeeds:
|
|
145
146
|
|
|
146
|
-
```
|
|
147
|
+
```bash
|
|
147
148
|
$ py.test test_demo.py
|
|
148
149
|
========================= test session starts ==========================
|
|
149
150
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -160,10 +161,10 @@ total number of failed regression tests: 0
|
|
|
160
161
|
|
|
161
162
|
### Break the test
|
|
162
163
|
|
|
163
|
-
Let us we break the test by changing the
|
|
164
|
+
Let us we break the test by changing the test function to compute
|
|
164
165
|
11 instead of 10 square numbers:
|
|
165
166
|
|
|
166
|
-
```
|
|
167
|
+
```py
|
|
167
168
|
def test_squares_up_to_ten(regtest):
|
|
168
169
|
|
|
169
170
|
result = [i*i for i in range(11)] # changed!
|
|
@@ -178,7 +179,7 @@ def test_squares_up_to_ten(regtest):
|
|
|
178
179
|
The next run of pytest delivers a nice diff of the current and expected output
|
|
179
180
|
from this test function:
|
|
180
181
|
|
|
181
|
-
```
|
|
182
|
+
```bash
|
|
182
183
|
$ pytest test_demo.py
|
|
183
184
|
========================= test session starts ==========================
|
|
184
185
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -215,7 +216,7 @@ FAILED test_demo.py::test_squares_up_to_ten
|
|
|
215
216
|
The `regtest` fixture also works as a context manager to capture
|
|
216
217
|
all output from the wrapped code block:
|
|
217
218
|
|
|
218
|
-
```
|
|
219
|
+
```py
|
|
219
220
|
def test_squares_up_to_ten(regtest):
|
|
220
221
|
|
|
221
222
|
result = [i*i for i in range(10)]
|
|
@@ -229,7 +230,7 @@ def test_squares_up_to_ten(regtest):
|
|
|
229
230
|
The `regtest_all` fixture leads to recording of all output to `stdout` in a
|
|
230
231
|
test function.
|
|
231
232
|
|
|
232
|
-
```
|
|
233
|
+
```py
|
|
233
234
|
def test_all(regtest_all):
|
|
234
235
|
print("this line will be recorded.")
|
|
235
236
|
print("and this line also.")
|
|
@@ -240,14 +241,18 @@ def test_all(regtest_all):
|
|
|
240
241
|
|
|
241
242
|
You can reset recorded output of files and functions individually as:
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
```bash
|
|
245
|
+
$ py.test --regtest-reset test_demo.py
|
|
246
|
+
$ py.test --regtest-reset test_demo.py::test_squares_up_to_ten
|
|
247
|
+
```
|
|
245
248
|
|
|
246
249
|
### Suppress diff for failed tests
|
|
247
250
|
|
|
248
|
-
To
|
|
251
|
+
To hide the diff and just show the number of lines changed, use:
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
```bash
|
|
254
|
+
$ py.test --regtest-nodiff ...
|
|
255
|
+
```
|
|
251
256
|
|
|
252
257
|
|
|
253
258
|
### Show all recorded output
|
|
@@ -256,7 +261,9 @@ To supress the diff and only see the stats use:
|
|
|
256
261
|
For complex diffs it helps to see the full recorded output also.
|
|
257
262
|
To enable this use:
|
|
258
263
|
|
|
259
|
-
|
|
264
|
+
```bash
|
|
265
|
+
$ py.test --regtest-tee...
|
|
266
|
+
```
|
|
260
267
|
|
|
261
268
|
|
|
262
269
|
### Line endings
|
|
@@ -284,31 +291,31 @@ Per default the plugin:
|
|
|
284
291
|
|
|
285
292
|
You can register own converters in `conftest.py`:
|
|
286
293
|
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
294
|
+
```py
|
|
295
|
+
import re
|
|
296
|
+
import pytest_regtest
|
|
297
|
+
|
|
298
|
+
@pytest_regtest.register_converter_pre
|
|
299
|
+
def remove_password_lines(txt):
|
|
300
|
+
"""modify recorded output BEFORE the default fixes
|
|
301
|
+
like temp folders or hex object ids are applied"""
|
|
302
|
+
|
|
303
|
+
# remove lines with passwords:
|
|
304
|
+
lines = txt.splitlines(keepends=True)
|
|
305
|
+
lines = [l for l in lines if "password is" not in l]
|
|
306
|
+
return "".join(lines)
|
|
307
|
+
|
|
308
|
+
@pytest_regtest.register_converter_post
|
|
309
|
+
def fix_time_measurements(txt):
|
|
310
|
+
"""modify recorded output AFTER the default fixes
|
|
311
|
+
like temp folders or hex object ids are applied"""
|
|
312
|
+
|
|
313
|
+
# fix time measurements:
|
|
314
|
+
return re.sub(
|
|
315
|
+
"\d+(\.\d+)? seconds",
|
|
316
|
+
"<SECONDS> seconds",
|
|
317
|
+
txt
|
|
318
|
+
)
|
|
312
319
|
```
|
|
313
320
|
|
|
314
321
|
If you register multiple converters they will be applied in the order of
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pytest-regtest
|
|
2
2
|
|
|
3
3
|
## About
|
|
4
4
|
|
|
5
5
|
`pytest-regtest` is a plugin for [pytest](https://pytest.org) to implement
|
|
6
|
-
|
|
6
|
+
**regression testing**.
|
|
7
7
|
|
|
8
8
|
Unlike [functional testing](https://en.wikipedia.org/wiki/Functional_testing),
|
|
9
|
-
regression testing
|
|
10
|
-
|
|
9
|
+
[regression testing](https://en.wikipedia.org/wiki/Regression_testing)
|
|
10
|
+
testing does not test whether the software produces the correct
|
|
11
|
+
results, but whether it behaves as it did before changes were introduced.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
More specifically, `pytest-regtest` provides **snapshot testing**, which
|
|
14
|
+
implements regression testing by recording the textual output of a test
|
|
15
|
+
function and comparing this recorded output to a reference output.
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
comparing this recorded output to a reference output.
|
|
17
|
+
**Regression testing** is a common technique to implement basic testing
|
|
18
|
+
before refactoring legacy code that lacks a test suite.
|
|
18
19
|
|
|
19
|
-
Snapshot testing can be used to implement tests for complex outcomes, such
|
|
20
|
-
recording textual database dumps or the results of a scientific analysis
|
|
20
|
+
Snapshot testing can also be used to implement tests for complex outcomes, such
|
|
21
|
+
as recording textual database dumps or the results of a scientific analysis
|
|
22
|
+
routine.
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
@@ -28,14 +30,13 @@ To install and activate this plugin execute:
|
|
|
28
30
|
|
|
29
31
|
## Basic Usage
|
|
30
32
|
|
|
31
|
-
*pytest-regtest* plugin provides multiple fixtures.
|
|
32
33
|
|
|
33
34
|
### Write a test
|
|
34
35
|
|
|
36
|
+
*pytest-regtest* plugin provides multiple fixtures.
|
|
35
37
|
To record output, use the fixture *regtest* that works like a file handle:
|
|
36
38
|
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
+
```py
|
|
39
40
|
def test_squares_up_to_ten(regtest):
|
|
40
41
|
|
|
41
42
|
result = [i*i for i in range(10)]
|
|
@@ -45,7 +46,6 @@ def test_squares_up_to_ten(regtest):
|
|
|
45
46
|
|
|
46
47
|
# alternative method to record output:
|
|
47
48
|
regtest.write("done")
|
|
48
|
-
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
You can also use the `regtest_all` fixture. This enables all output to stdout to be
|
|
@@ -59,7 +59,7 @@ recorded output for this test function so far and thus the test will
|
|
|
59
59
|
fail with a message including a diff:
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
```
|
|
62
|
+
```bash
|
|
63
63
|
$ pytest test_demo.py
|
|
64
64
|
========================= test session starts ==========================
|
|
65
65
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -95,7 +95,7 @@ This is a diff of the current output `is` to a previously recorded output
|
|
|
95
95
|
To record the current output, we run *pytest* with the *--reset-regtest*
|
|
96
96
|
flag:
|
|
97
97
|
|
|
98
|
-
```
|
|
98
|
+
```bash
|
|
99
99
|
$ py.test -v --regtest-reset test_demo.py
|
|
100
100
|
========================= test session starts ==========================
|
|
101
101
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -120,7 +120,7 @@ Don't forget to commit this folder to your version control system!
|
|
|
120
120
|
|
|
121
121
|
When we run the test again, it succeeds:
|
|
122
122
|
|
|
123
|
-
```
|
|
123
|
+
```bash
|
|
124
124
|
$ py.test test_demo.py
|
|
125
125
|
========================= test session starts ==========================
|
|
126
126
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -137,10 +137,10 @@ total number of failed regression tests: 0
|
|
|
137
137
|
|
|
138
138
|
### Break the test
|
|
139
139
|
|
|
140
|
-
Let us we break the test by changing the
|
|
140
|
+
Let us we break the test by changing the test function to compute
|
|
141
141
|
11 instead of 10 square numbers:
|
|
142
142
|
|
|
143
|
-
```
|
|
143
|
+
```py
|
|
144
144
|
def test_squares_up_to_ten(regtest):
|
|
145
145
|
|
|
146
146
|
result = [i*i for i in range(11)] # changed!
|
|
@@ -155,7 +155,7 @@ def test_squares_up_to_ten(regtest):
|
|
|
155
155
|
The next run of pytest delivers a nice diff of the current and expected output
|
|
156
156
|
from this test function:
|
|
157
157
|
|
|
158
|
-
```
|
|
158
|
+
```bash
|
|
159
159
|
$ pytest test_demo.py
|
|
160
160
|
========================= test session starts ==========================
|
|
161
161
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -192,7 +192,7 @@ FAILED test_demo.py::test_squares_up_to_ten
|
|
|
192
192
|
The `regtest` fixture also works as a context manager to capture
|
|
193
193
|
all output from the wrapped code block:
|
|
194
194
|
|
|
195
|
-
```
|
|
195
|
+
```py
|
|
196
196
|
def test_squares_up_to_ten(regtest):
|
|
197
197
|
|
|
198
198
|
result = [i*i for i in range(10)]
|
|
@@ -206,7 +206,7 @@ def test_squares_up_to_ten(regtest):
|
|
|
206
206
|
The `regtest_all` fixture leads to recording of all output to `stdout` in a
|
|
207
207
|
test function.
|
|
208
208
|
|
|
209
|
-
```
|
|
209
|
+
```py
|
|
210
210
|
def test_all(regtest_all):
|
|
211
211
|
print("this line will be recorded.")
|
|
212
212
|
print("and this line also.")
|
|
@@ -217,14 +217,18 @@ def test_all(regtest_all):
|
|
|
217
217
|
|
|
218
218
|
You can reset recorded output of files and functions individually as:
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
```bash
|
|
221
|
+
$ py.test --regtest-reset test_demo.py
|
|
222
|
+
$ py.test --regtest-reset test_demo.py::test_squares_up_to_ten
|
|
223
|
+
```
|
|
222
224
|
|
|
223
225
|
### Suppress diff for failed tests
|
|
224
226
|
|
|
225
|
-
To
|
|
227
|
+
To hide the diff and just show the number of lines changed, use:
|
|
226
228
|
|
|
227
|
-
|
|
229
|
+
```bash
|
|
230
|
+
$ py.test --regtest-nodiff ...
|
|
231
|
+
```
|
|
228
232
|
|
|
229
233
|
|
|
230
234
|
### Show all recorded output
|
|
@@ -233,7 +237,9 @@ To supress the diff and only see the stats use:
|
|
|
233
237
|
For complex diffs it helps to see the full recorded output also.
|
|
234
238
|
To enable this use:
|
|
235
239
|
|
|
236
|
-
|
|
240
|
+
```bash
|
|
241
|
+
$ py.test --regtest-tee...
|
|
242
|
+
```
|
|
237
243
|
|
|
238
244
|
|
|
239
245
|
### Line endings
|
|
@@ -261,31 +267,31 @@ Per default the plugin:
|
|
|
261
267
|
|
|
262
268
|
You can register own converters in `conftest.py`:
|
|
263
269
|
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
270
|
+
```py
|
|
271
|
+
import re
|
|
272
|
+
import pytest_regtest
|
|
273
|
+
|
|
274
|
+
@pytest_regtest.register_converter_pre
|
|
275
|
+
def remove_password_lines(txt):
|
|
276
|
+
"""modify recorded output BEFORE the default fixes
|
|
277
|
+
like temp folders or hex object ids are applied"""
|
|
278
|
+
|
|
279
|
+
# remove lines with passwords:
|
|
280
|
+
lines = txt.splitlines(keepends=True)
|
|
281
|
+
lines = [l for l in lines if "password is" not in l]
|
|
282
|
+
return "".join(lines)
|
|
283
|
+
|
|
284
|
+
@pytest_regtest.register_converter_post
|
|
285
|
+
def fix_time_measurements(txt):
|
|
286
|
+
"""modify recorded output AFTER the default fixes
|
|
287
|
+
like temp folders or hex object ids are applied"""
|
|
288
|
+
|
|
289
|
+
# fix time measurements:
|
|
290
|
+
return re.sub(
|
|
291
|
+
"\d+(\.\d+)? seconds",
|
|
292
|
+
"<SECONDS> seconds",
|
|
293
|
+
txt
|
|
294
|
+
)
|
|
289
295
|
```
|
|
290
296
|
|
|
291
297
|
If you register multiple converters they will be applied in the order of
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
[tool.tox]
|
|
6
|
+
legacy_tox_ini = '''
|
|
7
|
+
[tox]
|
|
8
|
+
isolated_build = true
|
|
9
|
+
envlist = py39,py310,py311,py312
|
|
10
|
+
package = sdist
|
|
11
|
+
|
|
12
|
+
[testenv]
|
|
13
|
+
skip_install = false
|
|
14
|
+
passenv = *
|
|
15
|
+
allowlist_externals = pytest
|
|
16
|
+
extras = dev
|
|
17
|
+
commands =
|
|
18
|
+
pytest -ra -v tests
|
|
19
|
+
'''
|
{pytest-regtest-2.0.1 → pytest-regtest-2.0.3/pytest_regtest/pytest_regtest.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytest-regtest
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: "pytest plugin for snapshot regression testing"
|
|
5
5
|
Author: Uwe Schmitt
|
|
6
6
|
Author-email: uwe.schmitt@id.ethz.ch
|
|
@@ -20,27 +20,30 @@ Provides-Extra: dev
|
|
|
20
20
|
Requires-Dist: twine; extra == "dev"
|
|
21
21
|
Requires-Dist: build; extra == "dev"
|
|
22
22
|
Requires-Dist: wheel; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
24
|
|
|
24
|
-
#
|
|
25
|
+
# pytest-regtest
|
|
25
26
|
|
|
26
27
|
## About
|
|
27
28
|
|
|
28
29
|
`pytest-regtest` is a plugin for [pytest](https://pytest.org) to implement
|
|
29
|
-
|
|
30
|
+
**regression testing**.
|
|
30
31
|
|
|
31
32
|
Unlike [functional testing](https://en.wikipedia.org/wiki/Functional_testing),
|
|
32
|
-
regression testing
|
|
33
|
-
|
|
33
|
+
[regression testing](https://en.wikipedia.org/wiki/Regression_testing)
|
|
34
|
+
testing does not test whether the software produces the correct
|
|
35
|
+
results, but whether it behaves as it did before changes were introduced.
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
More specifically, `pytest-regtest` provides **snapshot testing**, which
|
|
38
|
+
implements regression testing by recording the textual output of a test
|
|
39
|
+
function and comparing this recorded output to a reference output.
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
comparing this recorded output to a reference output.
|
|
41
|
+
**Regression testing** is a common technique to implement basic testing
|
|
42
|
+
before refactoring legacy code that lacks a test suite.
|
|
41
43
|
|
|
42
|
-
Snapshot testing can be used to implement tests for complex outcomes, such
|
|
43
|
-
recording textual database dumps or the results of a scientific analysis
|
|
44
|
+
Snapshot testing can also be used to implement tests for complex outcomes, such
|
|
45
|
+
as recording textual database dumps or the results of a scientific analysis
|
|
46
|
+
routine.
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
## Installation
|
|
@@ -51,14 +54,13 @@ To install and activate this plugin execute:
|
|
|
51
54
|
|
|
52
55
|
## Basic Usage
|
|
53
56
|
|
|
54
|
-
*pytest-regtest* plugin provides multiple fixtures.
|
|
55
57
|
|
|
56
58
|
### Write a test
|
|
57
59
|
|
|
60
|
+
*pytest-regtest* plugin provides multiple fixtures.
|
|
58
61
|
To record output, use the fixture *regtest* that works like a file handle:
|
|
59
62
|
|
|
60
|
-
```
|
|
61
|
-
|
|
63
|
+
```py
|
|
62
64
|
def test_squares_up_to_ten(regtest):
|
|
63
65
|
|
|
64
66
|
result = [i*i for i in range(10)]
|
|
@@ -68,7 +70,6 @@ def test_squares_up_to_ten(regtest):
|
|
|
68
70
|
|
|
69
71
|
# alternative method to record output:
|
|
70
72
|
regtest.write("done")
|
|
71
|
-
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
You can also use the `regtest_all` fixture. This enables all output to stdout to be
|
|
@@ -82,7 +83,7 @@ recorded output for this test function so far and thus the test will
|
|
|
82
83
|
fail with a message including a diff:
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
```
|
|
86
|
+
```bash
|
|
86
87
|
$ pytest test_demo.py
|
|
87
88
|
========================= test session starts ==========================
|
|
88
89
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -118,7 +119,7 @@ This is a diff of the current output `is` to a previously recorded output
|
|
|
118
119
|
To record the current output, we run *pytest* with the *--reset-regtest*
|
|
119
120
|
flag:
|
|
120
121
|
|
|
121
|
-
```
|
|
122
|
+
```bash
|
|
122
123
|
$ py.test -v --regtest-reset test_demo.py
|
|
123
124
|
========================= test session starts ==========================
|
|
124
125
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -143,7 +144,7 @@ Don't forget to commit this folder to your version control system!
|
|
|
143
144
|
|
|
144
145
|
When we run the test again, it succeeds:
|
|
145
146
|
|
|
146
|
-
```
|
|
147
|
+
```bash
|
|
147
148
|
$ py.test test_demo.py
|
|
148
149
|
========================= test session starts ==========================
|
|
149
150
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -160,10 +161,10 @@ total number of failed regression tests: 0
|
|
|
160
161
|
|
|
161
162
|
### Break the test
|
|
162
163
|
|
|
163
|
-
Let us we break the test by changing the
|
|
164
|
+
Let us we break the test by changing the test function to compute
|
|
164
165
|
11 instead of 10 square numbers:
|
|
165
166
|
|
|
166
|
-
```
|
|
167
|
+
```py
|
|
167
168
|
def test_squares_up_to_ten(regtest):
|
|
168
169
|
|
|
169
170
|
result = [i*i for i in range(11)] # changed!
|
|
@@ -178,7 +179,7 @@ def test_squares_up_to_ten(regtest):
|
|
|
178
179
|
The next run of pytest delivers a nice diff of the current and expected output
|
|
179
180
|
from this test function:
|
|
180
181
|
|
|
181
|
-
```
|
|
182
|
+
```bash
|
|
182
183
|
$ pytest test_demo.py
|
|
183
184
|
========================= test session starts ==========================
|
|
184
185
|
platform darwin -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
|
|
@@ -215,7 +216,7 @@ FAILED test_demo.py::test_squares_up_to_ten
|
|
|
215
216
|
The `regtest` fixture also works as a context manager to capture
|
|
216
217
|
all output from the wrapped code block:
|
|
217
218
|
|
|
218
|
-
```
|
|
219
|
+
```py
|
|
219
220
|
def test_squares_up_to_ten(regtest):
|
|
220
221
|
|
|
221
222
|
result = [i*i for i in range(10)]
|
|
@@ -229,7 +230,7 @@ def test_squares_up_to_ten(regtest):
|
|
|
229
230
|
The `regtest_all` fixture leads to recording of all output to `stdout` in a
|
|
230
231
|
test function.
|
|
231
232
|
|
|
232
|
-
```
|
|
233
|
+
```py
|
|
233
234
|
def test_all(regtest_all):
|
|
234
235
|
print("this line will be recorded.")
|
|
235
236
|
print("and this line also.")
|
|
@@ -240,14 +241,18 @@ def test_all(regtest_all):
|
|
|
240
241
|
|
|
241
242
|
You can reset recorded output of files and functions individually as:
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
```bash
|
|
245
|
+
$ py.test --regtest-reset test_demo.py
|
|
246
|
+
$ py.test --regtest-reset test_demo.py::test_squares_up_to_ten
|
|
247
|
+
```
|
|
245
248
|
|
|
246
249
|
### Suppress diff for failed tests
|
|
247
250
|
|
|
248
|
-
To
|
|
251
|
+
To hide the diff and just show the number of lines changed, use:
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
```bash
|
|
254
|
+
$ py.test --regtest-nodiff ...
|
|
255
|
+
```
|
|
251
256
|
|
|
252
257
|
|
|
253
258
|
### Show all recorded output
|
|
@@ -256,7 +261,9 @@ To supress the diff and only see the stats use:
|
|
|
256
261
|
For complex diffs it helps to see the full recorded output also.
|
|
257
262
|
To enable this use:
|
|
258
263
|
|
|
259
|
-
|
|
264
|
+
```bash
|
|
265
|
+
$ py.test --regtest-tee...
|
|
266
|
+
```
|
|
260
267
|
|
|
261
268
|
|
|
262
269
|
### Line endings
|
|
@@ -284,31 +291,31 @@ Per default the plugin:
|
|
|
284
291
|
|
|
285
292
|
You can register own converters in `conftest.py`:
|
|
286
293
|
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
294
|
+
```py
|
|
295
|
+
import re
|
|
296
|
+
import pytest_regtest
|
|
297
|
+
|
|
298
|
+
@pytest_regtest.register_converter_pre
|
|
299
|
+
def remove_password_lines(txt):
|
|
300
|
+
"""modify recorded output BEFORE the default fixes
|
|
301
|
+
like temp folders or hex object ids are applied"""
|
|
302
|
+
|
|
303
|
+
# remove lines with passwords:
|
|
304
|
+
lines = txt.splitlines(keepends=True)
|
|
305
|
+
lines = [l for l in lines if "password is" not in l]
|
|
306
|
+
return "".join(lines)
|
|
307
|
+
|
|
308
|
+
@pytest_regtest.register_converter_post
|
|
309
|
+
def fix_time_measurements(txt):
|
|
310
|
+
"""modify recorded output AFTER the default fixes
|
|
311
|
+
like temp folders or hex object ids are applied"""
|
|
312
|
+
|
|
313
|
+
# fix time measurements:
|
|
314
|
+
return re.sub(
|
|
315
|
+
"\d+(\.\d+)? seconds",
|
|
316
|
+
"<SECONDS> seconds",
|
|
317
|
+
txt
|
|
318
|
+
)
|
|
312
319
|
```
|
|
313
320
|
|
|
314
321
|
If you register multiple converters they will be applied in the order of
|
{pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/pytest_regtest/pytest_regtest.egg-info/SOURCES.txt
RENAMED
|
@@ -3,6 +3,8 @@ MANIFEST.in
|
|
|
3
3
|
README.md
|
|
4
4
|
pyproject.toml
|
|
5
5
|
setup.cfg
|
|
6
|
+
pytest_regtest/__init__.py
|
|
7
|
+
pytest_regtest/pytest_regtest.py
|
|
6
8
|
pytest_regtest/pytest_regtest.egg-info/PKG-INFO
|
|
7
9
|
pytest_regtest/pytest_regtest.egg-info/SOURCES.txt
|
|
8
10
|
pytest_regtest/pytest_regtest.egg-info/dependency_links.txt
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from hashlib import sha512
|
|
9
|
+
from io import StringIO
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from _pytest._code.code import TerminalRepr
|
|
13
|
+
from _pytest._io import TerminalWriter
|
|
14
|
+
|
|
15
|
+
pytest_plugins = ["pytester"]
|
|
16
|
+
|
|
17
|
+
IS_WIN = sys.platform == "win32"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ljust(s, *a):
|
|
21
|
+
return s.ljust(*a)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegtestException(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pytest_addoption(parser):
|
|
29
|
+
"""Add options to control the timeout plugin"""
|
|
30
|
+
group = parser.getgroup("regtest", "regression test plugin")
|
|
31
|
+
group.addoption(
|
|
32
|
+
"--regtest-reset",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="do not run regtest but record current output",
|
|
35
|
+
)
|
|
36
|
+
group.addoption(
|
|
37
|
+
"--regtest-tee",
|
|
38
|
+
action="store_true",
|
|
39
|
+
default=False,
|
|
40
|
+
help="print recorded results to console too",
|
|
41
|
+
)
|
|
42
|
+
group.addoption(
|
|
43
|
+
"--regtest-consider-line-endings",
|
|
44
|
+
action="store_true",
|
|
45
|
+
default=False,
|
|
46
|
+
help="do not strip whitespaces at end of recorded lines",
|
|
47
|
+
)
|
|
48
|
+
group.addoption(
|
|
49
|
+
"--regtest-nodiff",
|
|
50
|
+
action="store_true",
|
|
51
|
+
default=False,
|
|
52
|
+
help="do not show diff output for failed regresson tests",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def pytest_configure(config):
|
|
57
|
+
config.pluginmanager.register(PytestRegtestPlugin())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PytestRegtestPlugin:
|
|
61
|
+
def __init__(self):
|
|
62
|
+
self._reset_regtest_outputs = []
|
|
63
|
+
self._failed_regtests = []
|
|
64
|
+
|
|
65
|
+
@pytest.hookimpl(trylast=True)
|
|
66
|
+
def pytest_runtest_call(self, item):
|
|
67
|
+
if hasattr(item, "regtest_stream"):
|
|
68
|
+
self.check_recorded_output(item)
|
|
69
|
+
|
|
70
|
+
def check_recorded_output(self, item):
|
|
71
|
+
test_folder = item.fspath.dirname
|
|
72
|
+
regtest_stream = item.regtest_stream
|
|
73
|
+
identifier = regtest_stream.identifier
|
|
74
|
+
|
|
75
|
+
recorded_output_path = result_file_path(test_folder, item.nodeid, identifier)
|
|
76
|
+
|
|
77
|
+
config = item.config
|
|
78
|
+
|
|
79
|
+
ignore_line_endings = not config.getvalue("--regtest-consider-line-endings")
|
|
80
|
+
reset = config.getvalue("--regtest-reset")
|
|
81
|
+
|
|
82
|
+
if reset:
|
|
83
|
+
os.makedirs(os.path.dirname(recorded_output_path), exist_ok=True)
|
|
84
|
+
with open(recorded_output_path, "w", encoding="utf-8") as fh:
|
|
85
|
+
fh.write("".join(regtest_stream.get_lines()))
|
|
86
|
+
self._reset_regtest_outputs.append(recorded_output_path)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if os.path.exists(recorded_output_path):
|
|
90
|
+
with open(recorded_output_path, "r", encoding="utf-8") as fh:
|
|
91
|
+
tobe = fh.readlines()
|
|
92
|
+
else:
|
|
93
|
+
tobe = []
|
|
94
|
+
|
|
95
|
+
current = regtest_stream.get_lines()
|
|
96
|
+
if ignore_line_endings:
|
|
97
|
+
current = [line.rstrip() for line in current]
|
|
98
|
+
tobe = [line.rstrip() for line in tobe]
|
|
99
|
+
|
|
100
|
+
if current != tobe:
|
|
101
|
+
self._failed_regtests.append(item)
|
|
102
|
+
raise RegtestException(current, tobe, regtest_stream)
|
|
103
|
+
|
|
104
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
105
|
+
def pytest_pyfunc_call(self, pyfuncitem):
|
|
106
|
+
stdout = sys.stdout
|
|
107
|
+
if "regtest_all" in pyfuncitem.fixturenames and hasattr(
|
|
108
|
+
pyfuncitem, "regtest_stream"
|
|
109
|
+
):
|
|
110
|
+
sys.stdout = pyfuncitem.regtest_stream
|
|
111
|
+
yield
|
|
112
|
+
sys.stdout = stdout
|
|
113
|
+
|
|
114
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
115
|
+
def pytest_report_teststatus(self, report, config):
|
|
116
|
+
outcome = yield
|
|
117
|
+
if report.when == "call":
|
|
118
|
+
if config.getvalue("--regtest-reset"):
|
|
119
|
+
result = outcome.get_result()
|
|
120
|
+
if result[0] != "failed":
|
|
121
|
+
outcome.force_result((result[0], "R", "RESET"))
|
|
122
|
+
|
|
123
|
+
def pytest_terminal_summary(self, terminalreporter, exitstatus, config):
|
|
124
|
+
terminalreporter.ensure_newline()
|
|
125
|
+
terminalreporter.section("pytest-regtest report", sep="-", blue=True, bold=True)
|
|
126
|
+
terminalreporter.write("total number of failed regression tests: ", bold=True)
|
|
127
|
+
terminalreporter.line(str(len(self._failed_regtests)))
|
|
128
|
+
if config.getvalue("--regtest-reset"):
|
|
129
|
+
if config.option.verbose:
|
|
130
|
+
terminalreporter.line(
|
|
131
|
+
"the following output files have been reset:", bold=True
|
|
132
|
+
)
|
|
133
|
+
for path in self._reset_regtest_outputs:
|
|
134
|
+
rel_path = os.path.relpath(path)
|
|
135
|
+
terminalreporter.line(" " + rel_path)
|
|
136
|
+
else:
|
|
137
|
+
terminalreporter.write(
|
|
138
|
+
"total number of reset output files: ", bold=True
|
|
139
|
+
)
|
|
140
|
+
terminalreporter.line(str(len(self._reset_regtest_outputs)))
|
|
141
|
+
|
|
142
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
143
|
+
def pytest_runtest_makereport(self, item, call):
|
|
144
|
+
outcome = yield
|
|
145
|
+
if call.when == "teardown" and hasattr(item, "regtest_stream"):
|
|
146
|
+
if item.config.getvalue("--regtest-tee"):
|
|
147
|
+
tw = TerminalWriter()
|
|
148
|
+
tw.line()
|
|
149
|
+
line = "recorded raw output to regtest fixture: "
|
|
150
|
+
line = ljust(line, tw.fullwidth, "-")
|
|
151
|
+
tw.line(line, green=True)
|
|
152
|
+
tw.write(item.regtest_stream.get_output() + "\n", cyan=True)
|
|
153
|
+
tw.line("-" * tw.fullwidth, green=True)
|
|
154
|
+
tw.line()
|
|
155
|
+
tw.flush()
|
|
156
|
+
|
|
157
|
+
test_folder = item.fspath.dirname
|
|
158
|
+
regtest_stream = item.regtest_stream
|
|
159
|
+
identifier = regtest_stream.identifier
|
|
160
|
+
|
|
161
|
+
recorded_output_path = result_file_path(
|
|
162
|
+
test_folder, item.nodeid, identifier
|
|
163
|
+
)
|
|
164
|
+
result = outcome.get_result()
|
|
165
|
+
result.longrepr = CollectErrorRepr(
|
|
166
|
+
[f"wrote recorded output to {recorded_output_path}"], [dict()]
|
|
167
|
+
)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if call.when != "call" or not hasattr(item, "regtest_stream"):
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if call.excinfo is not None and call.excinfo.type is RegtestException:
|
|
174
|
+
current, recorded, regtest_stream = call.excinfo.value.args
|
|
175
|
+
|
|
176
|
+
nodeid = item.nodeid + (
|
|
177
|
+
""
|
|
178
|
+
if regtest_stream.identifier is None
|
|
179
|
+
else "__" + regtest_stream.identifier
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
nodiff = item.config.getvalue("--regtest-nodiff")
|
|
183
|
+
ignore_line_endings = not item.config.getvalue(
|
|
184
|
+
"--regtest-consider-line-endings"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
result = outcome.get_result()
|
|
188
|
+
|
|
189
|
+
if not ignore_line_endings:
|
|
190
|
+
# add quotes around lines in diff:
|
|
191
|
+
|
|
192
|
+
current = list(map(repr, current))
|
|
193
|
+
recorded = list(map(repr, recorded))
|
|
194
|
+
|
|
195
|
+
collected = list(
|
|
196
|
+
difflib.unified_diff(current, recorded, "is", "tobe", lineterm="")
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
msg = "\nregression test output differences for {}:\n".format(nodeid)
|
|
200
|
+
if nodiff:
|
|
201
|
+
msg_diff = f"{len(collected)} lines in diff"
|
|
202
|
+
else:
|
|
203
|
+
msg_diff = "> " + "\n> ".join(collected)
|
|
204
|
+
|
|
205
|
+
result.longrepr = CollectErrorRepr(
|
|
206
|
+
[msg, msg_diff + "\n"], [dict(), dict(red=True, bold=True)]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def result_file_path(test_folder, nodeid, identifier):
|
|
211
|
+
file_name, __, test_function = nodeid.partition("::")
|
|
212
|
+
file_name = os.path.basename(file_name)
|
|
213
|
+
|
|
214
|
+
for c in "/\\:*\"'?<>|":
|
|
215
|
+
test_function = test_function.replace(c, "-")
|
|
216
|
+
|
|
217
|
+
# If file name is too long, hash parameters.
|
|
218
|
+
if len(test_function) > 100:
|
|
219
|
+
test_function = (
|
|
220
|
+
test_function[:88]
|
|
221
|
+
+ "__"
|
|
222
|
+
+ sha512(test_function.encode("utf-8")).hexdigest()[:10]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
test_function = test_function.replace(" ", "_")
|
|
226
|
+
stem, __ = os.path.splitext(file_name)
|
|
227
|
+
if identifier is not None:
|
|
228
|
+
output_file_name = stem + "." + test_function + "__" + identifier + ".out"
|
|
229
|
+
else:
|
|
230
|
+
output_file_name = stem + "." + test_function + ".out"
|
|
231
|
+
|
|
232
|
+
return os.path.join(test_folder, "_regtest_outputs", output_file_name)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@pytest.fixture
|
|
236
|
+
def regtest(request):
|
|
237
|
+
yield RegtestStream(request)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.fixture
|
|
241
|
+
def regtest_all(regtest):
|
|
242
|
+
yield regtest
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class RegtestStream:
|
|
246
|
+
def __init__(self, request):
|
|
247
|
+
request.node.regtest_stream = self
|
|
248
|
+
self.request = request
|
|
249
|
+
self.buffer = StringIO()
|
|
250
|
+
self.identifier = None
|
|
251
|
+
|
|
252
|
+
def write(self, what):
|
|
253
|
+
self.buffer.write(what)
|
|
254
|
+
|
|
255
|
+
def flush(self):
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
def get_lines(self):
|
|
259
|
+
output = self.buffer.getvalue()
|
|
260
|
+
if not output:
|
|
261
|
+
return []
|
|
262
|
+
output = cleanup(output, self.request)
|
|
263
|
+
lines = output.splitlines(keepends=True)
|
|
264
|
+
return lines
|
|
265
|
+
|
|
266
|
+
def get_output(self):
|
|
267
|
+
return self.buffer.getvalue()
|
|
268
|
+
|
|
269
|
+
def __enter__(self):
|
|
270
|
+
sys.stdout = self
|
|
271
|
+
return self
|
|
272
|
+
|
|
273
|
+
def __exit__(self, *a):
|
|
274
|
+
sys.stdout = sys.__stdout__
|
|
275
|
+
return False # dont suppress exception
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def cleanup(output, request):
|
|
279
|
+
for converter in _converters_pre:
|
|
280
|
+
output = converter(output, request)
|
|
281
|
+
|
|
282
|
+
output = _std_conversion(output, request)
|
|
283
|
+
|
|
284
|
+
for converter in _converters_post:
|
|
285
|
+
output = converter(output, request)
|
|
286
|
+
|
|
287
|
+
# in python 3 a string should not contain binary symbols...:
|
|
288
|
+
if contains_binary(output):
|
|
289
|
+
request.raiseerror(
|
|
290
|
+
"recorded output for regression test contains unprintable characters."
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return output
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# the function below is modified version of http://stackoverflow.com/questions/898669/
|
|
297
|
+
textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7F})
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def contains_binary(txt):
|
|
301
|
+
return bool(txt.translate(dict(zip(textchars, " " * 9999))).replace(" ", ""))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
_converters_pre = []
|
|
305
|
+
_converters_post = []
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _fix_pre_v2_converter_function(function):
|
|
309
|
+
@functools.wraps(function)
|
|
310
|
+
def fixed_converter_function(output, request):
|
|
311
|
+
return function(output)
|
|
312
|
+
|
|
313
|
+
return fixed_converter_function
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def register_converter_pre(function):
|
|
317
|
+
if function not in _converters_pre:
|
|
318
|
+
signature = inspect.signature(function)
|
|
319
|
+
# keep downward compatibility:
|
|
320
|
+
if len(signature.parameters) == 1:
|
|
321
|
+
function = _fix_pre_v2_converter_function(function)
|
|
322
|
+
_converters_pre.append(function)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def register_converter_post(function):
|
|
326
|
+
if function not in _converters_post:
|
|
327
|
+
signature = inspect.signature(function)
|
|
328
|
+
# keep downward compatibility:
|
|
329
|
+
if len(signature.parameters) == 1:
|
|
330
|
+
function = _fix_pre_v2_converter_function(function)
|
|
331
|
+
_converters_post.append(function)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _std_replacements(request):
|
|
335
|
+
if "tmpdir" in request.fixturenames:
|
|
336
|
+
tmpdir = request.getfixturevalue("tmpdir").strpath + os.path.sep
|
|
337
|
+
yield tmpdir, "<tmpdir_from_fixture>/"
|
|
338
|
+
tmpdir = request.getfixturevalue("tmpdir").strpath
|
|
339
|
+
yield tmpdir, "<tmpdir_from_fixture>"
|
|
340
|
+
|
|
341
|
+
regexp = os.path.join(
|
|
342
|
+
os.path.realpath(tempfile.gettempdir()), "pytest-of-.*", r"pytest-\d+/"
|
|
343
|
+
)
|
|
344
|
+
yield regexp, "<pytest_tempdir>/"
|
|
345
|
+
|
|
346
|
+
regexp = os.path.join(tempfile.gettempdir(), "tmp[_a-zA-Z0-9]+")
|
|
347
|
+
|
|
348
|
+
yield regexp, "<tmpdir_from_tempfile_module>"
|
|
349
|
+
yield os.path.realpath(
|
|
350
|
+
tempfile.gettempdir()
|
|
351
|
+
) + os.path.sep, "<tmpdir_from_tempfile_module>/"
|
|
352
|
+
yield os.path.realpath(tempfile.gettempdir()), "<tmpdir_from_tempfile_module>"
|
|
353
|
+
yield tempfile.tempdir + os.path.sep, "<tmpdir_from_tempfile_module>/"
|
|
354
|
+
yield tempfile.tempdir, "<tmpdir_from_tempfile_module>"
|
|
355
|
+
yield r"var/folders/.*/pytest-of.*/", "<pytest_tempdir>/"
|
|
356
|
+
|
|
357
|
+
# replace hex object ids in output by 0x?????????
|
|
358
|
+
yield r" 0x[0-9a-fA-F]+", " 0x?????????"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _std_conversion(output, request):
|
|
362
|
+
fixed = []
|
|
363
|
+
for line in output.splitlines(keepends=True):
|
|
364
|
+
for regex, replacement in _std_replacements(request):
|
|
365
|
+
if IS_WIN:
|
|
366
|
+
# fix windows backwards slashes in regex
|
|
367
|
+
regex = regex.replace("\\", "\\\\")
|
|
368
|
+
line, __ = re.subn(regex, replacement, line)
|
|
369
|
+
fixed.append(line)
|
|
370
|
+
return "".join(fixed)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class CollectErrorRepr(TerminalRepr):
|
|
374
|
+
def __init__(self, messages, colors):
|
|
375
|
+
self.messages = messages
|
|
376
|
+
self.colors = colors
|
|
377
|
+
|
|
378
|
+
def toterminal(self, out):
|
|
379
|
+
for message, color in zip(self.messages, self.colors):
|
|
380
|
+
out.line(message, **color)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = pytest-regtest
|
|
3
|
-
version = 2.0.
|
|
3
|
+
version = 2.0.3
|
|
4
4
|
license = MIT
|
|
5
5
|
description = "pytest plugin for snapshot regression testing"
|
|
6
6
|
long_description = file: README.md
|
|
@@ -20,13 +20,11 @@ project_urls =
|
|
|
20
20
|
[options]
|
|
21
21
|
install_requires =
|
|
22
22
|
pytest>7.2
|
|
23
|
-
|
|
23
|
+
package_dir =
|
|
24
|
+
= pytest_regtest
|
|
24
25
|
zip_safe = False
|
|
25
26
|
python_requires = >=3.9
|
|
26
27
|
|
|
27
|
-
[options.packages.find]
|
|
28
|
-
where = pytest_regtest/
|
|
29
|
-
|
|
30
28
|
[options.entry_points]
|
|
31
29
|
pytest11 =
|
|
32
30
|
regtest = pytest_regtest
|
|
@@ -36,6 +34,7 @@ dev =
|
|
|
36
34
|
twine
|
|
37
35
|
build
|
|
38
36
|
wheel
|
|
37
|
+
pytest
|
|
39
38
|
|
|
40
39
|
[egg_info]
|
|
41
40
|
tag_build =
|
|
@@ -19,6 +19,7 @@ def create_test_regtest_context_manager(testdir):
|
|
|
19
19
|
print(tempfile.gettempdir())
|
|
20
20
|
print(tempfile.mkdtemp())
|
|
21
21
|
print("obj id is", hex(id(tempfile)))
|
|
22
|
+
regtest.flush()
|
|
22
23
|
|
|
23
24
|
"""
|
|
24
25
|
)
|
|
@@ -39,6 +40,7 @@ def create_test_regtest_fh(testdir):
|
|
|
39
40
|
print(tempfile.gettempdir(), file=regtest)
|
|
40
41
|
print(tempfile.mkdtemp(), file=regtest)
|
|
41
42
|
print("obj id is", hex(id(tempfile)), file=regtest)
|
|
43
|
+
regtest.flush()
|
|
42
44
|
|
|
43
45
|
"""
|
|
44
46
|
)
|
|
@@ -149,6 +151,42 @@ def test_failed_test(testdir):
|
|
|
149
151
|
result.assert_outcomes(failed=1)
|
|
150
152
|
|
|
151
153
|
|
|
154
|
+
def test_converter_pre_pre_v2(testdir):
|
|
155
|
+
testdir.makepyfile(
|
|
156
|
+
"""
|
|
157
|
+
import tempfile
|
|
158
|
+
from pytest_regtest import register_converter_pre
|
|
159
|
+
|
|
160
|
+
@register_converter_pre
|
|
161
|
+
def to_upper_conv(line):
|
|
162
|
+
return line.upper()
|
|
163
|
+
|
|
164
|
+
def test_regtest(regtest_all, tmpdir):
|
|
165
|
+
print("this is expected outcome")
|
|
166
|
+
print("obj id is 0xabcdeffff")
|
|
167
|
+
"""
|
|
168
|
+
)
|
|
169
|
+
result = testdir.runpytest_subprocess()
|
|
170
|
+
result.assert_outcomes(failed=1)
|
|
171
|
+
|
|
172
|
+
expected_diff = """
|
|
173
|
+
> --- is
|
|
174
|
+
> +++ tobe
|
|
175
|
+
> @@ -1,2 +0,0 @@
|
|
176
|
+
> -THIS IS EXPECTED OUTCOME
|
|
177
|
+
> -OBJ ID IS 0XABCDEFFFF
|
|
178
|
+
""".strip().split(
|
|
179
|
+
"\n"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
result.stdout.fnmatch_lines(
|
|
183
|
+
[line.lstrip() for line in expected_diff], consecutive=True
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
result = testdir.runpytest("--regtest-reset")
|
|
187
|
+
result.assert_outcomes(passed=1)
|
|
188
|
+
|
|
189
|
+
|
|
152
190
|
def test_converter_pre(testdir):
|
|
153
191
|
testdir.makepyfile(
|
|
154
192
|
"""
|
|
@@ -185,6 +223,48 @@ def test_converter_pre(testdir):
|
|
|
185
223
|
result.assert_outcomes(passed=1)
|
|
186
224
|
|
|
187
225
|
|
|
226
|
+
def test_converter_post_pre_v2(testdir):
|
|
227
|
+
testdir.makepyfile(
|
|
228
|
+
"""
|
|
229
|
+
import tempfile
|
|
230
|
+
from pytest_regtest import register_converter_post
|
|
231
|
+
|
|
232
|
+
@register_converter_post
|
|
233
|
+
def to_upper_conv(line):
|
|
234
|
+
return line.upper()
|
|
235
|
+
|
|
236
|
+
def test_regtest(regtest_all, tmpdir):
|
|
237
|
+
print("this is expected outcome")
|
|
238
|
+
print(tmpdir.join("test").strpath)
|
|
239
|
+
print(tempfile.gettempdir())
|
|
240
|
+
print(tempfile.mkdtemp())
|
|
241
|
+
print("obj id is", hex(id(tempfile)))
|
|
242
|
+
"""
|
|
243
|
+
)
|
|
244
|
+
result = testdir.runpytest_subprocess()
|
|
245
|
+
result.assert_outcomes(failed=1)
|
|
246
|
+
|
|
247
|
+
expected_diff = """
|
|
248
|
+
> --- is
|
|
249
|
+
> +++ tobe
|
|
250
|
+
> @@ -1,5 +0,0 @@
|
|
251
|
+
> -THIS IS EXPECTED OUTCOME
|
|
252
|
+
> -<TMPDIR_FROM_FIXTURE>/TEST
|
|
253
|
+
> -<TMPDIR_FROM_TEMPFILE_MODULE>
|
|
254
|
+
> -<TMPDIR_FROM_TEMPFILE_MODULE>
|
|
255
|
+
> -OBJ ID IS 0X?????????
|
|
256
|
+
""".strip().split(
|
|
257
|
+
"\n"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
result.stdout.fnmatch_lines(
|
|
261
|
+
[line.lstrip() for line in expected_diff], consecutive=True
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
result = testdir.runpytest("--regtest-reset")
|
|
265
|
+
result.assert_outcomes(passed=1)
|
|
266
|
+
|
|
267
|
+
|
|
188
268
|
def test_converter_post(testdir):
|
|
189
269
|
testdir.makepyfile(
|
|
190
270
|
"""
|
|
@@ -227,6 +307,7 @@ def test_converter_post(testdir):
|
|
|
227
307
|
result.assert_outcomes(passed=1)
|
|
228
308
|
|
|
229
309
|
|
|
310
|
+
|
|
230
311
|
def test_consider_line_endings(create_test_regtest_fh):
|
|
231
312
|
create_test_regtest_fh.runpytest("--regtest-reset")
|
|
232
313
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest-regtest-2.0.1 → pytest-regtest-2.0.3}/pytest_regtest/pytest_regtest.egg-info/not-zip-safe
RENAMED
|
File without changes
|