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