def suite(): """Return all easyblock --module-only tests.""" # initialize configuration (required for e.g. default modules_tool setting) cleanup() eb_go = eboptions.parse_options(args=['--prefix=%s' % TMPDIR]) config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'external_modules_metadata': {}, # enable --force --module-only 'force': True, 'module_only': True, 'silent': True, 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() # dynamically generate a separate test for each of the available easyblocks easyblocks_path = get_paths_for("easyblocks")[0] all_pys = glob.glob('%s/*/*.py' % easyblocks_path) easyblocks = [ eb for eb in all_pys if os.path.basename(eb) != '__init__.py' and '/test/' not in eb ] # filter out no longer supported easyblocks, or easyblocks that are tested in a different way excluded_easyblocks = ['versionindependendpythonpackage.py'] easyblocks = [ e for e in easyblocks if os.path.basename(e) not in excluded_easyblocks ] # add dummy PrgEnv-gnu/1.2.3 module, required for testing CrayToolchain easyblock write_file(os.path.join(TMPDIR, 'modules', 'all', 'PrgEnv-gnu', '1.2.3'), "#%Module") for easyblock in easyblocks: # dynamically define new inner functions that can be added as class methods to ModuleOnlyTest if os.path.basename(easyblock) == 'systemcompiler.py': # use GCC as name when testing SystemCompiler easyblock exec( "def innertest(self): template_module_only_test(self, '%s', name='GCC', version='system')" % easyblock) elif os.path.basename(easyblock) == 'craytoolchain.py': # make sure that a (known) PrgEnv is included as a dependency extra_txt = 'dependencies = [("PrgEnv-gnu/1.2.3", EXTERNAL_MODULE)]' exec( "def innertest(self): template_module_only_test(self, '%s', extra_txt='%s')" % (easyblock, extra_txt)) else: exec("def innertest(self): template_module_only_test(self, '%s')" % easyblock) innertest.__doc__ = "Test for using --module-only with easyblock %s" % easyblock innertest.__name__ = "test_easyblock_%s" % '_'.join( easyblock.replace('.py', '').split('/')) setattr(ModuleOnlyTest, innertest.__name__, innertest) return TestLoader().loadTestsFromTestCase(ModuleOnlyTest)
def suite(): """Return all easyblock --module-only tests.""" # initialize configuration (required for e.g. default modules_tool setting) cleanup() eb_go = eboptions.parse_options(args=['--prefix=%s' % TMPDIR]) config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'external_modules_metadata': {}, # enable --force --module-only 'force': True, 'module_only': True, 'silent': True, 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() # dynamically generate a separate test for each of the available easyblocks easyblocks_path = get_paths_for("easyblocks")[0] all_pys = glob.glob('%s/*/*.py' % easyblocks_path) easyblocks = [eb for eb in all_pys if os.path.basename(eb) != '__init__.py' and '/test/' not in eb] # filter out no longer supported easyblocks, or easyblocks that are tested in a different way excluded_easyblocks = ['versionindependendpythonpackage.py'] easyblocks = [e for e in easyblocks if os.path.basename(e) not in excluded_easyblocks] # add dummy PrgEnv-* modules, required for testing CrayToolchain easyblock for prgenv in ['PrgEnv-cray', 'PrgEnv-gnu', 'PrgEnv-intel', 'PrgEnv-pgi']: write_file(os.path.join(TMPDIR, 'modules', 'all', prgenv, '1.2.3'), "#%Module") for easyblock in easyblocks: # dynamically define new inner functions that can be added as class methods to ModuleOnlyTest if os.path.basename(easyblock) == 'systemcompiler.py': # use GCC as name when testing SystemCompiler easyblock exec("def innertest(self): template_module_only_test(self, '%s', name='GCC', version='system')" % easyblock) elif os.path.basename(easyblock) == 'systemmpi.py': # use OpenMPI as name when testing SystemMPI easyblock exec("def innertest(self): template_module_only_test(self, '%s', name='OpenMPI', version='system')" % easyblock) elif os.path.basename(easyblock) == 'craytoolchain.py': # make sure that a (known) PrgEnv is included as a dependency extra_txt = 'dependencies = [("PrgEnv-gnu/1.2.3", EXTERNAL_MODULE)]' exec("def innertest(self): template_module_only_test(self, '%s', extra_txt='%s')" % (easyblock, extra_txt)) else: exec("def innertest(self): template_module_only_test(self, '%s')" % easyblock) innertest.__doc__ = "Test for using --module-only with easyblock %s" % easyblock innertest.__name__ = "test_easyblock_%s" % '_'.join(easyblock.replace('.py', '').split('/')) setattr(ModuleOnlyTest, innertest.__name__, innertest) return TestLoader().loadTestsFromTestCase(ModuleOnlyTest)
def suite(): """Return all easyblock --module-only tests.""" # initialize configuration (required for e.g. default modules_tool setting) cleanup() eb_go = eboptions.parse_options(args=['--prefix=%s' % TMPDIR]) config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { # enable --force --module-only 'force': True, 'module_only': True, 'silent': True, 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() # dynamically generate a separate test for each of the available easyblocks easyblocks_path = get_paths_for("easyblocks")[0] all_pys = glob.glob('%s/*/*.py' % easyblocks_path) easyblocks = [eb for eb in all_pys if os.path.basename(eb) != '__init__.py' and '/test/' not in eb] # filter out no longer supported easyblocks, or easyblocks that are tested in a different way excluded_easyblocks = ['versionindependendpythonpackage.py'] easyblocks = [e for e in easyblocks if os.path.basename(e) not in excluded_easyblocks] for easyblock in easyblocks: # dynamically define new inner functions that can be added as class methods to ModuleOnlyTest if os.path.basename(easyblock) == 'systemcompiler.py': # use GCC as name when testing SystemCompiler easyblock exec("def innertest(self): template_module_only_test(self, '%s', name='GCC', version='system')" % easyblock) else: exec("def innertest(self): template_module_only_test(self, '%s')" % easyblock) innertest.__doc__ = "Test for using --module-only with easyblock %s" % easyblock innertest.__name__ = "test_easyblock_%s" % '_'.join(easyblock.replace('.py', '').split('/')) setattr(ModuleOnlyTest, innertest.__name__, innertest) return TestLoader().loadTestsFromTestCase(ModuleOnlyTest)
class InitTest(TestCase): """ Baseclass for easyblock testcases """ # initialize configuration (required for e.g. default modules_tool setting) eb_go = eboptions.parse_options() config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() del eb_go def write_ec(self, easyblock, name='foo', version='1.3.2', toolchain=None, extratxt=''): """ create temporary easyconfig file """ if toolchain is None: toolchain = 'SYSTEM' txt = '\n'.join([ 'easyblock = "%s"', 'name = "%s"' % name, 'version = "%s"' % version, 'homepage = "http://example.com"', 'description = "Dummy easyconfig file."', 'toolchain = %s' % toolchain, 'sources = []', extratxt, ]) write_file(self.eb_file, txt % easyblock) def setUp(self): """Setup test.""" self.log = fancylogger.getLogger("EasyblocksInitTest", fname=False) fd, self.eb_file = tempfile.mkstemp(prefix='easyblocks_init_test_', suffix='.eb') os.close(fd) def tearDown(self): """Cleanup.""" try: os.remove(self.eb_file) except OSError as err: self.log.error("Failed to remove %s: %s" % (self.eb_file, err))
def setUp(self): """Set up testcase.""" super(EnhancedTestCase, self).setUp() # make sure option parser doesn't pick up any cmdline arguments/options while len(sys.argv) > 1: sys.argv.pop() # keep track of log handlers log = fancylogger.getLogger(fname=False) self.orig_log_handlers = log.handlers[:] log.info("setting up test %s" % self.id()) self.orig_tmpdir = tempfile.gettempdir() # use a subdirectory for this test (which we can clean up easily after the test completes) self.test_prefix = set_tmpdir() self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) fd, self.logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-') os.close(fd) self.cwd = os.getcwd() # keep track of original environment to restore self.orig_environ = copy.deepcopy(os.environ) # keep track of original environment/Python search path to restore self.orig_sys_path = sys.path[:] testdir = os.path.dirname(os.path.abspath(__file__)) self.test_sourcepath = os.path.join(testdir, 'sandbox', 'sources') os.environ['EASYBUILD_SOURCEPATH'] = self.test_sourcepath os.environ['EASYBUILD_PREFIX'] = self.test_prefix self.test_buildpath = tempfile.mkdtemp() os.environ['EASYBUILD_BUILDPATH'] = self.test_buildpath self.test_installpath = tempfile.mkdtemp() os.environ['EASYBUILD_INSTALLPATH'] = self.test_installpath # make sure that the tests only pick up easyconfigs provided with the tests os.environ['EASYBUILD_ROBOT_PATHS'] = os.path.join(testdir, 'easyconfigs', 'test_ecs') # make sure no deprecated behaviour is being triggered (unless intended by the test) self.orig_current_version = eb_build_log.CURRENT_VERSION self.disallow_deprecated_behaviour() init_config() import easybuild # try to import easybuild.easyblocks(.generic) packages # it's OK if it fails here, but important to import first before fiddling with sys.path try: import easybuild.easyblocks import easybuild.easyblocks.generic except ImportError: pass # add sandbox to Python search path, update namespace packages sys.path.append(os.path.join(testdir, 'sandbox')) # required to make sure the 'easybuild' dir in the sandbox is picked up; # this relates to the other 'reload' statements below reload(easybuild) # required to 'reset' easybuild.tools.module_naming_scheme namespace reload(easybuild.tools) reload(easybuild.tools.module_naming_scheme) # remove any entries in Python search path that seem to provide easyblocks (except the sandbox) for path in sys.path[:]: if os.path.exists(os.path.join(path, 'easybuild', 'easyblocks', '__init__.py')): if not os.path.samefile(path, os.path.join(testdir, 'sandbox')): sys.path.remove(path) # hard inject location to (generic) test easyblocks into Python search path # only prepending to sys.path is not enough due to 'pkgutil.extend_path' in easybuild/easyblocks/__init__.py easybuild.__path__.insert(0, os.path.join(testdir, 'sandbox', 'easybuild')) import easybuild.easyblocks test_easyblocks_path = os.path.join(testdir, 'sandbox', 'easybuild', 'easyblocks') easybuild.easyblocks.__path__.insert(0, test_easyblocks_path) reload(easybuild.easyblocks) import easybuild.easyblocks.generic test_easyblocks_path = os.path.join(test_easyblocks_path, 'generic') easybuild.easyblocks.generic.__path__.insert(0, test_easyblocks_path) reload(easybuild.easyblocks.generic) # save values of $PATH & $PYTHONPATH, so they can be restored later # this is important in case EasyBuild was installed as a module, since that module may be unloaded, # for example due to changes to $MODULEPATH in case EasyBuild was installed in a module hierarchy # cfr. https://github.com/easybuilders/easybuild-framework/issues/1685 self.env_path = os.environ.get('PATH') self.env_pythonpath = os.environ.get('PYTHONPATH') self.modtool = modules_tool() self.reset_modulepath([os.path.join(testdir, 'modules')]) reset_module_caches()
class EasyConfigTest(TestCase): """Baseclass for easyconfig testcases.""" # initialize configuration (required for e.g. default modules_tool setting) eb_go = eboptions.parse_options() config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'check_osdeps': False, 'external_modules_metadata': {}, 'force': True, 'optarch': 'test', 'robot_path': get_paths_for("easyconfigs")[0], 'silent': True, 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() del eb_go # put dummy 'craype-test' module in place, which is required for parsing easyconfigs using Cray* toolchains TMPDIR = tempfile.mkdtemp() os.environ['MODULEPATH'] = TMPDIR write_file(os.path.join(TMPDIR, 'craype-test'), '#%Module\n') log = fancylogger.getLogger("EasyConfigTest", fname=False) # make sure a logger is present for main main._log = log ordered_specs = None parsed_easyconfigs = [] def process_all_easyconfigs(self): """Process all easyconfigs and resolve inter-easyconfig dependencies.""" # all available easyconfig files easyconfigs_path = get_paths_for("easyconfigs")[0] specs = glob.glob('%s/*/*/*.eb' % easyconfigs_path) # parse all easyconfigs if they haven't been already if not self.parsed_easyconfigs: for spec in specs: self.parsed_easyconfigs.extend(process_easyconfig(spec)) # filter out external modules for ec in self.parsed_easyconfigs: for dep in ec['dependencies'][:]: if dep.get('external_module', False): ec['dependencies'].remove(dep) self.ordered_specs = resolve_dependencies(self.parsed_easyconfigs, retain_all_deps=True) def test_dep_graph(self): """Unit test that builds a full dependency graph.""" # pygraph dependencies required for constructing dependency graph are not available prior to Python 2.6 if LooseVersion( sys.version) >= LooseVersion('2.6') and single_tests_ok: # temporary file for dep graph (hn, fn) = tempfile.mkstemp(suffix='.dot') os.close(hn) if self.ordered_specs is None: self.process_all_easyconfigs() dep_graph(fn, self.ordered_specs) try: os.remove(fn) except OSError, err: log.error("Failed to remove %s: %s" % (fn, err)) else:
class EasyConfigTest(TestCase): """Baseclass for easyconfig testcases.""" # initialize configuration (required for e.g. default modules_tool setting) eb_go = eboptions.parse_options() config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'check_osdeps': False, 'external_modules_metadata': {}, 'force': True, 'optarch': 'test', 'robot_path': get_paths_for("easyconfigs")[0], 'silent': True, 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() del eb_go # put dummy 'craype-test' module in place, which is required for parsing easyconfigs using Cray* toolchains TMPDIR = tempfile.mkdtemp() os.environ['MODULEPATH'] = TMPDIR write_file(os.path.join(TMPDIR, 'craype-test'), '#%Module\n') log = fancylogger.getLogger("EasyConfigTest", fname=False) # make sure a logger is present for main eb_main._log = log ordered_specs = None parsed_easyconfigs = [] def process_all_easyconfigs(self): """Process all easyconfigs and resolve inter-easyconfig dependencies.""" # all available easyconfig files easyconfigs_path = get_paths_for("easyconfigs")[0] specs = glob.glob('%s/*/*/*.eb' % easyconfigs_path) # parse all easyconfigs if they haven't been already if not self.parsed_easyconfigs: for spec in specs: self.parsed_easyconfigs.extend(process_easyconfig(spec)) # filter out external modules for ec in self.parsed_easyconfigs: for dep in ec['dependencies'][:]: if dep.get('external_module', False): ec['dependencies'].remove(dep) self.ordered_specs = resolve_dependencies(self.parsed_easyconfigs, modules_tool(), retain_all_deps=True) def test_dep_graph(self): """Unit test that builds a full dependency graph.""" # pygraph dependencies required for constructing dependency graph are not available prior to Python 2.6 if LooseVersion( sys.version) >= LooseVersion('2.6') and single_tests_ok: # temporary file for dep graph (hn, fn) = tempfile.mkstemp(suffix='.dot') os.close(hn) if self.ordered_specs is None: self.process_all_easyconfigs() dep_graph(fn, self.ordered_specs) remove_file(fn) else: print "(skipped dep graph test)" def test_conflicts(self): """Check whether any conflicts occur in software dependency graphs.""" if not single_tests_ok: print "(skipped conflicts test)" return if self.ordered_specs is None: self.process_all_easyconfigs() self.assertFalse( check_conflicts(self.ordered_specs, modules_tool(), check_inter_ec_conflicts=False), "No conflicts detected") def test_dep_versions_per_toolchain_generation(self): """ Check whether there's only one dependency version per toolchain generation actively used. This is enforced to try and limit the chance of running into conflicts when multiple modules built with the same toolchain are loaded together. """ if self.ordered_specs is None: self.process_all_easyconfigs() def get_deps_for(ec): """Get list of (direct) dependencies for specified easyconfig.""" deps = [] for dep in ec['ec']['dependencies']: dep_mod_name = dep['full_mod_name'] deps.append((dep['name'], dep['version'], dep['versionsuffix'], dep_mod_name)) res = [ x for x in self.ordered_specs if x['full_mod_name'] == dep_mod_name ] if len(res) == 1: deps.extend(get_deps_for(res[0])) else: raise EasyBuildError( "Failed to find %s in ordered list of easyconfigs", dep_mod_name) return deps def check_dep_vars(dep, dep_vars): """Check whether available variants of a particular dependency are acceptable or not.""" # 'guilty' until proven 'innocent' res = False # filter out binutils with empty versionsuffix which is used to build toolchain compiler if dep == 'binutils' and len(dep_vars) > 1: empty_vsuff_vars = [ v for v in dep_vars.keys() if v.endswith('versionsuffix: ') ] if len(empty_vsuff_vars) == 1: dep_vars = dict((k, v) for (k, v) in dep_vars.items() if k != empty_vsuff_vars[0]) # multiple variants of HTSlib is OK as long as they are deps for a matching version of BCFtools elif dep == 'HTSlib' and len(dep_vars) > 1: for key, ecs in dep_vars.items(): # filter out HTSlib variants that are only used as dependency for BCFtools with same version htslib_ver = re.search('^version: (?P<ver>[^;]+);', key).group('ver') if all( ec.startswith('BCFtools-%s-' % htslib_ver) for ec in ecs): dep_vars.pop(key) # filter out FFTW and imkl with -serial versionsuffix which are used in non-MPI subtoolchains elif dep in ['FFTW', 'imkl']: serial_vsuff_vars = [ v for v in dep_vars.keys() if v.endswith('versionsuffix: -serial') ] if len(serial_vsuff_vars) == 1: dep_vars = dict((k, v) for (k, v) in dep_vars.items() if k != serial_vsuff_vars[0]) # for some dependencies, we allow exceptions for software that depends on a particular version, # as long as that's indicated by the versionsuffix elif dep in ['Boost', 'R', 'PLUMED'] and len(dep_vars) > 1: for key in dep_vars.keys(): dep_ver = re.search('^version: (?P<ver>[^;]+);', key).group('ver') # filter out dep version if all easyconfig filenames using it include specific dep version if all( re.search('-%s-%s' % (dep, dep_ver), v) for v in dep_vars[key]): dep_vars.pop(key) # always retain at least one dep variant if len(dep_vars) == 1: break # filter R dep for a specific version of Python 2.x if dep == 'R' and len(dep_vars) > 1: for key in dep_vars.keys(): if '; versionsuffix: -Python-2' in key: dep_vars.pop(key) # always retain at least one variant if len(dep_vars) == 1: break # filter out Java 'wrapper' # i.e. if the version of one is a prefix of the version of the other one (e.g. 1.8 & 1.8.0_181) elif dep == 'Java' and len(dep_vars) == 2: key1, key2 = sorted(dep_vars.keys()) ver1, ver2 = [k.split(';')[0] for k in [key1, key2]] if ver1.startswith(ver2): dep_vars.pop(key2) elif ver2.startswith(ver1): dep_vars.pop(key1) # filter out variants that are specific to a particular version of CUDA cuda_dep_vars = [v for v in dep_vars.keys() if '-CUDA' in v] if len(dep_vars) > len(cuda_dep_vars): for key in dep_vars.keys(): if re.search('; versionsuffix: .*-CUDA-[0-9.]+', key): dep_vars.pop(key) # some software packages require an old version of a particular dependency old_dep_versions = { # libxc (CP2K & ABINIT require libxc 2.x or 3.x) 'libxc': r'[23]\.', # OPERA requires SAMtools 0.x 'SAMtools': r'0\.', # Kraken 1.0 requires Jellyfish 1.x 'Jellyfish': r'1\.', } if dep in old_dep_versions and len(dep_vars) > 1: for key in dep_vars.keys(): # filter out known old dependency versions if re.search('^version: %s' % old_dep_versions[dep], key): dep_vars.pop(key) # only single variant is always OK if len(dep_vars) == 1: res = True elif len(dep_vars) == 2 and dep in ['Python', 'Tkinter']: # for Python & Tkinter, it's OK to have on 2.x and one 3.x version v2_dep_vars = [ x for x in dep_vars.keys() if x.startswith('version: 2.') ] v3_dep_vars = [ x for x in dep_vars.keys() if x.startswith('version: 3.') ] if len(v2_dep_vars) == 1 and len(v3_dep_vars) == 1: res = True # two variants is OK if one is for Python 2.x and the other is for Python 3.x (based on versionsuffix) elif len(dep_vars) == 2: py2_dep_vars = [ x for x in dep_vars.keys() if '; versionsuffix: -Python-2.' in x ] py3_dep_vars = [ x for x in dep_vars.keys() if '; versionsuffix: -Python-3.' in x ] if len(py2_dep_vars) == 1 and len(py3_dep_vars) == 1: res = True return res # some software also follows <year>{a,b} versioning scheme, # which throws off the pattern matching done below for toolchain versions false_positives_regex = re.compile('^MATLAB-Engine-20[0-9][0-9][ab]') # restrict to checking dependencies of easyconfigs using common toolchains (start with 2018a) # and GCCcore subtoolchain for common toolchains, starting with GCCcore 7.x for pattern in [ '201[89][ab]', '20[2-9][0-9][ab]', 'GCCcore-[7-9]\.[0-9]' ]: all_deps = {} regex = re.compile('^.*-(?P<tc_gen>%s).*\.eb$' % pattern) # collect variants for all dependencies of easyconfigs that use a toolchain that matches for ec in self.ordered_specs: ec_file = os.path.basename(ec['spec']) # take into account software which also follows a <year>{a,b} versioning scheme ec_file = false_positives_regex.sub('', ec_file) res = regex.match(ec_file) if res: tc_gen = res.group('tc_gen') all_deps_tc_gen = all_deps.setdefault(tc_gen, {}) for dep_name, dep_ver, dep_versuff, dep_mod_name in get_deps_for( ec): dep_variants = all_deps_tc_gen.setdefault(dep_name, {}) # a variant is defined by version + versionsuffix variant = "version: %s; versionsuffix: %s" % ( dep_ver, dep_versuff) # keep track of which easyconfig this is a dependency dep_variants.setdefault(variant, set()).add(ec_file) # check which dependencies have more than 1 variant multi_dep_vars, multi_dep_vars_msg = [], '' for tc_gen in sorted(all_deps.keys()): for dep in sorted(all_deps[tc_gen].keys()): dep_vars = all_deps[tc_gen][dep] if not check_dep_vars(dep, dep_vars): multi_dep_vars.append(dep) multi_dep_vars_msg += "\nfound %s variants of '%s' dependency " % ( len(dep_vars), dep) multi_dep_vars_msg += "in easyconfigs using '%s' toolchain generation\n* " % tc_gen multi_dep_vars_msg += '\n* '.join( "%s as dep for %s" % v for v in sorted(dep_vars.items())) multi_dep_vars_msg += '\n' error_msg = "No multi-variant deps found for '%s' easyconfigs:\n%s" % ( regex.pattern, multi_dep_vars_msg) self.assertFalse(multi_dep_vars, error_msg) def test_sanity_check_paths(self): """Make sure specified sanity check paths adher to the requirements.""" if self.ordered_specs is None: self.process_all_easyconfigs() for ec in self.parsed_easyconfigs: ec_scp = ec['ec']['sanity_check_paths'] if ec_scp != {}: # if sanity_check_paths is specified (i.e., non-default), it must adher to the requirements # both 'files' and 'dirs' keys, both with list values and with at least one a non-empty list error_msg = "sanity_check_paths for %s does not meet requirements: %s" % ( ec['spec'], ec_scp) self.assertEqual(sorted(ec_scp.keys()), ['dirs', 'files'], error_msg) self.assertTrue(isinstance(ec_scp['dirs'], list), error_msg) self.assertTrue(isinstance(ec_scp['files'], list), error_msg) self.assertTrue(ec_scp['dirs'] or ec_scp['files'], error_msg) def test_easyconfig_locations(self): """Make sure all easyconfigs files are in the right location.""" easyconfig_dirs_regex = re.compile( r'/easybuild/easyconfigs/[0a-z]/[^/]+$') topdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) for (dirpath, _, filenames) in os.walk(topdir): # ignore git/svn dirs & archived easyconfigs if '/.git/' in dirpath or '/.svn/' in dirpath or '__archive__' in dirpath: continue # check whether list of .eb files is non-empty easyconfig_files = [fn for fn in filenames if fn.endswith('eb')] if easyconfig_files: # check whether path matches required pattern if not easyconfig_dirs_regex.search(dirpath): # only exception: TEMPLATE.eb if not (dirpath.endswith('/easybuild/easyconfigs') and filenames == ['TEMPLATE.eb']): self.assertTrue( False, "List of easyconfig files in %s is empty: %s" % (dirpath, filenames)) def check_sha256_checksums(self, changed_ecs): """Make sure changed easyconfigs have SHA256 checksums in place.""" # list of software for which checksums can not be required, # e.g. because 'source' files need to be constructed manually whitelist = ['Kent_tools-*', 'MATLAB-*', 'OCaml-*'] # the check_sha256_checksums function (again) creates an EasyBlock instance # for easyconfigs using the Bundle easyblock, this is a problem because the 'sources' easyconfig parameter # is updated in place (sources for components are added the 'parent' sources) in Bundle's __init__; # therefore, we need to reset 'sources' to an empty list here if Bundle is used... for ec in changed_ecs: if ec['easyblock'] == 'Bundle': ec['sources'] = [] # filter out deprecated easyconfigs retained_changed_ecs = [] for ec in changed_ecs: if not ec['deprecated']: retained_changed_ecs.append(ec) checksum_issues = check_sha256_checksums(retained_changed_ecs, whitelist=whitelist) self.assertTrue( len(checksum_issues) == 0, "No checksum issues:\n%s" % '\n'.join(checksum_issues)) def check_python_packages(self, changed_ecs): """Several checks for easyconfigs that install (bundles of) Python packages.""" # MATLAB-Engine, PyTorch do not support installation with 'pip' whitelist_pip = ['MATLAB-Engine-*', 'PyTorch-*'] failing_checks = [] for ec in changed_ecs: ec_fn = os.path.basename(ec.path) easyblock = ec.get('easyblock') exts_defaultclass = ec.get('exts_defaultclass') download_dep_fail = ec.get('download_dep_fail') exts_download_dep_fail = ec.get('exts_download_dep_fail') use_pip = ec.get('use_pip') # download_dep_fail should be set when using PythonPackage if easyblock == 'PythonPackage': if not download_dep_fail: failing_checks.append("'download_dep_fail' set in %s" % ec_fn) # use_pip should be set when using PythonPackage or PythonBundle (except for whitelisted easyconfigs) if easyblock in ['PythonBundle', 'PythonPackage']: if not use_pip and not any( re.match(regex, ec_fn) for regex in whitelist_pip): failing_checks.append("'use_pip' set in %s" % ec_fn) # download_dep_fail is enabled automatically in PythonBundle easyblock, so shouldn't be set if easyblock == 'PythonBundle': if download_dep_fail or exts_download_dep_fail: fail = "'*download_dep_fail' set in %s (shouldn't, since PythonBundle easyblock is used)" % ec_fn failing_checks.append(fail) elif exts_defaultclass == 'PythonPackage': # bundle of Python packages should use PythonBundle if easyblock == 'Bundle': fail = "'PythonBundle' easyblock is used for bundle of Python packages in %s" % ec_fn failing_checks.append(fail) else: # both download_dep_fail and use_pip should be set via exts_default_options # when installing Python packages as extensions exts_default_options = ec.get('exts_default_options', {}) for key in ['download_dep_fail', 'use_pip']: if not exts_default_options.get(key): failing_checks.append( "'%s' set in exts_default_options in %s" % (key, ec_fn)) # if Python is a dependency, that should be reflected in the versionsuffix if any(dep['name'] == 'Python' for dep in ec['dependencies']): if not re.search(r'-Python-[23]\.[0-9]+\.[0-9]+', ec['versionsuffix']): failing_checks.append( "'-Python-%%(pyver)s' included in versionsuffix in %s" % ec_fn) self.assertFalse(failing_checks, '\n'.join(failing_checks)) def test_changed_files_pull_request(self): """Specific checks only done for the (easyconfig) files that were changed in a pull request.""" # $TRAVIS_PULL_REQUEST should be a PR number, otherwise we're not running tests for a PR if re.match('^[0-9]+$', os.environ.get('TRAVIS_PULL_REQUEST', '(none)')): # target branch should be anything other than 'master'; # usually is 'develop', but could also be a release branch like '3.7.x' travis_branch = os.environ.get('TRAVIS_BRANCH', None) if travis_branch and travis_branch != 'master': if not self.parsed_easyconfigs: self.process_all_easyconfigs() # relocate to top-level directory of repository to run 'git diff' command top_dir = os.path.dirname( os.path.dirname(get_paths_for('easyconfigs')[0])) cwd = change_dir(top_dir) # get list of changed easyconfigs cmd = "git diff --name-only --diff-filter=AM %s...HEAD" % travis_branch out, ec = run_cmd(cmd, simple=False) changed_ecs_filenames = [ os.path.basename(f) for f in out.strip().split('\n') if f.endswith('.eb') ] print("\nList of changed easyconfig files in this PR: %s" % '\n'.join(changed_ecs_filenames)) change_dir(cwd) # grab parsed easyconfigs for changed easyconfig files changed_ecs = [] for ec_fn in changed_ecs_filenames: match = None for ec in self.parsed_easyconfigs: if os.path.basename(ec['spec']) == ec_fn: match = ec['ec'] break if match: changed_ecs.append(match) else: # if no easyconfig is found, it's possible some archived easyconfigs were touched in the PR... # so as a last resort, try to find the easyconfig file in __archive__ easyconfigs_path = get_paths_for("easyconfigs")[0] specs = glob.glob('%s/__archive__/*/*/%s' % (easyconfigs_path, ec_fn)) if len(specs) == 1: ec = process_easyconfig(specs[0])[0] changed_ecs.append(ec['ec']) else: error_msg = "Failed to find parsed easyconfig for %s" % ec_fn error_msg += " (and could not isolate it in easyconfigs archive either)" self.assertTrue(False, error_msg) # run checks on changed easyconfigs self.check_sha256_checksums(changed_ecs) self.check_python_packages(changed_ecs) def test_zzz_cleanup(self): """Dummy test to clean up global temporary directory.""" shutil.rmtree(self.TMPDIR)
def test_toolchain_external_modules(self): """Test use of Toolchain easyblock with external modules.""" external_modules = [ 'gcc/8.3.0', 'openmpi/4.0.2', 'openblas/0.3.7', 'fftw/3.3.8', 'scalapack/2.0.2' ] external_modules_metadata = { # all metadata for gcc/8.3.0 'gcc/8.3.0': { 'name': ['GCC'], 'version': ['8.3.0'], 'prefix': '/software/gcc/8.3.0', }, # only name/version for openmpi/4.0.2 'openmpi/4.0.2': { 'name': ['OpenMPI'], 'version': ['4.0.2'], }, # only name/prefix for openblas/0.3.7 'openblas/0.3.7': { 'name': ['OpenBLAS'], 'prefix': '/software/openblas/0.3.7', }, # only version/prefix for fftw/3.3.8 (no name) 'fftw/3.3.8': { 'version': ['3.3.8'], 'prefix': '/software/fftw/3.3.8', }, # no metadata for scalapack/2.0.2 } # initialize configuration cleanup() eb_go = eboptions.parse_options( args=['--installpath=%s' % self.tmpdir]) config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'external_modules_metadata': external_modules_metadata, 'valid_module_classes': config.module_classes(), } config.init_build_options(build_options=build_options) set_tmpdir() del eb_go modtool = modules_tool() # make sure no $EBROOT* or $EBVERSION* environment variables are set in current environment for key in os.environ: if any(key.startswith(x) for x in ['EBROOT', 'EBVERSION']): del os.environ[key] # create dummy module file for each of the external modules test_mod_path = os.path.join(self.tmpdir, 'modules', 'all') for mod in external_modules: write_file(os.path.join(test_mod_path, mod), "#%Module") modtool.use(test_mod_path) # test easyconfig file to install toolchain that uses external modules, # and enables set_env_external_modules test_ec_path = os.path.join(self.tmpdir, 'test.eb') test_ec_txt = '\n'.join([ "easyblock = 'Toolchain'", "name = 'test-toolchain'", "version = '1.2.3'", "homepage = 'https://example.com'", "description = 'just a test'", "toolchain = SYSTEM", "dependencies = [", " ('gcc/8.3.0', EXTERNAL_MODULE),", " ('openmpi/4.0.2', EXTERNAL_MODULE),", " ('openblas/0.3.7', EXTERNAL_MODULE),", " ('fftw/3.3.8', EXTERNAL_MODULE),", " ('scalapack/2.0.2', EXTERNAL_MODULE),", "]", "set_env_external_modules = True", "moduleclass = 'toolchain'", ]) write_file(test_ec_path, test_ec_txt) test_ec = process_easyconfig(test_ec_path)[0] # create easyblock & install module via run_all_steps tc_inst = get_easyblock_instance(test_ec) self.assertTrue(isinstance(tc_inst, Toolchain)) self.mock_stdout(True) tc_inst.run_all_steps(False) self.mock_stdout(False) # make sure expected module file exists test_mod = os.path.join(test_mod_path, 'test-toolchain', '1.2.3') if get_module_syntax() == 'Lua': test_mod += '.lua' self.assertTrue(os.path.exists(test_mod)) # load test-toolchain/1.2.3 module to get environment variable to check for defined modtool.load(['test-toolchain/1.2.3']) # check whether expected environment variables are defined self.assertEqual(os.environ.pop('EBROOTGCC'), '/software/gcc/8.3.0') self.assertEqual(os.environ.pop('EBVERSIONGCC'), '8.3.0') self.assertEqual(os.environ.pop('EBVERSIONOPENMPI'), '4.0.2') self.assertEqual(os.environ.pop('EBROOTOPENBLAS'), '/software/openblas/0.3.7') undefined_env_vars = [ 'EBROOTOPENMPI', # no prefix in metadata 'EBVERSIONOPENBLAS' # no version in metadata 'EBROOTFFTW', 'EBVERSIONFFTW', # no name in metadata 'EBROOTSCALAPACK', 'EBVERSIONSCALAPACK', # no metadata ] for env_var in undefined_env_vars: self.assertTrue(os.getenv(env_var) is None) # make sure no unexpected $EBROOT* or $EBVERSION* environment variables were defined del os.environ['EBROOTTESTMINTOOLCHAIN'] del os.environ['EBVERSIONTESTMINTOOLCHAIN'] extra_eb_env_vars = [] for key in os.environ: if any(key.startswith(x) for x in ['EBROOT', 'EBVERSION']): extra_eb_env_vars.append(key) self.assertEqual(extra_eb_env_vars, [])
def setUp(self): """Set up testcase.""" super(EnhancedTestCase, self).setUp() # keep track of log handlers log = fancylogger.getLogger(fname=False) self.orig_log_handlers = log.handlers[:] log.info("setting up test %s" % self.id()) self.orig_tmpdir = tempfile.gettempdir() # use a subdirectory for this test (which we can clean up easily after the test completes) self.test_prefix = set_tmpdir() self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) fd, self.logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-') os.close(fd) self.cwd = os.getcwd() # keep track of original environment to restore self.orig_environ = copy.deepcopy(os.environ) # keep track of original environment/Python search path to restore self.orig_sys_path = sys.path[:] testdir = os.path.dirname(os.path.abspath(__file__)) self.test_sourcepath = os.path.join(testdir, 'sandbox', 'sources') os.environ['EASYBUILD_SOURCEPATH'] = self.test_sourcepath os.environ['EASYBUILD_PREFIX'] = self.test_prefix self.test_buildpath = tempfile.mkdtemp() os.environ['EASYBUILD_BUILDPATH'] = self.test_buildpath self.test_installpath = tempfile.mkdtemp() os.environ['EASYBUILD_INSTALLPATH'] = self.test_installpath # make sure that the tests only pick up easyconfigs provided with the tests os.environ['EASYBUILD_ROBOT_PATHS'] = os.path.join(testdir, 'easyconfigs') # make sure no deprecated behaviour is being triggered (unless intended by the test) # trip *all* log.deprecated statements by setting deprecation version ridiculously high self.orig_current_version = eb_build_log.CURRENT_VERSION os.environ['EASYBUILD_DEPRECATED'] = '10000000' init_config() # remove any entries in Python search path that seem to provide easyblocks for path in sys.path[:]: if os.path.exists(os.path.join(path, 'easybuild', 'easyblocks', '__init__.py')): sys.path.remove(path) # add test easyblocks to Python search path and (re)import and reload easybuild modules import easybuild sys.path.append(os.path.join(testdir, 'sandbox')) reload(easybuild) import easybuild.easyblocks reload(easybuild.easyblocks) import easybuild.easyblocks.generic reload(easybuild.easyblocks.generic) reload(easybuild.tools.module_naming_scheme) # required to run options unit tests stand-alone modtool = modules_tool() # purge out any loaded modules with original $MODULEPATH before running each test modtool.purge() self.reset_modulepath([os.path.join(testdir, 'modules')])
def setUp(self): """Set up testcase.""" super(EnhancedTestCase, self).setUp() # make sure option parser doesn't pick up any cmdline arguments/options while len(sys.argv) > 1: sys.argv.pop() # keep track of log handlers log = fancylogger.getLogger(fname=False) self.orig_log_handlers = log.handlers[:] log.info("setting up test %s" % self.id()) self.orig_tmpdir = tempfile.gettempdir() # use a subdirectory for this test (which we can clean up easily after the test completes) self.test_prefix = set_tmpdir() self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) fd, self.logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-') os.close(fd) self.cwd = os.getcwd() # keep track of original environment to restore self.orig_environ = copy.deepcopy(os.environ) # keep track of original environment/Python search path to restore self.orig_sys_path = sys.path[:] testdir = os.path.dirname(os.path.abspath(__file__)) self.test_sourcepath = os.path.join(testdir, 'sandbox', 'sources') os.environ['EASYBUILD_SOURCEPATH'] = self.test_sourcepath os.environ['EASYBUILD_PREFIX'] = self.test_prefix self.test_buildpath = tempfile.mkdtemp() os.environ['EASYBUILD_BUILDPATH'] = self.test_buildpath self.test_installpath = tempfile.mkdtemp() os.environ['EASYBUILD_INSTALLPATH'] = self.test_installpath # make sure that the tests only pick up easyconfigs provided with the tests os.environ['EASYBUILD_ROBOT_PATHS'] = os.path.join(testdir, 'easyconfigs') # make sure no deprecated behaviour is being triggered (unless intended by the test) # trip *all* log.deprecated statements by setting deprecation version ridiculously high self.orig_current_version = eb_build_log.CURRENT_VERSION os.environ['EASYBUILD_DEPRECATED'] = '10000000' init_config() import easybuild # try to import easybuild.easyblocks(.generic) packages # it's OK if it fails here, but important to import first before fiddling with sys.path try: import easybuild.easyblocks import easybuild.easyblocks.generic except ImportError: pass # add sandbox to Python search path, update namespace packages sys.path.append(os.path.join(testdir, 'sandbox')) # workaround for bug in recent setuptools version (19.4 and newer, atleast until 20.3.1) # injecting <prefix>/easybuild is required to avoid a ValueError being thrown by fixup_namespace_packages # cfr. https://bitbucket.org/pypa/setuptools/issues/520/fixup_namespace_packages-may-trigger for path in sys.path[:]: if os.path.exists(os.path.join(path, 'easybuild', 'easyblocks', '__init__.py')): # keep track of 'easybuild' paths to inject into sys.path later sys.path.append(os.path.join(path, 'easybuild')) # required to make sure the 'easybuild' dir in the sandbox is picked up; # this relates to the other 'reload' statements below reload(easybuild) # this is strictly required to make the test modules in the sandbox available, due to declare_namespace fixup_namespace_packages(os.path.join(testdir, 'sandbox')) # remove any entries in Python search path that seem to provide easyblocks (except the sandbox) for path in sys.path[:]: if os.path.exists(os.path.join(path, 'easybuild', 'easyblocks', '__init__.py')): if not os.path.samefile(path, os.path.join(testdir, 'sandbox')): sys.path.remove(path) # hard inject location to (generic) test easyblocks into Python search path # only prepending to sys.path is not enough due to 'declare_namespace' in easybuild/easyblocks/__init__.py import easybuild.easyblocks reload(easybuild.easyblocks) test_easyblocks_path = os.path.join(testdir, 'sandbox', 'easybuild', 'easyblocks') easybuild.easyblocks.__path__.insert(0, test_easyblocks_path) import easybuild.easyblocks.generic reload(easybuild.easyblocks.generic) test_easyblocks_path = os.path.join(test_easyblocks_path, 'generic') easybuild.easyblocks.generic.__path__.insert(0, test_easyblocks_path) # save values of $PATH & $PYTHONPATH, so they can be restored later # this is important in case EasyBuild was installed as a module, since that module may be unloaded, # for example due to changes to $MODULEPATH in case EasyBuild was installed in a module hierarchy # cfr. https://github.com/hpcugent/easybuild-framework/issues/1685 self.env_path = os.environ['PATH'] self.env_pythonpath = os.environ['PYTHONPATH'] self.modtool = modules_tool() self.reset_modulepath([os.path.join(testdir, 'modules')]) reset_module_caches()
import test.framework.repository as r import test.framework.robot as robot import test.framework.run as run import test.framework.scripts as sc import test.framework.systemtools as s import test.framework.toolchain as tc import test.framework.toolchainvariables as tcv import test.framework.toy_build as t import test.framework.type_checking as et import test.framework.tweak as tw import test.framework.variables as v import test.framework.yeb as y # make sure temporary files can be created/used try: set_tmpdir(raise_error=True) except EasyBuildError, err: sys.stderr.write( "No execution rights on temporary files, specify another location via $TMPDIR: %s\n" % err) sys.exit(1) # initialize logger for all the unit tests fd, log_fn = tempfile.mkstemp(prefix='easybuild-tests-', suffix='.log') os.close(fd) os.remove(log_fn) fancylogger.logToFile(log_fn) log = fancylogger.getLogger() # call suite() for each module and then run them all # note: make sure the options unit tests run first, to avoid running some of them with a readily initialized config
def suite(): """Return all easyblock --module-only tests.""" def make_inner_test(easyblock, **kwargs): def innertest(self): template_module_only_test(self, easyblock, **kwargs) return innertest # initialize configuration (required for e.g. default modules_tool setting) cleanup() eb_go = eboptions.parse_options(args=['--prefix=%s' % TMPDIR]) config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'external_modules_metadata': {}, # enable --force --module-only 'force': True, 'module_only': True, 'silent': True, 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() # dynamically generate a separate test for each of the available easyblocks easyblocks_path = get_paths_for("easyblocks")[0] all_pys = glob.glob('%s/*/*.py' % easyblocks_path) easyblocks = [ eb for eb in all_pys if os.path.basename(eb) != '__init__.py' and '/test/' not in eb ] # filter out no longer supported easyblocks, or easyblocks that are tested in a different way excluded_easyblocks = ['versionindependendpythonpackage.py'] easyblocks = [ e for e in easyblocks if os.path.basename(e) not in excluded_easyblocks ] # add dummy PrgEnv-* modules, required for testing CrayToolchain easyblock for prgenv in ['PrgEnv-cray', 'PrgEnv-gnu', 'PrgEnv-intel', 'PrgEnv-pgi']: write_file(os.path.join(TMPDIR, 'modules', 'all', prgenv, '1.2.3'), "#%Module") # add foo/1.3.2.1.1 module, required for testing ModuleAlias easyblock write_file(os.path.join(TMPDIR, 'modules', 'all', 'foo', '1.2.3.4.5'), "#%Module") for easyblock in easyblocks: eb_fn = os.path.basename(easyblock) # dynamically define new inner functions that can be added as class methods to ModuleOnlyTest if eb_fn == 'systemcompiler.py': # use GCC as name when testing SystemCompiler easyblock innertest = make_inner_test(easyblock, name='GCC', version='system') elif eb_fn == 'systemmpi.py': # use OpenMPI as name when testing SystemMPI easyblock innertest = make_inner_test(easyblock, name='OpenMPI', version='system') elif eb_fn == 'craytoolchain.py': # make sure that a (known) PrgEnv is included as a dependency extra_txt = 'dependencies = [("PrgEnv-gnu/1.2.3", EXTERNAL_MODULE)]' innertest = make_inner_test(easyblock, name='CrayCC', extra_txt=extra_txt) elif eb_fn == 'modulerc.py': # exactly one dependency is included with ModuleRC generic easyblock (and name must match) extra_txt = 'dependencies = [("foo", "1.2.3.4.5")]' innertest = make_inner_test(easyblock, name='foo', version='1.2.3.4', extra_txt=extra_txt) elif eb_fn == 'intel_compilers.py': # custom easyblock for intel-compilers (oneAPI) requires v2021.x or newer innertest = make_inner_test(easyblock, name='intel-compilers', version='2021.1') elif eb_fn == 'openssl_wrapper.py': # easyblock to create OpenSSL wrapper expects an OpenSSL version innertest = make_inner_test(easyblock, name='OpenSSL-wrapper', version='1.1') elif eb_fn == 'ucx_plugins.py': # install fake ucx_info command (used in make_module_extra) tmpdir = tempfile.mkdtemp() install_fake_command('ucx_info', FAKE_UCX_INFO, tmpdir) innertest = make_inner_test(easyblock, name='UCX-CUDA', tmpdir=tmpdir) else: # Make up some unique name innertest = make_inner_test(easyblock, name=eb_fn.replace('.', '-') + '-sw') innertest.__doc__ = "Test for using --module-only with easyblock %s" % easyblock innertest.__name__ = "test_easyblock_%s" % '_'.join( easyblock.replace('.py', '').split('/')) setattr(ModuleOnlyTest, innertest.__name__, innertest) return TestLoader().loadTestsFromTestCase(ModuleOnlyTest)
class EasyConfigTest(TestCase): """Baseclass for easyconfig testcases.""" # initialize configuration (required for e.g. default modules_tool setting) eb_go = eboptions.parse_options() config.init(eb_go.options, eb_go.get_options_by_section('config')) build_options = { 'check_osdeps': False, 'external_modules_metadata': {}, 'force': True, 'local_var_naming_check': 'error', 'optarch': 'test', 'robot_path': get_paths_for("easyconfigs")[0], 'silent': True, 'suffix_modules_path': GENERAL_CLASS, 'valid_module_classes': config.module_classes(), 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } config.init_build_options(build_options=build_options) set_tmpdir() del eb_go # put dummy 'craype-test' module in place, which is required for parsing easyconfigs using Cray* toolchains TMPDIR = tempfile.mkdtemp() os.environ['MODULEPATH'] = TMPDIR write_file(os.path.join(TMPDIR, 'craype-test'), '#%Module\n') log = fancylogger.getLogger("EasyConfigTest", fname=False) # make sure a logger is present for main eb_main._log = log ordered_specs = None parsed_easyconfigs = [] def process_all_easyconfigs(self): """Process all easyconfigs and resolve inter-easyconfig dependencies.""" # all available easyconfig files easyconfigs_path = get_paths_for("easyconfigs")[0] specs = glob.glob('%s/*/*/*.eb' % easyconfigs_path) # parse all easyconfigs if they haven't been already if not EasyConfigTest.parsed_easyconfigs: for spec in specs: EasyConfigTest.parsed_easyconfigs.extend(process_easyconfig(spec)) # filter out external modules for ec in EasyConfigTest.parsed_easyconfigs: for dep in ec['dependencies'][:]: if dep.get('external_module', False): ec['dependencies'].remove(dep) EasyConfigTest.ordered_specs = resolve_dependencies(EasyConfigTest.parsed_easyconfigs, modules_tool(), retain_all_deps=True) def test_dep_graph(self): """Unit test that builds a full dependency graph.""" # pygraph dependencies required for constructing dependency graph are not available prior to Python 2.6 if LooseVersion(sys.version) >= LooseVersion('2.6') and single_tests_ok: # temporary file for dep graph (hn, fn) = tempfile.mkstemp(suffix='.dot') os.close(hn) if EasyConfigTest.ordered_specs is None: self.process_all_easyconfigs() dep_graph(fn, EasyConfigTest.ordered_specs) remove_file(fn) else: print("(skipped dep graph test)") def test_conflicts(self): """Check whether any conflicts occur in software dependency graphs.""" if not single_tests_ok: print("(skipped conflicts test)") return if EasyConfigTest.ordered_specs is None: self.process_all_easyconfigs() self.assertFalse(check_conflicts(EasyConfigTest.ordered_specs, modules_tool(), check_inter_ec_conflicts=False), "No conflicts detected") def check_dep_vars(self, dep, dep_vars): """Check whether available variants of a particular dependency are acceptable or not.""" # 'guilty' until proven 'innocent' res = False # filter out wrapped Java versions # i.e. if the version of one is a prefix of the version of the other one (e.g. 1.8 & 1.8.0_181) if dep == 'Java': dep_vars_to_check = sorted(dep_vars.keys()) retained_dep_vars = [] while dep_vars_to_check: dep_var = dep_vars_to_check.pop() dep_var_version = dep_var.split(';')[0] # remove dep vars wrapped by current dep var dep_vars_to_check = [x for x in dep_vars_to_check if not x.startswith(dep_var_version + '.')] retained_dep_vars = [x for x in retained_dep_vars if not x.startswith(dep_var_version + '.')] retained_dep_vars.append(dep_var) for key in list(dep_vars.keys()): if key not in retained_dep_vars: del dep_vars[key] # filter out binutils with empty versionsuffix which is used to build toolchain compiler if dep == 'binutils' and len(dep_vars) > 1: empty_vsuff_vars = [v for v in dep_vars.keys() if v.endswith('versionsuffix: ')] if len(empty_vsuff_vars) == 1: dep_vars = dict((k, v) for (k, v) in dep_vars.items() if k != empty_vsuff_vars[0]) # multiple variants of HTSlib is OK as long as they are deps for a matching version of BCFtools; # same goes for WRF and WPS for dep_name, parent_name in [('HTSlib', 'BCFtools'), ('WRF', 'WPS')]: if dep == dep_name and len(dep_vars) > 1: for key in list(dep_vars): ecs = dep_vars[key] # filter out dep variants that are only used as dependency for parent with same version dep_ver = re.search('^version: (?P<ver>[^;]+);', key).group('ver') if all(ec.startswith('%s-%s-' % (parent_name, dep_ver)) for ec in ecs) and len(dep_vars) > 1: dep_vars.pop(key) # multiple versions of Boost is OK as long as they are deps for a matching Boost.Python if dep == 'Boost' and len(dep_vars) > 1: for key in list(dep_vars): ecs = dep_vars[key] # filter out Boost variants that are only used as dependency for Boost.Python with same version boost_ver = re.search('^version: (?P<ver>[^;]+);', key).group('ver') if all(ec.startswith('Boost.Python-%s-' % boost_ver) for ec in ecs): dep_vars.pop(key) # filter out FFTW and imkl with -serial versionsuffix which are used in non-MPI subtoolchains if dep in ['FFTW', 'imkl']: serial_vsuff_vars = [v for v in dep_vars.keys() if v.endswith('versionsuffix: -serial')] if len(serial_vsuff_vars) == 1: dep_vars = dict((k, v) for (k, v) in dep_vars.items() if k != serial_vsuff_vars[0]) # for some dependencies, we allow exceptions for software that depends on a particular version, # as long as that's indicated by the versionsuffix if dep in ['ASE', 'Boost', 'Java', 'Lua', 'PLUMED', 'R', 'TensorFlow'] and len(dep_vars) > 1: for key in list(dep_vars): dep_ver = re.search('^version: (?P<ver>[^;]+);', key).group('ver') # use version of Java wrapper rather than full Java version if dep == 'Java': dep_ver = '.'.join(dep_ver.split('.')[:2]) # filter out dep version if all easyconfig filenames using it include specific dep version if all(re.search('-%s-%s' % (dep, dep_ver), v) for v in dep_vars[key]): dep_vars.pop(key) # always retain at least one dep variant if len(dep_vars) == 1: break # filter R dep for a specific version of Python 2.x if dep == 'R' and len(dep_vars) > 1: for key in list(dep_vars): if '; versionsuffix: -Python-2' in key: dep_vars.pop(key) # always retain at least one variant if len(dep_vars) == 1: break # filter out variants that are specific to a particular version of CUDA cuda_dep_vars = [v for v in dep_vars.keys() if '-CUDA' in v] if len(dep_vars) > len(cuda_dep_vars): for key in list(dep_vars): if re.search('; versionsuffix: .*-CUDA-[0-9.]+', key): dep_vars.pop(key) # some software packages require an old version of a particular dependency old_dep_versions = { # libxc 2.x or 3.x is required by ABINIT, AtomPAW, CP2K, GPAW, horton, PySCF, WIEN2k # (Qiskit depends on PySCF) 'libxc': (r'[23]\.', ['ABINIT-', 'AtomPAW-', 'CP2K-', 'GPAW-', 'horton-', 'PySCF-', 'Qiskit-', 'WIEN2k-']), # OPERA requires SAMtools 0.x 'SAMtools': (r'0\.', ['ChimPipe-0.9.5', 'Cufflinks-2.2.1', 'OPERA-2.0.6']), # Kraken 1.x requires Jellyfish 1.x (Roary & metaWRAP depend on Kraken 1.x) 'Jellyfish': (r'1\.', ['Kraken-1.', 'Roary-3.12.0', 'metaWRAP-1.2']), # EMAN2 2.3 requires Boost(.Python) 1.64.0 'Boost': ('1.64.0;', ['Boost.Python-1.64.0-', 'EMAN2-2.3-']), 'Boost.Python': ('1.64.0;', ['EMAN2-2.3-']), } if dep in old_dep_versions and len(dep_vars) > 1: for key in list(dep_vars): version_pattern, parents = old_dep_versions[dep] # filter out known old dependency versions if re.search('^version: %s' % version_pattern, key): # only filter if the easyconfig using this dep variants is known if all(any(x.startswith(p) for p in parents) for x in dep_vars[key]): dep_vars.pop(key) # only single variant is always OK if len(dep_vars) == 1: res = True elif len(dep_vars) == 2 and dep in ['Python', 'Tkinter']: # for Python & Tkinter, it's OK to have on 2.x and one 3.x version v2_dep_vars = [x for x in dep_vars.keys() if x.startswith('version: 2.')] v3_dep_vars = [x for x in dep_vars.keys() if x.startswith('version: 3.')] if len(v2_dep_vars) == 1 and len(v3_dep_vars) == 1: res = True # two variants is OK if one is for Python 2.x and the other is for Python 3.x (based on versionsuffix) elif len(dep_vars) == 2: py2_dep_vars = [x for x in dep_vars.keys() if '; versionsuffix: -Python-2.' in x] py3_dep_vars = [x for x in dep_vars.keys() if '; versionsuffix: -Python-3.' in x] if len(py2_dep_vars) == 1 and len(py3_dep_vars) == 1: res = True return res def test_check_dep_vars(self): """Test check_dep_vars utility method.""" # one single dep version: OK self.assertTrue(self.check_dep_vars('testdep', { 'version: 1.2.3; versionsuffix:': ['foo-1.2.3.eb', 'bar-4.5.6.eb'], })) self.assertTrue(self.check_dep_vars('testdep', { 'version: 1.2.3; versionsuffix: -test': ['foo-1.2.3.eb', 'bar-4.5.6.eb'], })) # two or more dep versions (no special case: not OK) self.assertFalse(self.check_dep_vars('testdep', { 'version: 1.2.3; versionsuffix:': ['foo-1.2.3.eb'], 'version: 4.5.6; versionsuffix:': ['bar-4.5.6.eb'], })) self.assertFalse(self.check_dep_vars('testdep', { 'version: 0.0; versionsuffix:': ['foobar-0.0.eb'], 'version: 1.2.3; versionsuffix:': ['foo-1.2.3.eb'], 'version: 4.5.6; versionsuffix:': ['bar-4.5.6.eb'], })) # Java is a special case, with wrapped Java versions self.assertTrue(self.check_dep_vars('Java', { 'version: 1.8.0_221; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], })) # two Java wrappers is not OK self.assertFalse(self.check_dep_vars('Java', { 'version: 1.8.0_221; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11.0.2; versionsuffix:': ['bar-4.5.6.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6.eb'], })) # OK to have two or more wrappers if versionsuffix is used to indicate exception self.assertTrue(self.check_dep_vars('Java', { 'version: 1.8.0_221; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11.0.2; versionsuffix:': ['bar-4.5.6-Java-11.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6-Java-11.eb'], })) # versionsuffix must be there for all easyconfigs to indicate exception self.assertFalse(self.check_dep_vars('Java', { 'version: 1.8.0_221; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11.0.2; versionsuffix:': ['bar-4.5.6-Java-11.eb', 'bar-4.5.6.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6-Java-11.eb', 'bar-4.5.6.eb'], })) self.assertTrue(self.check_dep_vars('Java', { 'version: 1.8.0_221; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11.0.2; versionsuffix:': ['bar-4.5.6-Java-11.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6-Java-11.eb'], 'version: 12.1.6; versionsuffix:': ['foobar-0.0-Java-12.eb'], 'version: 12; versionsuffix:': ['foobar-0.0-Java-12.eb'], })) # strange situation: odd number of Java versions # not OK: two Java wrappers (and no versionsuffix to indicate exception) self.assertFalse(self.check_dep_vars('Java', { 'version: 1.8.0_221; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6.eb'], })) # OK because of -Java-11 versionsuffix self.assertTrue(self.check_dep_vars('Java', { 'version: 1.8.0_221; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6-Java-11.eb'], })) # not OK: two Java wrappers (and no versionsuffix to indicate exception) self.assertFalse(self.check_dep_vars('Java', { 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11.0.2; versionsuffix:': ['bar-4.5.6.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6.eb'], })) # OK because of -Java-11 versionsuffix self.assertTrue(self.check_dep_vars('Java', { 'version: 1.8; versionsuffix:': ['foo-1.2.3.eb'], 'version: 11.0.2; versionsuffix:': ['bar-4.5.6-Java-11.eb'], 'version: 11; versionsuffix:': ['bar-4.5.6-Java-11.eb'], })) # two different versions of Boost is not OK self.assertFalse(self.check_dep_vars('Boost', { 'version: 1.64.0; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.70.0; versionsuffix:': ['foo-2.3.4.eb'], })) # a different Boost version that is only used as dependency for a matching Boost.Python is fine self.assertTrue(self.check_dep_vars('Boost', { 'version: 1.64.0; versionsuffix:': ['Boost.Python-1.64.0-gompi-2019a.eb'], 'version: 1.70.0; versionsuffix:': ['foo-2.3.4.eb'], })) self.assertTrue(self.check_dep_vars('Boost', { 'version: 1.64.0; versionsuffix:': ['Boost.Python-1.64.0-gompi-2018b.eb'], 'version: 1.66.0; versionsuffix:': ['Boost.Python-1.66.0-gompi-2019a.eb'], 'version: 1.70.0; versionsuffix:': ['foo-2.3.4.eb'], })) self.assertFalse(self.check_dep_vars('Boost', { 'version: 1.64.0; versionsuffix:': ['Boost.Python-1.64.0-gompi-2019a.eb'], 'version: 1.66.0; versionsuffix:': ['foo-1.2.3.eb'], 'version: 1.70.0; versionsuffix:': ['foo-2.3.4.eb'], })) self.assertTrue(self.check_dep_vars('Boost', { 'version: 1.63.0; versionsuffix: -Python-2.7.14': ['EMAN2-2.21a-foss-2018a-Python-2.7.14-Boost-1.63.0.eb'], 'version: 1.64.0; versionsuffix:': ['Boost.Python-1.64.0-gompi-2018a.eb'], 'version: 1.66.0; versionsuffix:': ['BLAST+-2.7.1-foss-2018a.eb'], })) self.assertTrue(self.check_dep_vars('Boost', { 'version: 1.64.0; versionsuffix:': [ 'Boost.Python-1.64.0-gompi-2019a.eb', 'EMAN2-2.3-foss-2019a-Python-2.7.15.eb', ], 'version: 1.70.0; versionsuffix:': [ 'BLAST+-2.9.0-gompi-2019a.eb', 'Boost.Python-1.70.0-gompi-2019a.eb', ], })) def test_dep_versions_per_toolchain_generation(self): """ Check whether there's only one dependency version per toolchain generation actively used. This is enforced to try and limit the chance of running into conflicts when multiple modules built with the same toolchain are loaded together. """ if EasyConfigTest.ordered_specs is None: self.process_all_easyconfigs() def get_deps_for(ec): """Get list of (direct) dependencies for specified easyconfig.""" deps = [] for dep in ec['ec']['dependencies']: dep_mod_name = dep['full_mod_name'] deps.append((dep['name'], dep['version'], dep['versionsuffix'], dep_mod_name)) res = [x for x in EasyConfigTest.ordered_specs if x['full_mod_name'] == dep_mod_name] if len(res) == 1: deps.extend(get_deps_for(res[0])) else: raise EasyBuildError("Failed to find %s in ordered list of easyconfigs", dep_mod_name) return deps # some software also follows <year>{a,b} versioning scheme, # which throws off the pattern matching done below for toolchain versions false_positives_regex = re.compile('^MATLAB-Engine-20[0-9][0-9][ab]') # restrict to checking dependencies of easyconfigs using common toolchains (start with 2018a) # and GCCcore subtoolchain for common toolchains, starting with GCCcore 7.x for pattern in ['201[89][ab]', '20[2-9][0-9][ab]', 'GCCcore-[7-9]\.[0-9]']: all_deps = {} regex = re.compile('^.*-(?P<tc_gen>%s).*\.eb$' % pattern) # collect variants for all dependencies of easyconfigs that use a toolchain that matches for ec in EasyConfigTest.ordered_specs: ec_file = os.path.basename(ec['spec']) # take into account software which also follows a <year>{a,b} versioning scheme ec_file = false_positives_regex.sub('', ec_file) res = regex.match(ec_file) if res: tc_gen = res.group('tc_gen') all_deps_tc_gen = all_deps.setdefault(tc_gen, {}) for dep_name, dep_ver, dep_versuff, dep_mod_name in get_deps_for(ec): dep_variants = all_deps_tc_gen.setdefault(dep_name, {}) # a variant is defined by version + versionsuffix variant = "version: %s; versionsuffix: %s" % (dep_ver, dep_versuff) # keep track of which easyconfig this is a dependency dep_variants.setdefault(variant, set()).add(ec_file) # check which dependencies have more than 1 variant multi_dep_vars, multi_dep_vars_msg = [], '' for tc_gen in sorted(all_deps.keys()): for dep in sorted(all_deps[tc_gen].keys()): dep_vars = all_deps[tc_gen][dep] if not self.check_dep_vars(dep, dep_vars): multi_dep_vars.append(dep) multi_dep_vars_msg += "\nfound %s variants of '%s' dependency " % (len(dep_vars), dep) multi_dep_vars_msg += "in easyconfigs using '%s' toolchain generation\n* " % tc_gen multi_dep_vars_msg += '\n* '.join("%s as dep for %s" % v for v in sorted(dep_vars.items())) multi_dep_vars_msg += '\n' error_msg = "No multi-variant deps found for '%s' easyconfigs:\n%s" % (regex.pattern, multi_dep_vars_msg) self.assertFalse(multi_dep_vars, error_msg) def test_sanity_check_paths(self): """Make sure specified sanity check paths adher to the requirements.""" if EasyConfigTest.ordered_specs is None: self.process_all_easyconfigs() for ec in EasyConfigTest.parsed_easyconfigs: ec_scp = ec['ec']['sanity_check_paths'] if ec_scp != {}: # if sanity_check_paths is specified (i.e., non-default), it must adher to the requirements # both 'files' and 'dirs' keys, both with list values and with at least one a non-empty list error_msg = "sanity_check_paths for %s does not meet requirements: %s" % (ec['spec'], ec_scp) self.assertEqual(sorted(ec_scp.keys()), ['dirs', 'files'], error_msg) self.assertTrue(isinstance(ec_scp['dirs'], list), error_msg) self.assertTrue(isinstance(ec_scp['files'], list), error_msg) self.assertTrue(ec_scp['dirs'] or ec_scp['files'], error_msg) def test_easyconfig_locations(self): """Make sure all easyconfigs files are in the right location.""" easyconfig_dirs_regex = re.compile(r'/easybuild/easyconfigs/[0a-z]/[^/]+$') topdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) for (dirpath, _, filenames) in os.walk(topdir): # ignore git/svn dirs & archived easyconfigs if '/.git/' in dirpath or '/.svn/' in dirpath or '__archive__' in dirpath: continue # check whether list of .eb files is non-empty easyconfig_files = [fn for fn in filenames if fn.endswith('eb')] if easyconfig_files: # check whether path matches required pattern if not easyconfig_dirs_regex.search(dirpath): # only exception: TEMPLATE.eb if not (dirpath.endswith('/easybuild/easyconfigs') and filenames == ['TEMPLATE.eb']): self.assertTrue(False, "List of easyconfig files in %s is empty: %s" % (dirpath, filenames)) def check_sha256_checksums(self, changed_ecs): """Make sure changed easyconfigs have SHA256 checksums in place.""" # list of software for which checksums can not be required, # e.g. because 'source' files need to be constructed manually whitelist = ['Kent_tools-*', 'MATLAB-*', 'OCaml-*'] # the check_sha256_checksums function (again) creates an EasyBlock instance # for easyconfigs using the Bundle easyblock, this is a problem because the 'sources' easyconfig parameter # is updated in place (sources for components are added the 'parent' sources) in Bundle's __init__; # therefore, we need to reset 'sources' to an empty list here if Bundle is used... # likewise for 'patches' and 'checksums' for ec in changed_ecs: if ec['easyblock'] == 'Bundle': ec['sources'] = [] ec['patches'] = [] ec['checksums'] = [] # filter out deprecated easyconfigs retained_changed_ecs = [] for ec in changed_ecs: if not ec['deprecated']: retained_changed_ecs.append(ec) checksum_issues = check_sha256_checksums(retained_changed_ecs, whitelist=whitelist) self.assertTrue(len(checksum_issues) == 0, "No checksum issues:\n%s" % '\n'.join(checksum_issues)) def check_python_packages(self, changed_ecs, added_ecs_filenames): """Several checks for easyconfigs that install (bundles of) Python packages.""" # These packages do not support installation with 'pip' whitelist_pip = [r'MATLAB-Engine-.*', r'PyTorch-.*', r'Meld-.*'] failing_checks = [] for ec in changed_ecs: ec_fn = os.path.basename(ec.path) easyblock = ec.get('easyblock') exts_defaultclass = ec.get('exts_defaultclass') exts_default_options = ec.get('exts_default_options', {}) download_dep_fail = ec.get('download_dep_fail') exts_download_dep_fail = ec.get('exts_download_dep_fail') use_pip = ec.get('use_pip') # download_dep_fail should be set when using PythonPackage if easyblock == 'PythonPackage': if download_dep_fail is None: failing_checks.append("'download_dep_fail' set in %s" % ec_fn) # use_pip should be set when using PythonPackage or PythonBundle (except for whitelisted easyconfigs) if easyblock in ['PythonBundle', 'PythonPackage']: if use_pip is None and not any(re.match(regex, ec_fn) for regex in whitelist_pip): failing_checks.append("'use_pip' set in %s" % ec_fn) # download_dep_fail is enabled automatically in PythonBundle easyblock, so shouldn't be set if easyblock == 'PythonBundle': if download_dep_fail or exts_download_dep_fail: fail = "'*download_dep_fail' set in %s (shouldn't, since PythonBundle easyblock is used)" % ec_fn failing_checks.append(fail) elif exts_defaultclass == 'PythonPackage': # bundle of Python packages should use PythonBundle if easyblock == 'Bundle': fail = "'PythonBundle' easyblock is used for bundle of Python packages in %s" % ec_fn failing_checks.append(fail) else: # both download_dep_fail and use_pip should be set via exts_default_options # when installing Python packages as extensions for key in ['download_dep_fail', 'use_pip']: if exts_default_options.get(key) is None: failing_checks.append("'%s' set in exts_default_options in %s" % (key, ec_fn)) # if Python is a dependency, that should be reflected in the versionsuffix # Tkinter is an exception, since its version always matches the Python version anyway if any(dep['name'] == 'Python' for dep in ec['dependencies']) and ec.name != 'Tkinter': if not re.search(r'-Python-[23]\.[0-9]+\.[0-9]+', ec['versionsuffix']): msg = "'-Python-%%(pyver)s' included in versionsuffix in %s" % ec_fn # This is only a failure for newly added ECs, not for existing ECS # As that would probably break many ECs if ec_fn in added_ecs_filenames: failing_checks.append(msg) else: print('\nNote: Failed non-critical check: ' + msg) # require that running of "pip check" during sanity check is enabled via sanity_pip_check if use_pip and easyblock in ['PythonBundle', 'PythonPackage']: sanity_pip_check = ec.get('sanity_pip_check') or exts_default_options.get('sanity_pip_check') if not sanity_pip_check and not any(re.match(regex, ec_fn) for regex in whitelist_pip): failing_checks.append("sanity_pip_check is enabled in %s" % ec_fn) self.assertFalse(failing_checks, '\n'.join(failing_checks)) def check_sanity_check_paths(self, changed_ecs): """Make sure a custom sanity_check_paths value is specified for easyconfigs that use a generic easyblock.""" # PythonBundle & PythonPackage already have a decent customised sanity_check_paths # BuildEnv, ModuleRC and Toolchain easyblocks doesn't install anything so there is nothing to check. whitelist = ['CrayToolchain', 'ModuleRC', 'PythonBundle', 'PythonPackage', 'Toolchain', 'BuildEnv'] # Autotools & (recent) GCC are just bundles (Autotools: Autoconf+Automake+libtool, GCC: GCCcore+binutils) bundles_whitelist = ['Autotools', 'GCC'] failing_checks = [] for ec in changed_ecs: easyblock = ec.get('easyblock') if is_generic_easyblock(easyblock) and not ec.get('sanity_check_paths'): if easyblock in whitelist or (easyblock == 'Bundle' and ec['name'] in bundles_whitelist): pass else: ec_fn = os.path.basename(ec.path) failing_checks.append("No custom sanity_check_paths found in %s" % ec_fn) self.assertFalse(failing_checks, '\n'.join(failing_checks)) def check_https(self, changed_ecs): """Make sure https:// URL is used (if it exists) for homepage/source_urls (rather than http://).""" whitelist = [ 'Kaiju', # invalid certificate at https://kaiju.binf.ku.dk 'libxml2', # https://xmlsoft.org works, but invalid certificate 'p4vasp', # https://www.p4vasp.at doesn't work 'ITSTool', # https://itstool.org/ doesn't work 'UCX-', # bad certificate for https://www.openucx.org ] http_regex = re.compile('http://[^"\'\n]+', re.M) failing_checks = [] for ec in changed_ecs: ec_fn = os.path.basename(ec.path) # skip whitelisted easyconfigs if any(ec_fn.startswith(x) for x in whitelist): continue # ignore commented out lines in easyconfig files when checking for http:// URLs ec_txt = '\n'.join(l for l in ec.rawtxt.split('\n') if not l.startswith('#')) for http_url in http_regex.findall(ec_txt): https_url = http_url.replace('http://', 'https://') try: https_url_works = bool(urlopen(https_url, timeout=5)) except Exception: https_url_works = False if https_url_works: failing_checks.append("Found http:// URL in %s, should be https:// : %s" % (ec_fn, http_url)) self.assertFalse(failing_checks, '\n'.join(failing_checks)) def test_changed_files_pull_request(self): """Specific checks only done for the (easyconfig) files that were changed in a pull request.""" def get_eb_files_from_diff(diff_filter): cmd = "git diff --name-only --diff-filter=%s %s...HEAD" % (diff_filter, target_branch) out, ec = run_cmd(cmd, simple=False) return [os.path.basename(f) for f in out.strip().split('\n') if f.endswith('.eb')] # $TRAVIS_PULL_REQUEST should be a PR number, otherwise we're not running tests for a PR travis_pr_test = re.match('^[0-9]+$', os.environ.get('TRAVIS_PULL_REQUEST', '(none)')) # when testing a PR in GitHub Actions, $GITHUB_EVENT_NAME will be set to 'pull_request' github_pr_test = os.environ.get('GITHUB_EVENT_NAME') == 'pull_request' if travis_pr_test or github_pr_test: # target branch should be anything other than 'master'; # usually is 'develop', but could also be a release branch like '3.7.x' if travis_pr_test: target_branch = os.environ.get('TRAVIS_BRANCH', None) else: target_branch = os.environ.get('GITHUB_BASE_REF', None) if target_branch is None: self.assertTrue(False, "Failed to determine target branch for current pull request.") if target_branch != 'master': if not EasyConfigTest.parsed_easyconfigs: self.process_all_easyconfigs() # relocate to top-level directory of repository to run 'git diff' command top_dir = os.path.dirname(os.path.dirname(get_paths_for('easyconfigs')[0])) cwd = change_dir(top_dir) # get list of changed easyconfigs changed_ecs_filenames = get_eb_files_from_diff(diff_filter='M') added_ecs_filenames = get_eb_files_from_diff(diff_filter='A') if changed_ecs_filenames: print("\nList of changed easyconfig files in this PR: %s" % '\n'.join(changed_ecs_filenames)) if added_ecs_filenames: print("\nList of added easyconfig files in this PR: %s" % '\n'.join(added_ecs_filenames)) change_dir(cwd) # grab parsed easyconfigs for changed easyconfig files changed_ecs = [] for ec_fn in changed_ecs_filenames + added_ecs_filenames: match = None for ec in EasyConfigTest.parsed_easyconfigs: if os.path.basename(ec['spec']) == ec_fn: match = ec['ec'] break if match: changed_ecs.append(match) else: # if no easyconfig is found, it's possible some archived easyconfigs were touched in the PR... # so as a last resort, try to find the easyconfig file in __archive__ easyconfigs_path = get_paths_for("easyconfigs")[0] specs = glob.glob('%s/__archive__/*/*/%s' % (easyconfigs_path, ec_fn)) if len(specs) == 1: ec = process_easyconfig(specs[0])[0] changed_ecs.append(ec['ec']) else: error_msg = "Failed to find parsed easyconfig for %s" % ec_fn error_msg += " (and could not isolate it in easyconfigs archive either)" self.assertTrue(False, error_msg) # run checks on changed easyconfigs self.check_sha256_checksums(changed_ecs) self.check_python_packages(changed_ecs, added_ecs_filenames) self.check_sanity_check_paths(changed_ecs) self.check_https(changed_ecs) def test_zzz_cleanup(self): """Dummy test to clean up global temporary directory.""" shutil.rmtree(self.TMPDIR)
import test.framework.run as run import test.framework.scripts as sc import test.framework.style as st import test.framework.systemtools as s import test.framework.toolchain as tc import test.framework.toolchainvariables as tcv import test.framework.toy_build as t import test.framework.type_checking as et import test.framework.tweak as tw import test.framework.variables as v import test.framework.yeb as y # make sure temporary files can be created/used try: set_tmpdir(raise_error=True) except EasyBuildError, err: sys.stderr.write("No execution rights on temporary files, specify another location via $TMPDIR: %s\n" % err) sys.exit(1) # initialize logger for all the unit tests fd, log_fn = tempfile.mkstemp(prefix='easybuild-tests-', suffix='.log') os.close(fd) os.remove(log_fn) fancylogger.logToFile(log_fn) log = fancylogger.getLogger() # call suite() for each module and then run them all # note: make sure the options unit tests run first, to avoid running some of them with a readily initialized config tests = [gen, bl, o, r, ef, ev, ebco, ep, e, mg, m, mt, f, run, a, robot, b, v, g, tcv, tc, t, c, s, l, f_c, sc, tw, p, i, pkg, d, env, et, y, st]
def setUp(self): """Set up testcase.""" super(EnhancedTestCase, self).setUp() # make sure option parser doesn't pick up any cmdline arguments/options while len(sys.argv) > 1: sys.argv.pop() # keep track of log handlers log = fancylogger.getLogger(fname=False) self.orig_log_handlers = log.handlers[:] log.info("setting up test %s" % self.id()) self.orig_tmpdir = tempfile.gettempdir() # use a subdirectory for this test (which we can clean up easily after the test completes) self.test_prefix = set_tmpdir() self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) fd, self.logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-') os.close(fd) self.cwd = os.getcwd() # keep track of original environment to restore self.orig_environ = copy.deepcopy(os.environ) # keep track of original environment/Python search path to restore self.orig_sys_path = sys.path[:] testdir = os.path.dirname(os.path.abspath(__file__)) self.test_sourcepath = os.path.join(testdir, 'sandbox', 'sources') os.environ['EASYBUILD_SOURCEPATH'] = self.test_sourcepath os.environ['EASYBUILD_PREFIX'] = self.test_prefix self.test_buildpath = tempfile.mkdtemp() os.environ['EASYBUILD_BUILDPATH'] = self.test_buildpath self.test_installpath = tempfile.mkdtemp() os.environ['EASYBUILD_INSTALLPATH'] = self.test_installpath # make sure that the tests only pick up easyconfigs provided with the tests os.environ['EASYBUILD_ROBOT_PATHS'] = os.path.join( testdir, 'easyconfigs') # make sure no deprecated behaviour is being triggered (unless intended by the test) # trip *all* log.deprecated statements by setting deprecation version ridiculously high self.orig_current_version = eb_build_log.CURRENT_VERSION os.environ['EASYBUILD_DEPRECATED'] = '10000000' init_config() import easybuild # try to import easybuild.easyblocks(.generic) packages # it's OK if it fails here, but important to import first before fiddling with sys.path try: import easybuild.easyblocks import easybuild.easyblocks.generic except ImportError: pass # add sandbox to Python search path, update namespace packages sys.path.append(os.path.join(testdir, 'sandbox')) # workaround for bug in recent setuptools version (19.4 and newer, atleast until 20.3.1) # injecting <prefix>/easybuild is required to avoid a ValueError being thrown by fixup_namespace_packages # cfr. https://bitbucket.org/pypa/setuptools/issues/520/fixup_namespace_packages-may-trigger for path in sys.path[:]: if os.path.exists( os.path.join(path, 'easybuild', 'easyblocks', '__init__.py')): # keep track of 'easybuild' paths to inject into sys.path later sys.path.append(os.path.join(path, 'easybuild')) # required to make sure the 'easybuild' dir in the sandbox is picked up; # this relates to the other 'reload' statements below reload(easybuild) # this is strictly required to make the test modules in the sandbox available, due to declare_namespace fixup_namespace_packages(os.path.join(testdir, 'sandbox')) # remove any entries in Python search path that seem to provide easyblocks (except the sandbox) for path in sys.path[:]: if os.path.exists( os.path.join(path, 'easybuild', 'easyblocks', '__init__.py')): if not os.path.samefile(path, os.path.join(testdir, 'sandbox')): sys.path.remove(path) # hard inject location to (generic) test easyblocks into Python search path # only prepending to sys.path is not enough due to 'declare_namespace' in easybuild/easyblocks/__init__.py import easybuild.easyblocks reload(easybuild.easyblocks) test_easyblocks_path = os.path.join(testdir, 'sandbox', 'easybuild', 'easyblocks') easybuild.easyblocks.__path__.insert(0, test_easyblocks_path) import easybuild.easyblocks.generic reload(easybuild.easyblocks.generic) test_easyblocks_path = os.path.join(test_easyblocks_path, 'generic') easybuild.easyblocks.generic.__path__.insert(0, test_easyblocks_path) self.modtool = modules_tool() self.reset_modulepath([os.path.join(testdir, 'modules')]) reset_module_caches()
def setUp(self): """Set up testcase.""" super(EnhancedTestCase, self).setUp() # keep track of log handlers log = fancylogger.getLogger(fname=False) self.orig_log_handlers = log.handlers[:] log.info("setting up test %s" % self.id()) self.orig_tmpdir = tempfile.gettempdir() # use a subdirectory for this test (which we can clean up easily after the test completes) self.test_prefix = set_tmpdir() self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) fd, self.logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-') os.close(fd) self.cwd = os.getcwd() # keep track of original environment to restore self.orig_environ = copy.deepcopy(os.environ) # keep track of original environment/Python search path to restore self.orig_sys_path = sys.path[:] testdir = os.path.dirname(os.path.abspath(__file__)) self.test_sourcepath = os.path.join(testdir, 'sandbox', 'sources') os.environ['EASYBUILD_SOURCEPATH'] = self.test_sourcepath os.environ['EASYBUILD_PREFIX'] = self.test_prefix self.test_buildpath = tempfile.mkdtemp() os.environ['EASYBUILD_BUILDPATH'] = self.test_buildpath self.test_installpath = tempfile.mkdtemp() os.environ['EASYBUILD_INSTALLPATH'] = self.test_installpath # make sure that the tests only pick up easyconfigs provided with the tests os.environ['EASYBUILD_ROBOT_PATHS'] = os.path.join( testdir, 'easyconfigs') # make sure no deprecated behaviour is being triggered (unless intended by the test) # trip *all* log.deprecated statements by setting deprecation version ridiculously high self.orig_current_version = eb_build_log.CURRENT_VERSION os.environ['EASYBUILD_DEPRECATED'] = '10000000' init_config() # remove any entries in Python search path that seem to provide easyblocks for path in sys.path[:]: if os.path.exists( os.path.join(path, 'easybuild', 'easyblocks', '__init__.py')): sys.path.remove(path) # add test easyblocks to Python search path and (re)import and reload easybuild modules import easybuild sys.path.append(os.path.join(testdir, 'sandbox')) reload(easybuild) import easybuild.easyblocks reload(easybuild.easyblocks) import easybuild.easyblocks.generic reload(easybuild.easyblocks.generic) reload(easybuild.tools.module_naming_scheme ) # required to run options unit tests stand-alone modtool = modules_tool() # purge out any loaded modules with original $MODULEPATH before running each test modtool.purge() self.reset_modulepath([os.path.join(testdir, 'modules')])