23 """Support code for running unit tests""" 37 __all__ = [
"init",
"MemoryTestCase",
"ExecutablesTestCase",
"getTempFilePath",
38 "TestCase",
"assertFloatsAlmostEqual",
"assertFloatsNotEqual",
"assertFloatsEqual"]
50 def _get_open_files():
51 """Return a set containing the list of files currently open in this 57 Set containing the list of open files. 65 """Initialize the memory tester and file descriptor leak tester.""" 68 open_files = _get_open_files()
72 """Sort supplied test suites such that MemoryTestCases are at the end. 74 `lsst.utils.tests.MemoryTestCase` tests should always run after any other 80 Sequence of test suites. 84 suite : `unittest.TestSuite` 85 A combined `~unittest.TestSuite` with 86 `~lsst.utils.tests.MemoryTestCase` at the end. 89 suite = unittest.TestSuite()
91 for test_suite
in tests:
98 for method
in test_suite:
99 bases = inspect.getmro(method.__class__)
101 if bases
is not None and MemoryTestCase
in bases:
102 memtests.append(test_suite)
104 suite.addTests(test_suite)
106 if isinstance(test_suite, MemoryTestCase):
107 memtests.append(test_suite)
109 suite.addTest(test_suite)
110 suite.addTests(memtests)
121 unittest.defaultTestLoader.suiteClass = suiteClassWrapper
125 """Check for resource leaks.""" 129 """Reset the leak counter when the tests have been completed""" 133 """Check if any file descriptors are open since init() called.""" 135 self.skipTest(
"Unable to test file descriptor leaks. psutil unavailable.")
138 now_open = _get_open_files()
141 now_open =
set(f
for f
in now_open
if not f.endswith(
".car")
and 142 not f.startswith(
"/proc/")
and 143 not f.endswith(
".ttf")
and 144 not (f.startswith(
"/var/lib/")
and f.endswith(
"/passwd"))
and 145 not f.endswith(
"astropy.log"))
147 diff = now_open.difference(open_files)
150 print(
"File open: %s" % f)
151 self.fail(
"Failed to close %d file%s" % (len(diff),
"s" if len(diff) != 1
else ""))
155 """Test that executables can be run and return good status. 157 The test methods are dynamically created. Callers 158 must subclass this class in their own test file and invoke 159 the create_executable_tests() class method to register the tests. 161 TESTS_DISCOVERED = -1
165 """Abort testing if automated test creation was enabled and 166 no tests were found.""" 169 raise Exception(
"No executables discovered.")
172 """This test exists to ensure that there is at least one test to be 173 executed. This allows the test runner to trigger the class set up 174 machinery to test whether there are some executables to test.""" 178 """Check an executable runs and returns good status. 180 Prints output to standard out. On bad exit status the test 181 fails. If the executable can not be located the test is skipped. 186 Path to an executable. ``root_dir`` is not used if this is an 188 root_dir : `str`, optional 189 Directory containing executable. Ignored if `None`. 190 args : `list` or `tuple`, optional 191 Arguments to be provided to the executable. 192 msg : `str`, optional 193 Message to use when the test fails. Can be `None` for default 199 The executable did not return 0 exit status. 202 if root_dir
is not None and not os.path.isabs(executable):
203 executable = os.path.join(root_dir, executable)
206 sp_args = [executable]
207 argstr =
"no arguments" 210 argstr =
'arguments "' +
" ".join(args) +
'"' 212 print(
"Running executable '{}' with {}...".
format(executable, argstr))
213 if not os.path.exists(executable):
214 self.skipTest(
"Executable {} is unexpectedly missing".
format(executable))
217 output = subprocess.check_output(sp_args)
218 except subprocess.CalledProcessError
as e:
220 failmsg =
"Bad exit status from '{}': {}".
format(executable, e.returncode)
221 print(output.decode(
'utf-8'))
228 def _build_test_method(cls, executable, root_dir):
229 """Build a test method and attach to class. 231 A test method is created for the supplied excutable located 232 in the supplied root directory. This method is attached to the class 233 so that the test runner will discover the test and run it. 238 The class in which to create the tests. 240 Name of executable. Can be absolute path. 242 Path to executable. Not used if executable path is absolute. 244 if not os.path.isabs(executable):
245 executable = os.path.abspath(os.path.join(root_dir, executable))
248 test_name =
"test_exe_" + executable.replace(
"/",
"_")
251 def test_executable_runs(*args):
253 self.assertExecutable(executable)
256 test_executable_runs.__name__ = test_name
257 setattr(cls, test_name, test_executable_runs)
261 """Discover executables to test and create corresponding test methods. 263 Scans the directory containing the supplied reference file 264 (usually ``__file__`` supplied from the test class) to look for 265 executables. If executables are found a test method is created 266 for each one. That test method will run the executable and 267 check the returned value. 269 Executable scripts with a ``.py`` extension and shared libraries 270 are ignored by the scanner. 272 This class method must be called before test discovery. 277 Path to a file within the directory to be searched. 278 If the files are in the same location as the test file, then 279 ``__file__`` can be used. 280 executables : `list` or `tuple`, optional 281 Sequence of executables that can override the automated 282 detection. If an executable mentioned here is not found, a 283 skipped test will be created for it, rather than a failed 288 >>> cls.create_executable_tests(__file__) 292 ref_dir = os.path.abspath(os.path.dirname(ref_file))
294 if executables
is None:
297 for root, dirs, files
in os.walk(ref_dir):
300 if not f.endswith(
".py")
and not f.endswith(
".so"):
301 full_path = os.path.join(root, f)
302 if os.access(full_path, os.X_OK):
303 executables.append(full_path)
312 for e
in executables:
316 @contextlib.contextmanager
318 """Return a path suitable for a temporary file and try to delete the 321 If the with block completes successfully then the file is deleted, 322 if possible; failure results in a printed warning. 323 If a file is remains when it should not, a RuntimeError exception is 324 raised. This exception is also raised if a file is not present on context 325 manager exit when one is expected to exist. 326 If the block exits with an exception the file if left on disk so it can be 327 examined. The file name has a random component such that nested context 328 managers can be used with the same file suffix. 334 File name extension, e.g. ``.fits``. 335 expectOutput : `bool`, optional 336 If `True`, a file should be created within the context manager. 337 If `False`, a file should not be present when the context manager 343 Path for a temporary file. The path is a combination of the caller's 344 file path and the name of the top-level function 350 # file tests/testFoo.py 352 import lsst.utils.tests 353 class FooTestCase(unittest.TestCase): 354 def testBasics(self): 358 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 359 # if tests/.tests exists then 360 # tmpFile = "tests/.tests/testFoo_testBasics.fits" 361 # otherwise tmpFile = "testFoo_testBasics.fits" 363 # at the end of this "with" block the path tmpFile will be 364 # deleted, but only if the file exists and the "with" 365 # block terminated normally (rather than with an exception) 368 stack = inspect.stack()
370 for i
in range(2, len(stack)):
371 frameInfo = inspect.getframeinfo(stack[i][0])
373 callerFilePath = frameInfo.filename
374 callerFuncName = frameInfo.function
375 elif callerFilePath == frameInfo.filename:
377 callerFuncName = frameInfo.function
381 callerDir, callerFileNameWithExt = os.path.split(callerFilePath)
382 callerFileName = os.path.splitext(callerFileNameWithExt)[0]
383 outDir = os.path.join(callerDir,
".tests")
384 if not os.path.isdir(outDir):
386 prefix =
"%s_%s-" % (callerFileName, callerFuncName)
387 outPath = tempfile.mktemp(dir=outDir, suffix=ext, prefix=prefix)
388 if os.path.exists(outPath):
391 warnings.warn(
"Unexpectedly found pre-existing tempfile named %r" % (outPath,),
400 fileExists = os.path.exists(outPath)
403 raise RuntimeError(
"Temp file expected named {} but none found".
format(outPath))
406 raise RuntimeError(
"Unexpectedly discovered temp file named {}".
format(outPath))
413 warnings.warn(
"Warning: could not remove file %r: %s" % (outPath, e), stacklevel=3)
417 """Subclass of unittest.TestCase that adds some custom assertions for 423 """A decorator to add a free function to our custom TestCase class, while also 424 making it available as a free function. 426 setattr(TestCase, func.__name__, func)
431 """Decorator to enter the debugger when there's an uncaught exception 433 To use, just slap a ``@debugger()`` on your function. 435 You may provide specific exception classes to catch as arguments to 436 the decorator function, e.g., 437 ``@debugger(RuntimeError, NotImplementedError)``. 438 This defaults to just `AssertionError`, for use on `unittest.TestCase` 441 Code provided by "Rosh Oxymoron" on StackOverflow: 442 http://stackoverflow.com/questions/4398967/python-unit-testing-automatically-running-the-debugger-when-a-test-fails 446 Consider using ``pytest --pdb`` instead of this decorator. 449 exceptions = (AssertionError, )
453 def wrapper(*args, **kwargs):
455 return f(*args, **kwargs)
459 pdb.post_mortem(sys.exc_info()[2])
465 """Plot the comparison of two 2-d NumPy arrays. 469 lhs : `numpy.ndarray` 470 LHS values to compare; a 2-d NumPy array 471 rhs : `numpy.ndarray` 472 RHS values to compare; a 2-d NumPy array 473 bad : `numpy.ndarray` 474 A 2-d boolean NumPy array of values to emphasize in the plots 475 diff : `numpy.ndarray` 476 difference array; a 2-d NumPy array, or None to show lhs-rhs 478 Filename to save the plot to. If None, the plot will be displayed in 483 This method uses `matplotlib` and imports it internally; it should be 484 wrapped in a try/except block within packages that do not depend on 485 `matplotlib` (including `~lsst.utils`). 487 from matplotlib
import pyplot
493 badImage = numpy.zeros(bad.shape + (4,), dtype=numpy.uint8)
494 badImage[:, :, 0] = 255
495 badImage[:, :, 1] = 0
496 badImage[:, :, 2] = 0
497 badImage[:, :, 3] = 255*bad
498 vmin1 = numpy.minimum(numpy.min(lhs), numpy.min(rhs))
499 vmax1 = numpy.maximum(numpy.max(lhs), numpy.max(rhs))
500 vmin2 = numpy.min(diff)
501 vmax2 = numpy.max(diff)
502 for n, (image, title)
in enumerate([(lhs,
"lhs"), (rhs,
"rhs"), (diff,
"diff")]):
503 pyplot.subplot(2, 3, n + 1)
504 im1 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation=
'nearest', origin=
'lower',
505 vmin=vmin1, vmax=vmax1)
507 pyplot.imshow(badImage, alpha=0.2, interpolation=
'nearest', origin=
'lower')
510 pyplot.subplot(2, 3, n + 4)
511 im2 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation=
'nearest', origin=
'lower',
512 vmin=vmin2, vmax=vmax2)
514 pyplot.imshow(badImage, alpha=0.2, interpolation=
'nearest', origin=
'lower')
517 pyplot.subplots_adjust(left=0.05, bottom=0.05, top=0.92, right=0.75, wspace=0.05, hspace=0.05)
518 cax1 = pyplot.axes([0.8, 0.55, 0.05, 0.4])
519 pyplot.colorbar(im1, cax=cax1)
520 cax2 = pyplot.axes([0.8, 0.05, 0.05, 0.4])
521 pyplot.colorbar(im2, cax=cax2)
523 pyplot.savefig(plotFileName)
530 atol=sys.float_info.epsilon, relTo=None,
531 printFailures=True, plotOnFailure=False,
532 plotFileName=None, invert=False, msg=None):
533 """Highly-configurable floating point comparisons for scalars and arrays. 535 The test assertion will fail if all elements ``lhs`` and ``rhs`` are not 536 equal to within the tolerances specified by ``rtol`` and ``atol``. 537 More precisely, the comparison is: 539 ``abs(lhs - rhs) <= relTo*rtol OR abs(lhs - rhs) <= atol`` 541 If ``rtol`` or ``atol`` is `None`, that term in the comparison is not 544 When not specified, ``relTo`` is the elementwise maximum of the absolute 545 values of ``lhs`` and ``rhs``. If set manually, it should usually be set 546 to either ``lhs`` or ``rhs``, or a scalar value typical of what is 551 testCase : `unittest.TestCase` 552 Instance the test is part of. 553 lhs : scalar or array-like 554 LHS value(s) to compare; may be a scalar or array-like of any 556 rhs : scalar or array-like 557 RHS value(s) to compare; may be a scalar or array-like of any 559 rtol : `float`, optional 560 Relative tolerance for comparison; defaults to double-precision 562 atol : `float`, optional 563 Absolute tolerance for comparison; defaults to double-precision 565 relTo : `float`, optional 566 Value to which comparison with rtol is relative. 567 printFailures : `bool`, optional 568 Upon failure, print all inequal elements as part of the message. 569 plotOnFailure : `bool`, optional 570 Upon failure, plot the originals and their residual with matplotlib. 571 Only 2-d arrays are supported. 572 plotFileName : `str`, optional 573 Filename to save the plot to. If `None`, the plot will be displayed in 575 invert : `bool`, optional 576 If `True`, invert the comparison and fail only if any elements *are* 577 equal. Used to implement `~lsst.utils.tests.assertFloatsNotEqual`, 578 which should generally be used instead for clarity. 579 msg : `str`, optional 580 String to append to the error message when assert fails. 585 The values are not almost equal. 587 if not numpy.isfinite(lhs).
all():
588 testCase.fail(
"Non-finite values in lhs")
589 if not numpy.isfinite(rhs).
all():
590 testCase.fail(
"Non-finite values in rhs")
592 absDiff = numpy.abs(lhs - rhs)
595 relTo = numpy.maximum(numpy.abs(lhs), numpy.abs(rhs))
597 relTo = numpy.abs(relTo)
598 bad = absDiff > rtol*relTo
600 bad = numpy.logical_and(bad, absDiff > atol)
603 raise ValueError(
"rtol and atol cannot both be None")
605 failed = numpy.any(bad)
608 bad = numpy.logical_not(bad)
610 failStr =
"are the same" 616 if numpy.isscalar(bad):
618 errMsg = [
"%s %s %s; diff=%s with atol=%s" 619 % (lhs, cmpStr, rhs, absDiff, atol)]
621 errMsg = [
"%s %s %s; diff=%s/%s=%s with rtol=%s" 622 % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol)]
624 errMsg = [
"%s %s %s; diff=%s/%s=%s with rtol=%s, atol=%s" 625 % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol, atol)]
627 errMsg = [
"%d/%d elements %s with rtol=%s, atol=%s" 628 % (bad.sum(), bad.size, failStr, rtol, atol)]
630 if len(lhs.shape) != 2
or len(rhs.shape) != 2:
631 raise ValueError(
"plotOnFailure is only valid for 2-d arrays")
633 plotImageDiff(lhs, rhs, bad, diff=diff, plotFileName=plotFileName)
635 errMsg.append(
"Failure plot requested but matplotlib could not be imported.")
640 if numpy.isscalar(relTo):
641 relTo = numpy.ones(bad.shape, dtype=float) * relTo
642 if numpy.isscalar(lhs):
643 lhs = numpy.ones(bad.shape, dtype=float) * lhs
644 if numpy.isscalar(rhs):
645 rhs = numpy.ones(bad.shape, dtype=float) * rhs
647 for a, b, diff
in zip(lhs[bad], rhs[bad], absDiff[bad]):
648 errMsg.append(
"%s %s %s (diff=%s)" % (a, cmpStr, b, diff))
650 for a, b, diff, rel
in zip(lhs[bad], rhs[bad], absDiff[bad], relTo[bad]):
651 errMsg.append(
"%s %s %s (diff=%s/%s=%s)" % (a, cmpStr, b, diff, rel, diff/rel))
655 testCase.assertFalse(failed, msg=
"\n".join(errMsg))
660 """Fail a test if the given floating point values are equal to within the 663 See `~lsst.utils.tests.assertFloatsAlmostEqual` (called with 664 ``rtol=atol=0``) for more information. 668 testCase : `unittest.TestCase` 669 Instance the test is part of. 670 lhs : scalar or array-like 671 LHS value(s) to compare; may be a scalar or array-like of any 673 rhs : scalar or array-like 674 RHS value(s) to compare; may be a scalar or array-like of any 680 The values are almost equal. 688 Assert that lhs == rhs (both numeric types, whether scalar or array). 690 See `~lsst.utils.tests.assertFloatsAlmostEqual` (called with 691 ``rtol=atol=0``) for more information. 695 testCase : `unittest.TestCase` 696 Instance the test is part of. 697 lhs : scalar or array-like 698 LHS value(s) to compare; may be a scalar or array-like of any 700 rhs : scalar or array-like 701 RHS value(s) to compare; may be a scalar or array-like of any 707 The values are not equal. def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
def suiteClassWrapper(tests)
def assertExecutable(self, executable, root_dir=None, args=None, msg=None)
def assertFloatsEqual(testCase, lhs, rhs, kwargs)
def plotImageDiff(lhs, rhs, bad=None, diff=None, plotFileName=None)
daf::base::PropertySet * set
bool all(CoordinateExpr< N > const &expr) noexcept
Return true if all elements are true.
def _build_test_method(cls, executable, root_dir)
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)
def assertFloatsNotEqual(testCase, lhs, rhs, kwds)
def getTempFilePath(ext, expectOutput=True)
def create_executable_tests(cls, ref_file, executables=None)
def testFileDescriptorLeaks(self)