|
3 | 3 | Shared variables/markers for testing. |
4 | 4 | """ |
5 | 5 |
|
6 | | -import os |
7 | | -import numpy as np |
8 | 6 | from pathlib import Path |
9 | 7 |
|
10 | 8 | import pytest |
11 | 9 |
|
12 | | -try: |
13 | | - import inspect |
14 | | - |
15 | | -except ImportError: |
16 | | - VTKPLOTLIB_WINDOWLESS_TEST = False |
17 | | - DEFAULT_MODE = "" |
18 | | -else: |
19 | | - VTKPLOTLIB_WINDOWLESS_TEST = bool( |
20 | | - int(os.environ.get("VTKPLOTLIB_WINDOWLESS_TEST", "0"))) |
21 | | - |
22 | | - if VTKPLOTLIB_WINDOWLESS_TEST: |
23 | | - DEFAULT_MODE = "r" |
24 | | - else: |
25 | | - DEFAULT_MODE = "w" |
26 | | - |
27 | 10 | TEST_DIR = Path(__file__).parent.absolute().resolve() / "temp" |
28 | 11 | TEST_DIR.mkdir(exist_ok=True) |
29 | 12 |
|
30 | 13 |
|
31 | | -class AutoChecker(object): |
32 | | - """Attempts to assess reproducibility of visualisations between runs so I |
33 | | - don't have to manually click through each test to verify that it looks OK. |
34 | | -
|
35 | | - Wraps around any test function which plots something but doesn't call |
36 | | - ``vpl.show()`` at the end. The wrapped function, if called in **write** mode |
37 | | - (determined by the **mode** argument to that newly wrapped function) it will |
38 | | - call the original function, screenshot and store the image, then call |
39 | | - ``vpl.show()`` so you can verify it. If called in **read** mode, then it |
40 | | - will call the original function, screenshot the result and verify it matches |
41 | | - the stored one, then closes so that you don't have to interact with it.""" |
42 | | - |
43 | | - def __init__(self): |
44 | | - self.path = TEST_DIR / "test-data.json" |
45 | | - self.load() |
46 | | - |
47 | | - def auto_check_contents(self, test_func): |
48 | | - name = self.name_function(test_func) |
49 | | - |
50 | | - def wrapped(testcase=None, mode=DEFAULT_MODE): |
51 | | - from vtkplotlib.figures.figure_manager import gcf, close, show |
52 | | - |
53 | | - close() |
54 | | - |
55 | | - np.random.seed(0) |
56 | | - if testcase is None: |
57 | | - out = test_func() |
58 | | - else: |
59 | | - out = test_func(testcase) |
60 | | - from vtkplotlib.plots.BasePlot import BasePlot |
61 | | - from vtkplotlib.figures.BaseFigure import BaseFigure |
62 | | - if isinstance(out, BasePlot): |
63 | | - out = None |
64 | | - if out is None: |
65 | | - out = gcf() |
66 | | - |
67 | | - if isinstance(out, BaseFigure): |
68 | | - fig = out |
69 | | - else: |
70 | | - fig = "gcf" |
71 | | - |
72 | | - if mode == "r": |
73 | | - correct_value = self.data[name] |
74 | | - self.validate(self.reduce(out), correct_value) |
75 | | - close(fig=fig) |
76 | | - elif mode == "w": |
77 | | - self.data[name] = self.reduce(out) |
78 | | - if not isinstance(out, np.ndarray): |
79 | | - show(fig=fig) |
80 | | - self.save() |
81 | | - else: |
82 | | - show(fig=fig) |
83 | | - |
84 | | - return wrapped |
85 | | - |
86 | | - __call__ = auto_check_contents |
87 | | - |
88 | | - @staticmethod |
89 | | - def reduce_image_array(arr): |
90 | | - """Serialise an image array into something that can go into a json file |
91 | | - i.e plain text.""" |
92 | | - # Ideally I'd just use 'shape' and 'crc32_checksum' but there are |
93 | | - # subtle, visually invisible differences between snapshots from |
94 | | - # different OSs and VTK versions. So image matching must be fuzzy |
95 | | - # meaning we have to store the whole image. |
96 | | - # Interestingly bz2 gives better compression than PNG. |
97 | | - import zlib, bz2, base64 |
98 | | - return { |
99 | | - "shape": list(arr.shape), |
100 | | - "crc32_checksum": zlib.crc32(arr.tobytes("C")), |
101 | | - "image": base64.b64encode(bz2.compress(arr.tobytes("C"))).decode() |
102 | | - } |
103 | | - |
104 | | - @classmethod |
105 | | - def reduce_fig(cls, fig): |
106 | | - from vtkplotlib.figures.figure_manager import screenshot_fig |
107 | | - return cls.reduce_image_array(screenshot_fig(fig=fig)) |
108 | | - |
109 | | - @classmethod |
110 | | - def reduce(cls, obj): |
111 | | - if isinstance(obj, dict): |
112 | | - return obj |
113 | | - if isinstance(obj, np.ndarray): |
114 | | - return cls.reduce_image_array(obj) |
115 | | - return cls.reduce_fig(obj) |
116 | | - |
117 | | - def save(self): |
118 | | - import json |
119 | | - text = json.dumps(self.data, indent=2) |
120 | | - self.path.write_text(text) |
121 | | - |
122 | | - def load(self): |
123 | | - import json |
124 | | - if self.path.exists(): |
125 | | - text = self.path.read_text() |
126 | | - self.data = json.loads(text) |
127 | | - else: |
128 | | - self.data = {} |
129 | | - |
130 | | - @staticmethod |
131 | | - def name_function(func): |
132 | | - module = inspect.getmodulename(func.__code__.co_filename) |
133 | | - name = str(module) + "." + func.__name__ |
134 | | - assert "__main__" not in name |
135 | | - return name |
136 | | - |
137 | | - def validate(self, old_dic, new_dic): |
138 | | - assert old_dic["shape"] == new_dic["shape"] |
139 | | - if old_dic["crc32_checksum"] == new_dic["crc32_checksum"]: |
140 | | - return True |
141 | | - old_im = self.extract_dic_image(old_dic) |
142 | | - new_im = self.extract_dic_image(new_dic) |
143 | | - self.assertLess(np.abs(old_im - new_im.astype(float)).mean(), 5) |
144 | | - |
145 | | - @staticmethod |
146 | | - def extract_dic_image(dic): |
147 | | - import bz2, base64 |
148 | | - shape = dic["shape"] |
149 | | - bin = bz2.decompress(base64.b64decode(dic["image"].encode())) |
150 | | - arr = np.frombuffer(bin, np.uint8).reshape(shape) |
151 | | - return arr |
152 | | - |
153 | | - def show_saved(self, func_name): |
154 | | - from matplotlib import pylab |
155 | | - pylab.imshow(self.extract_dic_image(self.data[func_name])) |
156 | | - pylab.show() |
157 | | - |
158 | | - |
159 | | -_checker = None |
160 | | - |
161 | | - |
162 | | -def checker(*no_arguments): |
163 | | - if len(no_arguments): |
164 | | - raise TypeError("Checker takes no arguments. You probably forgot the " |
165 | | - "parenthesis. Should be `@checker()\\n" |
166 | | - "def function...`.") |
167 | | - global _checker |
168 | | - if _checker is None: |
169 | | - _checker = AutoChecker() |
170 | | - return _checker |
171 | | - |
172 | | - |
173 | | -def reset(): |
174 | | - checker().data.clear() |
175 | | - checker().save() |
176 | | - global _checker |
177 | | - _checker = None |
178 | | - |
179 | | - |
180 | | -@checker() |
181 | | -def test_quick_test_plot(): |
182 | | - import vtkplotlib as vpl |
183 | | - vpl.quick_test_plot() |
184 | | - |
185 | | - |
186 | | -def test_checker(): |
187 | | - |
188 | | - test_quick_test_plot(mode=DEFAULT_MODE and "w") |
189 | | - |
190 | | - checker().save() |
191 | | - checker().load() |
192 | | - |
193 | | - test_quick_test_plot(mode=DEFAULT_MODE and "r") |
194 | | - |
195 | | - |
196 | | -requires_interaction = pytest.mark.skipif(VTKPLOTLIB_WINDOWLESS_TEST, |
197 | | - reason="Requires manual interaction.") |
198 | | - |
199 | | - |
200 | 14 | def numpy_stl(): |
201 | 15 | return pytest.importorskip("stl.mesh", reason="Requires numpy-stl") |
202 | | - |
203 | | - |
204 | | -if __name__ == "__main__": |
205 | | - test_checker() |
0 commit comments