@@ -217,7 +217,8 @@ def pytest_configure(config):
217217 config .pluginmanager .register (ArrayComparison (config ,
218218 reference_dir = reference_dir ,
219219 generate_dir = generate_dir ,
220- default_format = default_format ))
220+ default_format = default_format ),
221+ name = 'arraydiff' )
221222 else :
222223 config .pluginmanager .register (ArrayInterceptor (config ))
223224
@@ -233,6 +234,100 @@ def generate_test_name(item):
233234 return name
234235
235236
237+ def _compare_array (array , item , options , * , plugin_reference_dir ,
238+ generate_dir , default_format ):
239+ """
240+ Compare ``array`` against the reference for ``item``, or, in generate mode,
241+ write it out.
242+
243+ ``options`` is a mapping accepting the same keys as the ``array_compare``
244+ marker and the ``array_compare`` fixture's ``check`` method (``file_format``,
245+ ``extension``, ``atol``, ``rtol``, ``single_reference``, ``write_kwargs``,
246+ ``reference_dir``, ``filename``). This is the shared core used both by the
247+ marker-based API (which captures the test's return value) and the
248+ fixture-based API (where the test passes the array in explicitly).
249+ """
250+ file_format = options .get ('file_format' , default_format )
251+
252+ if file_format not in FORMATS :
253+ raise ValueError (f"Unknown format: { file_format } " )
254+
255+ extension = options .get ('extension' , FORMATS [file_format ].extension )
256+
257+ atol = options .get ('atol' , 0. )
258+ rtol = options .get ('rtol' , 1e-7 )
259+
260+ single_reference = options .get ('single_reference' , False )
261+
262+ write_kwargs = options .get ('write_kwargs' , {})
263+
264+ reference_dir = options .get ('reference_dir' , None )
265+ if reference_dir is None :
266+ if plugin_reference_dir is None :
267+ reference_dir = os .path .join (os .path .dirname (item .fspath .strpath ), 'reference' )
268+ else :
269+ reference_dir = plugin_reference_dir
270+ else :
271+ if not reference_dir .startswith (('http://' , 'https://' )):
272+ reference_dir = os .path .join (os .path .dirname (item .fspath .strpath ), reference_dir )
273+
274+ baseline_remote = reference_dir .startswith ('http' )
275+
276+ # Find test name to use as the reference filename
277+ filename = options .get ('filename' , None )
278+ if filename is None :
279+ if single_reference :
280+ filename = item .originalname + '.' + extension
281+ else :
282+ filename = item .name + '.' + extension
283+ filename = filename .replace ('[' , '_' ).replace (']' , '_' )
284+ filename = filename .replace ('_.' + extension , '.' + extension )
285+
286+ # What we do now depends on whether we are generating the reference
287+ # files or simply running the test.
288+ if generate_dir is None :
289+
290+ # Save the array
291+ result_dir = tempfile .mkdtemp ()
292+ test_array = os .path .abspath (os .path .join (result_dir , filename ))
293+
294+ FORMATS [file_format ].write (test_array , array , ** write_kwargs )
295+
296+ # Find path to baseline array
297+ if baseline_remote :
298+ baseline_file_ref = _download_file (reference_dir + filename )
299+ else :
300+ baseline_file_ref = os .path .abspath (os .path .join (os .path .dirname (item .fspath .strpath ), reference_dir , filename ))
301+
302+ if not os .path .exists (baseline_file_ref ):
303+ raise Exception ("""File not found for comparison test
304+ Generated file:
305+ \t {test}
306+ This is expected for new tests.""" .format (
307+ test = test_array ))
308+
309+ # setuptools may put the baseline arrays in non-accessible places,
310+ # copy to our tmpdir to be sure to keep them in case of failure
311+ baseline_file = os .path .abspath (os .path .join (result_dir , 'reference-' + filename ))
312+ shutil .copyfile (baseline_file_ref , baseline_file )
313+
314+ identical , msg = FORMATS [file_format ].compare (baseline_file , test_array , atol = atol , rtol = rtol )
315+
316+ if identical :
317+ shutil .rmtree (result_dir )
318+ else :
319+ raise Exception (msg )
320+
321+ else :
322+
323+ if not os .path .exists (generate_dir ):
324+ os .makedirs (generate_dir )
325+
326+ FORMATS [file_format ].write (os .path .abspath (os .path .join (generate_dir , filename )), array , ** write_kwargs )
327+
328+ pytest .skip ("Skipping test, since generating data" )
329+
330+
236331def wrap_array_interceptor (plugin , item ):
237332 """
238333 Intercept and store arrays returned by test functions.
@@ -279,95 +374,18 @@ def pytest_runtest_call(self, item):
279374 yield
280375 return
281376
282- file_format = compare .kwargs .get ('file_format' , self .default_format )
283-
284- if file_format not in FORMATS :
285- raise ValueError (f"Unknown format: { file_format } " )
286-
287- if 'extension' in compare .kwargs :
288- extension = compare .kwargs ['extension' ]
289- else :
290- extension = FORMATS [file_format ].extension
291-
292- atol = compare .kwargs .get ('atol' , 0. )
293- rtol = compare .kwargs .get ('rtol' , 1e-7 )
294-
295- single_reference = compare .kwargs .get ('single_reference' , False )
296-
297- write_kwargs = compare .kwargs .get ('write_kwargs' , {})
298-
299- reference_dir = compare .kwargs .get ('reference_dir' , None )
300- if reference_dir is None :
301- if self .reference_dir is None :
302- reference_dir = os .path .join (os .path .dirname (item .fspath .strpath ), 'reference' )
303- else :
304- reference_dir = self .reference_dir
305- else :
306- if not reference_dir .startswith (('http://' , 'https://' )):
307- reference_dir = os .path .join (os .path .dirname (item .fspath .strpath ), reference_dir )
308-
309- baseline_remote = reference_dir .startswith ('http' )
310-
311377 yield
378+
312379 test_name = generate_test_name (item )
313380 if test_name not in self .return_value :
314381 # Test function did not complete successfully
315382 return
316383 array = self .return_value [test_name ]
317384
318- # Find test name to use as plot name
319- filename = compare .kwargs .get ('filename' , None )
320- if filename is None :
321- if single_reference :
322- filename = item .originalname + '.' + extension
323- else :
324- filename = item .name + '.' + extension
325- filename = filename .replace ('[' , '_' ).replace (']' , '_' )
326- filename = filename .replace ('_.' + extension , '.' + extension )
327-
328- # What we do now depends on whether we are generating the reference
329- # files or simply running the test.
330- if self .generate_dir is None :
331-
332- # Save the figure
333- result_dir = tempfile .mkdtemp ()
334- test_array = os .path .abspath (os .path .join (result_dir , filename ))
335-
336- FORMATS [file_format ].write (test_array , array , ** write_kwargs )
337-
338- # Find path to baseline array
339- if baseline_remote :
340- baseline_file_ref = _download_file (reference_dir + filename )
341- else :
342- baseline_file_ref = os .path .abspath (os .path .join (os .path .dirname (item .fspath .strpath ), reference_dir , filename ))
343-
344- if not os .path .exists (baseline_file_ref ):
345- raise Exception ("""File not found for comparison test
346- Generated file:
347- \t {test}
348- This is expected for new tests.""" .format (
349- test = test_array ))
350-
351- # setuptools may put the baseline arrays in non-accessible places,
352- # copy to our tmpdir to be sure to keep them in case of failure
353- baseline_file = os .path .abspath (os .path .join (result_dir , 'reference-' + filename ))
354- shutil .copyfile (baseline_file_ref , baseline_file )
355-
356- identical , msg = FORMATS [file_format ].compare (baseline_file , test_array , atol = atol , rtol = rtol )
357-
358- if identical :
359- shutil .rmtree (result_dir )
360- else :
361- raise Exception (msg )
362-
363- else :
364-
365- if not os .path .exists (self .generate_dir ):
366- os .makedirs (self .generate_dir )
367-
368- FORMATS [file_format ].write (os .path .abspath (os .path .join (self .generate_dir , filename )), array , ** write_kwargs )
369-
370- pytest .skip ("Skipping test, since generating data" )
385+ _compare_array (array , item , compare .kwargs ,
386+ plugin_reference_dir = self .reference_dir ,
387+ generate_dir = self .generate_dir ,
388+ default_format = self .default_format )
371389
372390
373391class ArrayInterceptor :
@@ -383,3 +401,42 @@ def __init__(self, config):
383401 def pytest_collection_modifyitems (self , items ):
384402 for item in items :
385403 wrap_array_interceptor (self , item )
404+
405+
406+ class ArrayCompareFixture :
407+ """
408+ Object returned by the ``array_compare`` fixture; call ``check(array,
409+ **kwargs)`` to compare an array, where ``kwargs`` accepts the same options
410+ as the ``@pytest.mark.array_compare`` marker.
411+
412+ Unlike the marker, this never replaces ``item.obj``, so the test function
413+ is collected and run as written and plugins that introspect the test source
414+ keep working (notably pytest-run-parallel's thread-unsafe-call detection).
415+ """
416+
417+ def __init__ (self , request , comparison ):
418+ self ._request = request
419+ self ._comparison = comparison
420+
421+ def check (self , array , ** kwargs ):
422+ if self ._comparison is None :
423+ # Array comparison not requested this run (no --arraydiff); no-op,
424+ # mirroring the marker-based API.
425+ return
426+ _compare_array (array , self ._request .node , kwargs ,
427+ plugin_reference_dir = self ._comparison .reference_dir ,
428+ generate_dir = self ._comparison .generate_dir ,
429+ default_format = self ._comparison .default_format )
430+
431+
432+ @pytest .fixture
433+ def array_compare (request ):
434+ """
435+ Fixture alternative to the ``@pytest.mark.array_compare`` marker::
436+
437+ def test_something(array_compare):
438+ array_compare.check(compute(), atol=1e-6)
439+ """
440+ # 'arraydiff' only resolves when comparison is enabled (see pytest_configure)
441+ comparison = request .config .pluginmanager .get_plugin ('arraydiff' )
442+ return ArrayCompareFixture (request , comparison )
0 commit comments