23 """Support code for running unit tests""" 24 from __future__
import print_function
25 from __future__
import division
26 from builtins
import zip
27 from builtins
import range
41 __all__ = [
"init",
"run",
"MemoryTestCase",
"ExecutablesTestCase",
"getTempFilePath",
42 "TestCase",
"assertFloatsAlmostEqual",
"assertFloatsNotEqual",
"assertFloatsEqual"]
65 def _get_open_files():
66 """Return a set containing the list of open files.""" 73 """Initialize the memory tester""" 77 memId0 = dafBase.Citizen.getNextMemId()
79 open_files = _get_open_files()
82 def run(suite, exit=True):
83 """Exit with the status code resulting from running the provided test suite""" 85 if unittest.TextTestRunner().
run(suite).wasSuccessful():
97 """Go through the supplied sequence of test suites and sort them to ensure that 98 MemoryTestCases are at the end of the test list. Returns a combined 101 suite = unittest.TestSuite()
103 for test_suite
in tests:
110 for method
in test_suite:
111 bases = inspect.getmro(method.__class__)
113 if bases
is not None and MemoryTestCase
in bases:
114 memtests.append(test_suite)
116 suite.addTests(test_suite)
118 if isinstance(test_suite, MemoryTestCase):
119 memtests.append(test_suite)
121 suite.addTest(test_suite)
122 suite.addTests(memtests)
133 unittest.defaultTestLoader.suiteClass = suiteClassWrapper
137 """Check for memory leaks since memId0 was allocated""" 144 """Reset the leak counter when the tests have been completed""" 148 """Check for memory leaks in the preceding tests""" 151 global memId0, nleakPrintMax
152 nleak = dafBase.Citizen.census(0, memId0)
154 plural =
"s" if nleak != 1
else "" 155 print(
"\n%d Object%s leaked:" % (nleak, plural))
157 if nleak <= nleakPrintMax:
158 print(dafBase.Citizen.census(memId0))
160 census = dafBase.Citizen.census()
162 for i
in range(nleakPrintMax - 1, -1, -1):
163 print(census[i].repr())
165 self.fail(
"Leaked %d block%s" % (nleak, plural))
169 self.skipTest(
"Unable to test file descriptor leaks. psutil unavailable.")
172 now_open = _get_open_files()
175 now_open =
set(f
for f
in now_open
if not f.endswith(
".car")
and 176 not f.startswith(
"/proc/")
and 177 not f.endswith(
".ttf")
and 178 f !=
"/var/lib/sss/mc/passwd" and 179 not f.endswith(
"astropy.log"))
181 diff = now_open.difference(open_files)
184 print(
"File open: %s" % f)
185 self.fail(
"Failed to close %d file%s" % (len(diff),
"s" if len(diff) != 1
else ""))
189 """Test that executables can be run and return good status. 191 The test methods are dynamically created. Callers 192 must subclass this class in their own test file and invoke 193 the create_executable_tests() class method to register the tests. 195 TESTS_DISCOVERED = -1
199 """Abort testing if automated test creation was enabled and 200 yet not tests were found.""" 203 raise Exception(
"No executables discovered.")
206 """This test exists to ensure that there is at least one test to be 207 executed. This allows the test runner to trigger the class set up 208 machinery to test whether there are some executables to test.""" 212 """Check an executable runs and returns good status. 214 Prints output to standard out. On bad exit status the test 215 fails. If the executable can not be located the test is skipped. 220 Path to an executable. root_dir is not used if this is an 224 Directory containing exe. Ignored if None. 227 Arguments to be provided to the executable. 230 Message to use when the test fails. Can be None for default message. 233 if root_dir
is not None and not os.path.isabs(executable):
234 executable = os.path.join(root_dir, executable)
237 sp_args = [executable]
238 argstr =
"no arguments" 241 argstr =
'arguments "' +
" ".join(args) +
'"' 243 print(
"Running executable '{}' with {}...".
format(executable, argstr))
244 if not os.path.exists(executable):
245 self.skipTest(
"Executable {} is unexpectedly missing".
format(executable))
248 output = subprocess.check_output(sp_args)
249 except subprocess.CalledProcessError
as e:
251 failmsg =
"Bad exit status from '{}': {}".
format(executable, e.returncode)
252 print(output.decode(
'utf-8'))
259 def _build_test_method(cls, executable, root_dir):
260 """Build a test method and attach to class. 262 The method is built for the supplied excutable located 263 in the supplied root directory. 265 cls._build_test_method(root_dir, executable) 270 The class in which to create the tests. 273 Name of executable. Can be absolute path. 276 Path to executable. Not used if executable path is absolute. 278 if not os.path.isabs(executable):
279 executable = os.path.abspath(os.path.join(root_dir, executable))
282 test_name =
"test_exe_" + executable.replace(
"/",
"_")
285 def test_executable_runs(*args):
287 self.assertExecutable(executable)
290 test_executable_runs.__name__ = test_name
291 setattr(cls, test_name, test_executable_runs)
295 """Discover executables to test and create corresponding test methods. 297 Scans the directory containing the supplied reference file 298 (usually __file__ supplied from the test class) to look for 299 executables. If executables are found a test method is created 300 for each one. That test method will run the executable and 301 check the returned value. 303 Executable scripts with a .py extension and shared libraries 304 are ignored by the scanner. 306 This class method must be called before test discovery. 310 cls.create_executable_tests(__file__) 312 The list of executables can be overridden by passing in a 313 sequence of explicit executables that should be tested. 314 If an item in the sequence can not be found the 315 test will be configured to skip rather than fail. 319 ref_dir = os.path.abspath(os.path.dirname(ref_file))
321 if executables
is None:
324 for root, dirs, files
in os.walk(ref_dir):
327 if not f.endswith(
".py")
and not f.endswith(
".so"):
328 full_path = os.path.join(root, f)
329 if os.access(full_path, os.X_OK):
330 executables.append(full_path)
339 for e
in executables:
344 """Find file which is specified as a path relative to the toplevel directory; 345 we start in $cwd and walk up until we find the file (or throw IOError if it doesn't exist) 347 This is useful for running tests that may be run from _dir_/tests or _dir_""" 349 if os.path.isfile(ifile):
355 dirname, basename = os.path.split(file)
357 ofile = os.path.join(basename, ofile)
361 if os.path.isfile(ofile):
366 raise IOError(
"Can't find %s" % ifile)
369 @contextlib.contextmanager
371 """Return a path suitable for a temporary file and try to delete the file on success 373 If the with block completes successfully then the file is deleted, if possible; 374 failure results in a printed warning. 375 If a file is remains when it should not, a RuntimeError exception is raised. This 376 exception is also raised if a file is not present on context manager exit when one 377 is expected to exist. 378 If the block exits with an exception the file if left on disk so it can be examined. 379 The file name has a random component such that nested context managers can be used 380 with the same file suffix. 385 ext : `str` file name extension, e.g. ".fits" 386 expectOutput : `bool` 387 If true, a file should be created within the context manager. 388 If false, a file should not be present when the context manager exits. 393 Path for a temporary file. The path is a combination of the caller's 394 file path and the name of the top-level function 400 # file tests/testFoo.py 402 import lsst.utils.tests 403 class FooTestCase(unittest.TestCase): 404 def testBasics(self): 408 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 409 # if tests/.tests exists then tmpFile = "tests/.tests/testFoo_testBasics.fits" 410 # otherwise tmpFile = "testFoo_testBasics.fits" 412 # at the end of this "with" block the path tmpFile will be deleted, but only if 413 # the file exists and the "with" block terminated normally (rather than with an exception) 416 stack = inspect.stack()
418 for i
in range(2, len(stack)):
419 frameInfo = inspect.getframeinfo(stack[i][0])
421 callerFilePath = frameInfo.filename
422 callerFuncName = frameInfo.function
423 elif callerFilePath == frameInfo.filename:
425 callerFuncName = frameInfo.function
429 callerDir, callerFileNameWithExt = os.path.split(callerFilePath)
430 callerFileName = os.path.splitext(callerFileNameWithExt)[0]
431 outDir = os.path.join(callerDir,
".tests")
432 if not os.path.isdir(outDir):
434 prefix =
"%s_%s-" % (callerFileName, callerFuncName)
435 outPath = tempfile.mktemp(dir=outDir, suffix=ext, prefix=prefix)
436 if os.path.exists(outPath):
439 warnings.warn(
"Unexpectedly found pre-existing tempfile named %r" % (outPath,),
448 fileExists = os.path.exists(outPath)
451 raise RuntimeError(
"Temp file expected named {} but none found".
format(outPath))
454 raise RuntimeError(
"Unexpectedly discovered temp file named {}".
format(outPath))
461 warnings.warn(
"Warning: could not remove file %r: %s" % (outPath, e), stacklevel=3)
465 """Subclass of unittest.TestCase that adds some custom assertions for 471 """A decorator to add a free function to our custom TestCase class, while also 472 making it available as a free function. 474 setattr(TestCase, func.__name__, func)
480 """.. note:: Deprecated in 12_0""" 481 warnings.warn(
"assertRaisesLsstCpp is deprecated; please just use TestCase.assertRaises",
482 DeprecationWarning, stacklevel=2)
483 return testcase.assertRaises(excClass, callableObj, *args, **kwargs)
487 """Decorator to enter the debugger when there's an uncaught exception 489 To use, just slap a "@debugger()" on your function. 491 You may provide specific exception classes to catch as arguments to 492 the decorator function, e.g., "@debugger(RuntimeError, NotImplementedError)". 493 This defaults to just 'AssertionError', for use on unittest.TestCase methods. 495 Code provided by "Rosh Oxymoron" on StackOverflow: 496 http://stackoverflow.com/questions/4398967/python-unit-testing-automatically-running-the-debugger-when-a-test-fails 499 exceptions = (AssertionError, )
503 def wrapper(*args, **kwargs):
505 return f(*args, **kwargs)
509 pdb.post_mortem(sys.exc_info()[2])
515 """Plot the comparison of two 2-d NumPy arrays. 517 NOTE: this method uses matplotlib and imports it internally; it should be 518 wrapped in a try/except block within packages that do not depend on 519 matplotlib (including utils). 523 lhs : `numpy.ndarray` 524 LHS values to compare; a 2-d NumPy array 525 rhs : `numpy.ndarray` 526 RHS values to compare; a 2-d NumPy array 527 bad : `numpy.ndarray` 528 A 2-d boolean NumPy array of values to emphasize in the plots 529 diff : `numpy.ndarray` 530 difference array; a 2-d NumPy array, or None to show lhs-rhs 532 Filename to save the plot to. If None, the plot will be displayed in 535 from matplotlib
import pyplot
541 badImage = numpy.zeros(bad.shape + (4,), dtype=numpy.uint8)
542 badImage[:, :, 0] = 255
543 badImage[:, :, 1] = 0
544 badImage[:, :, 2] = 0
545 badImage[:, :, 3] = 255*bad
546 vmin1 = numpy.minimum(numpy.min(lhs), numpy.min(rhs))
547 vmax1 = numpy.maximum(numpy.max(lhs), numpy.max(rhs))
548 vmin2 = numpy.min(diff)
549 vmax2 = numpy.max(diff)
550 for n, (image, title)
in enumerate([(lhs,
"lhs"), (rhs,
"rhs"), (diff,
"diff")]):
551 pyplot.subplot(2, 3, n + 1)
552 im1 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation=
'nearest', origin=
'lower',
553 vmin=vmin1, vmax=vmax1)
555 pyplot.imshow(badImage, alpha=0.2, interpolation=
'nearest', origin=
'lower')
558 pyplot.subplot(2, 3, n + 4)
559 im2 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation=
'nearest', origin=
'lower',
560 vmin=vmin2, vmax=vmax2)
562 pyplot.imshow(badImage, alpha=0.2, interpolation=
'nearest', origin=
'lower')
565 pyplot.subplots_adjust(left=0.05, bottom=0.05, top=0.92, right=0.75, wspace=0.05, hspace=0.05)
566 cax1 = pyplot.axes([0.8, 0.55, 0.05, 0.4])
567 pyplot.colorbar(im1, cax=cax1)
568 cax2 = pyplot.axes([0.8, 0.05, 0.05, 0.4])
569 pyplot.colorbar(im2, cax=cax2)
571 pyplot.savefig(plotFileName)
578 atol=sys.float_info.epsilon, relTo=None,
579 printFailures=True, plotOnFailure=False,
580 plotFileName=None, invert=False, msg=None):
581 """Highly-configurable floating point comparisons for scalars and arrays. 583 The test assertion will fail if all elements lhs and rhs are not equal to within the tolerances 584 specified by rtol and atol. More precisely, the comparison is: 586 ``abs(lhs - rhs) <= relTo*rtol OR abs(lhs - rhs) <= atol`` 588 If rtol or atol is None, that term in the comparison is not performed at all. 590 When not specified, relTo is the elementwise maximum of the absolute values of lhs and rhs. If 591 set manually, it should usually be set to either lhs or rhs, or a scalar value typical of what 596 testCase : `unittest.TestCase` 597 Instance the test is part of. 598 lhs : scalar or array-like 599 LHS value(s) to compare; may be a scalar or array-like of any dimension 600 rhs : scalar or array-like 601 RHS value(s) to compare; may be a scalar or array-like of any dimension 602 rtol : `float` or None 603 Relative tolerance for comparison; defaults to double-precision epsilon. 604 atol : `float` or None 605 Absolute tolerance for comparison; defaults to double-precision epsilon. 607 Value to which comparison with rtol is relative. 608 printFailures : `bool` 609 Upon failure, print all inequal elements as part of the message. 610 plotOnFailure : `bool` 611 Upon failure, plot the originals and their residual with matplotlib. 612 Only 2-d arrays are supported. 614 Filename to save the plot to. If None, the plot will be displayed in 617 If True, invert the comparison and fail only if any elements *are* equal. 618 Used to implement assertFloatsNotEqual, which should generally be used instead 621 String to append to the error message when assert fails. 623 if not numpy.isfinite(lhs).
all():
624 testCase.fail(
"Non-finite values in lhs")
625 if not numpy.isfinite(rhs).
all():
626 testCase.fail(
"Non-finite values in rhs")
628 absDiff = numpy.abs(lhs - rhs)
631 relTo = numpy.maximum(numpy.abs(lhs), numpy.abs(rhs))
633 relTo = numpy.abs(relTo)
634 bad = absDiff > rtol*relTo
636 bad = numpy.logical_and(bad, absDiff > atol)
639 raise ValueError(
"rtol and atol cannot both be None")
641 failed = numpy.any(bad)
644 bad = numpy.logical_not(bad)
646 failStr =
"are the same" 652 if numpy.isscalar(bad):
654 errMsg = [
"%s %s %s; diff=%s with atol=%s" 655 % (lhs, cmpStr, rhs, absDiff, atol)]
657 errMsg = [
"%s %s %s; diff=%s/%s=%s with rtol=%s" 658 % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol)]
660 errMsg = [
"%s %s %s; diff=%s/%s=%s with rtol=%s, atol=%s" 661 % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol, atol)]
663 errMsg = [
"%d/%d elements %s with rtol=%s, atol=%s" 664 % (bad.sum(), bad.size, failStr, rtol, atol)]
666 if len(lhs.shape) != 2
or len(rhs.shape) != 2:
667 raise ValueError(
"plotOnFailure is only valid for 2-d arrays")
669 plotImageDiff(lhs, rhs, bad, diff=diff, plotFileName=plotFileName)
671 errMsg.append(
"Failure plot requested but matplotlib could not be imported.")
676 if numpy.isscalar(relTo):
677 relTo = numpy.ones(bad.shape, dtype=float) * relTo
678 if numpy.isscalar(lhs):
679 lhs = numpy.ones(bad.shape, dtype=float) * lhs
680 if numpy.isscalar(rhs):
681 rhs = numpy.ones(bad.shape, dtype=float) * rhs
683 for a, b, diff
in zip(lhs[bad], rhs[bad], absDiff[bad]):
684 errMsg.append(
"%s %s %s (diff=%s)" % (a, cmpStr, b, diff))
686 for a, b, diff, rel
in zip(lhs[bad], rhs[bad], absDiff[bad], relTo[bad]):
687 errMsg.append(
"%s %s %s (diff=%s/%s=%s)" % (a, cmpStr, b, diff, rel, diff/rel))
691 testCase.assertFalse(failed, msg=
"\n".join(errMsg))
697 Fail a test if the given floating point values are equal to within the given tolerances. 699 See assertFloatsAlmostEqual (called with rtol=atol=0) for more information. 707 Assert that lhs == rhs (both numeric types, whether scalar or array). 709 See assertFloatsAlmostEqual (called with rtol=atol=0) for more information. 716 """.. note:: Deprecated in 12_0""" 717 warnings.warn(
"assertClose is deprecated; please use TestCase.assertFloatsAlmostEqual",
718 DeprecationWarning, stacklevel=2)
724 """.. note:: Deprecated in 12_0""" 725 warnings.warn(
"assertNotClose is deprecated; please use TestCase.assertFloatsNotEqual",
726 DeprecationWarning, stacklevel=2)
def suiteClassWrapper(tests)
bool all(CoordinateExpr< N > const &expr)
Return true if all elements are true.
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)
def assertClose(args, kwargs)
daf::base::PropertySet * set
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 format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
def assertFloatsNotEqual(testCase, lhs, rhs, kwds)
def run(suite, exit=True)
def getTempFilePath(ext, expectOutput=True)
def findFileFromRoot(ifile)
def assertNotClose(args, kwargs)
def create_executable_tests(cls, ref_file, executables=None)
def testFileDescriptorLeaks(self)
def assertRaisesLsstCpp(testcase, excClass, callableObj, args, kwargs)