IMP logo
IMP Reference Guide  develop.27926d84dc,2024/04/16
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)**.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  sf.evaluate(True)
225  derivs = xyz.get_derivatives()
226  num_derivs = xyz_numerical_derivatives(sf, xyz, 0.01)
227  pct = percentage / 100.0
228  self.assertAlmostEqual(
229  self.get_magnitude(derivs-num_derivs), 0,
230  delta=tolerance+percentage*self.get_magnitude(num_derivs),
231  msg="Don't match "+str(derivs) + str(num_derivs))
232  self.assertAlmostEqual(derivs[0], num_derivs[0],
233  delta=max(tolerance, abs(derivs[0]) * pct))
234  self.assertAlmostEqual(derivs[1], num_derivs[1],
235  delta=max(tolerance, abs(derivs[1]) * pct))
236  self.assertAlmostEqual(derivs[2], num_derivs[2],
237  delta=max(tolerance, abs(derivs[2]) * pct))
238 
239  def assertNumPyArrayEqual(self, numpy_array, exp_array):
240  """Fail if the given numpy array doesn't match expected"""
241  if IMP.IMP_KERNEL_HAS_NUMPY:
242  import numpy.testing
243  self.assertIsInstance(numpy_array, numpy.ndarray)
244  numpy.testing.assert_array_equal(numpy_array, exp_array)
245  else:
246  self.assertEqual(numpy_array, exp_array)
247 
248  def assertSequenceAlmostEqual(self, first, second, places=None, msg=None,
249  delta=None):
250  """Fail if the difference between any two items in the two sequences
251  are exceed the specified number of places or delta. See
252  `assertAlmostEqual`.
253  """
254  if delta is not None and places is not None:
255  raise TypeError("specify delta or places not both")
256 
257  ftype = type(first)
258  ftypename = ftype.__name__
259  stype = type(second)
260  stypename = stype.__name__
261  if ftype != stype:
262  raise self.failureException(
263  'Sequences are of different types: %s != %s' % (
264  ftypename, stypename))
265 
266  try:
267  flen = len(first)
268  except (NotImplementedError, TypeError):
269  raise self.failureException(
270  'First %s has no length' % (ftypename))
271  try:
272  slen = len(second)
273  except (NotImplementedError, TypeError):
274  raise self.failureException(
275  'Second %s has no length' % (stypename))
276 
277  if flen != slen:
278  raise self.failureException(
279  'Sequences have non equal lengths: %d != %d' % (flen, slen))
280 
281  differing = None
282  for i in range(min(flen, slen)):
283  differing = '%ss differ: %s != %s\n' % (
284  ftypename.capitalize(), safe_repr(first),
285  safe_repr(second))
286 
287  try:
288  f = first[i]
289  except (TypeError, IndexError, NotImplementedError):
290  differing += ('\nUnable to index element %d of first %s\n' %
291  (i, ftypename))
292  break
293 
294  try:
295  s = second[i]
296  except (TypeError, IndexError, NotImplementedError):
297  differing += ('\nUnable to index element %d of second %s\n' %
298  (i, stypename))
299  break
300 
301  try:
302  self.assertAlmostEqual(
303  f, s, places=places, msg=msg, delta=delta)
304  except (TypeError, ValueError, NotImplementedError,
305  AssertionError):
306  differing += (
307  "\nFirst differing element "
308  "%d:\n%s\n%s\n") % (i, safe_repr(f), safe_repr(s))
309  break
310  else:
311  return
312 
313  standardMsg = differing
314  diffMsg = '\n' + '\n'.join(
315  difflib.ndiff(pprint.pformat(first).splitlines(),
316  pprint.pformat(second).splitlines()))
317  standardMsg = self._truncateMessage(standardMsg, diffMsg)
318  msg = self._formatMessage(msg, standardMsg)
319  raise self.failureException(msg)
320 
321  def _read_cmake_cfg(self, cmake_cfg):
322  """Parse IMPConfig.cmake and extract info on the C++ compiler"""
323  cxx = flags = sysroot = None
324  includes = []
325  with open(cmake_cfg) as fh:
326  for line in fh:
327  if line.startswith('set(IMP_CXX_COMPILER '):
328  cxx = line.split('"')[1]
329  elif line.startswith('set(IMP_CXX_FLAGS '):
330  flags = line.split('"')[1]
331  elif line.startswith('set(IMP_OSX_SYSROOT '):
332  sysroot = line.split('"')[1]
333  elif line.startswith('SET(Boost_INCLUDE_DIR '):
334  includes.append(line.split('"')[1])
335  elif line.startswith('SET(EIGEN3_INCLUDE_DIR '):
336  includes.append(line.split('"')[1])
337  elif line.startswith('SET(cereal_INCLUDE_DIRS '):
338  includes.append(line.split('"')[1])
339  return cxx, flags, includes, sysroot
340 
341  def assertCompileFails(self, headers, body):
342  """Test that the given C++ code fails to compile with a static
343  assertion."""
344  if sys.platform == 'win32':
345  self.skipTest("No support for Windows yet")
346  libdir = os.path.dirname(IMP.__file__)
347  cmake_cfg = os.path.join(libdir, '..', '..', 'IMPConfig.cmake')
348  if not os.path.exists(cmake_cfg):
349  self.skipTest("cannot find IMPConfig.cmake")
350  cxx, flags, includes, sysroot = self._read_cmake_cfg(cmake_cfg)
351  # On Mac we need to point to the SDK
352  if sys.platform == 'darwin' and sysroot:
353  flags = flags + " -isysroot" + sysroot
354  includes.append(os.path.join(libdir, '..', '..', 'include'))
355  include = " ".join("-I" + inc for inc in includes)
356  with temporary_directory() as tmpdir:
357  fname = os.path.join(tmpdir, 'test.cpp')
358  with open(fname, 'w') as fh:
359  for h in headers:
360  fh.write("#include <%s>\n" % h)
361  fh.write("\nint main() {\n" + body + "\n return 0;\n}\n")
362  cmdline = "%s %s %s %s" % (cxx, flags, include, fname)
363  print(cmdline)
364  p = subprocess.Popen(cmdline, shell=True,
365  stdout=subprocess.PIPE,
366  stderr=subprocess.PIPE,
367  universal_newlines=True)
368  out, err = p.communicate()
369  self.assertIn('error: static assertion failed', err)
370 
371  def create_point_particle(self, model, x, y, z):
372  """Make a particle with optimizable x, y and z attributes, and
373  add it to the model."""
374  p = IMP.Particle(model)
375  p.add_attribute(IMP.FloatKey("x"), x, True)
376  p.add_attribute(IMP.FloatKey("y"), y, True)
377  p.add_attribute(IMP.FloatKey("z"), z, True)
378  return p
379 
380  def probabilistic_check(self, testcall, chance_of_failure):
381  """Help handle a test which is expected to fail some fraction of
382  the time. The test is run multiple times and an exception
383  is thrown only if it fails too many times.
384  @note Use of this function should be avoided. If there is a corner
385  case that results in a test 'occasionally' failing, write a
386  new test specifically for that corner case and assert that
387  it fails consistently (and remove the corner case from the
388  old test).
389  """
390  prob = chance_of_failure
391  tries = 1
392  while prob > .001:
393  tries += 1
394  prob = prob*chance_of_failure
395  for i in range(0, tries):
396  try:
397  eval(testcall)
398  except: # noqa: E722
399  pass
400  else:
401  return
402  eval(testcall)
403  raise AssertionError("Too many failures")
404 
405  def failure_probability(self, testcall):
406  """Estimate how likely a given block of code is to raise an
407  AssertionError."""
408  failures = 0
409  tries = 0.0
410  while failures < 10 and tries < 1000:
411  try:
412  eval(testcall)
413  except: # noqa: E722
414  failures += 1
415  tries = tries+1
416  return failures/tries
417 
418  def randomize_particles(self, particles, deviation):
419  """Randomize the xyz coordinates of a list of particles"""
420  # Note: cannot use XYZ here since that pulls in IMP.core
421  xkey = IMP.FloatKey("x")
422  ykey = IMP.FloatKey("y")
423  zkey = IMP.FloatKey("z")
424  for p in particles:
425  p.set_value(xkey, random.uniform(-deviation, deviation))
426  p.set_value(ykey, random.uniform(-deviation, deviation))
427  p.set_value(zkey, random.uniform(-deviation, deviation))
428 
429  def particle_distance(self, p1, p2):
430  """Return distance between two given particles"""
431  xkey = IMP.FloatKey("x")
432  ykey = IMP.FloatKey("y")
433  zkey = IMP.FloatKey("z")
434  dx = p1.get_value(xkey) - p2.get_value(xkey)
435  dy = p1.get_value(ykey) - p2.get_value(ykey)
436  dz = p1.get_value(zkey) - p2.get_value(zkey)
437  return math.sqrt(dx*dx + dy*dy + dz*dz)
438 
439  def check_unary_function_deriv(self, func, lb, ub, step):
440  """Check the unary function func's derivatives against numerical
441  approximations between lb and ub"""
442  for f in [lb + i * step for i in range(1, int((ub-lb)/step))]:
443  (v, d) = func.evaluate_with_derivative(f)
444  da = numerical_derivative(func.evaluate, f, step / 10.)
445  self.assertAlmostEqual(d, da, delta=max(abs(.1 * d), 0.01))
446 
447  def check_unary_function_min(self, func, lb, ub, step, expected_fmin):
448  """Make sure that the minimum of the unary function func over the
449  range between lb and ub is at expected_fmin"""
450  fmin, vmin = lb, func.evaluate(lb)
451  for f in [lb + i * step for i in range(1, int((ub-lb)/step))]:
452  v = func.evaluate(f)
453  if v < vmin:
454  fmin, vmin = f, v
455  self.assertAlmostEqual(fmin, expected_fmin, delta=step)
456 
458  """Check methods that every IMP::Object class should have"""
459  obj.set_was_used(True)
460  # Test get_from static method
461  cls = type(obj)
462  self.assertIsNotNone(cls.get_from(obj))
463  self.assertRaises(ValueError, cls.get_from, IMP.Model())
464  # Test __str__ and __repr__
465  self.assertIsInstance(str(obj), str)
466  self.assertIsInstance(repr(obj), str)
467  # Test get_version_info()
468  verinf = obj.get_version_info()
469  self.assertIsInstance(verinf, IMP.VersionInfo)
470  # Test SWIG thisown flag
471  o = obj.thisown
472  obj.thisown = o
473 
474  def create_particles_in_box(self, model, num=10,
475  lb=[0, 0, 0],
476  ub=[10, 10, 10]):
477  """Create a bunch of particles in a box"""
478  import IMP.algebra
479  lbv = IMP.algebra.Vector3D(lb[0], lb[1], lb[2])
480  ubv = IMP.algebra.Vector3D(ub[0], ub[1], ub[2])
481  ps = []
482  for i in range(0, num):
484  IMP.algebra.BoundingBox3D(lbv, ubv))
485  p = self.create_point_particle(model, v[0], v[1], v[2])
486  ps.append(p)
487  return ps
488 
489  def _get_type(self, module, name):
490  return eval('type('+module+"."+name+')')
491 
492  def assertValueObjects(self, module, exceptions_list):
493  "Check that all the C++ classes in the module are values or objects."
494  all = dir(module)
495  ok = set(exceptions_list + module._value_types + module._object_types
496  + module._raii_types + module._plural_types)
497 
498  bad = []
499  for name in all:
500  if self._get_type(module.__name__, name) == type \
501  and not name.startswith("_"):
502  if name.find("SwigPyIterator") != -1:
503  continue
504  # Exclude Python-only classes
505  if not eval('hasattr(%s.%s, "__swig_destroy__")'
506  % (module.__name__, name)):
507  continue
508  if name in ok:
509  continue
510  bad.append(name)
511  self.assertEqual(
512  len(bad), 0,
513  "All IMP classes should be labeled as values or objects to get "
514  "memory management correct in Python. The following are not:\n%s\n"
515  "Please add an IMP_SWIG_OBJECT or IMP_SWIG_VALUE call to the "
516  "Python wrapper, or if the class has a good reason to be "
517  "neither, add the name to the value_object_exceptions list in "
518  "the IMPModuleTest call." % str(bad))
519  for e in exceptions_list:
520  self.assertTrue(
521  e not in module._value_types + module._object_types
522  + module._raii_types + module._plural_types,
523  "Value/Object exception "+e+" is not an exception")
524 
525  def _check_spelling(self, word, words):
526  """Check that the word is spelled correctly"""
527  if "words" not in dir(self):
528  with open(IMP.test.get_data_path("linux.words"), "r") as fh:
529  wordlist = fh.read().split("\n")
530  # why is "all" missing on my mac?
531  custom_words = ["info", "prechange", "int", "ints", "optimizeds",
532  "graphviz", "voxel", "voxels", "endian", 'rna',
533  'dna', "xyzr", "pdbs", "fft", "ccc", "gaussian"]
534  # Exclude some common alternative spellings - we want to
535  # be consistent
536  exclude_words = set(["adapter", "grey"])
537  self.words = set(wordlist+custom_words) - exclude_words
538  if self.words:
539  for i in "0123456789":
540  if i in word:
541  return True
542  if word in words:
543  return True
544  if word in self.words:
545  return True
546  else:
547  return False
548  else:
549  return True
550 
551  def assertClassNames(self, module, exceptions, words):
552  """Check that all the classes in the module follow the IMP
553  naming conventions."""
554  all = dir(module)
555  misspelled = []
556  bad = []
557  cc = re.compile("([A-Z][a-z]*)")
558  for name in all:
559  if self._get_type(module.__name__, name) == type \
560  and not name.startswith("_"):
561  if name.find("SwigPyIterator") != -1:
562  continue
563  for t in re.findall(cc, name):
564  if not self._check_spelling(t.lower(), words):
565  misspelled.append(t.lower())
566  bad.append(name)
567 
568  self.assertEqual(
569  len(bad), 0,
570  "All IMP classes should be properly spelled. The following "
571  "are not: %s.\nMisspelled words: %s. Add words to the "
572  "spelling_exceptions variable of the IMPModuleTest if needed."
573  % (str(bad), ", ".join(set(misspelled))))
574 
575  for name in all:
576  if self._get_type(module.__name__, name) == type \
577  and not name.startswith("_"):
578  if name.find("SwigPyIterator") != -1:
579  continue
580  if name.find('_') != -1:
581  bad.append(name)
582  if name.lower == name:
583  bad.append(name)
584  for t in re.findall(cc, name):
585  if not self._check_spelling(t.lower(), words):
586  print("misspelled %s in %s" % (t, name))
587  bad.append(name)
588  self.assertEqual(
589  len(bad), 0,
590  "All IMP classes should have CamelCase names. The following "
591  "do not: %s." % "\n".join(bad))
592 
593  def _check_function_name(self, prefix, name, verbs, all, exceptions, words,
594  misspelled):
595  if prefix:
596  fullname = prefix+"."+name
597  else:
598  fullname = name
599  old_exceptions = [
600  'unprotected_evaluate', "unprotected_evaluate_if_good",
601  "unprotected_evaluate_if_below",
602  'unprotected_evaluate_moved', "unprotected_evaluate_moved_if_good",
603  "unprotected_evaluate_moved_if_below",
604  "after_evaluate", "before_evaluate", "has_attribute",
605  "decorate_particle", "particle_is_instance"]
606  if name in old_exceptions:
607  return []
608  if fullname in exceptions:
609  return []
610  if name.endswith("swigregister"):
611  return []
612  if name.lower() != name:
613  if name[0].lower() != name[0] and name.split('_')[0] in all:
614  # static methods
615  return []
616  else:
617  return [fullname]
618  tokens = name.split("_")
619  if tokens[0] not in verbs:
620  return [fullname]
621  for t in tokens:
622  if not self._check_spelling(t, words):
623  misspelled.append(t)
624  print("misspelled %s in %s" % (t, name))
625  return [fullname]
626  return []
627 
628  def _static_method(self, module, prefix, name):
629  """For static methods of the form Foo.bar SWIG creates free functions
630  named Foo_bar. Exclude these from spelling checks since the method
631  Foo.bar has already been checked."""
632  if prefix is None and '_' in name:
633  modobj = eval(module)
634  cls, meth = name.split('_', 1)
635  if hasattr(modobj, cls):
636  clsobj = eval(module + '.' + cls)
637  if hasattr(clsobj, meth):
638  return True
639 
640  def _check_function_names(self, module, prefix, names, verbs, all,
641  exceptions, words, misspelled):
642  bad = []
643  for name in names:
644  typ = self._get_type(module, name)
645  if name.startswith("_") or name == "weakref_proxy":
646  continue
647  if typ in (types.BuiltinMethodType, types.MethodType) \
648  or (typ == types.FunctionType and # noqa: E721
649  not self._static_method(module, prefix, name)):
650  bad.extend(self._check_function_name(prefix, name, verbs, all,
651  exceptions, words,
652  misspelled))
653  if typ == type and "SwigPyIterator" not in name:
654  members = eval("dir("+module+"."+name+")")
655  bad.extend(self._check_function_names(module+"."+name,
656  name, members, verbs, [],
657  exceptions, words,
658  misspelled))
659  return bad
660 
661  def assertFunctionNames(self, module, exceptions, words):
662  """Check that all the functions in the module follow the IMP
663  naming conventions."""
664  all = dir(module)
665  verbs = set(["add", "remove", "get", "set", "evaluate", "compute",
666  "show", "create", "destroy", "push", "pop", "write",
667  "read", "do", "show", "load", "save", "reset", "accept",
668  "reject", "clear", "handle", "update", "apply",
669  "optimize", "reserve", "dump", "propose", "setup",
670  "teardown", "visit", "find", "run", "swap", "link",
671  "validate", "erase"])
672  misspelled = []
673  bad = self._check_function_names(module.__name__, None, all, verbs,
674  all, exceptions, words, misspelled)
675  message = ("All IMP methods and functions should have lower case "
676  "names separated by underscores and beginning with a "
677  "verb, preferably one of ['add', 'remove', 'get', 'set', "
678  "'create', 'destroy']. Each of the words should be a "
679  "properly spelled English word. The following do not "
680  "(given our limited list of verbs that we check for):\n"
681  "%(bad)s\nIf there is a good reason for them not to "
682  "(eg it does start with a verb, just one with a meaning "
683  "that is not covered by the normal list), add them to the "
684  "function_name_exceptions variable in the "
685  "standards_exceptions file. Otherwise, please fix. "
686  "The current verb list is %(verbs)s"
687  % {"bad": "\n".join(bad), "verbs": verbs})
688  if len(misspelled) > 0:
689  message += "\nMisspelled words: " + ", ".join(set(misspelled)) \
690  + ". Add words to the spelling_exceptions variable " \
691  + "of the standards_exceptions file if needed."
692  self.assertEqual(len(bad), 0, message)
693 
694  def assertShow(self, modulename, exceptions):
695  """Check that all the classes in modulename have a show method"""
696  all = dir(modulename)
697  if hasattr(modulename, '_raii_types'):
698  excludes = frozenset(
699  modulename._raii_types + modulename._plural_types)
700  else:
701  # Python-only modules don't have these two lists
702  excludes = frozenset()
703  not_found = []
704  for f in all:
705  # Exclude SWIG C global variables object
706  if f == 'cvar':
707  continue
708  # Exclude Python-only classes; they are all showable
709  if not eval('hasattr(%s.%s, "__swig_destroy__")'
710  % (modulename.__name__, f)):
711  continue
712  if self._get_type(modulename.__name__, f) == type \
713  and not f.startswith("_") \
714  and not f.endswith("_swigregister")\
715  and f not in exceptions\
716  and not f.endswith("Temp") and not f.endswith("Iterator")\
717  and not f.endswith("Exception") and\
718  f not in excludes:
719  if not hasattr(getattr(modulename, f), 'show'):
720  not_found.append(f)
721  self.assertEqual(
722  len(not_found), 0,
723  "All IMP classes should support show and __str__. The following "
724  "do not:\n%s\n If there is a good reason for them not to, add "
725  "them to the show_exceptions variable in the IMPModuleTest "
726  "call. Otherwise, please fix." % "\n".join(not_found))
727  for e in exceptions:
728  self.assertIn(e, all,
729  "Show exception "+e+" is not a class in module")
730  self.assertTrue(not hasattr(getattr(modulename, e), 'show'),
731  "Exception "+e+" is not really a show exception")
732 
733  def run_example(self, filename):
734  """Run the named example script.
735  @return a dictionary of all the script's global variables.
736  This can be queried in a test case to make sure
737  the example performed correctly."""
738  class _FatalError(Exception):
739  pass
740 
741  # Add directory containing the example to sys.path, so it can import
742  # other Python modules in the same directory
743  path, name = os.path.split(filename)
744  oldsyspath = sys.path[:]
745  olssysargv = sys.argv[:]
746  sys.path.insert(0, path)
747  sys.argv = [filename]
748  vars = {}
749  try:
750  exec(open(filename).read(), vars)
751  # Catch sys.exit() called from within the example; a non-zero exit
752  # value should cause the test case to fail
753  except SystemExit as e:
754  if e.code != 0 and e.code is not None:
755  raise _FatalError(
756  "Example exit with code %s" % str(e.code))
757  finally:
758  # Restore sys.path
759  sys.path = oldsyspath
760  sys.argv = olssysargv
761 
762  return _ExecDictProxy(vars)
763 
764  def run_python_module(self, module, args):
765  """Run a Python module as if with "python -m <modname>",
766  with the given list of arguments as sys.argv.
767 
768  If module is an already-imported Python module, run its 'main'
769  function and return the result.
770 
771  If module is a string, run the module in a subprocess and return
772  a subprocess.Popen-like object containing the child stdin,
773  stdout and stderr.
774  """
775  def mock_setup_from_argv(*args, **kwargs):
776  # do-nothing replacement for boost command line parser
777  pass
778  if type(module) == type(os): # noqa: E721
779  mod = module
780  else:
781  mod = __import__(module, {}, {}, [''])
782  modpath = mod.__file__
783  if modpath.endswith('.pyc'):
784  modpath = modpath[:-1]
785  if type(module) == type(os): # noqa: E721
786  old_sys_argv = sys.argv
787  # boost parser doesn't like being called multiple times per process
788  old_setup = IMP.setup_from_argv
789  IMP.setup_from_argv = mock_setup_from_argv
790  try:
791  sys.argv = [modpath] + args
792  return module.main()
793  finally:
794  IMP.setup_from_argv = old_setup
795  sys.argv = old_sys_argv
796  else:
797  return _SubprocessWrapper(sys.executable, [modpath] + args)
798 
799  def check_runnable_python_module(self, module):
800  """Check a Python module designed to be runnable with 'python -m'
801  to make sure it supports standard command line options."""
802  # --help should return with exit 0, no errors
803  r = self.run_python_module(module, ['--help'])
804  out, err = r.communicate()
805  self.assertEqual(r.returncode, 0)
806  self.assertNotEqual(err, "")
807  self.assertEqual(out, "")
808 
809 
810 class _ExecDictProxy(object):
811  """exec returns a Python dictionary, which contains IMP objects, other
812  Python objects, as well as base Python modules (such as sys and
813  __builtins__). If we just delete this dictionary, it is entirely
814  possible that base Python modules are removed from the dictionary
815  *before* some IMP objects. This will prevent the IMP objects' Python
816  destructors from running properly, so C++ objects will not be
817  cleaned up. This class proxies the base dict class, and on deletion
818  attempts to remove keys from the dictionary in an order that allows
819  IMP destructors to fire."""
820  def __init__(self, d):
821  self._d = d
822 
823  def __del__(self):
824  # Try to release example objects in a sensible order
825  module_type = type(IMP)
826  d = self._d
827  for k in d.keys():
828  if type(d[k]) != module_type: # noqa: E721
829  del d[k]
830 
831  for meth in ['__contains__', '__getitem__', '__iter__', '__len__',
832  'get', 'has_key', 'items', 'keys', 'values']:
833  exec("def %s(self, *args, **keys): "
834  "return self._d.%s(*args, **keys)" % (meth, meth))
835 
836 
837 class _TestResult(unittest.TextTestResult):
838 
839  def __init__(self, stream=None, descriptions=None, verbosity=None):
840  super(_TestResult, self).__init__(stream, descriptions, verbosity)
841  self.all_tests = []
842 
843  def stopTestRun(self):
844  if 'IMP_TEST_DETAIL_DIR' in os.environ:
845  # Various parts of the IMP build pipeline use Python 3.6,
846  # which predates pickle protocol 5
847  protocol = min(pickle.HIGHEST_PROTOCOL, 4)
848  fname = (Path(os.environ['IMP_TEST_DETAIL_DIR'])
849  / Path(sys.argv[0]).name)
850  # In Wine builds, we may have cd'd to a different drive, e.g. C:
851  # in which case we will no longer be able to see /tmp. In this
852  # case, try to disambiguate by adding a drive.
853  if not fname.exists():
854  fname = Path("Z:") / fname
855  with open(str(fname), 'wb') as fh:
856  pickle.dump(self.all_tests, fh, protocol)
857  super(_TestResult, self).stopTestRun()
858 
859  def startTest(self, test):
860  super(_TestResult, self).startTest(test)
861  test.start_time = datetime.datetime.now()
862 
863  def _test_finished(self, test, state, detail=None):
864  if hasattr(test, 'start_time'):
865  delta = datetime.datetime.now() - test.start_time
866  try:
867  pv = delta.total_seconds()
868  except AttributeError:
869  pv = (float(delta.microseconds)
870  + (delta.seconds
871  + delta.days * 24 * 3600) * 10**6) / 10**6
872  if pv > 1:
873  self.stream.write("in %.3fs ... " % pv)
874  else:
875  # If entire test was skipped, startTest() may not have been
876  # called, in which case start_time won't be set
877  pv = 0
878  if detail is not None and not isinstance(detail, str):
879  detail = self._exc_info_to_string(detail, test)
880  test_doc = self.getDescription(test)
881  test_name = test.id()
882  if test_name.startswith('__main__.'):
883  test_name = test_name[9:]
884  self.all_tests.append({'name': test_name,
885  'docstring': test_doc,
886  'time': pv, 'state': state, 'detail': detail})
887 
888  def addSuccess(self, test):
889  self._test_finished(test, 'OK')
890  super(_TestResult, self).addSuccess(test)
891 
892  def addError(self, test, err):
893  self._test_finished(test, 'ERROR', err)
894  super(_TestResult, self).addError(test, err)
895 
896  def addFailure(self, test, err):
897  self._test_finished(test, 'FAIL', err)
898  super(_TestResult, self).addFailure(test, err)
899 
900  def addSkip(self, test, reason):
901  self._test_finished(test, 'SKIP', reason)
902  super(_TestResult, self).addSkip(test, reason)
903 
904  def addExpectedFailure(self, test, err):
905  self._test_finished(test, 'EXPFAIL', err)
906  super(_TestResult, self).addExpectedFailure(test, err)
907 
908  def addUnexpectedSuccess(self, test):
909  self._test_finished(test, 'UNEXPSUC')
910  super(_TestResult, self).addUnexpectedSuccess(test)
911 
912  def getDescription(self, test):
913  doc_first_line = test.shortDescription()
914  if self.descriptions and doc_first_line:
915  return doc_first_line
916  else:
917  return str(test)
918 
919 
920 class _TestRunner(unittest.TextTestRunner):
921  def _makeResult(self):
922  return _TestResult(self.stream, self.descriptions, self.verbosity)
923 
924 
925 def main(*args, **keys):
926  """Run a set of tests; similar to unittest.main().
927  Obviates the need to separately import the 'unittest' module, and
928  ensures that main() is from the same unittest module that the
929  IMP.test testcases are. In addition, turns on some extra checks
930  (e.g. trying to use deprecated code will cause an exception
931  to be thrown)."""
932  import IMP
934  return unittest.main(testRunner=_TestRunner, *args, **keys)
935 
936 
937 class _SubprocessWrapper(subprocess.Popen):
938  def __init__(self, app, args, cwd=None):
939  # For (non-Python) applications to work on Windows, the
940  # PATH must include the directory containing built DLLs
941  if sys.platform == 'win32' and app != sys.executable:
942  # Hack to find the location of build/lib/
943  libdir = os.environ['PYTHONPATH'].split(';')[0]
944  env = os.environ.copy()
945  env['PATH'] += ';' + libdir
946  else:
947  env = None
948  subprocess.Popen.__init__(self, [app]+list(args),
949  stdin=subprocess.PIPE,
950  stdout=subprocess.PIPE,
951  stderr=subprocess.PIPE, env=env, cwd=cwd,
952  universal_newlines=True)
953 
954 
956  """Super class for simple IMP application test cases"""
957  def _get_application_file_name(self, filename):
958  # If we ran from run-all-tests.py, it set an env variable for us with
959  # the top-level test directory
960  if sys.platform == 'win32':
961  filename += '.exe'
962  return filename
963 
964  def run_application(self, app, args, cwd=None):
965  """Run an application with the given list of arguments.
966  @return a subprocess.Popen-like object containing the child stdin,
967  stdout and stderr.
968  """
969  filename = self._get_application_file_name(app)
970  if sys.platform == 'win32':
971  # Cannot rely on PATH on wine builds, so use full pathname
972  return _SubprocessWrapper(os.path.join(os.environ['IMP_BIN_DIR'],
973  filename), args, cwd=cwd)
974  else:
975  return _SubprocessWrapper(filename, args, cwd=cwd)
976 
977  def run_python_application(self, app, args):
978  """Run a Python application with the given list of arguments.
979  The Python application should be self-runnable (i.e. it should
980  be executable and with a #! on the first line).
981  @return a subprocess.Popen-like object containing the child stdin,
982  stdout and stderr.
983  """
984  # Handle platforms where /usr/bin/python doesn't work
985  if sys.executable != '/usr/bin/python' and 'IMP_BIN_DIR' in os.environ:
986  return _SubprocessWrapper(
987  sys.executable,
988  [os.path.join(os.environ['IMP_BIN_DIR'], app)] + args)
989  else:
990  return _SubprocessWrapper(app, args)
991 
993  """Import an installed Python application, rather than running it.
994  This is useful to directly test components of the application.
995  @return the Python module object."""
996  try:
997  import importlib.machinery
998  imp = None
999  except ImportError:
1000  import imp
1001  name = os.path.splitext(app)[0]
1002  pathname = os.path.join(os.environ['IMP_BIN_DIR'], app)
1003  if imp is None:
1004  return importlib.machinery.SourceFileLoader(name,
1005  pathname).load_module()
1006  else:
1007  return imp.load_source(name, pathname)
1008 
1009  def run_script(self, app, args):
1010  """Run an application with the given list of arguments.
1011  @return a subprocess.Popen-like object containing the child stdin,
1012  stdout and stderr.
1013  """
1014  return _SubprocessWrapper(sys.executable, [app]+args)
1015 
1016  def assertApplicationExitedCleanly(self, ret, error):
1017  """Assert that the application exited cleanly (return value = 0)."""
1018  if ret < 0:
1019  raise OSError("Application exited with signal %d\n" % -ret
1020  + error)
1021  else:
1022  self.assertEqual(
1023  ret, 0,
1024  "Application exited uncleanly, with exit code %d\n" % ret
1025  + error)
1026 
1027  def read_shell_commands(self, doxfile):
1028  """Read and return a set of shell commands from a doxygen file.
1029  Each command is assumed to be in a \code{.sh}...\endcode block.
1030  The doxygen file is specified relative to the test file itself.
1031  This is used to make sure the commands shown in an application
1032  example actually work (the testcase can also check the resulting
1033  files for correctness)."""
1034  def win32_normpath(p):
1035  # Sometimes Windows can read Unix-style paths, but sometimes it
1036  # gets confused... so normalize all paths to be sure
1037  return " ".join([os.path.normpath(x) for x in p.split()])
1038 
1039  def fix_win32_command(cmd):
1040  # Make substitutions so a Unix shell command works on Windows
1041  if cmd.startswith('cp -r '):
1042  return 'xcopy /E ' + win32_normpath(cmd[6:])
1043  elif cmd.startswith('cp '):
1044  return 'copy ' + win32_normpath(cmd[3:])
1045  else:
1046  return cmd
1047  d = os.path.dirname(sys.argv[0])
1048  doc = os.path.join(d, doxfile)
1049  inline = False
1050  cmds = []
1051  example_path = os.path.abspath(IMP.get_example_path('..'))
1052  with open(doc) as fh:
1053  for line in fh.readlines():
1054  if '\code{.sh}' in line:
1055  inline = True
1056  elif '\endcode' in line:
1057  inline = False
1058  elif inline:
1059  cmds.append(line.rstrip('\r\n').replace(
1060  '<imp_example_path>', example_path))
1061  if sys.platform == 'win32':
1062  cmds = [fix_win32_command(x) for x in cmds]
1063  return cmds
1064 
1065  def run_shell_command(self, cmd):
1066  "Print and run a shell command, as returned by read_shell_commands()"
1067  print(cmd)
1068  p = subprocess.call(cmd, shell=True)
1069  if p != 0:
1070  raise OSError("%s failed with exit value %d" % (cmd, p))
1071 
1072 
1073 class RefCountChecker(object):
1074  """Check to make sure the number of C++ object references is as expected"""
1075 
1076  def __init__(self, testcase):
1077  # Make sure no director objects are hanging around; otherwise these
1078  # may be unexpectedly garbage collected later, decreasing the
1079  # live object count
1080  IMP._director_objects.cleanup()
1081  self.__testcase = testcase
1082  if IMP.get_check_level() >= IMP.USAGE_AND_INTERNAL:
1083  self.__basenum = IMP.Object.get_number_of_live_objects()
1084  self.__names = IMP.get_live_object_names()
1085 
1086  def assert_number(self, expected):
1087  "Make sure that the number of references matches the expected value."
1088  t = self.__testcase
1089  IMP._director_objects.cleanup()
1090  if IMP.get_check_level() >= IMP.USAGE_AND_INTERNAL:
1091  newnames = [x for x in IMP.get_live_object_names()
1092  if x not in self.__names]
1093  newnum = IMP.Object.get_number_of_live_objects()-self.__basenum
1094  t.assertEqual(newnum, expected,
1095  "Number of objects don't match: "
1096  + str(newnum) + " != " + str(expected) + " found "
1097  + str(newnames))
1098 
1099 
1101  """Check to make sure the number of director references is as expected"""
1102 
1103  def __init__(self, testcase):
1104  IMP._director_objects.cleanup()
1105  self.__testcase = testcase
1106  self.__basenum = IMP._director_objects.get_object_count()
1107 
1108  def assert_number(self, expected, force_cleanup=True):
1109  """Make sure that the number of references matches the expected value.
1110  If force_cleanup is set, clean up any unused references first before
1111  doing the assertion.
1112  """
1113  t = self.__testcase
1114  if force_cleanup:
1115  IMP._director_objects.cleanup()
1116  t.assertEqual(IMP._director_objects.get_object_count()
1117  - self.__basenum, expected)
1118 
1119 
1120 # Make sure that the IMP binary directory (build/bin) is in the PATH, if
1121 # we're running under wine (the imppy.sh script normally ensures this, but
1122 # wine overrides the PATH). This is needed so that tests of imported Python
1123 # applications can successfully spawn C++ applications (e.g. idock.py tries
1124 # to run recompute_zscore.exe). build/lib also needs to be in the PATH, since
1125 # that's how Windows locates dependent DLLs such as libimp.dll.
1126 if sys.platform == 'win32' and 'PYTHONPATH' in os.environ \
1127  and 'IMP_BIN_DIR' in os.environ:
1128  libdir = os.environ['PYTHONPATH'].split(';')[0]
1129  bindir = os.environ['IMP_BIN_DIR']
1130  path = os.environ['PATH']
1131  if libdir not in path or bindir not in path:
1132  os.environ['PATH'] = bindir + ';' + libdir + ';' + path
1133 
1134 
1135 __version__ = "20240416.develop.27926d84dc"
1136 
1138  '''Return the version of this module, as a string'''
1139  return "20240416.develop.27926d84dc"
1140 
1141 def get_module_name():
1142  '''Return the fully-qualified name of this module'''
1143  return "IMP::test"
1144 
1145 def get_data_path(fname):
1146  '''Return the full path to one of this module's data files'''
1147  import IMP
1148  return IMP._get_module_data_path("test", fname)
1149 
1150 def get_example_path(fname):
1151  '''Return the full path to one of this module's example files'''
1152  import IMP
1153  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 assertCompileFails
Test that the given C++ code fails to compile with a static assertion.
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:86
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:29
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:408
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:43
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