1import sys 2import os 3from pathlib import Path 4from subprocess import Popen, PIPE, check_output 5import zlib 6 7import pytest 8import numpy as np 9 10import ase 11from ase.utils import workdir, seterr 12from ase.test.factories import (CalculatorInputs, 13 factory_classes, 14 NoSuchCalculator, 15 get_factories, 16 make_factory_fixture) 17from ase.dependencies import all_dependencies 18 19helpful_message = """\ 20 * Use --calculators option to select calculators. 21 22 * See "ase test --help-calculators" on how to configure calculators. 23 24 * This listing only includes external calculators known by the test 25 system. Others are "configured" by setting an environment variable 26 like "ASE_xxx_COMMAND" in order to allow tests to run. Please see 27 the documentation of that individual calculator. 28""" 29 30 31def pytest_report_header(config, startdir): 32 yield from library_header() 33 yield '' 34 yield from calculators_header(config) 35 36 37def library_header(): 38 yield '' 39 yield 'Libraries' 40 yield '=========' 41 yield '' 42 for name, path in all_dependencies(): 43 yield '{:24} {}'.format(name, path) 44 45 46def calculators_header(config): 47 try: 48 factories = get_factories(config) 49 except NoSuchCalculator as err: 50 pytest.exit(f'No such calculator: {err}') 51 52 configpaths = factories.executable_config_paths 53 module = factories.datafiles_module 54 55 yield '' 56 yield 'Calculators' 57 yield '===========' 58 59 if not configpaths: 60 configtext = 'No configuration file specified' 61 else: 62 configtext = ', '.join(str(path) for path in configpaths) 63 yield f'Config: {configtext}' 64 65 if module is None: 66 datafiles_text = 'ase-datafiles package not installed' 67 else: 68 datafiles_text = str(Path(module.__file__).parent) 69 70 yield f'Datafiles: {datafiles_text}' 71 yield '' 72 73 for name in sorted(factory_classes): 74 if name in factories.builtin_calculators: 75 # Not interesting to test presence of builtin calculators. 76 continue 77 78 factory = factories.factories.get(name) 79 80 if factory is None: 81 configinfo = 'not installed' 82 else: 83 # Some really ugly hacks here: 84 if hasattr(factory, 'importname'): 85 import importlib 86 module = importlib.import_module(factory.importname) 87 configinfo = str(module.__path__[0]) # type: ignore 88 else: 89 configtokens = [] 90 for varname, variable in vars(factory).items(): 91 configtokens.append(f'{varname}={variable}') 92 configinfo = ', '.join(configtokens) 93 94 enabled = factories.enabled(name) 95 if enabled: 96 version = '<unknown version>' 97 if hasattr(factory, 'version'): 98 try: 99 version = factory.version() 100 except Exception: 101 # XXX Add a test for the version numbers so that 102 # will fail without crashing the whole test suite. 103 pass 104 name = f'{name}-{version}' 105 106 run = '[x]' if enabled else '[ ]' 107 line = f' {run} {name:16} {configinfo}' 108 yield line 109 110 yield '' 111 yield helpful_message 112 yield '' 113 114 # (Where should we do this check?) 115 for name in factories.requested_calculators: 116 if not factories.is_adhoc(name) and not factories.installed(name): 117 pytest.exit(f'Calculator "{name}" is not installed. ' 118 'Please run "ase test --help-calculators" on how ' 119 'to install calculators') 120 121 122@pytest.fixture(scope='session', autouse=True) 123def monkeypatch_disabled_calculators(request, factories): 124 # XXX Replace with another mechanism. 125 factories.monkeypatch_disabled_calculators() 126 127 128@pytest.fixture(scope='session', autouse=True) 129def sessionlevel_testing_path(): 130 # We cd into a tempdir so tests and fixtures won't create files 131 # elsewhere (e.g. in the unsuspecting user's directory). 132 # 133 # However we regard it as an error if the tests leave files there, 134 # because they can access each others' files and hence are not 135 # independent. Therefore we want them to explicitly use the 136 # "testdir" fixture which ensures that each has a clean directory. 137 # 138 # To prevent tests from writing files, we chmod the directory. 139 # But if the tests are killed, we cannot clean it up and it will 140 # disturb other pytest runs if we use the pytest tempdir factory. 141 # 142 # So we use the tempfile module for this temporary directory. 143 import tempfile 144 with tempfile.TemporaryDirectory(prefix='ase-test-workdir-') as tempdir: 145 path = Path(tempdir) 146 path.chmod(0o555) 147 with workdir(path): 148 yield path 149 path.chmod(0o755) 150 151 152@pytest.fixture(autouse=False) 153def testdir(tmp_path): 154 # Pytest can on some systems provide a Path from pathlib2. Normalize: 155 path = Path(str(tmp_path)) 156 with workdir(path, mkdir=True): 157 yield tmp_path 158 # We print the path so user can see where test failed, if it failed. 159 print(f'Testdir: {path}') 160 161 162@pytest.fixture 163def allraise(): 164 with seterr(all='raise'): 165 yield 166 167 168@pytest.fixture 169def KIM(): 170 pytest.importorskip('kimpy') 171 from ase.calculators.kim import KIM as _KIM 172 from ase.calculators.kim.exceptions import KIMModelNotFound 173 174 def KIM(*args, **kwargs): 175 try: 176 return _KIM(*args, **kwargs) 177 except KIMModelNotFound: 178 pytest.skip('KIM tests require the example KIM models. ' 179 'These models are available if the KIM API is ' 180 'built from source. See https://openkim.org/kim-api/' 181 'for more information.') 182 183 return KIM 184 185 186@pytest.fixture(scope='session') 187def tkinter(): 188 import tkinter 189 try: 190 tkinter.Tk() 191 except tkinter.TclError as err: 192 pytest.skip('no tkinter: {}'.format(err)) 193 194 195@pytest.fixture(autouse=True) 196def _plt_close_figures(): 197 yield 198 plt = sys.modules.get('matplotlib.pyplot') 199 if plt is None: 200 return 201 fignums = plt.get_fignums() 202 for fignum in fignums: 203 plt.close(fignum) 204 205 206@pytest.fixture(scope='session', autouse=True) 207def _plt_use_agg(): 208 try: 209 import matplotlib 210 except ImportError: 211 pass 212 else: 213 matplotlib.use('Agg') 214 215 216@pytest.fixture(scope='session') 217def plt(_plt_use_agg): 218 pytest.importorskip('matplotlib') 219 220 import matplotlib.pyplot as plt 221 return plt 222 223 224@pytest.fixture 225def figure(plt): 226 fig = plt.figure() 227 yield fig 228 plt.close(fig) 229 230 231@pytest.fixture(scope='session') 232def psycopg2(): 233 return pytest.importorskip('psycopg2') 234 235 236@pytest.fixture(scope='session') 237def factories(pytestconfig): 238 return get_factories(pytestconfig) 239 240 241# XXX Maybe we should not have individual factory fixtures, we could use 242# the decorator @pytest.mark.calculator(name) instead. 243abinit_factory = make_factory_fixture('abinit') 244cp2k_factory = make_factory_fixture('cp2k') 245dftb_factory = make_factory_fixture('dftb') 246espresso_factory = make_factory_fixture('espresso') 247gpaw_factory = make_factory_fixture('gpaw') 248octopus_factory = make_factory_fixture('octopus') 249siesta_factory = make_factory_fixture('siesta') 250 251 252@pytest.fixture 253def factory(request, factories): 254 name, kwargs = request.param 255 if not factories.installed(name): 256 pytest.skip(f'Not installed: {name}') 257 if not factories.enabled(name): 258 pytest.skip(f'Not enabled: {name}') 259 factory = factories[name] 260 return CalculatorInputs(factory, kwargs) 261 262 263def pytest_generate_tests(metafunc): 264 from ase.test.factories import parametrize_calculator_tests 265 parametrize_calculator_tests(metafunc) 266 267 if 'seed' in metafunc.fixturenames: 268 seeds = metafunc.config.getoption('seed') 269 if len(seeds) == 0: 270 seeds = [0] 271 else: 272 seeds = list(map(int, seeds)) 273 metafunc.parametrize('seed', seeds) 274 275 276class CLI: 277 def __init__(self, calculators): 278 self.calculators = calculators 279 280 def ase(self, *args, expect_fail=False): 281 environment = {} 282 environment.update(os.environ) 283 # Prevent failures due to Tkinter-related default backend 284 # on systems without Tkinter. 285 environment['MPLBACKEND'] = 'Agg' 286 287 proc = Popen(['ase', '-T'] + list(args), 288 stdout=PIPE, stdin=PIPE, 289 env=environment) 290 stdout, _ = proc.communicate(b'') 291 status = proc.wait() 292 assert (status != 0) == expect_fail 293 return stdout.decode('utf-8') 294 295 def shell(self, command, calculator_name=None): 296 # Please avoid using shell comamnds including this method! 297 if calculator_name is not None: 298 self.calculators.require(calculator_name) 299 300 actual_command = ' '.join(command.split('\n')).strip() 301 output = check_output(actual_command, shell=True) 302 return output.decode() 303 304 305@pytest.fixture(scope='session') 306def cli(factories): 307 return CLI(factories) 308 309 310@pytest.fixture(scope='session') 311def datadir(): 312 test_basedir = Path(__file__).parent 313 return test_basedir / 'testdata' 314 315 316@pytest.fixture 317def pt_eam_potential_file(datadir): 318 # EAM potential for Pt from LAMMPS, also used with eam calculator. 319 # (Where should this fixture really live?) 320 return datadir / 'eam_Pt_u3.dat' 321 322 323@pytest.fixture(scope='session') 324def asap3(): 325 return pytest.importorskip('asap3') 326 327 328@pytest.fixture(autouse=True) 329def arbitrarily_seed_rng(request): 330 # We want tests to not use global stuff such as np.random.seed(). 331 # But they do. 332 # 333 # So in lieu of (yet) fixing it, we reseed and unseed the random 334 # state for every test. That makes each test deterministic if it 335 # uses random numbers without seeding, but also repairs the damage 336 # done to global state if it did seed. 337 # 338 # In order not to generate all the same random numbers in every test, 339 # we seed according to a kind of hash: 340 ase_path = ase.__path__[0] 341 abspath = Path(request.module.__file__) 342 relpath = abspath.relative_to(ase_path) 343 module_identifier = relpath.as_posix() # Same on all platforms 344 function_name = request.function.__name__ 345 hashable_string = f'{module_identifier}:{function_name}' 346 # We use zlib.adler32() rather than hash() because Python randomizes 347 # the string hashing at startup for security reasons. 348 seed = zlib.adler32(hashable_string.encode('ascii')) % 12345 349 # (We should really use the full qualified name of the test method.) 350 state = np.random.get_state() 351 np.random.seed(seed) 352 yield 353 print(f'Global seed for "{hashable_string}" was: {seed}') 354 np.random.set_state(state) 355 356 357@pytest.fixture(scope='session') 358def povray_executable(): 359 import shutil 360 exe = shutil.which('povray') 361 if exe is None: 362 pytest.skip('povray not installed') 363 return exe 364 365 366def pytest_addoption(parser): 367 parser.addoption('--calculators', metavar='NAMES', default='', 368 help='comma-separated list of calculators to test or ' 369 '"auto" for all configured calculators') 370 parser.addoption('--seed', action='append', default=[], 371 help='add a seed for tests where random number generators' 372 ' are involved. This option can be applied more' 373 ' than once.') 374