PageRenderTime 20ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/ase/test/testsuite.py

https://gitlab.com/ikondov/ase
Python | 436 lines | 339 code | 67 blank | 30 comment | 92 complexity | f045153ce009855e61e900b23a9a2c8b MD5 | raw file
  1. from __future__ import print_function
  2. import os
  3. import sys
  4. import subprocess
  5. from multiprocessing import Process, cpu_count, Queue
  6. import tempfile
  7. import unittest
  8. from glob import glob
  9. from distutils.version import LooseVersion
  10. import time
  11. import traceback
  12. import warnings
  13. import numpy as np
  14. from ase.calculators.calculator import names as calc_names, get_calculator
  15. from ase.utils import devnull
  16. from ase.cli.info import print_info
  17. NotAvailable = unittest.SkipTest
  18. test_calculator_names = []
  19. if sys.version_info[0] == 2:
  20. class ResourceWarning(UserWarning):
  21. pass # Placeholder - this warning does not exist in Py2 at all.
  22. def require(calcname):
  23. if calcname not in test_calculator_names:
  24. raise NotAvailable('use --calculators={0} to enable'.format(calcname))
  25. def get_tests(files=None):
  26. dirname, _ = os.path.split(__file__)
  27. if files:
  28. fnames = [os.path.join(dirname, f) for f in files]
  29. files = set()
  30. for fname in fnames:
  31. files.update(glob(fname))
  32. files = list(files)
  33. else:
  34. files = glob(os.path.join(dirname, '*'))
  35. files.remove(os.path.join(dirname, 'testsuite.py'))
  36. sdirtests = [] # tests from subdirectories: only one level assumed
  37. tests = []
  38. for f in files:
  39. if os.path.isdir(f):
  40. # add test subdirectories (like calculators)
  41. sdirtests.extend(glob(os.path.join(f, '*.py')))
  42. else:
  43. # add py files in testdir
  44. if f.endswith('.py'):
  45. tests.append(f)
  46. tests.sort()
  47. sdirtests.sort()
  48. tests.extend(sdirtests) # run test subdirectories at the end
  49. tests = [os.path.relpath(test, dirname)
  50. for test in tests if not test.endswith('__.py')]
  51. return tests
  52. def runtest_almost_no_magic(test):
  53. dirname, _ = os.path.split(__file__)
  54. path = os.path.join(dirname, test)
  55. # exclude some test for windows, not done automatic
  56. if os.name == 'nt':
  57. skip = [name for name in calc_names]
  58. skip += ['db_web', 'h2.py', 'bandgap.py', 'al.py',
  59. 'runpy.py', 'oi.py']
  60. if any(s in test for s in skip):
  61. raise NotAvailable('not on windows')
  62. try:
  63. with open(path) as fd:
  64. exec(compile(fd.read(), path, 'exec'), {})
  65. except ImportError as ex:
  66. module = ex.args[0].split()[-1].replace("'", '').split('.')[0]
  67. if module in ['scipy', 'matplotlib', 'Scientific', 'lxml',
  68. 'flask', 'gpaw', 'GPAW', 'netCDF4', 'psycopg2']:
  69. raise unittest.SkipTest('no {} module'.format(module))
  70. else:
  71. raise
  72. def run_single_test(filename):
  73. """Execute single test and return results as dictionary."""
  74. result = Result(name=filename)
  75. # Some tests may write to files with the same name as other tests.
  76. # Hence, create new subdir for each test:
  77. cwd = os.getcwd()
  78. testsubdir = filename.replace(os.sep, '_').replace('.', '_')
  79. os.mkdir(testsubdir)
  80. os.chdir(testsubdir)
  81. t1 = time.time()
  82. sys.stdout = devnull
  83. try:
  84. with warnings.catch_warnings():
  85. # We want all warnings to be errors. Except some that are
  86. # normally entirely ignored by Python, and which we don't want
  87. # to bother about.
  88. warnings.filterwarnings('error')
  89. for warntype in [PendingDeprecationWarning, ImportWarning,
  90. ResourceWarning]:
  91. warnings.filterwarnings('ignore', category=warntype)
  92. # This happens from matplotlib sometimes.
  93. # How can we allow matplotlib to import badly and yet keep
  94. # a higher standard for modules within our own codebase?
  95. warnings.filterwarnings('ignore',
  96. 'Using or importing the ABCs from',
  97. DeprecationWarning)
  98. runtest_almost_no_magic(filename)
  99. except KeyboardInterrupt:
  100. raise
  101. except unittest.SkipTest as ex:
  102. result.status = 'SKIPPED'
  103. result.whyskipped = str(ex)
  104. result.exception = ex
  105. except AssertionError as ex:
  106. result.status = 'FAIL'
  107. result.exception = ex
  108. result.traceback = traceback.format_exc()
  109. except BaseException as ex:
  110. result.status = 'ERROR'
  111. result.exception = ex
  112. result.traceback = traceback.format_exc()
  113. else:
  114. result.status = 'OK'
  115. finally:
  116. sys.stdout = sys.__stdout__
  117. t2 = time.time()
  118. os.chdir(cwd)
  119. result.time = t2 - t1
  120. return result
  121. class Result:
  122. """Represents the result of a test; for communicating between processes."""
  123. attributes = ['name', 'pid', 'exception', 'traceback', 'time', 'status',
  124. 'whyskipped']
  125. def __init__(self, **kwargs):
  126. d = {key: None for key in self.attributes}
  127. d['pid'] = os.getpid()
  128. for key in kwargs:
  129. assert key in d
  130. d[key] = kwargs[key]
  131. self.__dict__ = d
  132. def runtests_subprocess(task_queue, result_queue):
  133. """Main test loop to be called within subprocess."""
  134. try:
  135. while True:
  136. result = test = None
  137. test = task_queue.get()
  138. if test == 'no more tests':
  139. return
  140. # We need to run some tests on master:
  141. # * doctest exceptions appear to be unpicklable.
  142. # Probably they contain a reference to a module or something.
  143. # * gui/run may deadlock for unknown reasons in subprocess
  144. if test in ['bandstructure.py', 'doctests.py', 'gui/run.py',
  145. 'matplotlib_plot.py', 'fio/oi.py', 'fio/v_sim.py',
  146. 'db/db_web.py']:
  147. result = Result(name=test, status='please run on master')
  148. result_queue.put(result)
  149. continue
  150. result = run_single_test(test)
  151. # Any subprocess that uses multithreading is unsafe in
  152. # subprocesses due to a fork() issue:
  153. # https://gitlab.com/ase/ase/issues/244
  154. # Matplotlib uses multithreading and we must therefore make sure
  155. # that any test which imports matplotlib runs on master.
  156. # Hence check whether matplotlib was somehow imported:
  157. assert 'matplotlib' not in sys.modules, test
  158. result_queue.put(result)
  159. except KeyboardInterrupt:
  160. print('Worker pid={} interrupted by keyboard while {}'
  161. .format(os.getpid(),
  162. 'running ' + test if test else 'not running'))
  163. except BaseException as err:
  164. # Failure outside actual test -- i.e. internal test suite error.
  165. result = Result(pid=os.getpid(), name=test, exception=err,
  166. traceback=traceback.format_exc(),
  167. time=0.0, status='ABORT')
  168. result_queue.put(result)
  169. def print_test_result(result):
  170. msg = result.status
  171. if msg == 'SKIPPED':
  172. msg = 'SKIPPED: {}'.format(result.whyskipped)
  173. print('{name:36} {time:6.2f}s {msg}'
  174. .format(name=result.name, time=result.time, msg=msg))
  175. if result.traceback:
  176. print('=' * 78)
  177. print('Error in {} on pid {}:'.format(result.name, result.pid))
  178. print(result.traceback.rstrip())
  179. print('=' * 78)
  180. def runtests_parallel(nprocs, tests):
  181. # Test names will be sent, and results received, into synchronized queues:
  182. task_queue = Queue()
  183. result_queue = Queue()
  184. for test in tests:
  185. task_queue.put(test)
  186. for i in range(nprocs): # Each process needs to receive this
  187. task_queue.put('no more tests')
  188. procs = []
  189. try:
  190. # Start tasks:
  191. for i in range(nprocs):
  192. p = Process(target=runtests_subprocess,
  193. name='ASE-test-worker-{}'.format(i),
  194. args=[task_queue, result_queue])
  195. procs.append(p)
  196. p.start()
  197. # Collect results:
  198. for i in range(len(tests)):
  199. if nprocs == 0:
  200. # No external workers so we do everything.
  201. task = task_queue.get()
  202. result = run_single_test(task)
  203. else:
  204. result = result_queue.get() # blocking call
  205. if result.status == 'please run on master':
  206. result = run_single_test(result.name)
  207. print_test_result(result)
  208. yield result
  209. if result.status == 'ABORT':
  210. raise RuntimeError('ABORT: Internal error in test suite')
  211. except KeyboardInterrupt:
  212. raise
  213. except BaseException:
  214. for proc in procs:
  215. proc.terminate()
  216. raise
  217. finally:
  218. for proc in procs:
  219. proc.join()
  220. def summary(results):
  221. ntests = len(results)
  222. err = [r for r in results if r.status == 'ERROR']
  223. fail = [r for r in results if r.status == 'FAIL']
  224. skip = [r for r in results if r.status == 'SKIPPED']
  225. ok = [r for r in results if r.status == 'OK']
  226. print('========== Summary ==========')
  227. print('Number of tests {:3d}'.format(ntests))
  228. print('Passes: {:3d}'.format(len(ok)))
  229. print('Failures: {:3d}'.format(len(fail)))
  230. print('Errors: {:3d}'.format(len(err)))
  231. print('Skipped: {:3d}'.format(len(skip)))
  232. print('=============================')
  233. if fail or err:
  234. print('Test suite failed!')
  235. else:
  236. print('Test suite passed!')
  237. def test(calculators=[], jobs=0,
  238. stream=sys.stdout, files=None):
  239. """Main test-runner for ASE."""
  240. if LooseVersion(np.__version__) >= '1.14':
  241. # Our doctests need this (spacegroup.py)
  242. np.set_printoptions(legacy='1.13')
  243. test_calculator_names.extend(calculators)
  244. disable_calculators([name for name in calc_names
  245. if name not in calculators])
  246. tests = get_tests(files)
  247. if len(set(tests)) != len(tests):
  248. # Since testsubdirs are based on test name, we will get race
  249. # conditions on IO if the same test runs more than once.
  250. print('Error: One or more tests specified multiple times',
  251. file=sys.stderr)
  252. sys.exit(1)
  253. if jobs == -1: # -1 == auto
  254. jobs = min(cpu_count(), len(tests), 32)
  255. print_info()
  256. origcwd = os.getcwd()
  257. testdir = tempfile.mkdtemp(prefix='ase-test-')
  258. os.chdir(testdir)
  259. # Note: :25 corresponds to ase.cli indentation
  260. print('{:25}{}'.format('test directory', testdir))
  261. print('{:25}{}'.format('number of processes',
  262. jobs or '1 (multiprocessing disabled)'))
  263. print('{:25}{}'.format('time', time.strftime('%c')))
  264. print()
  265. t1 = time.time()
  266. results = []
  267. try:
  268. for result in runtests_parallel(jobs, tests):
  269. results.append(result)
  270. except KeyboardInterrupt:
  271. print('Interrupted by keyboard')
  272. return 1
  273. else:
  274. summary(results)
  275. ntrouble = len([r for r in results if r.status in ['FAIL', 'ERROR']])
  276. return ntrouble
  277. finally:
  278. t2 = time.time()
  279. print('Time elapsed: {:.1f} s'.format(t2 - t1))
  280. os.chdir(origcwd)
  281. def disable_calculators(names):
  282. for name in names:
  283. if name in ['emt', 'lj', 'eam', 'morse', 'tip3p']:
  284. continue
  285. try:
  286. cls = get_calculator(name)
  287. except ImportError:
  288. pass
  289. else:
  290. def get_mock_init(name):
  291. def mock_init(obj, *args, **kwargs):
  292. raise NotAvailable('use --calculators={0} to enable'
  293. .format(name))
  294. return mock_init
  295. def mock_del(obj):
  296. pass
  297. cls.__init__ = get_mock_init(name)
  298. cls.__del__ = mock_del
  299. def cli(command, calculator_name=None):
  300. if (calculator_name is not None and
  301. calculator_name not in test_calculator_names):
  302. return
  303. proc = subprocess.Popen(' '.join(command.split('\n')),
  304. shell=True,
  305. stdout=subprocess.PIPE)
  306. print(proc.stdout.read().decode())
  307. proc.wait()
  308. if proc.returncode != 0:
  309. raise RuntimeError('Failed running a shell command. '
  310. 'Please set you $PATH environment variable!')
  311. class must_raise:
  312. """Context manager for checking raising of exceptions."""
  313. def __init__(self, exception):
  314. self.exception = exception
  315. def __enter__(self):
  316. pass
  317. def __exit__(self, exc_type, exc_value, tb):
  318. if exc_type is None:
  319. raise RuntimeError('Failed to fail: ' + str(self.exception))
  320. return issubclass(exc_type, self.exception)
  321. class CLICommand:
  322. short_description = 'Test ASE'
  323. @staticmethod
  324. def add_arguments(parser):
  325. parser.add_argument(
  326. '-c', '--calculators',
  327. help='Comma-separated list of calculators to test.')
  328. parser.add_argument('--list', action='store_true',
  329. help='print all tests and exit')
  330. parser.add_argument('--list-calculators', action='store_true',
  331. help='print all calculator names and exit')
  332. parser.add_argument('-j', '--jobs', type=int, default=-1,
  333. metavar='N',
  334. help='number of worker processes. '
  335. 'By default use all available processors '
  336. 'up to a maximum of 32. '
  337. '0 disables multiprocessing.')
  338. parser.add_argument('tests', nargs='*',
  339. help='Specify particular test files. '
  340. 'Glob patterns are accepted.')
  341. @staticmethod
  342. def run(args):
  343. if args.calculators:
  344. calculators = args.calculators.split(',')
  345. else:
  346. calculators = []
  347. if args.list:
  348. dirname, _ = os.path.split(__file__)
  349. for testfile in get_tests(args.tests):
  350. print(os.path.join(dirname, testfile))
  351. sys.exit(0)
  352. if args.list_calculators:
  353. for name in calc_names:
  354. print(name)
  355. sys.exit(0)
  356. for calculator in calculators:
  357. if calculator not in calc_names:
  358. sys.stderr.write('No calculator named "{}".\n'
  359. 'Possible CALCULATORS are: '
  360. '{}.\n'.format(calculator,
  361. ', '.join(calc_names)))
  362. sys.exit(1)
  363. ntrouble = test(calculators=calculators, jobs=args.jobs,
  364. files=args.tests)
  365. sys.exit(ntrouble)