LSSTApplications  10.0+286,10.0+36,10.0+46,10.0-2-g4f67435,10.1+152,10.1+37,11.0,11.0+1,11.0-1-g47edd16,11.0-1-g60db491,11.0-1-g7418c06,11.0-2-g04d2804,11.0-2-g68503cd,11.0-2-g818369d,11.0-2-gb8b8ce7
LSSTDataManagementBasePackage
tests.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 """Support code for running unit tests"""
24 
25 from contextlib import contextmanager
26 import gc
27 import inspect
28 import os
29 import sys
30 import unittest
31 import warnings
32 
33 import numpy
34 
35 try:
36  import lsst.daf.base as dafBase
37 except ImportError:
38  dafBase = None
39 
40 try:
41  type(memId0)
42 except NameError:
43  memId0 = 0 # ignore leaked blocks with IDs before memId0
44  nleakPrintMax = 20 # maximum number of leaked blocks to print
45 
46 def init():
47  global memId0
48  if dafBase:
49  memId0 = dafBase.Citizen_getNextMemId() # used by MemoryTestCase
50 
51 def run(suite, exit=True):
52  """!Exit with the status code resulting from running the provided test suite"""
53 
54  if unittest.TextTestRunner().run(suite).wasSuccessful():
55  status = 0
56  else:
57  status = 1
58 
59  if exit:
60  sys.exit(status)
61  else:
62  return status
63 
64 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
65 
66 class MemoryTestCase(unittest.TestCase):
67  """!Check for memory leaks since memId0 was allocated"""
68  def setUp(self):
69  pass
70 
71  def testLeaks(self):
72  """!Check for memory leaks in the preceding tests"""
73  if dafBase:
74  gc.collect()
75  global memId0, nleakPrintMax
76  nleak = dafBase.Citizen_census(0, memId0)
77  if nleak != 0:
78  print "\n%d Objects leaked:" % dafBase.Citizen_census(0, memId0)
79 
80  if nleak <= nleakPrintMax:
81  print dafBase.Citizen_census(dafBase.cout, memId0)
82  else:
83  census = dafBase.Citizen_census()
84  print "..."
85  for i in range(nleakPrintMax - 1, -1, -1):
86  print census[i].repr()
87 
88  self.fail("Leaked %d blocks" % dafBase.Citizen_census(0, memId0))
89 
90 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
91 
92 def findFileFromRoot(ifile):
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)
95 
96  This is useful for running tests that may be run from _dir_/tests or _dir_"""
97 
98  if os.path.isfile(ifile):
99  return ifile
100 
101  ofile = None
102  file = ifile
103  while file != "":
104  dirname, basename = os.path.split(file)
105  if ofile:
106  ofile = os.path.join(basename, ofile)
107  else:
108  ofile = basename
109 
110  if os.path.isfile(ofile):
111  return ofile
112 
113  file = dirname
114 
115  raise IOError, "Can't find %s" % ifile
116 
117 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
118 
119 @contextmanager
121  """!Return a path suitable for a temporary file and try to delete the file on success
122 
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.
126 
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:
130  @code
131  # file tests/testFoo.py
132  import unittest
133  import lsst.utils.tests
134  class FooTestCase(unittest.TestCase):
135  def testBasics(self):
136  self.runTest()
137 
138  def runTest(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"
142  ...
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)
145  ...
146  @endcode
147  """
148  stack = inspect.stack()
149  # get name of first function in the file
150  for i in range(2, len(stack)):
151  frameInfo = inspect.getframeinfo(stack[i][0])
152  if i == 2:
153  callerFilePath = frameInfo.filename
154  callerFuncName = frameInfo.function
155  elif callerFilePath == frameInfo.filename:
156  # this function called the previous function
157  callerFuncName = frameInfo.function
158  else:
159  break
160 
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):
165  outDir = ""
166  outName = "%s_%s%s" % (callerFileName, callerFuncName, ext)
167  outPath = os.path.join(outDir, outName)
168  yield outPath
169  if os.path.isfile(outPath):
170  try:
171  os.remove(outPath)
172  except OSError as e:
173  print "Warning: could not remove file %r: %s" % (outPath, e)
174  else:
175  print "Warning: could not find file %r" % (outPath,)
176 
177 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
178 
179 class TestCase(unittest.TestCase):
180  """!Subclass of unittest.TestCase that adds some custom assertions for
181  convenience.
182  """
183 
184 def inTestCase(func):
185  """!A decorator to add a free function to our custom TestCase class, while also
186  making it available as a free function.
187  """
188  setattr(TestCase, func.__name__, func)
189  return func
190 
191 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
192 
193 @inTestCase
194 def assertRaisesLsstCpp(testcase, excClass, callableObj, *args, **kwargs):
195  warnings.warn("assertRaisesLsstCpp is deprecated; please just use TestCase.assertRaises",
196  DeprecationWarning)
197  return testcase.assertRaises(excClass, callableObj, *args, **kwargs)
198 
199 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
200 
201 import functools
202 def debugger(*exceptions):
203  """!Decorator to enter the debugger when there's an uncaught exception
204 
205  To use, just slap a "@debugger()" on your function.
206 
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.
210 
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
213  """
214  if not exceptions:
215  exceptions = (AssertionError, )
216  def decorator(f):
217  @functools.wraps(f)
218  def wrapper(*args, **kwargs):
219  try:
220  return f(*args, **kwargs)
221  except exceptions:
222  import sys, pdb
223  pdb.post_mortem(sys.exc_info()[2])
224  return wrapper
225  return decorator
226 
227 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
228 
229 def plotImageDiff(lhs, rhs, bad=None, diff=None, plotFileName=None):
230  """!Plot the comparison of two 2-d NumPy arrays.
231 
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).
235 
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
241  a window.
242  """
243  from matplotlib import pyplot
244  if diff is None:
245  diff = lhs - rhs
246  pyplot.figure()
247  if bad is not None:
248  # make an rgba image that's red and transparent where not bad
249  badImage = numpy.zeros(bad.shape + (4,), dtype=numpy.uint8)
250  badImage[:,:,0] = 255
251  badImage[:,:,1] = 0
252  badImage[:,:,2] = 0
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)
262  if bad is not None:
263  pyplot.imshow(badImage, alpha=0.2, interpolation='nearest', origin='lower')
264  pyplot.axis("off")
265  pyplot.title(title)
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)
269  if bad is not None:
270  pyplot.imshow(badImage, alpha=0.2, interpolation='nearest', origin='lower')
271  pyplot.axis("off")
272  pyplot.title(title)
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)
278  if plotFileName:
279  pyplot.savefig(plotFileName)
280  else:
281  pyplot.show()
282 
283 @inTestCase
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.
287 
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:
290 
291  abs(lhs - rhs) <= relTo*rtol OR abs(lhs - rhs) <= atol
292 
293  If rtol or atol is None, that term in the comparison is not performed at all.
294 
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
297  is expected.
298 
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
309  a window.
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
312  for clarity.
313  """
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")
318  diff = lhs - rhs
319  absDiff = numpy.abs(lhs - rhs)
320  if rtol is not None:
321  if relTo is None:
322  relTo = numpy.maximum(numpy.abs(lhs), numpy.abs(rhs))
323  else:
324  relTo = numpy.abs(relTo)
325  bad = absDiff > rtol*relTo
326  if atol is not None:
327  bad = numpy.logical_and(bad, absDiff > atol)
328  else:
329  if atol is None:
330  raise ValueError("rtol and atol cannot both be None")
331  bad = absDiff > atol
332  failed = numpy.any(bad)
333  if invert:
334  failed = not failed
335  bad = numpy.logical_not(bad)
336  cmpStr = "=="
337  failStr = "are the same"
338  else:
339  cmpStr = "!="
340  failStr = "differ"
341  msg = []
342  if failed:
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)]
346  else:
347  msg = ["%d/%d elements %s with rtol=%s, atol=%s"
348  % (bad.sum(), bad.size, failStr, rtol, atol)]
349  if plotOnFailure:
350  if len(lhs.shape) != 2 or len(rhs.shape) != 2:
351  raise ValueError("plotOnFailure is only valid for 2-d arrays")
352  try:
353  plotImageDiff(lhs, rhs, bad, diff=diff, plotFileName=plotFileName)
354  except ImportError:
355  msg.append("Failure plot requested but matplotlib could not be imported.")
356  if printFailures:
357  # Make sure everything is an array if any of them are, so we can treat
358  # them the same (diff and absDiff are arrays if either rhs or lhs is),
359  # and we don't get here if neither is.
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))
369 
370 @inTestCase
371 def assertNotClose(testCase, lhs, rhs, **kwds):
372  """!Fail a test if the given floating point values are completely equal to within the given tolerances.
373 
374  See assertClose for more information.
375  """
376  return assertClose(testCase, lhs, rhs, invert=True, **kwds)
def assertClose
Highly-configurable floating point comparisons for scalars and arrays.
Definition: tests.py:285
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...
Definition: tests.py:371
def debugger
Decorator to enter the debugger when there&#39;s an uncaught exception.
Definition: tests.py:202
def assertRaisesLsstCpp
Definition: tests.py:194
def findFileFromRoot
Find file which is specified as a path relative to the toplevel directory; we start in $cwd and walk ...
Definition: tests.py:92
def plotImageDiff
Plot the comparison of two 2-d NumPy arrays.
Definition: tests.py:229
def getTempFilePath
Return a path suitable for a temporary file and try to delete the file on success.
Definition: tests.py:120
Subclass of unittest.TestCase that adds some custom assertions for convenience.
Definition: tests.py:179
def testLeaks
Check for memory leaks in the preceding tests.
Definition: tests.py:71
def inTestCase
A decorator to add a free function to our custom TestCase class, while also making it available as a ...
Definition: tests.py:184
def run
Exit with the status code resulting from running the provided test suite.
Definition: tests.py:51
Check for memory leaks since memId0 was allocated.
Definition: tests.py:66