23 """Support code for running unit tests"""
25 from contextlib
import contextmanager
49 memId0 = dafBase.Citizen_getNextMemId()
51 def run(suite, exit=True):
52 """!Exit with the status code resulting from running the provided test suite"""
54 if unittest.TextTestRunner().
run(suite).wasSuccessful():
67 """!Check for memory leaks since memId0 was allocated"""
72 """!Check for memory leaks in the preceding tests"""
75 global memId0, nleakPrintMax
76 nleak = dafBase.Citizen_census(0, memId0)
78 print "\n%d Objects leaked:" % dafBase.Citizen_census(0, memId0)
80 if nleak <= nleakPrintMax:
81 print dafBase.Citizen_census(dafBase.cout, memId0)
83 census = dafBase.Citizen_census()
85 for i
in range(nleakPrintMax - 1, -1, -1):
86 print census[i].repr()
88 self.fail(
"Leaked %d blocks" % dafBase.Citizen_census(0, memId0))
93 """!Find file which is specified as a path relative to the toplevel directory;
94 we start in $cwd and walk up until we find the file (or throw IOError if it doesn't exist)
96 This is useful for running tests that may be run from _dir_/tests or _dir_"""
98 if os.path.isfile(ifile):
104 dirname, basename = os.path.split(file)
106 ofile = os.path.join(basename, ofile)
110 if os.path.isfile(ofile):
115 raise IOError,
"Can't find %s" % ifile
121 """!Return a path suitable for a temporary file and try to delete the file on success
123 If the with block completes successfully then the file is deleted, if possible;
124 failure results in a printed warning.
125 If the block exits with an exception the file if left on disk so it can be examined.
127 @param[in] ext file name extension, e.g. ".fits"
128 @return path for a temporary file. The path is a combination of the caller's file path
129 and the name of the top-level function, as per this simple example:
131 # file tests/testFoo.py
133 import lsst.utils.tests
134 class FooTestCase(unittest.TestCase):
135 def testBasics(self):
139 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
140 # if tests/.tests exists then tmpFile = "tests/.tests/testFoo_testBasics.fits"
141 # otherwise tmpFile = "testFoo_testBasics.fits"
143 # at the end of this "with" block the path tmpFile will be deleted, but only if
144 # the file exists and the "with" block terminated normally (rather than with an exception)
148 stack = inspect.stack()
150 for i
in range(2, len(stack)):
151 frameInfo = inspect.getframeinfo(stack[i][0])
153 callerFilePath = frameInfo.filename
154 callerFuncName = frameInfo.function
155 elif callerFilePath == frameInfo.filename:
157 callerFuncName = frameInfo.function
161 callerDir, callerFileNameWithExt = os.path.split(callerFilePath)
162 callerFileName = os.path.splitext(callerFileNameWithExt)[0]
163 outDir = os.path.join(callerDir,
".tests")
164 if not os.path.isdir(outDir):
166 outName =
"%s_%s%s" % (callerFileName, callerFuncName, ext)
167 outPath = os.path.join(outDir, outName)
169 if os.path.isfile(outPath):
173 print "Warning: could not remove file %r: %s" % (outPath, e)
175 print "Warning: could not find file %r" % (outPath,)
180 """!Subclass of unittest.TestCase that adds some custom assertions for
185 """!A decorator to add a free function to our custom TestCase class, while also
186 making it available as a free function.
188 setattr(TestCase, func.__name__, func)
195 warnings.warn(
"assertRaisesLsstCpp is deprecated; please just use TestCase.assertRaises",
197 return testcase.assertRaises(excClass, callableObj, *args, **kwargs)
203 """!Decorator to enter the debugger when there's an uncaught exception
205 To use, just slap a "@debugger()" on your function.
207 You may provide specific exception classes to catch as arguments to
208 the decorator function, e.g., "@debugger(RuntimeError, NotImplementedError)".
209 This defaults to just 'AssertionError', for use on unittest.TestCase methods.
211 Code provided by "Rosh Oxymoron" on StackOverflow:
212 http://stackoverflow.com/questions/4398967/python-unit-testing-automatically-running-the-debugger-when-a-test-fails
215 exceptions = (AssertionError, )
218 def wrapper(*args, **kwargs):
220 return f(*args, **kwargs)
223 pdb.post_mortem(sys.exc_info()[2])
230 """!Plot the comparison of two 2-d NumPy arrays.
232 NOTE: this method uses matplotlib and imports it internally; it should be
233 wrapped in a try/except block within packages that do not depend on
234 matplotlib (including utils).
236 @param[in] lhs LHS values to compare; a 2-d NumPy array
237 @param[in] rhs RHS values to compare; a 2-d NumPy array
238 @param[in] bad A 2-d boolean NumPy array of values to emphasize in the plots
239 @param[in] diff difference array; a 2-d NumPy array, or None to show lhs-rhs
240 @param[in] plotFileName Filename to save the plot to. If None, the plot will be displayed in a
243 from matplotlib
import pyplot
249 badImage = numpy.zeros(bad.shape + (4,), dtype=numpy.uint8)
250 badImage[:,:,0] = 255
253 badImage[:,:,3] = 255*bad
254 vmin1 = numpy.minimum(numpy.min(lhs), numpy.min(rhs))
255 vmax1 = numpy.maximum(numpy.max(lhs), numpy.max(rhs))
256 vmin2 = numpy.min(diff)
257 vmax2 = numpy.max(diff)
258 for n, (image, title)
in enumerate([(lhs,
"lhs"), (rhs,
"rhs"), (diff,
"diff")]):
259 pyplot.subplot(2,3,n+1)
260 im1 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation=
'nearest', origin=
'lower',
261 vmin=vmin1, vmax=vmax1)
263 pyplot.imshow(badImage, alpha=0.2, interpolation=
'nearest', origin=
'lower')
266 pyplot.subplot(2,3,n+4)
267 im2 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation=
'nearest', origin=
'lower',
268 vmin=vmin2, vmax=vmax2)
270 pyplot.imshow(badImage, alpha=0.2, interpolation=
'nearest', origin=
'lower')
273 pyplot.subplots_adjust(left=0.05, bottom=0.05, top=0.92, right=0.75, wspace=0.05, hspace=0.05)
274 cax1 = pyplot.axes([0.8, 0.55, 0.05, 0.4])
275 pyplot.colorbar(im1, cax=cax1)
276 cax2 = pyplot.axes([0.8, 0.05, 0.05, 0.4])
277 pyplot.colorbar(im2, cax=cax2)
279 pyplot.savefig(plotFileName)
284 def assertClose(testCase, lhs, rhs, rtol=sys.float_info.epsilon, atol=sys.float_info.epsilon, relTo=None,
285 printFailures=
True, plotOnFailure=
False, plotFileName=
None, invert=
False):
286 """!Highly-configurable floating point comparisons for scalars and arrays.
288 The test assertion will fail if all elements lhs and rhs are not equal to within the tolerances
289 specified by rtol and atol. More precisely, the comparison is:
291 abs(lhs - rhs) <= relTo*rtol OR abs(lhs - rhs) <= atol
293 If rtol or atol is None, that term in the comparison is not performed at all.
295 When not specified, relTo is the elementwise maximum of the absolute values of lhs and rhs. If
296 set manually, it should usually be set to either lhs or rhs, or a scalar value typical of what
299 @param[in] testCase unittest.TestCase instance the test is part of
300 @param[in] lhs LHS value(s) to compare; may be a scalar or a numpy array of any dimension
301 @param[in] rhs RHS value(s) to compare; may be a scalar or a numpy array of any dimension
302 @param[in] rtol Relative tolerance for comparison; defaults to double-precision epsilon.
303 @param[in] atol Absolute tolerance for comparison; defaults to double-precision epsilon.
304 @param[in] relTo Value to which comparison with rtol is relative.
305 @param[in] printFailures Upon failure, print all inequal elements as part of the message.
306 @param[in] plotOnFailure Upon failure, plot the originals and their residual with matplotlib.
307 Only 2-d arrays are supported.
308 @param[in] plotFileName Filename to save the plot to. If None, the plot will be displayed in a
310 @param[in] invert If True, invert the comparison and fail only if any elements *are* equal.
311 Used to implement assertNotClose, which should generally be used instead
314 if not numpy.isfinite(lhs).
all():
315 testCase.fail(
"Non-finite values in lhs")
316 if not numpy.isfinite(rhs).
all():
317 testCase.fail(
"Non-finite values in rhs")
319 absDiff = numpy.abs(lhs - rhs)
322 relTo = numpy.maximum(numpy.abs(lhs), numpy.abs(rhs))
324 relTo = numpy.abs(relTo)
325 bad = absDiff > rtol*relTo
327 bad = numpy.logical_and(bad, absDiff > atol)
330 raise ValueError(
"rtol and atol cannot both be None")
332 failed = numpy.any(bad)
335 bad = numpy.logical_not(bad)
337 failStr =
"are the same"
343 if numpy.isscalar(bad):
344 msg = [
"%s %s %s; diff=%s/%s=%s with rtol=%s, atol=%s"
345 % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol, atol)]
347 msg = [
"%d/%d elements %s with rtol=%s, atol=%s"
348 % (bad.sum(), bad.size, failStr, rtol, atol)]
350 if len(lhs.shape) != 2
or len(rhs.shape) != 2:
351 raise ValueError(
"plotOnFailure is only valid for 2-d arrays")
353 plotImageDiff(lhs, rhs, bad, diff=diff, plotFileName=plotFileName)
355 msg.append(
"Failure plot requested but matplotlib could not be imported.")
360 if numpy.isscalar(relTo):
361 relTo = numpy.ones(bad.shape, dtype=float) * relTo
362 if numpy.isscalar(lhs):
363 lhs = numpy.ones(bad.shape, dtype=float) * lhs
364 if numpy.isscalar(rhs):
365 rhs = numpy.ones(bad.shape, dtype=float) * rhs
366 for a, b, diff, rel
in zip(lhs[bad], rhs[bad], absDiff[bad], relTo[bad]):
367 msg.append(
"%s %s %s (diff=%s/%s=%s)" % (a, cmpStr, b, diff, rel, diff/rel))
368 testCase.assertFalse(failed, msg=
"\n".join(msg))
372 """!Fail a test if the given floating point values are completely equal to within the given tolerances.
374 See assertClose for more information.
376 return assertClose(testCase, lhs, rhs, invert=
True, **kwds)
def assertClose
Highly-configurable floating point comparisons for scalars and arrays.
bool all(CoordinateExpr< N > const &expr)
Return true if all elements are true.
def assertNotClose
Fail a test if the given floating point values are completely equal to within the given tolerances...
def debugger
Decorator to enter the debugger when there's an uncaught exception.
def findFileFromRoot
Find file which is specified as a path relative to the toplevel directory; we start in $cwd and walk ...
def plotImageDiff
Plot the comparison of two 2-d NumPy arrays.
def getTempFilePath
Return a path suitable for a temporary file and try to delete the file on success.
Subclass of unittest.TestCase that adds some custom assertions for convenience.
def testLeaks
Check for memory leaks in the preceding tests.
def inTestCase
A decorator to add a free function to our custom TestCase class, while also making it available as a ...
def run
Exit with the status code resulting from running the provided test suite.
Check for memory leaks since memId0 was allocated.