IMP logo
IMP Reference Guide  2.17.0
The Integrative Modeling Platform
test/__init__.py
1 """@namespace IMP::test
2  @brief Methods and classes for testing the IMP kernel and modules.
3  @ingroup python
4 """
5 
6 import re
7 import math
8 import sys
9 import os
10 import tempfile
11 import random
12 import IMP
13 import time
14 import types
15 import shutil
16 import difflib
17 import pprint
18 import unittest
19 from unittest.util import safe_repr
20 import datetime
21 import pickle
22 import contextlib
23 import subprocess
24 try:
25  from pathlib import Path
26 except ImportError: # Use bundled pathlib on Python 2 without pathlib
27  from IMP._compat_pathlib import Path
28 
29 
30 # Expose some unittest decorators for convenience
31 expectedFailure = unittest.expectedFailure
32 skip = unittest.skip
33 skipIf = unittest.skipIf
34 skipUnless = unittest.skipUnless
35 
36 
37 class _TempDir(object):
38  def __init__(self, dir=None):
39  self.tmpdir = tempfile.mkdtemp(dir=dir)
40 
41  def __del__(self):
42  shutil.rmtree(self.tmpdir, ignore_errors=True)
43 
44 
45 @contextlib.contextmanager
47  """Simple context manager to run in a temporary directory.
48  While the context manager is active (within the 'with' block)
49  the current working directory is set to a temporary directory.
50  When the context manager exists, the working directory is reset
51  and the temporary directory deleted."""
52  origdir = os.getcwd()
53  tmpdir = tempfile.mkdtemp()
54  os.chdir(tmpdir)
55  yield tmpdir
56  os.chdir(origdir)
57  shutil.rmtree(tmpdir, ignore_errors=True)
58 
59 
60 @contextlib.contextmanager
61 def temporary_directory(dir=None):
62  """Simple context manager to make a temporary directory.
63  The temporary directory has the same lifetime as the context manager
64  (i.e. it is created at the start of the 'with' block, and deleted
65  at the end of the block).
66  @param dir If given, the temporary directory is made as a subdirectory
67  of that directory, rather than in the default temporary
68  directory location (e.g. /tmp)
69  @return the full path to the temporary directory.
70  """
71  tmpdir = tempfile.mkdtemp(dir=dir)
72  yield tmpdir
73  shutil.rmtree(tmpdir, ignore_errors=True)
74 
75 
76 def numerical_derivative(func, val, step):
77  """Calculate the derivative of the single-value function `func` at
78  point `val`. The derivative is calculated using simple finite
79  differences starting with the given `step`; Richardson extrapolation
80  is then used to extrapolate the derivative at step=0."""
81  maxsteps = 50
82  con = 1.4
83  safe = 2.0
84  err = 1.0e30
85  f1 = func(val + step)
86  f2 = func(val - step)
87  # create first element in triangular matrix d of derivatives
88  d = [[(f1 - f2) / (2.0 * step)]]
89  retval = None
90  for i in range(1, maxsteps):
91  d.append([0.] * (i + 1))
92  step /= con
93  f1 = func(val + step)
94  f2 = func(val - step)
95  d[i][0] = (f1 - f2) / (2.0 * step)
96  fac = con * con
97  for j in range(1, i + 1):
98  d[i][j] = (d[i][j-1] * fac - d[i-1][j-1]) / (fac - 1.)
99  fac *= con * con
100  errt = max(abs(d[i][j] - d[i][j-1]),
101  abs(d[i][j] - d[i-1][j-1]))
102  if errt <= err:
103  err = errt
104  retval = d[i][j]
105  if abs(d[i][i] - d[i-1][i-1]) >= safe * err:
106  break
107  if retval is None:
108  raise ValueError("Cannot calculate numerical derivative")
109  return retval
110 
111 
112 def xyz_numerical_derivatives(sf, xyz, step):
113  """Calculate the x,y and z derivatives of the scoring function `sf`
114  on the `xyz` particle. The derivatives are approximated numerically
115  using the numerical_derivatives() function."""
116  class _XYZDerivativeFunc(object):
117  def __init__(self, sf, xyz, basis_vector):
118  self._xyz = xyz
119  self._sf = sf
120  self._basis_vector = basis_vector
121  self._starting_coordinates = xyz.get_coordinates()
122 
123  def __call__(self, val):
124  self._xyz.set_coordinates(self._starting_coordinates +
125  self._basis_vector * val)
126  return self._sf.evaluate(False)
127 
128  return tuple([IMP.test.numerical_derivative(
129  _XYZDerivativeFunc(sf, xyz, IMP.algebra.Vector3D(*x)),
130  0, 0.01)
131  for x in ((1, 0, 0), (0, 1, 0), (0, 0, 1))])
132 
133 
134 class TestCase(unittest.TestCase):
135  """Super class for IMP test cases.
136  This provides a number of useful IMP-specific methods on top of
137  the standard Python `unittest.TestCase` class.
138  Test scripts should generally contain a subclass of this class,
139  conventionally called `Tests` (this makes it easier to run an
140  individual test from the command line) and use IMP::test::main()
141  as their main function."""
142 
143  # Provide assert(Not)Regex for Python 2 users (assertRegexMatches is
144  # deprecated in Python 3)
145  if not hasattr(unittest.TestCase, 'assertRegex'):
146  assertRegex = unittest.TestCase.assertRegexpMatches
147  assertNotRegex = unittest.TestCase.assertNotRegexpMatches
148 
149  def __init__(self, *args, **keys):
150  unittest.TestCase.__init__(self, *args, **keys)
151  self._progname = Path(sys.argv[0]).absolute()
152 
153  def setUp(self):
154  self.__check_level = IMP.get_check_level()
155  # Turn on expensive runtime checks while running the test suite:
156  IMP.set_check_level(IMP.USAGE_AND_INTERNAL)
157  # python ints are bigger than C++ ones, so we need to make sure it fits
158  # otherwise python throws fits
159  IMP.random_number_generator.seed(hash(time.time()) % 2**30)
160 
161  def tearDown(self):
162  # Restore original check level
163  IMP.set_check_level(self.__check_level)
164  # Clean up any temporary files
165  if hasattr(self, '_tmpdir'):
166  del self._tmpdir
167 
168  def get_input_file_name(self, filename):
169  """Get the full name of an input file in the top-level
170  test directory."""
171  if self.__module__ == '__main__':
172  testdir = self._progname
173  else:
174  testdir = Path(sys.modules[self.__module__].__file__)
175  for p in testdir.parents:
176  input = p / "input"
177  if input.is_dir():
178  ret = input / filename
179  if not ret.exists():
180  raise IOError("Test input file %s does not exist" % ret)
181  return str(ret)
182  raise IOError("No test input directory found")
183 
184  def open_input_file(self, filename, mode='rb'):
185  """Open and return an input file in the top-level test directory."""
186  return open(self.get_input_file_name(filename), mode)
187 
188  def get_tmp_file_name(self, filename):
189  """Get the full name of an output file in the tmp directory.
190  The directory containing this file will be automatically
191  cleaned up when the test completes."""
192  if not hasattr(self, '_tmpdir'):
193  self._tmpdir = _TempDir(os.environ.get('IMP_TMP_DIR'))
194  tmpdir = self._tmpdir.tmpdir
195  return str(Path(tmpdir) / filename)
196 
197  def get_magnitude(self, vector):
198  """Get the magnitude of a list of floats"""
199  return sum([x*x for x in vector], 0)**.5
200 
201  def assertRaisesUsageException(self, c, *args, **keys):
202  """Assert that the given callable object raises UsageException.
203  This differs from unittest's assertRaises in that the test
204  is skipped in fast mode (where usage checks are turned off)."""
205  if IMP.get_check_level() >= IMP.USAGE:
206  return self.assertRaises(IMP.UsageException, c, *args, **keys)
207 
208  def assertRaisesInternalException(self, c, *args, **keys):
209  """Assert that the given callable object raises InternalException.
210  This differs from unittest's assertRaises in that the test
211  is skipped in fast mode (where internal checks are turned off)."""
212  if IMP.get_check_level() >= IMP.USAGE_AND_INTERNAL:
213  return self.assertRaises(IMP.InternalException, c, *args, **keys)
214 
215  def assertNotImplemented(self, c, *args, **keys):
216  """Assert that the given callable object is not implemented."""
217  return self.assertRaises(IMP.InternalException, c, *args, **keys)
218 
219  def assertXYZDerivativesInTolerance(self, sf, xyz, tolerance=0,
220  percentage=0):
221  """Assert that x,y,z analytical derivatives match numerical within
222  a tolerance, or a percentage (of the analytical value), whichever
223  is larger. `sf` should be a ScoringFunction or Restraint,
224  although for backwards compatibility a Model is also accepted."""
225  sf.evaluate(True)
226  derivs = xyz.get_derivatives()
227  num_derivs = xyz_numerical_derivatives(sf, xyz, 0.01)
228  pct = percentage / 100.0
229  self.assertAlmostEqual(
230  self.get_magnitude(derivs-num_derivs), 0,
231  delta=tolerance+percentage*self.get_magnitude(num_derivs),
232  msg="Don't match "+str(derivs) + str(num_derivs))
233  self.assertAlmostEqual(derivs[0], num_derivs[0],
234  delta=max(tolerance, abs(derivs[0]) * pct))
235  self.assertAlmostEqual(derivs[1], num_derivs[1],
236  delta=max(tolerance, abs(derivs[1]) * pct))
237  self.assertAlmostEqual(derivs[2], num_derivs[2],
238  delta=max(tolerance, abs(derivs[2]) * pct))
239 
240  def assertNumPyArrayEqual(self, numpy_array, exp_array):
241  """Fail if the given numpy array doesn't match expected"""
242  if IMP.IMP_KERNEL_HAS_NUMPY:
243  import numpy.testing
244  self.assertIsInstance(numpy_array, numpy.ndarray)
245  numpy.testing.assert_array_equal(numpy_array, exp_array)
246  else:
247  self.assertEqual(numpy_array, exp_array)
248 
249  def assertSequenceAlmostEqual(self, first, second, places=None, msg=None,
250  delta=None):
251  """Fail if the difference between any two items in the two sequences
252  are exceed the specified number of places or delta. See
253  `assertAlmostEqual`.
254  """
255  if delta is not None and places is not None:
256  raise TypeError("specify delta or places not both")
257 
258  ftype = type(first)
259  ftypename = ftype.__name__
260  stype = type(second)
261  stypename = stype.__name__
262  if ftype != stype:
263  raise self.failureException(
264  'Sequences are of different types: %s != %s' % (
265  ftypename, stypename))
266 
267  try:
268  flen = len(first)
269  except (NotImplementedError, TypeError):
270  raise self.failureException(
271  'First %s has no length' % (ftypename))
272  try:
273  slen = len(second)
274  except (NotImplementedError, TypeError):
275  raise self.failureException(
276  'Second %s has no length' % (stypename))
277 
278  if flen != slen:
279  raise self.failureException(
280  'Sequences have non equal lengths: %d != %d' % (flen, slen))
281 
282  differing = None
283  for i in range(min(flen, slen)):
284  differing = '%ss differ: %s != %s\n' % (
285  ftypename.capitalize(), safe_repr(first),
286  safe_repr(second))
287 
288  try:
289  f = first[i]
290  except (TypeError, IndexError, NotImplementedError):
291  differing += ('\nUnable to index element %d of first %s\n' %
292  (i, ftypename))
293  break
294 
295  try:
296  s = second[i]
297  except (TypeError, IndexError, NotImplementedError):
298  differing += ('\nUnable to index element %d of second %s\n' %
299  (i, stypename))
300  break
301 
302  try:
303  self.assertAlmostEqual(
304  f, s, places=places, msg=msg, delta=delta)
305  except (TypeError, ValueError, NotImplementedError,
306  AssertionError):
307  differing += (
308  "\nFirst differing element "
309  "%d:\n%s\n%s\n") % (i, safe_repr(f), safe_repr(s))
310  break
311  else:
312  return
313 
314  standardMsg = differing
315  diffMsg = '\n' + '\n'.join(
316  difflib.ndiff(pprint.pformat(first).splitlines(),
317  pprint.pformat(second).splitlines()))
318  standardMsg = self._truncateMessage(standardMsg, diffMsg)
319  msg = self._formatMessage(msg, standardMsg)
320  raise self.failureException(msg)
321 
322  def create_point_particle(self, model, x, y, z):
323  """Make a particle with optimizable x, y and z attributes, and
324  add it to the model."""
325  p = IMP.Particle(model)
326  p.add_attribute(IMP.FloatKey("x"), x, True)
327  p.add_attribute(IMP.FloatKey("y"), y, True)
328  p.add_attribute(IMP.FloatKey("z"), z, True)
329  return p
330 
331  def probabilistic_check(self, testcall, chance_of_failure):
332  """Help handle a test which is expected to fail some fraction of
333  the time. The test is run multiple times and an exception
334  is thrown only if it fails too many times.
335  @note Use of this function should be avoided. If there is a corner
336  case that results in a test 'occasionally' failing, write a
337  new test specifically for that corner case and assert that
338  it fails consistently (and remove the corner case from the
339  old test).
340  """
341  prob = chance_of_failure
342  tries = 1
343  while prob > .001:
344  tries += 1
345  prob = prob*chance_of_failure
346  for i in range(0, tries):
347  try:
348  eval(testcall)
349  except: # noqa: E722
350  pass
351  else:
352  return
353  eval(testcall)
354  raise AssertionError("Too many failures")
355 
356  def failure_probability(self, testcall):
357  """Estimate how likely a given block of code is to raise an
358  AssertionError."""
359  failures = 0
360  tries = 0.0
361  while failures < 10 and tries < 1000:
362  try:
363  eval(testcall)
364  except: # noqa: E722
365  failures += 1
366  tries = tries+1
367  return failures/tries
368 
369  def randomize_particles(self, particles, deviation):
370  """Randomize the xyz coordinates of a list of particles"""
371  # Note: cannot use XYZ here since that pulls in IMP.core
372  xkey = IMP.FloatKey("x")
373  ykey = IMP.FloatKey("y")
374  zkey = IMP.FloatKey("z")
375  for p in particles:
376  p.set_value(xkey, random.uniform(-deviation, deviation))
377  p.set_value(ykey, random.uniform(-deviation, deviation))
378  p.set_value(zkey, random.uniform(-deviation, deviation))
379 
380  def particle_distance(self, p1, p2):
381  """Return distance between two given particles"""
382  xkey = IMP.FloatKey("x")
383  ykey = IMP.FloatKey("y")
384  zkey = IMP.FloatKey("z")
385  dx = p1.get_value(xkey) - p2.get_value(xkey)
386  dy = p1.get_value(ykey) - p2.get_value(ykey)
387  dz = p1.get_value(zkey) - p2.get_value(zkey)
388  return math.sqrt(dx*dx + dy*dy + dz*dz)
389 
390  def check_unary_function_deriv(self, func, lb, ub, step):
391  """Check the unary function func's derivatives against numerical
392  approximations between lb and ub"""
393  for f in [lb + i * step for i in range(1, int((ub-lb)/step))]:
394  (v, d) = func.evaluate_with_derivative(f)
395  da = numerical_derivative(func.evaluate, f, step / 10.)
396  self.assertAlmostEqual(d, da, delta=max(abs(.1 * d), 0.01))
397 
398  def check_unary_function_min(self, func, lb, ub, step, expected_fmin):
399  """Make sure that the minimum of the unary function func over the
400  range between lb and ub is at expected_fmin"""
401  fmin, vmin = lb, func.evaluate(lb)
402  for f in [lb + i * step for i in range(1, int((ub-lb)/step))]:
403  v = func.evaluate(f)
404  if v < vmin:
405  fmin, vmin = f, v
406  self.assertAlmostEqual(fmin, expected_fmin, delta=step)
407 
409  """Check methods that every IMP::Object class should have"""
410  obj.set_was_used(True)
411  # Test get_from static method
412  cls = type(obj)
413  self.assertIsNotNone(cls.get_from(obj))
414  self.assertRaises(ValueError, cls.get_from, IMP.Model())
415  # Test __str__ and __repr__
416  self.assertIsInstance(str(obj), str)
417  self.assertIsInstance(repr(obj), str)
418  # Test get_version_info()
419  verinf = obj.get_version_info()
420  self.assertIsInstance(verinf, IMP.VersionInfo)
421  # Test SWIG thisown flag
422  o = obj.thisown
423  obj.thisown = o
424 
425  def create_particles_in_box(self, model, num=10,
426  lb=[0, 0, 0],
427  ub=[10, 10, 10]):
428  """Create a bunch of particles in a box"""
429  import IMP.algebra
430  lbv = IMP.algebra.Vector3D(lb[0], lb[1], lb[2])
431  ubv = IMP.algebra.Vector3D(ub[0], ub[1], ub[2])
432  ps = []
433  for i in range(0, num):
435  IMP.algebra.BoundingBox3D(lbv, ubv))
436  p = self.create_point_particle(model, v[0], v[1], v[2])
437  ps.append(p)
438  return ps
439 
440  def _get_type(self, module, name):
441  return eval('type('+module+"."+name+')')
442 
443  def assertValueObjects(self, module, exceptions_list):
444  "Check that all the C++ classes in the module are values or objects."
445  all = dir(module)
446  ok = set(exceptions_list + module._value_types + module._object_types
447  + module._raii_types + module._plural_types)
448 
449  bad = []
450  for name in all:
451  if self._get_type(module.__name__, name) == type \
452  and not name.startswith("_"):
453  if name.find("SwigPyIterator") != -1:
454  continue
455  # Exclude Python-only classes
456  if not eval('hasattr(%s.%s, "__swig_destroy__")'
457  % (module.__name__, name)):
458  continue
459  if name in ok:
460  continue
461  bad.append(name)
462  self.assertEqual(
463  len(bad), 0,
464  "All IMP classes should be labeled as values or objects to get "
465  "memory management correct in Python. The following are not:\n%s\n"
466  "Please add an IMP_SWIG_OBJECT or IMP_SWIG_VALUE call to the "
467  "Python wrapper, or if the class has a good reason to be "
468  "neither, add the name to the value_object_exceptions list in "
469  "the IMPModuleTest call." % str(bad))
470  for e in exceptions_list:
471  self.assertTrue(
472  e not in module._value_types + module._object_types
473  + module._raii_types + module._plural_types,
474  "Value/Object exception "+e+" is not an exception")
475 
476  def _check_spelling(self, word, words):
477  """Check that the word is spelled correctly"""
478  if "words" not in dir(self):
479  with open(IMP.test.get_data_path("linux.words"), "r") as fh:
480  wordlist = fh.read().split("\n")
481  # why is "all" missing on my mac?
482  custom_words = ["info", "prechange", "int", "ints", "optimizeds",
483  "graphviz", "voxel", "voxels", "endian", 'rna',
484  'dna', "xyzr", "pdbs", "fft", "ccc", "gaussian"]
485  # Exclude some common alternative spellings - we want to
486  # be consistent
487  exclude_words = set(["adapter", "grey"])
488  self.words = set(wordlist+custom_words) - exclude_words
489  if self.words:
490  for i in "0123456789":
491  if i in word:
492  return True
493  if word in words:
494  return True
495  if word in self.words:
496  return True
497  else:
498  return False
499  else:
500  return True
501 
502  def assertClassNames(self, module, exceptions, words):
503  """Check that all the classes in the module follow the IMP
504  naming conventions."""
505  all = dir(module)
506  misspelled = []
507  bad = []
508  cc = re.compile("([A-Z][a-z]*)")
509  for name in all:
510  if self._get_type(module.__name__, name) == type \
511  and not name.startswith("_"):
512  if name.find("SwigPyIterator") != -1:
513  continue
514  for t in re.findall(cc, name):
515  if not self._check_spelling(t.lower(), words):
516  misspelled.append(t.lower())
517  bad.append(name)
518 
519  self.assertEqual(
520  len(bad), 0,
521  "All IMP classes should be properly spelled. The following "
522  "are not: %s.\nMisspelled words: %s. Add words to the "
523  "spelling_exceptions variable of the IMPModuleTest if needed."
524  % (str(bad), ", ".join(set(misspelled))))
525 
526  for name in all:
527  if self._get_type(module.__name__, name) == type \
528  and not name.startswith("_"):
529  if name.find("SwigPyIterator") != -1:
530  continue
531  if name.find('_') != -1:
532  bad.append(name)
533  if name.lower == name:
534  bad.append(name)
535  for t in re.findall(cc, name):
536  if not self._check_spelling(t.lower(), words):
537  print("misspelled %s in %s" % (t, name))
538  bad.append(name)
539  self.assertEqual(
540  len(bad), 0,
541  "All IMP classes should have CamelCase names. The following "
542  "do not: %s." % "\n".join(bad))
543 
544  def _check_function_name(self, prefix, name, verbs, all, exceptions, words,
545  misspelled):
546  if prefix:
547  fullname = prefix+"."+name
548  else:
549  fullname = name
550  old_exceptions = [
551  'unprotected_evaluate', "unprotected_evaluate_if_good",
552  "unprotected_evaluate_if_below",
553  'unprotected_evaluate_moved', "unprotected_evaluate_moved_if_good",
554  "unprotected_evaluate_moved_if_below",
555  "after_evaluate", "before_evaluate", "has_attribute",
556  "decorate_particle", "particle_is_instance"]
557  if name in old_exceptions:
558  return []
559  if fullname in exceptions:
560  return []
561  if name.endswith("swigregister"):
562  return []
563  if name.lower() != name:
564  if name[0].lower() != name[0] and name.split('_')[0] in all:
565  # static methods
566  return []
567  else:
568  return [fullname]
569  tokens = name.split("_")
570  if tokens[0] not in verbs:
571  return [fullname]
572  for t in tokens:
573  if not self._check_spelling(t, words):
574  misspelled.append(t)
575  print("misspelled %s in %s" % (t, name))
576  return [fullname]
577  return []
578 
579  def _static_method(self, module, prefix, name):
580  """For static methods of the form Foo.bar SWIG creates free functions
581  named Foo_bar. Exclude these from spelling checks since the method
582  Foo.bar has already been checked."""
583  if prefix is None and '_' in name:
584  modobj = eval(module)
585  cls, meth = name.split('_', 1)
586  if hasattr(modobj, cls):
587  clsobj = eval(module + '.' + cls)
588  if hasattr(clsobj, meth):
589  return True
590 
591  def _check_function_names(self, module, prefix, names, verbs, all,
592  exceptions, words, misspelled):
593  bad = []
594  for name in names:
595  typ = self._get_type(module, name)
596  if name.startswith("_") or name == "weakref_proxy":
597  continue
598  if typ in (types.BuiltinMethodType, types.MethodType) \
599  or (typ == types.FunctionType and # noqa: E721
600  not self._static_method(module, prefix, name)):
601  bad.extend(self._check_function_name(prefix, name, verbs, all,
602  exceptions, words,
603  misspelled))
604  if typ == type and "SwigPyIterator" not in name:
605  members = eval("dir("+module+"."+name+")")
606  bad.extend(self._check_function_names(module+"."+name,
607  name, members, verbs, [],
608  exceptions, words,
609  misspelled))
610  return bad
611 
612  def assertFunctionNames(self, module, exceptions, words):
613  """Check that all the functions in the module follow the IMP
614  naming conventions."""
615  all = dir(module)
616  verbs = set(["add", "remove", "get", "set", "evaluate", "compute",
617  "show", "create", "destroy", "push", "pop", "write",
618  "read", "do", "show", "load", "save", "reset", "accept",
619  "reject", "clear", "handle", "update", "apply",
620  "optimize", "reserve", "dump", "propose", "setup",
621  "teardown", "visit", "find", "run", "swap", "link",
622  "validate"])
623  misspelled = []
624  bad = self._check_function_names(module.__name__, None, all, verbs,
625  all, exceptions, words, misspelled)
626  message = ("All IMP methods and functions should have lower case "
627  "names separated by underscores and beginning with a "
628  "verb, preferably one of ['add', 'remove', 'get', 'set', "
629  "'create', 'destroy']. Each of the words should be a "
630  "properly spelled English word. The following do not "
631  "(given our limited list of verbs that we check for):\n"
632  "%(bad)s\nIf there is a good reason for them not to "
633  "(eg it does start with a verb, just one with a meaning "
634  "that is not covered by the normal list), add them to the "
635  "function_name_exceptions variable in the "
636  "standards_exceptions file. Otherwise, please fix. "
637  "The current verb list is %(verbs)s"
638  % {"bad": "\n".join(bad), "verbs": verbs})
639  if len(misspelled) > 0:
640  message += "\nMisspelled words: " + ", ".join(set(misspelled)) \
641  + ". Add words to the spelling_exceptions variable " \
642  + "of the standards_exceptions file if needed."
643  self.assertEqual(len(bad), 0, message)
644 
645  def assertShow(self, modulename, exceptions):
646  """Check that all the classes in modulename have a show method"""
647  all = dir(modulename)
648  not_found = []
649  for f in all:
650  # Exclude SWIG C global variables object
651  if f == 'cvar':
652  continue
653  # Exclude Python-only classes; they are all showable
654  if not eval('hasattr(%s.%s, "__swig_destroy__")'
655  % (modulename.__name__, f)):
656  continue
657  if self._get_type(modulename.__name__, f) == type \
658  and not f.startswith("_") \
659  and not f.endswith("_swigregister")\
660  and f not in exceptions\
661  and not f.endswith("Temp") and not f.endswith("Iterator")\
662  and not f.endswith("Exception") and\
663  f not in modulename._raii_types and \
664  f not in modulename._plural_types:
665  if not hasattr(getattr(modulename, f), 'show'):
666  not_found.append(f)
667  self.assertEqual(
668  len(not_found), 0,
669  "All IMP classes should support show and __str__. The following "
670  "do not:\n%s\n If there is a good reason for them not to, add "
671  "them to the show_exceptions variable in the IMPModuleTest "
672  "call. Otherwise, please fix." % "\n".join(not_found))
673  for e in exceptions:
674  self.assertIn(e, all,
675  "Show exception "+e+" is not a class in module")
676  self.assertTrue(not hasattr(getattr(modulename, e), 'show'),
677  "Exception "+e+" is not really a show exception")
678 
679  def run_example(self, filename):
680  """Run the named example script.
681  @return a dictionary of all the script's global variables.
682  This can be queried in a test case to make sure
683  the example performed correctly."""
684  class _FatalError(Exception):
685  pass
686 
687  # Add directory containing the example to sys.path, so it can import
688  # other Python modules in the same directory
689  path, name = os.path.split(filename)
690  oldsyspath = sys.path[:]
691  olssysargv = sys.argv[:]
692  sys.path.insert(0, path)
693  sys.argv = [filename]
694  vars = {}
695  try:
696  try:
697  exec(open(filename).read(), vars)
698  # Catch sys.exit() called from within the example; a non-zero exit
699  # value should cause the test case to fail
700  except SystemExit as e:
701  if e.code != 0 and e.code is not None:
702  raise _FatalError(
703  "Example exit with code %s" % str(e.code))
704  finally:
705  # Restore sys.path (note that Python 2.3 does not allow
706  # try/except/finally, so we need to use nested trys)
707  sys.path = oldsyspath
708  sys.argv = olssysargv
709 
710  return _ExecDictProxy(vars)
711 
712  def run_python_module(self, module, args):
713  """Run a Python module as if with "python -m <modname>",
714  with the given list of arguments as sys.argv.
715 
716  If module is an already-imported Python module, run its 'main'
717  function and return the result.
718 
719  If module is a string, run the module in a subprocess and return
720  a subprocess.Popen-like object containing the child stdin,
721  stdout and stderr.
722  """
723  def mock_setup_from_argv(*args, **kwargs):
724  # do-nothing replacement for boost command line parser
725  pass
726  if type(module) == type(os):
727  mod = module
728  else:
729  mod = __import__(module, {}, {}, [''])
730  modpath = mod.__file__
731  if modpath.endswith('.pyc'):
732  modpath = modpath[:-1]
733  if type(module) == type(os):
734  old_sys_argv = sys.argv
735  # boost parser doesn't like being called multiple times per process
736  old_setup = IMP.setup_from_argv
737  IMP.setup_from_argv = mock_setup_from_argv
738  try:
739  sys.argv = [modpath] + args
740  return module.main()
741  finally:
742  IMP.setup_from_argv = old_setup
743  sys.argv = old_sys_argv
744  else:
745  return _SubprocessWrapper(sys.executable, [modpath] + args)
746 
747  def check_runnable_python_module(self, module):
748  """Check a Python module designed to be runnable with 'python -m'
749  to make sure it supports standard command line options."""
750  # --help should return with exit 0, no errors
751  r = self.run_python_module(module, ['--help'])
752  out, err = r.communicate()
753  self.assertEqual(r.returncode, 0)
754  self.assertNotEqual(err, "")
755  self.assertEqual(out, "")
756 
757 
758 class _ExecDictProxy(object):
759  """exec returns a Python dictionary, which contains IMP objects, other
760  Python objects, as well as base Python modules (such as sys and
761  __builtins__). If we just delete this dictionary, it is entirely
762  possible that base Python modules are removed from the dictionary
763  *before* some IMP objects. This will prevent the IMP objects' Python
764  destructors from running properly, so C++ objects will not be
765  cleaned up. This class proxies the base dict class, and on deletion
766  attempts to remove keys from the dictionary in an order that allows
767  IMP destructors to fire."""
768  def __init__(self, d):
769  self._d = d
770 
771  def __del__(self):
772  # Try to release example objects in a sensible order
773  module_type = type(IMP)
774  d = self._d
775  for k in d.keys():
776  if type(d[k]) != module_type:
777  del d[k]
778 
779  for meth in ['__contains__', '__getitem__', '__iter__', '__len__',
780  'get', 'has_key', 'items', 'keys', 'values']:
781  exec("def %s(self, *args, **keys): "
782  "return self._d.%s(*args, **keys)" % (meth, meth))
783 
784 
785 class _TestResult(unittest.TextTestResult):
786 
787  def __init__(self, stream=None, descriptions=None, verbosity=None):
788  super(_TestResult, self).__init__(stream, descriptions, verbosity)
789  self.all_tests = []
790 
791  def stopTestRun(self):
792  if 'IMP_TEST_DETAIL_DIR' in os.environ:
793  fname = (Path(os.environ['IMP_TEST_DETAIL_DIR'])
794  / Path(sys.argv[0]).name)
795  with open(str(fname), 'wb') as fh:
796  pickle.dump(self.all_tests, fh, -1)
797  super(_TestResult, self).stopTestRun()
798 
799  def startTest(self, test):
800  super(_TestResult, self).startTest(test)
801  test.start_time = datetime.datetime.now()
802 
803  def _test_finished(self, test, state, detail=None):
804  delta = datetime.datetime.now() - test.start_time
805  try:
806  pv = delta.total_seconds()
807  except AttributeError:
808  pv = (float(delta.microseconds)
809  + (delta.seconds + delta.days * 24 * 3600) * 10**6) / 10**6
810  if pv > 1:
811  self.stream.write("in %.3fs ... " % pv)
812  if detail is not None and not isinstance(detail, str):
813  detail = self._exc_info_to_string(detail, test)
814  test_doc = self.getDescription(test)
815  test_name = test.id()
816  if test_name.startswith('__main__.'):
817  test_name = test_name[9:]
818  self.all_tests.append({'name': test_name,
819  'docstring': test_doc,
820  'time': pv, 'state': state, 'detail': detail})
821 
822  def addSuccess(self, test):
823  self._test_finished(test, 'OK')
824  super(_TestResult, self).addSuccess(test)
825 
826  def addError(self, test, err):
827  self._test_finished(test, 'ERROR', err)
828  super(_TestResult, self).addError(test, err)
829 
830  def addFailure(self, test, err):
831  self._test_finished(test, 'FAIL', err)
832  super(_TestResult, self).addFailure(test, err)
833 
834  def addSkip(self, test, reason):
835  self._test_finished(test, 'SKIP', reason)
836  super(_TestResult, self).addSkip(test, reason)
837 
838  def addExpectedFailure(self, test, err):
839  self._test_finished(test, 'EXPFAIL', err)
840  super(_TestResult, self).addExpectedFailure(test, err)
841 
842  def addUnexpectedSuccess(self, test):
843  self._test_finished(test, 'UNEXPSUC')
844  super(_TestResult, self).addUnexpectedSuccess(test)
845 
846  def getDescription(self, test):
847  doc_first_line = test.shortDescription()
848  if self.descriptions and doc_first_line:
849  return doc_first_line
850  else:
851  return str(test)
852 
853 
854 class _TestRunner(unittest.TextTestRunner):
855  def _makeResult(self):
856  return _TestResult(self.stream, self.descriptions, self.verbosity)
857 
858 
859 def main(*args, **keys):
860  """Run a set of tests; similar to unittest.main().
861  Obviates the need to separately import the 'unittest' module, and
862  ensures that main() is from the same unittest module that the
863  IMP.test testcases are. In addition, turns on some extra checks
864  (e.g. trying to use deprecated code will cause an exception
865  to be thrown)."""
866  import IMP
868  return unittest.main(testRunner=_TestRunner, *args, **keys)
869 
870 
871 class _SubprocessWrapper(subprocess.Popen):
872  def __init__(self, app, args, cwd=None):
873  # For (non-Python) applications to work on Windows, the
874  # PATH must include the directory containing built DLLs
875  if sys.platform == 'win32' and app != sys.executable:
876  # Hack to find the location of build/lib/
877  libdir = os.environ['PYTHONPATH'].split(';')[0]
878  env = os.environ.copy()
879  env['PATH'] += ';' + libdir
880  else:
881  env = None
882  subprocess.Popen.__init__(self, [app]+list(args),
883  stdin=subprocess.PIPE,
884  stdout=subprocess.PIPE,
885  stderr=subprocess.PIPE, env=env, cwd=cwd,
886  universal_newlines=True)
887 
888 
890  """Super class for simple IMP application test cases"""
891  def _get_application_file_name(self, filename):
892  # If we ran from run-all-tests.py, it set an env variable for us with
893  # the top-level test directory
894  if sys.platform == 'win32':
895  filename += '.exe'
896  return filename
897 
898  def run_application(self, app, args, cwd=None):
899  """Run an application with the given list of arguments.
900  @return a subprocess.Popen-like object containing the child stdin,
901  stdout and stderr.
902  """
903  filename = self._get_application_file_name(app)
904  if sys.platform == 'win32':
905  # Cannot rely on PATH on wine builds, so use full pathname
906  return _SubprocessWrapper(os.path.join(os.environ['IMP_BIN_DIR'],
907  filename), args, cwd=cwd)
908  else:
909  return _SubprocessWrapper(filename, args, cwd=cwd)
910 
911  def run_python_application(self, app, args):
912  """Run a Python application with the given list of arguments.
913  The Python application should be self-runnable (i.e. it should
914  be executable and with a #! on the first line).
915  @return a subprocess.Popen-like object containing the child stdin,
916  stdout and stderr.
917  """
918  # Handle platforms where /usr/bin/python doesn't work
919  if sys.executable != '/usr/bin/python' and 'IMP_BIN_DIR' in os.environ:
920  return _SubprocessWrapper(
921  sys.executable,
922  [os.path.join(os.environ['IMP_BIN_DIR'], app)] + args)
923  else:
924  return _SubprocessWrapper(app, args)
925 
927  """Import an installed Python application, rather than running it.
928  This is useful to directly test components of the application.
929  @return the Python module object."""
930  try:
931  import importlib.machinery
932  imp = None
933  except ImportError:
934  import imp
935  name = os.path.splitext(app)[0]
936  pathname = os.path.join(os.environ['IMP_BIN_DIR'], app)
937  if imp is None:
938  return importlib.machinery.SourceFileLoader(name,
939  pathname).load_module()
940  else:
941  return imp.load_source(name, pathname)
942 
943  def run_script(self, app, args):
944  """Run an application with the given list of arguments.
945  @return a subprocess.Popen-like object containing the child stdin,
946  stdout and stderr.
947  """
948  return _SubprocessWrapper(sys.executable, [app]+args)
949 
950  def assertApplicationExitedCleanly(self, ret, error):
951  """Assert that the application exited cleanly (return value = 0)."""
952  if ret < 0:
953  raise OSError("Application exited with signal %d\n" % -ret
954  + error)
955  else:
956  self.assertEqual(
957  ret, 0,
958  "Application exited uncleanly, with exit code %d\n" % ret
959  + error)
960 
961  def read_shell_commands(self, doxfile):
962  """Read and return a set of shell commands from a doxygen file.
963  Each command is assumed to be in a \code{.sh}...\endcode block.
964  The doxygen file is specified relative to the test file itself.
965  This is used to make sure the commands shown in an application
966  example actually work (the testcase can also check the resulting
967  files for correctness)."""
968  def win32_normpath(p):
969  # Sometimes Windows can read Unix-style paths, but sometimes it
970  # gets confused... so normalize all paths to be sure
971  return " ".join([os.path.normpath(x) for x in p.split()])
972 
973  def fix_win32_command(cmd):
974  # Make substitutions so a Unix shell command works on Windows
975  if cmd.startswith('cp -r '):
976  return 'xcopy /E ' + win32_normpath(cmd[6:])
977  elif cmd.startswith('cp '):
978  return 'copy ' + win32_normpath(cmd[3:])
979  else:
980  return cmd
981  d = os.path.dirname(sys.argv[0])
982  doc = os.path.join(d, doxfile)
983  inline = False
984  cmds = []
985  example_path = os.path.abspath(IMP.get_example_path('..'))
986  with open(doc) as fh:
987  for line in fh.readlines():
988  if '\code{.sh}' in line:
989  inline = True
990  elif '\endcode' in line:
991  inline = False
992  elif inline:
993  cmds.append(line.rstrip('\r\n').replace(
994  '<imp_example_path>', example_path))
995  if sys.platform == 'win32':
996  cmds = [fix_win32_command(x) for x in cmds]
997  return cmds
998 
999  def run_shell_command(self, cmd):
1000  "Print and run a shell command, as returned by read_shell_commands()"
1001  print(cmd)
1002  p = subprocess.call(cmd, shell=True)
1003  if p != 0:
1004  raise OSError("%s failed with exit value %d" % (cmd, p))
1005 
1006 
1007 class RefCountChecker(object):
1008  """Check to make sure the number of C++ object references is as expected"""
1009 
1010  def __init__(self, testcase):
1011  # Make sure no director objects are hanging around; otherwise these
1012  # may be unexpectedly garbage collected later, decreasing the
1013  # live object count
1014  IMP._director_objects.cleanup()
1015  self.__testcase = testcase
1016  if IMP.get_check_level() >= IMP.USAGE_AND_INTERNAL:
1017  self.__basenum = IMP.Object.get_number_of_live_objects()
1018  self.__names = IMP.get_live_object_names()
1019 
1020  def assert_number(self, expected):
1021  "Make sure that the number of references matches the expected value."
1022  t = self.__testcase
1023  IMP._director_objects.cleanup()
1024  if IMP.get_check_level() >= IMP.USAGE_AND_INTERNAL:
1025  newnames = [x for x in IMP.get_live_object_names()
1026  if x not in self.__names]
1027  newnum = IMP.Object.get_number_of_live_objects()-self.__basenum
1028  t.assertEqual(newnum, expected,
1029  "Number of objects don't match: "
1030  + str(newnum) + " != " + str(expected) + " found "
1031  + str(newnames))
1032 
1033 
1035  """Check to make sure the number of director references is as expected"""
1036 
1037  def __init__(self, testcase):
1038  IMP._director_objects.cleanup()
1039  self.__testcase = testcase
1040  self.__basenum = IMP._director_objects.get_object_count()
1041 
1042  def assert_number(self, expected, force_cleanup=True):
1043  """Make sure that the number of references matches the expected value.
1044  If force_cleanup is set, clean up any unused references first before
1045  doing the assertion.
1046  """
1047  t = self.__testcase
1048  if force_cleanup:
1049  IMP._director_objects.cleanup()
1050  t.assertEqual(IMP._director_objects.get_object_count()
1051  - self.__basenum, expected)
1052 
1053 
1054 # Make sure that the IMP binary directory (build/bin) is in the PATH, if
1055 # we're running under wine (the imppy.sh script normally ensures this, but
1056 # wine overrides the PATH). This is needed so that tests of imported Python
1057 # applications can successfully spawn C++ applications (e.g. idock.py tries
1058 # to run recompute_zscore.exe). build/lib also needs to be in the PATH, since
1059 # that's how Windows locates dependent DLLs such as libimp.dll.
1060 if sys.platform == 'win32' and 'PYTHONPATH' in os.environ \
1061  and 'IMP_BIN_DIR' in os.environ:
1062  libdir = os.environ['PYTHONPATH'].split(';')[0]
1063  bindir = os.environ['IMP_BIN_DIR']
1064  path = os.environ['PATH']
1065  if libdir not in path or bindir not in path:
1066  os.environ['PATH'] = bindir + ';' + libdir + ';' + path
1067 
1068 
1069 __version__ = "2.17.0"
1070 
1072  '''Return the version of this module, as a string'''
1073  return "2.17.0"
1074 
1075 def get_module_name():
1076  '''Return the fully-qualified name of this module'''
1077  return "IMP::test"
1078 
1079 def get_data_path(fname):
1080  '''Return the full path to one of this module's data files'''
1081  import IMP
1082  return IMP._get_module_data_path("test", fname)
1083 
1084 def get_example_path(fname):
1085  '''Return the full path to one of this module's example files'''
1086  import IMP
1087  return IMP._get_module_example_path("test", fname)
def run_python_module
Run a Python module as if with "python -m <modname>", with the given list of arguments as sys...
def temporary_working_directory
Simple context manager to run in a temporary directory.
def assertApplicationExitedCleanly
Assert that the application exited cleanly (return value = 0).
CheckLevel get_check_level()
Get the current audit mode.
Definition: exception.h:80
def import_python_application
Import an installed Python application, rather than running it.
def open_input_file
Open and return an input file in the top-level test directory.
def run_application
Run an application with the given list of arguments.
def randomize_particles
Randomize the xyz coordinates of a list of particles.
def get_module_version
Return the version of this module, as a string.
A general exception for an internal error in IMP.
Definition: exception.h:101
def main
Run a set of tests; similar to unittest.main().
An exception for an invalid usage of IMP.
Definition: exception.h:122
Super class for simple IMP application test cases.
def assertRaisesInternalException
Assert that the given callable object raises InternalException.
def assert_number
Make sure that the number of references matches the expected value.
Check to make sure the number of director references is as expected.
def assertShow
Check that all the classes in modulename have a show method.
def get_data_path
Return the full path to one of this module's data files.
def get_example_path
Return the full path to one of this module's example files.
def run_shell_command
Print and run a shell command, as returned by read_shell_commands()
def assertRaisesUsageException
Assert that the given callable object raises UsageException.
Vector3D get_random_vector_in(const Cylinder3D &c)
Generate a random vector in a cylinder with uniform density.
def assertSequenceAlmostEqual
Fail if the difference between any two items in the two sequences are exceed the specified number of ...
Class for storing model, its restraints, constraints, and particles.
Definition: Model.h:73
def run_python_application
Run a Python application with the given list of arguments.
def assert_number
Make sure that the number of references matches the expected value.
Strings get_live_object_names()
Return the names of all live objects.
def check_standard_object_methods
Check methods that every IMP::Object class should have.
def particle_distance
Return distance between two given particles.
def check_unary_function_deriv
Check the unary function func's derivatives against numerical approximations between lb and ub...
def get_tmp_file_name
Get the full name of an output file in the tmp directory.
Version and module information for Objects.
Definition: VersionInfo.h:28
def run_example
Run the named example script.
def get_magnitude
Get the magnitude of a list of floats.
void set_deprecation_exceptions(bool tf)
Toggle whether an exception is thrown when a deprecated method is used.
def check_unary_function_min
Make sure that the minimum of the unary function func over the range between lb and ub is at expected...
def probabilistic_check
Help handle a test which is expected to fail some fraction of the time.
def create_particles_in_box
Create a bunch of particles in a box.
General purpose algebraic and geometric methods that are expected to be used by a wide variety of IMP...
def assertNotImplemented
Assert that the given callable object is not implemented.
The general base class for IMP exceptions.
Definition: exception.h:48
def numerical_derivative
Calculate the derivative of the single-value function func at point val.
std::string get_example_path(std::string file_name)
Return the full path to one of this module's example files.
def xyz_numerical_derivatives
Calculate the x,y and z derivatives of the scoring function sf on the xyz particle.
def failure_probability
Estimate how likely a given block of code is to raise an AssertionError.
VectorD< 3 > Vector3D
Definition: VectorD.h:421
def assertClassNames
Check that all the classes in the module follow the IMP naming conventions.
def create_point_particle
Make a particle with optimizable x, y and z attributes, and add it to the model.
Class to handle individual particles of a Model object.
Definition: Particle.h:41
def read_shell_commands
Read and return a set of shell commands from a doxygen file.
Check to make sure the number of C++ object references is as expected.
def assertFunctionNames
Check that all the functions in the module follow the IMP naming conventions.
def assertValueObjects
Check that all the C++ classes in the module are values or objects.
def assertNumPyArrayEqual
Fail if the given numpy array doesn't match expected.
def get_module_name
Return the fully-qualified name of this module.
Super class for IMP test cases.
def assertXYZDerivativesInTolerance
Assert that x,y,z analytical derivatives match numerical within a tolerance, or a percentage (of the ...
def temporary_directory
Simple context manager to make a temporary directory.
def run_script
Run an application with the given list of arguments.
def get_input_file_name
Get the full name of an input file in the top-level test directory.
def check_runnable_python_module
Check a Python module designed to be runnable with 'python -m' to make sure it supports standard comm...
void set_check_level(CheckLevel tf)
Control runtime checks in the code.
Definition: exception.h:72