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