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