PageRenderTime 32ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/ase/test/testsuite.py

https://gitlab.com/Sthaa/ase
Python | 461 lines | 418 code | 25 blank | 18 comment | 37 complexity | 551e32de97db4a8b5bb9b7453656532a 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', 'Tkinter',
  68. 'flask', 'gpaw', 'GPAW', 'netCDF4', 'psycopg2']:
  69. raise unittest.SkipTest('no {} module'.format(module))
  70. else:
  71. raise
  72. def run_single_test(filename, verbose, strict):
  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. if not verbose:
  83. sys.stdout = devnull
  84. try:
  85. with warnings.catch_warnings():
  86. if strict:
  87. # We want all warnings to be errors. Except some that are
  88. # normally entirely ignored by Python, and which we don't want
  89. # to bother about.
  90. warnings.filterwarnings('error')
  91. for warntype in [PendingDeprecationWarning, ImportWarning,
  92. ResourceWarning]:
  93. warnings.filterwarnings('ignore', category=warntype)
  94. # This happens from matplotlib sometimes.
  95. # How can we allow matplotlib to import badly and yet keep
  96. # a higher standard for modules within our own codebase?
  97. warnings.filterwarnings('ignore',
  98. 'Using or importing the ABCs from',
  99. category=DeprecationWarning)
  100. runtest_almost_no_magic(filename)
  101. except KeyboardInterrupt:
  102. raise
  103. except unittest.SkipTest as ex:
  104. result.status = 'SKIPPED'
  105. result.whyskipped = str(ex)
  106. result.exception = ex
  107. except AssertionError as ex:
  108. result.status = 'FAIL'
  109. result.exception = ex
  110. result.traceback = traceback.format_exc()
  111. except BaseException as ex:
  112. result.status = 'ERROR'
  113. result.exception = ex
  114. result.traceback = traceback.format_exc()
  115. else:
  116. result.status = 'OK'
  117. finally:
  118. sys.stdout = sys.__stdout__
  119. t2 = time.time()
  120. os.chdir(cwd)
  121. result.time = t2 - t1
  122. return result
  123. class Result:
  124. """Represents the result of a test; for communicating between processes."""
  125. attributes = ['name', 'pid', 'exception', 'traceback', 'time', 'status',
  126. 'whyskipped']
  127. def __init__(self, **kwargs):
  128. d = {key: None for key in self.attributes}
  129. d['pid'] = os.getpid()
  130. for key in kwargs:
  131. assert key in d
  132. d[key] = kwargs[key]
  133. self.__dict__ = d
  134. def runtests_subprocess(task_queue, result_queue, verbose, strict):
  135. """Main test loop to be called within subprocess."""
  136. try:
  137. while True:
  138. result = test = None
  139. test = task_queue.get()
  140. if test == 'no more tests':
  141. return
  142. # We need to run some tests on master:
  143. # * doctest exceptions appear to be unpicklable.
  144. # Probably they contain a reference to a module or something.
  145. # * gui/run may deadlock for unknown reasons in subprocess
  146. t = test.replace('\\', '/')
  147. if t in ['bandstructure.py', 'doctests.py', 'gui/run.py',
  148. 'matplotlib_plot.py', 'fio/oi.py', 'fio/v_sim.py',
  149. 'fio/animate.py', 'db/db_web.py', 'x3d.py']:
  150. result = Result(name=test, status='please run on master')
  151. result_queue.put(result)
  152. continue
  153. result = run_single_test(test, verbose, strict)
  154. # Any subprocess that uses multithreading is unsafe in
  155. # subprocesses due to a fork() issue:
  156. # https://gitlab.com/ase/ase/issues/244
  157. # Matplotlib uses multithreading and we must therefore make sure
  158. # that any test which imports matplotlib runs on master.
  159. # Hence check whether matplotlib was somehow imported:
  160. assert 'matplotlib' not in sys.modules, test
  161. result_queue.put(result)
  162. except KeyboardInterrupt:
  163. print('Worker pid={} interrupted by keyboard while {}'
  164. .format(os.getpid(),
  165. 'running ' + test if test else 'not running'))
  166. except BaseException as err:
  167. # Failure outside actual test -- i.e. internal test suite error.
  168. result = Result(pid=os.getpid(), name=test, exception=err,
  169. traceback=traceback.format_exc(),
  170. time=0.0, status='ABORT')
  171. result_queue.put(result)
  172. def print_test_result(result):
  173. msg = result.status
  174. if msg == 'SKIPPED':
  175. msg = 'SKIPPED: {}'.format(result.whyskipped)
  176. print('{name:36} {time:6.2f}s {msg}'
  177. .format(name=result.name, time=result.time, msg=msg))
  178. if result.traceback:
  179. print('=' * 78)
  180. print('Error in {} on pid {}:'.format(result.name, result.pid))
  181. print(result.traceback.rstrip())
  182. print('=' * 78)
  183. def runtests_parallel(nprocs, tests, verbose, strict):
  184. # Test names will be sent, and results received, into synchronized queues:
  185. task_queue = Queue()
  186. result_queue = Queue()
  187. for test in tests:
  188. task_queue.put(test)
  189. for i in range(nprocs): # Each process needs to receive this
  190. task_queue.put('no more tests')
  191. procs = []
  192. try:
  193. # Start tasks:
  194. for i in range(nprocs):
  195. p = Process(target=runtests_subprocess,
  196. name='ASE-test-worker-{}'.format(i),
  197. args=[task_queue, result_queue, verbose, strict])
  198. procs.append(p)
  199. p.start()
  200. # Collect results:
  201. for i in range(len(tests)):
  202. if nprocs == 0:
  203. # No external workers so we do everything.
  204. task = task_queue.get()
  205. result = run_single_test(task, verbose, strict)
  206. else:
  207. result = result_queue.get() # blocking call
  208. if result.status == 'please run on master':
  209. result = run_single_test(result.name, verbose, strict)
  210. print_test_result(result)
  211. yield result
  212. if result.status == 'ABORT':
  213. raise RuntimeError('ABORT: Internal error in test suite')
  214. except KeyboardInterrupt:
  215. raise
  216. except BaseException:
  217. for proc in procs:
  218. proc.terminate()
  219. raise
  220. finally:
  221. for proc in procs:
  222. proc.join()
  223. def summary(results):
  224. ntests = len(results)
  225. err = [r for r in results if r.status == 'ERROR']
  226. fail = [r for r in results if r.status == 'FAIL']
  227. skip = [r for r in results if r.status == 'SKIPPED']
  228. ok = [r for r in results if r.status == 'OK']
  229. if fail or err:
  230. print()
  231. print('Failures and errors:')
  232. for r in err + fail:
  233. print('{}: {}: {}'.format(r.name, r.exception.__class__.__name__,
  234. r.exception))
  235. print('========== Summary ==========')
  236. print('Number of tests {:3d}'.format(ntests))
  237. print('Passes: {:3d}'.format(len(ok)))
  238. print('Failures: {:3d}'.format(len(fail)))
  239. print('Errors: {:3d}'.format(len(err)))
  240. print('Skipped: {:3d}'.format(len(skip)))
  241. print('=============================')
  242. if fail or err:
  243. print('Test suite failed!')
  244. else:
  245. print('Test suite passed!')
  246. def test(calculators=[], jobs=0,
  247. stream=sys.stdout, files=None, verbose=False, strict=False):
  248. """Main test-runner for ASE."""
  249. if LooseVersion(np.__version__) >= '1.14':
  250. # Our doctests need this (spacegroup.py)
  251. np.set_printoptions(legacy='1.13')
  252. test_calculator_names.extend(calculators)
  253. disable_calculators([name for name in calc_names
  254. if name not in calculators])
  255. tests = get_tests(files)
  256. if len(set(tests)) != len(tests):
  257. # Since testsubdirs are based on test name, we will get race
  258. # conditions on IO if the same test runs more than once.
  259. print('Error: One or more tests specified multiple times',
  260. file=sys.stderr)
  261. sys.exit(1)
  262. if jobs == -1: # -1 == auto
  263. jobs = min(cpu_count(), len(tests), 32)
  264. print_info()
  265. origcwd = os.getcwd()
  266. testdir = tempfile.mkdtemp(prefix='ase-test-')
  267. os.chdir(testdir)
  268. # Note: :25 corresponds to ase.cli indentation
  269. print('{:25}{}'.format('test directory', testdir))
  270. if test_calculator_names:
  271. print('{:25}{}'.format('Enabled calculators:',
  272. ' '.join(test_calculator_names)))
  273. print('{:25}{}'.format('number of processes',
  274. jobs or '1 (multiprocessing disabled)'))
  275. print('{:25}{}'.format('time', time.strftime('%c')))
  276. if strict:
  277. print('Strict mode: Convert most warnings to errors')
  278. print()
  279. t1 = time.time()
  280. results = []
  281. try:
  282. for result in runtests_parallel(jobs, tests, verbose, strict):
  283. results.append(result)
  284. except KeyboardInterrupt:
  285. print('Interrupted by keyboard')
  286. return 1
  287. else:
  288. summary(results)
  289. ntrouble = len([r for r in results if r.status in ['FAIL', 'ERROR']])
  290. return ntrouble
  291. finally:
  292. t2 = time.time()
  293. print('Time elapsed: {:.1f} s'.format(t2 - t1))
  294. os.chdir(origcwd)
  295. def disable_calculators(names):
  296. for name in names:
  297. if name in ['emt', 'lj', 'eam', 'morse', 'tip3p']:
  298. continue
  299. try:
  300. cls = get_calculator(name)
  301. except ImportError:
  302. pass
  303. else:
  304. def get_mock_init(name):
  305. def mock_init(obj, *args, **kwargs):
  306. raise NotAvailable('use --calculators={0} to enable'
  307. .format(name))
  308. return mock_init
  309. def mock_del(obj):
  310. pass
  311. cls.__init__ = get_mock_init(name)
  312. cls.__del__ = mock_del
  313. def cli(command, calculator_name=None):
  314. if (calculator_name is not None and
  315. calculator_name not in test_calculator_names):
  316. return
  317. proc = subprocess.Popen(' '.join(command.split('\n')),
  318. shell=True,
  319. stdout=subprocess.PIPE)
  320. print(proc.stdout.read().decode())
  321. proc.wait()
  322. if proc.returncode != 0:
  323. raise RuntimeError('Failed running a shell command. '
  324. 'Please set you $PATH environment variable!')
  325. class must_raise:
  326. """Context manager for checking raising of exceptions."""
  327. def __init__(self, exception):
  328. self.exception = exception
  329. def __enter__(self):
  330. pass
  331. def __exit__(self, exc_type, exc_value, tb):
  332. if exc_type is None:
  333. raise RuntimeError('Failed to fail: ' + str(self.exception))
  334. return issubclass(exc_type, self.exception)
  335. class CLICommand:
  336. """Run ASE's test-suite.
  337. By default, tests for external calculators are skipped. Enable with
  338. "-c name".
  339. """
  340. @staticmethod
  341. def add_arguments(parser):
  342. parser.add_argument(
  343. '-c', '--calculators',
  344. help='Comma-separated list of calculators to test')
  345. parser.add_argument('--list', action='store_true',
  346. help='print all tests and exit')
  347. parser.add_argument('--list-calculators', action='store_true',
  348. help='print all calculator names and exit')
  349. parser.add_argument('-j', '--jobs', type=int, default=-1,
  350. metavar='N',
  351. help='number of worker processes. '
  352. 'By default use all available processors '
  353. 'up to a maximum of 32. '
  354. '0 disables multiprocessing')
  355. parser.add_argument('-v', '--verbose', action='store_true',
  356. help='Write test outputs to stdout. '
  357. 'Mostly useful when inspecting a single test')
  358. parser.add_argument('--strict', action='store_true',
  359. help='convert warnings to errors')
  360. parser.add_argument('tests', nargs='*',
  361. help='Specify particular test files. '
  362. 'Glob patterns are accepted.')
  363. @staticmethod
  364. def run(args):
  365. if args.calculators:
  366. calculators = args.calculators.split(',')
  367. else:
  368. calculators = []
  369. if args.list:
  370. dirname, _ = os.path.split(__file__)
  371. for testfile in get_tests(args.tests):
  372. print(os.path.join(dirname, testfile))
  373. sys.exit(0)
  374. if args.list_calculators:
  375. for name in calc_names:
  376. print(name)
  377. sys.exit(0)
  378. for calculator in calculators:
  379. if calculator not in calc_names:
  380. sys.stderr.write('No calculator named "{}".\n'
  381. 'Possible CALCULATORS are: '
  382. '{}.\n'.format(calculator,
  383. ', '.join(calc_names)))
  384. sys.exit(1)
  385. ntrouble = test(calculators=calculators, jobs=args.jobs,
  386. strict=args.strict,
  387. files=args.tests, verbose=args.verbose)
  388. sys.exit(ntrouble)