LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
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 import unittest
26 import warnings
27 import numpy
28 try:
29  import lsst.daf.base as dafBase
30 except ImportError:
31  dafBase = None
32 
33 import lsst.pex.exceptions as pexExcept
34 import os
35 import sys
36 import gc
37 
38 try:
39  type(memId0)
40 except NameError:
41  memId0 = 0 # ignore leaked blocks with IDs before memId0
42  nleakPrintMax = 20 # maximum number of leaked blocks to print
43 
44 def init():
45  global memId0
46  if dafBase:
47  memId0 = dafBase.Citizen_getNextMemId() # used by MemoryTestCase
48 
49 def run(suite, exit=True):
50  """Exit with the status code resulting from running the provided test suite"""
51 
52  if unittest.TextTestRunner().run(suite).wasSuccessful():
53  status = 0
54  else:
55  status = 1
56 
57  if exit:
58  sys.exit(status)
59  else:
60  return status
61 
62 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
63 
64 class MemoryTestCase(unittest.TestCase):
65  """Check for memory leaks since memId0 was allocated"""
66  def setUp(self):
67  pass
68 
69  def testLeaks(self):
70  """Check for memory leaks in the preceding tests"""
71  if dafBase:
72  gc.collect()
73  global memId0, nleakPrintMax
74  nleak = dafBase.Citizen_census(0, memId0)
75  if nleak != 0:
76  print "\n%d Objects leaked:" % dafBase.Citizen_census(0, memId0)
77 
78  if nleak <= nleakPrintMax:
79  print dafBase.Citizen_census(dafBase.cout, memId0)
80  else:
81  census = dafBase.Citizen_census()
82  print "..."
83  for i in range(nleakPrintMax - 1, -1, -1):
84  print census[i].repr()
85 
86  self.fail("Leaked %d blocks" % dafBase.Citizen_census(0, memId0))
87 
88 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
89 
90 def findFileFromRoot(ifile):
91  """Find file which is specified as a path relative to the toplevel directory;
92  we start in $cwd and walk up until we find the file (or throw IOError if it doesn't exist)
93 
94  This is useful for running tests that may be run from <dir>/tests or <dir>"""
95 
96  if os.path.isfile(ifile):
97  return ifile
98 
99  ofile = None
100  file = ifile
101  while file != "":
102  dirname, basename = os.path.split(file)
103  if ofile:
104  ofile = os.path.join(basename, ofile)
105  else:
106  ofile = basename
107 
108  if os.path.isfile(ofile):
109  return ofile
110 
111  file = dirname
112 
113  raise IOError, "Can't find %s" % ifile
114 
115 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
116 
118  """A class to be used with a with statement to ensure that a file is deleted
119 E.g.
120 
121 with temporaryFile("foo.fits") as filename:
122  image.writeFits(filename)
123  readImage = Image(filename)
124  """
125  def __init__(self, filename):
126  self.filename = filename
127 
128  def __enter__(self):
129  return self.filename
130 
131  def __exit__(self, type, value, traceback):
132  import os
133  try:
134  os.remove(self.filename)
135  except OSError:
136  pass
137 
138 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
139 
140 class TestCase(unittest.TestCase):
141  """Subclass of unittest.TestCase that adds some custom assertions for
142  convenience.
143  """
144 
145 def inTestCase(func):
146  """A decorator to add a free function to our custom TestCase class, while also
147  making it available as a free function.
148  """
149  setattr(TestCase, func.__name__, func)
150  return func
151 
152 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
153 
154 @inTestCase
155 def assertRaisesLsstCpp(testcase, excClass, callableObj, *args, **kwargs):
156  warnings.warn("assertRaisesLsstCpp is deprecated; please just use TestCase.assertRaises",
157  DeprecationWarning)
158  return testcase.assertRaises(excClass, callableObj, *args, **kwargs)
159 
160 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
161 
162 import functools
163 def debugger(*exceptions):
164  """Decorator to enter the debugger when there's an uncaught exception
165 
166  To use, just slap a "@debugger()" on your function.
167 
168  You may provide specific exception classes to catch as arguments to
169  the decorator function, e.g., "@debugger(RuntimeError, NotImplementedError)".
170  This defaults to just 'AssertionError', for use on unittest.TestCase methods.
171 
172  Code provided by "Rosh Oxymoron" on StackOverflow:
173  http://stackoverflow.com/questions/4398967/python-unit-testing-automatically-running-the-debugger-when-a-test-fails
174  """
175  if not exceptions:
176  exceptions = (AssertionError, )
177  def decorator(f):
178  @functools.wraps(f)
179  def wrapper(*args, **kwargs):
180  try:
181  return f(*args, **kwargs)
182  except exceptions:
183  import sys, pdb
184  pdb.post_mortem(sys.exc_info()[2])
185  return wrapper
186  return decorator
187 
188 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
189 
190 def plotImageDiff(lhs, rhs, bad=None, diff=None, plotFileName=None):
191  """Plot the comparison of two 2-d NumPy arrays.
192 
193  NOTE: this method uses matplotlib and imports it internally; it should be
194  wrapped in a try/except block within packages that do not depend on
195  matplotlib (including utils).
196 
197  @param[in] lhs LHS values to compare; a 2-d NumPy array
198  @param[in] rhs RHS values to compare; a 2-d NumPy array
199  @param[in] bad A 2-d boolean NumPy array of values to emphasize in the plots
200  @param[in] plotFileName Filename to save the plot to. If None, the plot will be displayed in a
201  a window.
202  """
203  from matplotlib import pyplot
204  if diff is None:
205  diff = lhs - rhs
206  pyplot.figure()
207  if bad is not None:
208  # make an rgba image that's red and transparent where not bad
209  badImage = numpy.zeros(bad.shape + (4,), dtype=numpy.uint8)
210  badImage[:,:,0] = 255
211  badImage[:,:,1] = 0
212  badImage[:,:,2] = 0
213  badImage[:,:,3] = 255*bad
214  vmin1 = numpy.minimum(numpy.min(lhs), numpy.min(rhs))
215  vmax1 = numpy.maximum(numpy.max(lhs), numpy.max(rhs))
216  vmin2 = numpy.min(diff)
217  vmax2 = numpy.max(diff)
218  for n, (image, title) in enumerate([(lhs, "lhs"), (rhs, "rhs"), (diff, "diff")]):
219  pyplot.subplot(2,3,n+1)
220  im1 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation='nearest', origin='lower',
221  vmin=vmin1, vmax=vmax1)
222  if bad is not None:
223  pyplot.imshow(badImage, alpha=0.2, interpolation='nearest', origin='lower')
224  pyplot.axis("off")
225  pyplot.title(title)
226  pyplot.subplot(2,3,n+4)
227  im2 = pyplot.imshow(image, cmap=pyplot.cm.gray, interpolation='nearest', origin='lower',
228  vmin=vmin2, vmax=vmax2)
229  if bad is not None:
230  pyplot.imshow(badImage, alpha=0.2, interpolation='nearest', origin='lower')
231  pyplot.axis("off")
232  pyplot.title(title)
233  pyplot.subplots_adjust(left=0.05, bottom=0.05, top=0.92, right=0.75, wspace=0.05, hspace=0.05)
234  cax1 = pyplot.axes([0.8, 0.55, 0.05, 0.4])
235  pyplot.colorbar(im1, cax=cax1)
236  cax2 = pyplot.axes([0.8, 0.05, 0.05, 0.4])
237  pyplot.colorbar(im2, cax=cax2)
238  if plotFileName:
239  pyplot.savefig(plotFileName)
240  else:
241  pyplot.show()
242 
243 @inTestCase
244 def assertClose(testCase, lhs, rhs, rtol=sys.float_info.epsilon, atol=sys.float_info.epsilon, relTo=None,
245  printFailures=True, plotOnFailure=False, plotFileName=None, invert=False):
246  """Highly-configurable floating point comparisons for scalars and arrays.
247 
248  The test assertion will fail if all elements lhs and rhs are not equal to within the tolerances
249  specified by rtol and atol. More precisely, the comparison is:
250 
251  abs(lhs - rhs) <= relTo*rtol OR abs(lhs - rhs) <= atol
252 
253  If rtol or atol is None, that term in the comparison is not performed at all.
254 
255  When not specified, relTo is the elementwise maximum of the absolute values of lhs and rhs. If
256  set manually, it should usually be set to either lhs or rhs, or a scalar value typical of what
257  is expected.
258 
259  @param[in] testCase unittest.TestCase instance the test is part of
260  @param[in] lhs LHS value(s) to compare; may be a scalar or a numpy array of any dimension
261  @param[in] rhs RHS value(s) to compare; may be a scalar or a numpy array of any dimension
262  @param[in] rtol Relative tolerance for comparison; defaults to double-precision epsilon.
263  @param[in] atol Absolute tolerance for comparison; defaults to double-precision epsilon.
264  @param[in] relTo Value to which comparison with rtol is relative.
265  @param[in] printFailures Upon failure, print all inequal elements as part of the message.
266  @param[in] plotOnFailure Upon failure, plot the originals and their residual with matplotlib.
267  Only 2-d arrays are supported.
268  @param[in] plotFileName Filename to save the plot to. If None, the plot will be displayed in a
269  a window.
270  @param[in] invert If True, invert the comparison and fail only if any elements *are* equal.
271  Used to implement assertNotClose, which should generally be used instead
272  for clarity.
273  """
274  if not numpy.isfinite(lhs).all():
275  testCase.fail("Non-finite values in lhs")
276  if not numpy.isfinite(rhs).all():
277  testCase.fail("Non-finite values in rhs")
278  diff = lhs - rhs
279  absDiff = numpy.abs(lhs - rhs)
280  if rtol is not None:
281  if relTo is None:
282  relTo = numpy.maximum(numpy.abs(lhs), numpy.abs(rhs))
283  else:
284  relTo = numpy.abs(relTo)
285  bad = absDiff > rtol*relTo
286  if atol is not None:
287  bad = numpy.logical_and(bad, absDiff > atol)
288  else:
289  if atol is None:
290  raise ValueError("rtol and atol cannot both be None")
291  bad = absDiff > atol
292  failed = numpy.any(bad)
293  if invert:
294  failed = not failed
295  bad = numpy.logical_not(bad)
296  cmpStr = "=="
297  failStr = "are the same"
298  else:
299  cmpStr = "!="
300  failStr = "differ"
301  msg = []
302  if failed:
303  if numpy.isscalar(bad):
304  msg = ["%s %s %s; diff=%s/%s=%s with rtol=%s, atol=%s"
305  % (lhs, cmpStr, rhs, absDiff, relTo, absDiff/relTo, rtol, atol)]
306  else:
307  msg = ["%d/%d elements %s with rtol=%s, atol=%s"
308  % (bad.sum(), bad.size, failStr, rtol, atol)]
309  if plotOnFailure:
310  if len(lhs.shape) != 2 or len(rhs.shape) != 2:
311  raise ValueError("plotOnFailure is only valid for 2-d arrays")
312  try:
313  plotImageDiff(lhs, rhs, bad, diff=diff, plotFileName=plotFileName)
314  except ImportError:
315  msg.append("Failure plot requested but matplotlib could not be imported.")
316  if printFailures:
317  # Make sure everything is an array if any of them are, so we can treat
318  # them the same (diff and absDiff are arrays if either rhs or lhs is),
319  # and we don't get here if neither is.
320  if numpy.isscalar(relTo):
321  relTo = numpy.ones(bad.shape, dtype=float) * relTo
322  if numpy.isscalar(lhs):
323  lhs = numpy.ones(bad.shape, dtype=float) * lhs
324  if numpy.isscalar(rhs):
325  rhs = numpy.ones(bad.shape, dtype=float) * rhs
326  for a, b, diff, rel in zip(lhs[bad], rhs[bad], absDiff[bad], relTo[bad]):
327  msg.append("%s %s %s (diff=%s/%s=%s)" % (a, cmpStr, b, diff, rel, diff/rel))
328  testCase.assertFalse(failed, msg="\n".join(msg))
329 
330 @inTestCase
331 def assertNotClose(testCase, lhs, rhs, **kwds):
332  """Fail a test if the given floating point values are completely equal to within the given tolerances.
333 
334  See assertClose for more information.
335  """
336  return assertClose(testCase, lhs, rhs, invert=True, **kwds)
def assertNotClose
Definition: tests.py:331
def assertRaisesLsstCpp
Definition: tests.py:155
def findFileFromRoot
Definition: tests.py:90
def plotImageDiff
Definition: tests.py:190
boost::enable_if< typename ExpressionTraits< Scalar >::IsScalar, bool >::type all(Scalar const &scalar)
Definition: operators.h:1186