pytest-regtest 2.2.0a1__py2.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.
@@ -0,0 +1,67 @@
1
+ from importlib.metadata import version as _version
2
+
3
+ import pytest
4
+
5
+ from . import register_third_party_handlers # noqa: F401
6
+ from .pytest_regtest import PytestRegtestPlugin # noqa: F401
7
+ from .pytest_regtest import RegtestStream # noqa: F401
8
+ from .pytest_regtest import clear_converters # noqa: F401
9
+ from .pytest_regtest import patch_terminal_size # noqa: F401
10
+ from .pytest_regtest import register_converter_post # noqa: F401
11
+ from .pytest_regtest import register_converter_pre # noqa: F401
12
+
13
+ __version__ = _version(__package__)
14
+
15
+
16
+ def pytest_addoption(parser):
17
+ """Add options to control the timeout plugin"""
18
+ group = parser.getgroup("regtest", "regression test plugin")
19
+ group.addoption(
20
+ "--regtest-reset",
21
+ action="store_true",
22
+ help="do not run regtest but record current output",
23
+ )
24
+ group.addoption(
25
+ "--regtest-tee",
26
+ action="store_true",
27
+ default=False,
28
+ help="print recorded results to console too",
29
+ )
30
+ group.addoption(
31
+ "--regtest-consider-line-endings",
32
+ action="store_true",
33
+ default=False,
34
+ help="do not strip whitespaces at end of recorded lines",
35
+ )
36
+ group.addoption(
37
+ "--regtest-nodiff",
38
+ action="store_true",
39
+ default=False,
40
+ help="do not show diff output for failed regresson tests",
41
+ )
42
+ group.addoption(
43
+ "--regtest-disable-stdconv",
44
+ action="store_true",
45
+ default=False,
46
+ help="do not apply standard output converters to clean up indeterministic output",
47
+ )
48
+
49
+
50
+ def pytest_configure(config):
51
+ config.pluginmanager.register(PytestRegtestPlugin())
52
+
53
+
54
+ @pytest.fixture
55
+ def regtest(request):
56
+ yield RegtestStream(request)
57
+
58
+
59
+ snapshot = regtest
60
+
61
+
62
+ @pytest.fixture
63
+ def regtest_all(regtest):
64
+ yield regtest
65
+
66
+
67
+ snapshot_all_output = regtest_all
@@ -0,0 +1,196 @@
1
+ import difflib
2
+ import io
3
+ import os.path
4
+ import warnings
5
+
6
+ import numpy as np
7
+
8
+ from .snapshot_handler import BaseSnapshotHandler
9
+ from .utils import highlight_mismatches
10
+
11
+
12
+ class NumpyHandler(BaseSnapshotHandler):
13
+ def __init__(self, handler_options, pytest_config, tw):
14
+ self.print_options = np.get_printoptions() | handler_options.get(
15
+ "print_options", {}
16
+ )
17
+ self.atol = handler_options.get("atol", 0.0)
18
+ self.rtol = handler_options.get("rtol", 0.0)
19
+ self.equal_nan = handler_options.get("equal_nan", True)
20
+
21
+ def _filename(self, folder):
22
+ return os.path.join(folder, "arrays.npy")
23
+
24
+ def save(self, folder, obj):
25
+ np.save(self._filename(folder), obj)
26
+
27
+ def load(self, folder):
28
+ return np.load(self._filename(folder))
29
+
30
+ def show(self, obj):
31
+ stream = io.StringIO()
32
+ with np.printoptions(**self.print_options):
33
+ print(obj, file=stream)
34
+ return stream.getvalue().splitlines()
35
+
36
+ def compare(self, current_obj, recorded_obj):
37
+ return (
38
+ isinstance(current_obj, np.ndarray)
39
+ and current_obj.shape == recorded_obj.shape
40
+ and current_obj.dtype == recorded_obj.dtype
41
+ and np.allclose(
42
+ recorded_obj,
43
+ current_obj,
44
+ atol=self.atol,
45
+ rtol=self.rtol,
46
+ equal_nan=self.equal_nan,
47
+ )
48
+ )
49
+
50
+ def show_differences(self, current_obj, recorded_obj, has_markup):
51
+ lines = []
52
+
53
+ if recorded_obj.dtype != current_obj.dtype:
54
+ lines.extend(
55
+ [
56
+ f"dtype mismatch: current dtype: {current_obj.dtype}",
57
+ f" recorded dtype: {recorded_obj.dtype}",
58
+ ]
59
+ )
60
+
61
+ recorded_as_text = self.show(recorded_obj)
62
+ current_as_text = self.show(current_obj)
63
+
64
+ if recorded_obj.shape == current_obj.shape:
65
+ if np.allclose(current_obj, recorded_obj, rtol=self.rtol, atol=self.atol):
66
+ return lines or None
67
+
68
+ lines.extend(self.error_diagnostics(recorded_obj, current_obj))
69
+
70
+ else:
71
+ lines.extend(
72
+ [
73
+ f"shape mismatch: current shape: {current_obj.shape}",
74
+ f" recorded shape: {recorded_obj.shape}",
75
+ ]
76
+ )
77
+
78
+ if recorded_obj.ndim > 2:
79
+ return lines
80
+
81
+ if recorded_obj.ndim == 1:
82
+ diff_lines = list(
83
+ difflib.unified_diff(
84
+ current_as_text,
85
+ recorded_as_text,
86
+ "current",
87
+ "expected",
88
+ lineterm="",
89
+ )
90
+ )
91
+ lines.append("")
92
+ lines.extend(diff_lines)
93
+
94
+ else:
95
+ diff_lines = self.error_diagnostics_2d_linewise(
96
+ current_obj,
97
+ current_as_text,
98
+ recorded_obj,
99
+ recorded_as_text,
100
+ has_markup,
101
+ )
102
+ lines.extend(diff_lines)
103
+
104
+ if not diff_lines:
105
+ lines.append("diff is empty, you may want to change the print options")
106
+
107
+ return lines
108
+
109
+ def error_diagnostics(self, recorded_obj, current_obj):
110
+ with warnings.catch_warnings():
111
+ warnings.simplefilter("ignore", RuntimeWarning)
112
+ rel_err = np.abs(current_obj - recorded_obj) / recorded_obj
113
+ rel_err[(recorded_obj == 0) * (current_obj == recorded_obj)] = 0.0
114
+ rel_err_max_1 = np.max(rel_err)
115
+ rel_err_max_2 = np.max(rel_err[recorded_obj != 0])
116
+
117
+ abs_err = np.abs(current_obj - recorded_obj)
118
+ abs_err_max = np.max(abs_err)
119
+
120
+ lines = []
121
+
122
+ if rel_err_max_1 == rel_err_max_2:
123
+ lines.append(f"max relative deviation: {rel_err_max_1:e}")
124
+ else:
125
+ lines.append(f"max relative deviation: {rel_err_max_1:e}")
126
+ lines.append(f"max relative deviation except inf: {rel_err_max_2:e}")
127
+
128
+ lines.append(f"max absolute deviation: {abs_err_max:e}")
129
+
130
+ n_diff = np.sum(
131
+ np.logical_not(
132
+ np.isclose(current_obj, recorded_obj, rtol=self.rtol, atol=self.atol)
133
+ )
134
+ )
135
+
136
+ lines.append(
137
+ f"both arrays differ in {n_diff} out of {np.prod(recorded_obj.shape)}"
138
+ " entries"
139
+ )
140
+ lines.append(
141
+ f"up to given precision settings rtol={self.rtol:e} and"
142
+ f" atol={self.atol:e}"
143
+ )
144
+
145
+ return lines
146
+
147
+ def error_diagnostics_2d_linewise(
148
+ self, current_obj, current_as_text, recorded_obj, recorded_as_text, has_markup
149
+ ):
150
+ sub_diff = []
151
+ for i, (l1, l2, r1, r2) in enumerate(
152
+ zip(current_as_text, recorded_as_text, current_obj, recorded_obj)
153
+ ):
154
+ if r1.shape == r2.shape and np.allclose(
155
+ r1, r2, rtol=self.rtol, atol=self.atol
156
+ ):
157
+ continue
158
+
159
+ if r1.shape == r2.shape:
160
+ # enforces more uniform formatting of both lines:
161
+ rows_together = np.vstack((r1, r2))
162
+ lines_together = self.show(rows_together)
163
+ line_diff = list(
164
+ difflib.unified_diff(
165
+ [lines_together[0][1:].strip()],
166
+ [lines_together[1][:-1].strip()],
167
+ "current",
168
+ "expected",
169
+ lineterm="",
170
+ )
171
+ )
172
+ else:
173
+ row_1 = self.show(r1)
174
+ row_2 = self.show(r2)
175
+ line_diff = list(
176
+ difflib.unified_diff(
177
+ row_1,
178
+ row_2,
179
+ "current",
180
+ "expected",
181
+ lineterm="",
182
+ )
183
+ )
184
+
185
+ if line_diff:
186
+ if not sub_diff:
187
+ sub_diff = line_diff[:2]
188
+
189
+ l1, l2 = line_diff[-2], line_diff[-1]
190
+ if has_markup:
191
+ l1, l2 = highlight_mismatches(l1, l2)
192
+
193
+ sub_diff.append(f"row {i:3d}: {l1}")
194
+ sub_diff.append(f" {l2}")
195
+
196
+ return sub_diff
@@ -0,0 +1,135 @@
1
+ import difflib
2
+ import io
3
+ import os.path
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from .snapshot_handler import BaseSnapshotHandler
9
+
10
+
11
+ class DataFrameHandler(BaseSnapshotHandler):
12
+ def __init__(self, handler_options, pytest_config, tw):
13
+ # default contains a few nested dicts and we flatten those, e.g.
14
+ # { "html": {"border": 1} } -> { "html.border": 1 }
15
+ default = list(pd.options.display.d.items())
16
+ default_flattened = {}
17
+ for k, v in default:
18
+ if isinstance(v, dict):
19
+ for k0, v0 in v.items():
20
+ default_flattened[f"{k}.{k0}"] = v0
21
+ else:
22
+ default_flattened[k] = v
23
+
24
+ # overwrite with user settings:
25
+ items = (default_flattened | handler_options.get("display_options", {})).items()
26
+
27
+ # flatten items as required by pandas.option_context:
28
+ self.display_options_flat = [
29
+ entry for item in items for entry in (f"display.{item[0]}", item[1])
30
+ ]
31
+ self.atol = handler_options.get("atol", 0.0)
32
+ self.rtol = handler_options.get("rtol", 0.0)
33
+
34
+ def _filename(self, folder):
35
+ return os.path.join(folder, "dataframe.pkl")
36
+
37
+ def save(self, folder, obj):
38
+ obj.to_pickle(self._filename(folder), compression="gzip")
39
+
40
+ def load(self, folder):
41
+ return pd.read_pickle(self._filename(folder), compression="gzip")
42
+
43
+ def show(self, obj):
44
+ stream = io.StringIO()
45
+ with pd.option_context(*self.display_options_flat):
46
+ print(obj, file=stream)
47
+ return stream.getvalue().splitlines()
48
+
49
+ def compare(self, current, recorded):
50
+ missing = set(
51
+ n
52
+ for (n, t) in set(zip(recorded.columns, recorded.dtypes))
53
+ ^ set(zip(current.columns, current.dtypes))
54
+ )
55
+
56
+ if missing:
57
+ return False
58
+
59
+ common = set(
60
+ n
61
+ for (n, t) in set(zip(recorded.columns, recorded.dtypes))
62
+ & set(zip(current.columns, current.dtypes))
63
+ )
64
+ current_reduced = current[[n for n in current.columns if n in common]]
65
+ recorded_reduced = recorded[[n for n in recorded.columns if n in common]]
66
+
67
+ def extract(df, selector):
68
+ return df[[n for (n, t) in zip(df.columns, df.dtypes) if selector(t)]]
69
+
70
+ current_reduced_floats = extract(
71
+ current_reduced, lambda t: t.type is np.float64
72
+ ).to_numpy()
73
+
74
+ current_reduced_other = extract(
75
+ current_reduced, lambda t: t.type is not np.float64
76
+ )
77
+
78
+ recorded_reduced_floats = extract(
79
+ recorded_reduced, lambda t: t.type is np.float64
80
+ ).to_numpy()
81
+
82
+ recorded_reduced_other = extract(
83
+ recorded_reduced, lambda t: t.type is not np.float64
84
+ )
85
+
86
+ return np.allclose(
87
+ current_reduced_floats,
88
+ recorded_reduced_floats,
89
+ atol=self.atol,
90
+ rtol=self.rtol,
91
+ equal_nan=True,
92
+ ) and (current_reduced_other == recorded_reduced_other).all(axis=None)
93
+
94
+ def show_differences(self, current, recorded, has_markup):
95
+ lines = []
96
+
97
+ stream = io.StringIO()
98
+ current.info(buf=stream, verbose=True, memory_usage=False)
99
+ current_info = stream.getvalue().splitlines()[2:][:-1]
100
+
101
+ stream = io.StringIO()
102
+ recorded.info(buf=stream, verbose=True, memory_usage=False)
103
+ recorded_info = stream.getvalue().splitlines()[2:][:-1]
104
+
105
+ info_diff = list(
106
+ difflib.unified_diff(
107
+ current_info,
108
+ recorded_info,
109
+ "current",
110
+ "expected",
111
+ lineterm="",
112
+ )
113
+ )
114
+ lines.extend(info_diff)
115
+
116
+ recorded_as_text = self.show(recorded)
117
+ current_as_text = self.show(current)
118
+
119
+ diffs = list(
120
+ difflib.unified_diff(
121
+ current_as_text,
122
+ recorded_as_text,
123
+ "current",
124
+ "expected",
125
+ lineterm="",
126
+ )
127
+ )
128
+
129
+ lines.append("")
130
+ if diffs:
131
+ lines.extend(diffs)
132
+ else:
133
+ lines.append("diff is empty, you may want to change the print options")
134
+
135
+ return lines