LSSTApplications  18.0.0+74,19.0.0+5,19.0.0+75,19.0.0+9,19.0.0-1-g20d9b18+30,19.0.0-1-g49a97f9+3,19.0.0-1-g8c57eb9+30,19.0.0-1-g9a028c0+10,19.0.0-1-ga72da6b+3,19.0.0-1-gb77924a+11,19.0.0-1-gbfe0924+62,19.0.0-1-ge272bc4+30,19.0.0-1-gefe1d0d+46,19.0.0-1-gf8cb8b4,19.0.0-14-g7511ce4+2,19.0.0-16-g3dc1a33c+2,19.0.0-17-g59f0e8a,19.0.0-17-g9c22e3c+6,19.0.0-18-g2c901ce+6,19.0.0-18-g35bb99870+1,19.0.0-19-g2772d4a+5,19.0.0-19-g85fbfd3+5,19.0.0-2-g0d9f9cd+70,19.0.0-2-g260436e+50,19.0.0-2-g9675b69+3,19.0.0-2-gbcc4de1,19.0.0-2-gd6f004e+2,19.0.0-2-gde8e5e3+4,19.0.0-2-gff6972b+14,19.0.0-3-ga642a0f,19.0.0-3-gf3b1435+2,19.0.0-4-gac56cce+13,19.0.0-47-g0e06596+2,19.0.0-49-g6858528b,19.0.0-5-g66946eb+2,19.0.0-5-gd8897ba+2,19.0.0-7-g3343160,19.0.0-7-g686a884+4,19.0.0-7-g886f805+2,w.2020.17
LSSTDataManagementBasePackage
tests.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 #
4 # Copyright 2008-2017 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 """Support code for running unit tests"""
24 
25 import contextlib
26 import gc
27 import inspect
28 import os
29 import subprocess
30 import sys
31 import unittest
32 import warnings
33 import numpy
34 import functools
35 import tempfile
36 
37 __all__ = ["init", "MemoryTestCase", "ExecutablesTestCase", "getTempFilePath",
38  "TestCase", "assertFloatsAlmostEqual", "assertFloatsNotEqual", "assertFloatsEqual",
39  "debugger", "classParameters", "methodParameters"]
40 
41 # File descriptor leak test will be skipped if psutil can not be imported
42 try:
43  import psutil
44 except ImportError:
45  psutil = None
46 
47 # Initialize the list of open files to an empty set
48 open_files = set()
49 
50 
51 def _get_open_files():
52  """Return a set containing the list of files currently open in this
53  process.
54 
55  Returns
56  -------
57  open_files : `set`
58  Set containing the list of open files.
59  """
60  if psutil is None:
61  return set()
62  return set(p.path for p in psutil.Process().open_files())
63 
64 
65 def init():
66  """Initialize the memory tester and file descriptor leak tester."""
67  global open_files
68  # Reset the list of open files
69  open_files = _get_open_files()
70 
71 
72 def sort_tests(tests):
73  """Sort supplied test suites such that MemoryTestCases are at the end.
74 
75  `lsst.utils.tests.MemoryTestCase` tests should always run after any other
76  tests in the module.
77 
78  Parameters
79  ----------
80  tests : sequence
81  Sequence of test suites.
82 
83  Returns
84  -------
85  suite : `unittest.TestSuite`
86  A combined `~unittest.TestSuite` with
87  `~lsst.utils.tests.MemoryTestCase` at the end.
88  """
89 
90  suite = unittest.TestSuite()
91  memtests = []
92  for test_suite in tests:
93  try:
94  # Just test the first test method in the suite for MemoryTestCase
95  # Use loop rather than next as it is possible for a test class
96  # to not have any test methods and the Python community prefers
97  # for loops over catching a StopIteration exception.
98  bases = None
99  for method in test_suite:
100  bases = inspect.getmro(method.__class__)
101  break
102  if bases is not None and MemoryTestCase in bases:
103  memtests.append(test_suite)
104  else:
105  suite.addTests(test_suite)
106  except TypeError:
107  if isinstance(test_suite, MemoryTestCase):
108  memtests.append(test_suite)
109  else:
110  suite.addTest(test_suite)
111  suite.addTests(memtests)
112  return suite
113 
114 
115 def suiteClassWrapper(tests):
116  return unittest.TestSuite(sort_tests(tests))
117 
118 
119 # Replace the suiteClass callable in the defaultTestLoader
120 # so that we can reorder the test ordering. This will have
121 # no effect if no memory test cases are found.
122 unittest.defaultTestLoader.suiteClass = suiteClassWrapper
123 
124 
125 class MemoryTestCase(unittest.TestCase):
126  """Check for resource leaks."""
127 
128  @classmethod
129  def tearDownClass(cls):
130  """Reset the leak counter when the tests have been completed"""
131  init()
132 
134  """Check if any file descriptors are open since init() called."""
135  if psutil is None:
136  self.skipTest("Unable to test file descriptor leaks. psutil unavailable.")
137  gc.collect()
138  global open_files
139  now_open = _get_open_files()
140 
141  # Some files are opened out of the control of the stack.
142  now_open = set(f for f in now_open if not f.endswith(".car") and
143  not f.startswith("/proc/") and
144  not f.endswith(".ttf") and
145  not (f.startswith("/var/lib/") and f.endswith("/passwd")) and
146  not f.endswith("astropy.log"))
147 
148  diff = now_open.difference(open_files)
149  if diff:
150  for f in diff:
151  print("File open: %s" % f)
152  self.fail("Failed to close %d file%s" % (len(diff), "s" if len(diff) != 1 else ""))
153 
154 
155 class ExecutablesTestCase(unittest.TestCase):
156  """Test that executables can be run and return good status.
157 
158  The test methods are dynamically created. Callers
159  must subclass this class in their own test file and invoke
160  the create_executable_tests() class method to register the tests.
161  """
162  TESTS_DISCOVERED = -1
163 
164  @classmethod
165  def setUpClass(cls):
166  """Abort testing if automated test creation was enabled and
167  no tests were found."""
168 
169  if cls.TESTS_DISCOVERED == 0:
170  raise Exception("No executables discovered.")
171 
172  def testSanity(self):
173  """This test exists to ensure that there is at least one test to be
174  executed. This allows the test runner to trigger the class set up
175  machinery to test whether there are some executables to test."""
176  pass
177 
178  def assertExecutable(self, executable, root_dir=None, args=None, msg=None):
179  """Check an executable runs and returns good status.
180 
181  Prints output to standard out. On bad exit status the test
182  fails. If the executable can not be located the test is skipped.
183 
184  Parameters
185  ----------
186  executable : `str`
187  Path to an executable. ``root_dir`` is not used if this is an
188  absolute path.
189  root_dir : `str`, optional
190  Directory containing executable. Ignored if `None`.
191  args : `list` or `tuple`, optional
192  Arguments to be provided to the executable.
193  msg : `str`, optional
194  Message to use when the test fails. Can be `None` for default
195  message.
196 
197  Raises
198  ------
199  AssertionError
200  The executable did not return 0 exit status.
201  """
202 
203  if root_dir is not None and not os.path.isabs(executable):
204  executable = os.path.join(root_dir, executable)
205 
206  # Form the argument list for subprocess
207  sp_args = [executable]
208  argstr = "no arguments"
209  if args is not None:
210  sp_args.extend(args)
211  argstr = 'arguments "' + " ".join(args) + '"'
212 
213  print("Running executable '{}' with {}...".format(executable, argstr))
214  if not os.path.exists(executable):
215  self.skipTest("Executable {} is unexpectedly missing".format(executable))
216  failmsg = None
217  try:
218  output = subprocess.check_output(sp_args)
219  except subprocess.CalledProcessError as e:
220  output = e.output
221  failmsg = "Bad exit status from '{}': {}".format(executable, e.returncode)
222  print(output.decode('utf-8'))
223  if failmsg:
224  if msg is None:
225  msg = failmsg
226  self.fail(msg)
227 
228  @classmethod
229  def _build_test_method(cls, executable, root_dir):
230  """Build a test method and attach to class.
231 
232  A test method is created for the supplied excutable located
233  in the supplied root directory. This method is attached to the class
234  so that the test runner will discover the test and run it.
235 
236  Parameters
237  ----------
238  cls : `object`
239  The class in which to create the tests.
240  executable : `str`
241  Name of executable. Can be absolute path.
242  root_dir : `str`
243  Path to executable. Not used if executable path is absolute.
244  """
245  if not os.path.isabs(executable):
246  executable = os.path.abspath(os.path.join(root_dir, executable))
247 
248  # Create the test name from the executable path.
249  test_name = "test_exe_" + executable.replace("/", "_")
250 
251  # This is the function that will become the test method
252  def test_executable_runs(*args):
253  self = args[0]
254  self.assertExecutable(executable)
255 
256  # Give it a name and attach it to the class
257  test_executable_runs.__name__ = test_name
258  setattr(cls, test_name, test_executable_runs)
259 
260  @classmethod
261  def create_executable_tests(cls, ref_file, executables=None):
262  """Discover executables to test and create corresponding test methods.
263 
264  Scans the directory containing the supplied reference file
265  (usually ``__file__`` supplied from the test class) to look for
266  executables. If executables are found a test method is created
267  for each one. That test method will run the executable and
268  check the returned value.
269 
270  Executable scripts with a ``.py`` extension and shared libraries
271  are ignored by the scanner.
272 
273  This class method must be called before test discovery.
274 
275  Parameters
276  ----------
277  ref_file : `str`
278  Path to a file within the directory to be searched.
279  If the files are in the same location as the test file, then
280  ``__file__`` can be used.
281  executables : `list` or `tuple`, optional
282  Sequence of executables that can override the automated
283  detection. If an executable mentioned here is not found, a
284  skipped test will be created for it, rather than a failed
285  test.
286 
287  Examples
288  --------
289  >>> cls.create_executable_tests(__file__)
290  """
291 
292  # Get the search directory from the reference file
293  ref_dir = os.path.abspath(os.path.dirname(ref_file))
294 
295  if executables is None:
296  # Look for executables to test by walking the tree
297  executables = []
298  for root, dirs, files in os.walk(ref_dir):
299  for f in files:
300  # Skip Python files. Shared libraries are executable.
301  if not f.endswith(".py") and not f.endswith(".so"):
302  full_path = os.path.join(root, f)
303  if os.access(full_path, os.X_OK):
304  executables.append(full_path)
305 
306  # Store the number of tests found for later assessment.
307  # Do not raise an exception if we have no executables as this would
308  # cause the testing to abort before the test runner could properly
309  # integrate it into the failure report.
310  cls.TESTS_DISCOVERED = len(executables)
311 
312  # Create the test functions and attach them to the class
313  for e in executables:
314  cls._build_test_method(e, ref_dir)
315 
316 
317 @contextlib.contextmanager
318 def getTempFilePath(ext, expectOutput=True):
319  """Return a path suitable for a temporary file and try to delete the
320  file on success
321 
322  If the with block completes successfully then the file is deleted,
323  if possible; failure results in a printed warning.
324  If a file is remains when it should not, a RuntimeError exception is
325  raised. This exception is also raised if a file is not present on context
326  manager exit when one is expected to exist.
327  If the block exits with an exception the file if left on disk so it can be
328  examined. The file name has a random component such that nested context
329  managers can be used with the same file suffix.
330 
331  Parameters
332  ----------
333 
334  ext : `str`
335  File name extension, e.g. ``.fits``.
336  expectOutput : `bool`, optional
337  If `True`, a file should be created within the context manager.
338  If `False`, a file should not be present when the context manager
339  exits.
340 
341  Returns
342  -------
343  `str`
344  Path for a temporary file. The path is a combination of the caller's
345  file path and the name of the top-level function
346 
347  Notes
348  -----
349  ::
350 
351  # file tests/testFoo.py
352  import unittest
353  import lsst.utils.tests
354  class FooTestCase(unittest.TestCase):
355  def testBasics(self):
356  self.runTest()
357 
358  def runTest(self):
359  with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
360  # if tests/.tests exists then
361  # tmpFile = "tests/.tests/testFoo_testBasics.fits"
362  # otherwise tmpFile = "testFoo_testBasics.fits"
363  ...
364  # at the end of this "with" block the path tmpFile will be
365  # deleted, but only if the file exists and the "with"
366  # block terminated normally (rather than with an exception)
367  ...
368  """
369  stack = inspect.stack()
370  # get name of first function in the file
371  for i in range(2, len(stack)):
372  frameInfo = inspect.getframeinfo(stack[i][0])
373  if i == 2:
374  callerFilePath = frameInfo.filename
375  callerFuncName = frameInfo.function
376  elif callerFilePath == frameInfo.filename:
377  # this function called the previous function
378  callerFuncName = frameInfo.function
379  else:
380  break
381 
382  callerDir, callerFileNameWithExt = os.path.split(callerFilePath)
383  callerFileName = os.path.splitext(callerFileNameWithExt)[0]
384  outDir = os.path.join(callerDir, ".tests")
385  if not os.path.isdir(outDir):
386  outDir = ""
387  prefix = "%s_%s-" % (callerFileName, callerFuncName)
388  outPath = tempfile.mktemp(dir=outDir, suffix=ext, prefix=prefix)
389  if os.path.exists(outPath):
390  # There should not be a file there given the randomizer. Warn and remove.
391  # Use stacklevel 3 so that the warning is reported from the end of the with block
392  warnings.warn("Unexpectedly found pre-existing tempfile named %r" % (outPath,),
393  stacklevel=3)
394  try:
395  os.remove(outPath)
396  except OSError:
397  pass
398 
399  yield outPath
400 
401  fileExists = os.path.exists(outPath)
402  if expectOutput:
403  if not fileExists:
404  raise RuntimeError("Temp file expected named {} but none found".format(outPath))
405  else:
406  if fileExists:
407  raise RuntimeError("Unexpectedly discovered temp file named {}".format(outPath))
408  # Try to clean up the file regardless
409  if fileExists:
410  try:
411  os.remove(outPath)
412  except OSError as e:
413  # Use stacklevel 3 so that the warning is reported from the end of the with block
414  warnings.warn("Warning: could not remove file %r: %s" % (outPath, e), stacklevel=3)
415 
416 
417 class TestCase(unittest.TestCase):
418  """Subclass of unittest.TestCase that adds some custom assertions for
419  convenience.
420  """
421 
422 
423 def inTestCase(func):
424  """A decorator to add a free function to our custom TestCase class, while also
425  making it available as a free function.
426  """
427  setattr(TestCase, func.__name__, func)
428  return func
429 
430 
431 def debugger(*exceptions):
432  """Decorator to enter the debugger when there's an uncaught exception
433 
434  To use, just slap a ``@debugger()`` on your function.
435 
436  You may provide specific exception classes to catch as arguments to
437  the decorator function, e.g.,
438  ``@debugger(RuntimeError, NotImplementedError)``.
439  This defaults to just `AssertionError`, for use on `unittest.TestCase`
440  methods.
441 
442  Code provided by "Rosh Oxymoron" on StackOverflow:
443  http://stackoverflow.com/questions/4398967/python-unit-testing-automatically-running-the-debugger-when-a-test-fails
444 
445  Notes
446  -----
447  Consider using ``pytest --pdb`` instead of this decorator.
448  """
449  if not exceptions:
450  exceptions = (Exception, )
451 
452  def decorator(f):
453  @functools.wraps(f)
454  def wrapper(*args, **kwargs):
455  try:
456  return f(*args, **kwargs)
457  except exceptions:
458  import sys
459  import pdb
460  pdb.post_mortem(sys.exc_info()[2])
461  return wrapper
462  return decorator
463 
464 
465 def plotImageDiff(lhs, rhs, bad=None, diff=None, plotFileName=None):
466  """Plot the comparison of two 2-d NumPy arrays.
467 
468  Parameters
469  ----------
470  lhs : `numpy.ndarray`
471  LHS values to compare; a 2-d NumPy array
472  rhs : `numpy.ndarray`
473  RHS values to compare; a 2-d NumPy array
474  bad : `numpy.ndarray`
475  A 2-d boolean NumPy array of values to emphasize in the plots
476  diff : `numpy.ndarray`
477  difference array; a 2-d NumPy array, or None to show lhs-rhs
478  plotFileName : `str`
479  Filename to save the plot to. If None, the plot will be displayed in
480  a window.
481 
482  Notes
483  -----
484  This method uses `matplotlib` and imports it internally; it should be
485  wrapped in a try/except block within packages that do not depend on
486  `matplotlib` (including `~lsst.utils`).
487  """
488  from matplotlib import pyplot
489  if diff is None:
490  diff = lhs - rhs
491  pyplot.figure()
492  if bad is not None:
493  # make an rgba image that's red and transparent where not bad
494  badImage = numpy.zeros(bad.shape + (4,), dtype=numpy.uint8)
495  badImage[:, :, 0] = 255
496  badImage[:, :, 1] = 0
497  badImage[:, :, 2] = 0
498  badImage[:, :, 3] = 255*bad
499  vmin1 = numpy.minimum(numpy.min(lhs), numpy.min(rhs))
500  vmax1 = numpy.maximum(numpy.max(lhs), numpy.max(rhs))
501  vmin2 = numpy.min(diff)
502  vmax2 = numpy.max(diff)
503  for n, (image, title) in enumerate([(lhs, "lhs"), (rhs, "rhs"), (diff, "diff")]):
504  pyplot.subplot(2, 3, n + 1)
505  im1 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation='nearest', origin='lower',
506  vmin=vmin1, vmax=vmax1)
507  if bad is not None:
508  pyplot.imshow(badImage, alpha=0.2, interpolation='nearest', origin='lower')
509  pyplot.axis("off")
510  pyplot.title(title)
511  pyplot.subplot(2, 3, n + 4)
512  im2 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation='nearest', origin='lower',
513  vmin=vmin2, vmax=vmax2)
514  if bad is not None:
515  pyplot.imshow(badImage, alpha=0.2, interpolation='nearest', origin='lower')
516  pyplot.axis("off")
517  pyplot.title(title)
518  pyplot.subplots_adjust(left=0.05, bottom=0.05, top=0.92, right=0.75, wspace=0.05, hspace=0.05)
519  cax1 = pyplot.axes([0.8, 0.55, 0.05, 0.4])
520  pyplot.colorbar(im1, cax=cax1)
521  cax2 = pyplot.axes([0.8, 0.05, 0.05, 0.4])
522  pyplot.colorbar(im2, cax=cax2)
523  if plotFileName:
524  pyplot.savefig(plotFileName)
525  else:
526  pyplot.show()
527 
528 
529 @inTestCase
530 def assertFloatsAlmostEqual(testCase, lhs, rhs, rtol=sys.float_info.epsilon,
531  atol=sys.float_info.epsilon, relTo=None,
532  printFailures=True, plotOnFailure=False,
533  plotFileName=None, invert=False, msg=None):
534  """Highly-configurable floating point comparisons for scalars and arrays.
535 
536  The test assertion will fail if all elements ``lhs`` and ``rhs`` are not
537  equal to within the tolerances specified by ``rtol`` and ``atol``.
538  More precisely, the comparison is:
539 
540  ``abs(lhs - rhs) <= relTo*rtol OR abs(lhs - rhs) <= atol``
541 
542  If ``rtol`` or ``atol`` is `None`, that term in the comparison is not
543  performed at all.
544 
545  When not specified, ``relTo`` is the elementwise maximum of the absolute
546  values of ``lhs`` and ``rhs``. If set manually, it should usually be set
547  to either ``lhs`` or ``rhs``, or a scalar value typical of what is
548  expected.
549 
550  Parameters
551  ----------
552  testCase : `unittest.TestCase`
553  Instance the test is part of.
554  lhs : scalar or array-like
555  LHS value(s) to compare; may be a scalar or array-like of any
556  dimension.
557  rhs : scalar or array-like
558  RHS value(s) to compare; may be a scalar or array-like of any
559  dimension.
560  rtol : `float`, optional
561  Relative tolerance for comparison; defaults to double-precision
562  epsilon.
563  atol : `float`, optional
564  Absolute tolerance for comparison; defaults to double-precision
565  epsilon.
566  relTo : `float`, optional
567  Value to which comparison with rtol is relative.
568  printFailures : `bool`, optional
569  Upon failure, print all inequal elements as part of the message.
570  plotOnFailure : `bool`, optional
571  Upon failure, plot the originals and their residual with matplotlib.
572  Only 2-d arrays are supported.
573  plotFileName : `str`, optional
574  Filename to save the plot to. If `None`, the plot will be displayed in
575  a window.
576  invert : `bool`, optional
577  If `True`, invert the comparison and fail only if any elements *are*
578  equal. Used to implement `~lsst.utils.tests.assertFloatsNotEqual`,
579  which should generally be used instead for clarity.
580  msg : `str`, optional
581  String to append to the error message when assert fails.
582 
583  Raises
584  ------
585  AssertionError
586  The values are not almost equal.
587  """
588  if not numpy.isfinite(lhs).all():
589  testCase.fail("Non-finite values in lhs")
590  if not numpy.isfinite(rhs).all():
591  testCase.fail("Non-finite values in rhs")
592  diff = lhs - rhs
593  absDiff = numpy.abs(lhs - rhs)
594  if rtol is not None:
595  if relTo is None:
596  relTo = numpy.maximum(numpy.abs(lhs), numpy.abs(rhs))
597  else:
598  relTo = numpy.abs(relTo)
599  bad = absDiff > rtol*relTo
600  if atol is not None:
601  bad = numpy.logical_and(bad, absDiff > atol)
602  else:
603  if atol is None:
604  raise ValueError("rtol and atol cannot both be None")
605  bad = absDiff > atol
606  failed = numpy.any(bad)
607  if invert:
608  failed = not failed
609  bad = numpy.logical_not(bad)
610  cmpStr = "=="
611  failStr = "are the same"
612  else:
613  cmpStr = "!="
614  failStr = "differ"
615  errMsg = []
616  if failed:
617  if numpy.isscalar(bad):
618  if rtol is None:
619  errMsg = ["%s %s %s; diff=%s with atol=%s"
620  % (lhs, cmpStr, rhs, absDiff, atol)]
621  elif atol is None:
622  errMsg = ["%s %s %s; diff=%s/%s=%s with rtol=%s"
623  % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol)]
624  else:
625  errMsg = ["%s %s %s; diff=%s/%s=%s with rtol=%s, atol=%s"
626  % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol, atol)]
627  else:
628  errMsg = ["%d/%d elements %s with rtol=%s, atol=%s"
629  % (bad.sum(), bad.size, failStr, rtol, atol)]
630  if plotOnFailure:
631  if len(lhs.shape) != 2 or len(rhs.shape) != 2:
632  raise ValueError("plotOnFailure is only valid for 2-d arrays")
633  try:
634  plotImageDiff(lhs, rhs, bad, diff=diff, plotFileName=plotFileName)
635  except ImportError:
636  errMsg.append("Failure plot requested but matplotlib could not be imported.")
637  if printFailures:
638  # Make sure everything is an array if any of them are, so we can treat
639  # them the same (diff and absDiff are arrays if either rhs or lhs is),
640  # and we don't get here if neither is.
641  if numpy.isscalar(relTo):
642  relTo = numpy.ones(bad.shape, dtype=float) * relTo
643  if numpy.isscalar(lhs):
644  lhs = numpy.ones(bad.shape, dtype=float) * lhs
645  if numpy.isscalar(rhs):
646  rhs = numpy.ones(bad.shape, dtype=float) * rhs
647  if rtol is None:
648  for a, b, diff in zip(lhs[bad], rhs[bad], absDiff[bad]):
649  errMsg.append("%s %s %s (diff=%s)" % (a, cmpStr, b, diff))
650  else:
651  for a, b, diff, rel in zip(lhs[bad], rhs[bad], absDiff[bad], relTo[bad]):
652  errMsg.append("%s %s %s (diff=%s/%s=%s)" % (a, cmpStr, b, diff, rel, diff/rel))
653 
654  if msg is not None:
655  errMsg.append(msg)
656  testCase.assertFalse(failed, msg="\n".join(errMsg))
657 
658 
659 @inTestCase
660 def assertFloatsNotEqual(testCase, lhs, rhs, **kwds):
661  """Fail a test if the given floating point values are equal to within the
662  given tolerances.
663 
664  See `~lsst.utils.tests.assertFloatsAlmostEqual` (called with
665  ``rtol=atol=0``) for more information.
666 
667  Parameters
668  ----------
669  testCase : `unittest.TestCase`
670  Instance the test is part of.
671  lhs : scalar or array-like
672  LHS value(s) to compare; may be a scalar or array-like of any
673  dimension.
674  rhs : scalar or array-like
675  RHS value(s) to compare; may be a scalar or array-like of any
676  dimension.
677 
678  Raises
679  ------
680  AssertionError
681  The values are almost equal.
682  """
683  return assertFloatsAlmostEqual(testCase, lhs, rhs, invert=True, **kwds)
684 
685 
686 @inTestCase
687 def assertFloatsEqual(testCase, lhs, rhs, **kwargs):
688  """
689  Assert that lhs == rhs (both numeric types, whether scalar or array).
690 
691  See `~lsst.utils.tests.assertFloatsAlmostEqual` (called with
692  ``rtol=atol=0``) for more information.
693 
694  Parameters
695  ----------
696  testCase : `unittest.TestCase`
697  Instance the test is part of.
698  lhs : scalar or array-like
699  LHS value(s) to compare; may be a scalar or array-like of any
700  dimension.
701  rhs : scalar or array-like
702  RHS value(s) to compare; may be a scalar or array-like of any
703  dimension.
704 
705  Raises
706  ------
707  AssertionError
708  The values are not equal.
709  """
710  return assertFloatsAlmostEqual(testCase, lhs, rhs, rtol=0, atol=0, **kwargs)
711 
712 
713 def _settingsIterator(settings):
714  """Return an iterator for the provided test settings
715 
716  Parameters
717  ----------
718  settings : `dict` (`str`: iterable)
719  Lists of test parameters. Each should be an iterable of the same length.
720  If a string is provided as an iterable, it will be converted to a list
721  of a single string.
722 
723  Raises
724  ------
725  AssertionError
726  If the ``settings`` are not of the same length.
727 
728  Yields
729  ------
730  parameters : `dict` (`str`: anything)
731  Set of parameters.
732  """
733  for name, values in settings.items():
734  if isinstance(values, str):
735  # Probably meant as a single-element string, rather than an iterable of chars
736  settings[name] = [values]
737  num = len(next(iter(settings.values()))) # Number of settings
738  for name, values in settings.items():
739  assert len(values) == num, f"Length mismatch for setting {name}: {len(values)} vs {num}"
740  for ii in range(num):
741  values = [settings[kk][ii] for kk in settings]
742  yield dict(zip(settings.keys(), values))
743 
744 
745 def classParameters(**settings):
746  """Class decorator for generating unit tests
747 
748  This decorator generates classes with class variables according to the
749  supplied ``settings``.
750 
751  Parameters
752  ----------
753  **settings : `dict` (`str`: iterable)
754  The lists of test parameters to set as class variables in turn. Each
755  should be an iterable of the same length.
756 
757  Examples
758  --------
759  ::
760 
761  @classParameters(foo=[1, 2], bar=[3, 4])
762  class MyTestCase(unittest.TestCase):
763  ...
764 
765  will generate two classes, as if you wrote::
766 
767  class MyTestCase_1_3(unittest.TestCase):
768  foo = 1
769  bar = 3
770  ...
771 
772  class MyTestCase_2_4(unittest.TestCase):
773  foo = 2
774  bar = 4
775  ...
776 
777  Note that the values are embedded in the class name.
778  """
779  def decorator(cls):
780  module = sys.modules[cls.__module__].__dict__
781  for params in _settingsIterator(settings):
782  name = f"{cls.__name__}_{'_'.join(str(vv) for vv in params.values())}"
783  bindings = dict(cls.__dict__)
784  bindings.update(params)
785  module[name] = type(name, (cls,), bindings)
786  return decorator
787 
788 
789 def methodParameters(**settings):
790  """Method decorator for unit tests
791 
792  This decorator iterates over the supplied settings, using
793  ``TestCase.subTest`` to communicate the values in the event of a failure.
794 
795  Parameters
796  ----------
797  **settings : `dict` (`str`: iterable)
798  The lists of test parameters. Each should be an iterable of the same
799  length.
800 
801  Examples
802  --------
803  ::
804 
805  @methodParameters(foo=[1, 2], bar=[3, 4])
806  def testSomething(self, foo, bar):
807  ...
808 
809  will run::
810 
811  testSomething(foo=1, bar=3)
812  testSomething(foo=2, bar=4)
813  """
814  def decorator(func):
815  @functools.wraps(func)
816  def wrapper(self, *args, **kwargs):
817  for params in _settingsIterator(settings):
818  kwargs.update(params)
819  with self.subTest(**params):
820  func(self, *args, **kwargs)
821  return wrapper
822  return decorator
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
def suiteClassWrapper(tests)
Definition: tests.py:115
def assertExecutable(self, executable, root_dir=None, args=None, msg=None)
Definition: tests.py:178
def assertFloatsEqual(testCase, lhs, rhs, kwargs)
Definition: tests.py:687
def init()
Definition: tests.py:65
def plotImageDiff(lhs, rhs, bad=None, diff=None, plotFileName=None)
Definition: tests.py:465
def inTestCase(func)
Definition: tests.py:423
def classParameters(settings)
Definition: tests.py:745
daf::base::PropertySet * set
Definition: fits.cc:912
bool all(CoordinateExpr< N > const &expr) noexcept
Return true if all elements are true.
def _build_test_method(cls, executable, root_dir)
Definition: tests.py:229
table::Key< int > type
Definition: Detector.cc:163
def methodParameters(settings)
Definition: tests.py:789
def assertFloatsAlmostEqual(testCase, lhs, rhs, rtol=sys.float_info.epsilon, atol=sys.float_info.epsilon, relTo=None, printFailures=True, plotOnFailure=False, plotFileName=None, invert=False, msg=None)
Definition: tests.py:533
def assertFloatsNotEqual(testCase, lhs, rhs, kwds)
Definition: tests.py:660
def getTempFilePath(ext, expectOutput=True)
Definition: tests.py:318
def debugger(exceptions)
Definition: tests.py:431
def create_executable_tests(cls, ref_file, executables=None)
Definition: tests.py:261
def sort_tests(tests)
Definition: tests.py:72