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