Skip to content
10 changes: 10 additions & 0 deletions easybuild/base/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ def mocked_stdout_stderr(self, mock_stdout=True, mock_stderr=True):
if mock_stderr:
self.mock_stderr(False)

@contextmanager
def saved_env(self):
"""Context manager to reset environment to state when it was entered"""
orig_env = os.environ.copy()
try:
yield
finally:
os.environ.clear()
os.environ.update(orig_env)

def tearDown(self):
"""Cleanup after running a test."""
self.mock_stdout(False)
Expand Down
79 changes: 47 additions & 32 deletions easybuild/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def __init__(self, ec, logfile=None):
self.skip = None
self.module_extra_extensions = '' # extra stuff for module file required by extensions

# indicates whether or not this instance represents an extension or not;
# indicates whether or not this instance represents an extension
# may be set to True by ExtensionEasyBlock
self.is_extension = False

Expand Down Expand Up @@ -670,12 +670,11 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
'name': ext_name,
'version': ext_version,
'options': ext_options,
# if a particular easyblock is specified, make sure it's used
# (this is picked up by init_ext_instances)
'easyblock': ext_options.get('easyblock', None),
}

# if a particular easyblock is specified, make sure it's used
# (this is picked up by init_ext_instances)
ext_src['easyblock'] = ext_options.get('easyblock', None)

# construct dictionary with template values;
# inherited from parent, except for name/version templates which are specific to this extension
template_values = copy.deepcopy(self.cfg.template_values)
Expand Down Expand Up @@ -1810,7 +1809,7 @@ def inject_module_extra_paths(self):
msg += f"and paths='{env_var}'"
self.log.debug(msg)

def expand_module_search_path(self, search_path, path_type=ModEnvVarType.PATH_WITH_FILES):
def expand_module_search_path(self, *_, **__):
"""
REMOVED in EasyBuild 5.1, use EasyBlock.module_load_environment.expand_paths instead
"""
Expand Down Expand Up @@ -2773,7 +2772,7 @@ def check_checksums_for(self, ent, sub='', source_cnt=None):
# if the filename is a dict, the actual source file name is the "filename" element
if isinstance(fn, dict):
fn = fn["filename"]
if fn in checksums_from_json.keys():
if fn in checksums_from_json:
checksums += [checksums_from_json[fn]]

if source_cnt is None:
Expand Down Expand Up @@ -2964,7 +2963,8 @@ def prepare_step(self, start_dir=True, load_tc_deps_modules=True):
if os.path.isabs(self.rpath_wrappers_dir):
_log.info(f"Using {self.rpath_wrappers_dir} to store/use RPATH wrappers")
else:
raise EasyBuildError(f"Path used for rpath_wrappers_dir is not an absolute path: {path}")
raise EasyBuildError("Path used for rpath_wrappers_dir is not an absolute path: %s",
self.rpath_wrappers_dir)

if self.iter_idx > 0:
# reset toolchain for iterative runs before preparing it again
Expand Down Expand Up @@ -3360,6 +3360,16 @@ def post_processing_step(self):

def _dispatch_sanity_check_step(self, *args, **kwargs):
"""Decide whether to run the dry-run or the real version of the sanity-check step"""
if 'extension' in kwargs:
extension = kwargs.pop('extension')
self.log.deprecated(
"Passing `extension` to `sanity_check_step` is no longer necessary and will be ignored "
f"(Easyblock: {self.__class__.__name__}).",
'6.0',
)
if extension != self.is_extension:
raise EasyBuildError('Unexpected value for `extension` argument. '
f'Should be: {self.is_extension}, got: {extension}')
if self.dry_run:
self._sanity_check_step_dry_run(*args, **kwargs)
else:
Expand Down Expand Up @@ -4076,8 +4086,10 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
paths = {}
for key in path_keys_and_check:
paths.setdefault(key, [])
paths.update({SANITY_CHECK_PATHS_DIRS: ['bin', ('lib', 'lib64')]})
self.log.info("Using default sanity check paths: %s", paths)
# Default paths for extensions are handled in the parent easyconfig if desired
if not self.is_extension:
paths.update({SANITY_CHECK_PATHS_DIRS: ['bin', ('lib', 'lib64')]})
self.log.info("Using default sanity check paths: %s", paths)

# if enhance_sanity_check is enabled *and* sanity_check_paths are specified in the easyconfig,
# those paths are used to enhance the paths provided by the easyblock
Expand All @@ -4097,9 +4109,11 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
# verify sanity_check_paths value: only known keys, correct value types, at least one non-empty value
only_list_values = all(isinstance(x, list) for x in paths.values())
only_empty_lists = all(not x for x in paths.values())
if sorted_keys != known_keys or not only_list_values or only_empty_lists:
if sorted_keys != known_keys or not only_list_values or (only_empty_lists and not self.is_extension):
error_msg = "Incorrect format for sanity_check_paths: should (only) have %s keys, "
error_msg += "values should be lists (at least one non-empty)."
error_msg += "values should be lists"
if not self.is_extension:
error_msg += " (at least one non-empty)."
raise EasyBuildError(error_msg % ', '.join("'%s'" % k for k in known_keys))

# Resolve arch specific entries
Expand Down Expand Up @@ -4256,7 +4270,7 @@ def sanity_check_load_module(self, extension=False, extra_modules=None):

return self.fake_mod_data

def _sanity_check_step(self, custom_paths=None, custom_commands=None, extension=False, extra_modules=None):
def _sanity_check_step(self, custom_paths=None, custom_commands=None, extra_modules=None):
"""
Real version of sanity_check_step method.

Expand Down Expand Up @@ -4322,7 +4336,7 @@ def xs2str(xs):
trace_msg("%s %s found: %s" % (typ, xs2str(xs), ('FAILED', 'OK')[found]))

if not self.sanity_check_module_loaded:
self.sanity_check_load_module(extension=extension, extra_modules=extra_modules)
self.sanity_check_load_module(extension=self.is_extension, extra_modules=extra_modules)

# allow oversubscription of P processes on C cores (P>C) for software installed on top of Open MPI;
# this is useful to avoid failing of sanity check commands that involve MPI
Expand Down Expand Up @@ -4351,26 +4365,27 @@ def xs2str(xs):
trace_msg(f"result for command '{cmd}': {cmd_result_str}")

# also run sanity check for extensions (unless we are an extension ourselves)
if not extension:
if not self.is_extension:
if build_option('skip_extensions'):
self.log.info("Skipping sanity check for extensions since skip-extensions is enabled...")
else:
self._sanity_check_step_extensions()

linked_shared_lib_fails = self.sanity_check_linked_shared_libs()
if linked_shared_lib_fails:
self.log.warning("Check for required/banned linked shared libraries failed!")
self.sanity_check_fail_msgs.append(linked_shared_lib_fails)

# software installed with GCCcore toolchain should not have Fortran module files (.mod),
# unless that's explicitly allowed
if self.toolchain.name in ('GCCcore',) and not self.cfg['skip_mod_files_sanity_check']:
mod_files_found_msg = self.sanity_check_mod_files()
if mod_files_found_msg:
if build_option('fail_on_mod_files_gcccore'):
self.sanity_check_fail_msgs.append(mod_files_found_msg)
else:
print_warning(mod_files_found_msg)
# Do not do those checks for extensions, only in the main easyconfig
linked_shared_lib_fails = self.sanity_check_linked_shared_libs()
if linked_shared_lib_fails:
self.log.warning("Check for required/banned linked shared libraries failed!")
self.sanity_check_fail_msgs.append(linked_shared_lib_fails)

# software installed with GCCcore toolchain should not have Fortran module files (.mod),
# unless that's explicitly allowed
if self.toolchain.name in ('GCCcore',) and not self.cfg['skip_mod_files_sanity_check']:
mod_files_found_msg = self.sanity_check_mod_files()
if mod_files_found_msg:
if build_option('fail_on_mod_files_gcccore'):
self.sanity_check_fail_msgs.append(mod_files_found_msg)
else:
print_warning(mod_files_found_msg)

# cleanup
if self.fake_mod_data:
Expand Down Expand Up @@ -4400,14 +4415,14 @@ def xs2str(xs):
self.log.debug("Skipping CUDA sanity check: CUDA is not in dependencies")

# pass or fail
if self.sanity_check_fail_msgs:
if not self.sanity_check_fail_msgs:
self.log.debug("Sanity check passed!")
elif not self.is_extension:
raise EasyBuildError(
"Sanity check failed: " + '\n'.join(self.sanity_check_fail_msgs),
exit_code=EasyBuildExit.FAIL_SANITY_CHECK,
)

self.log.debug("Sanity check passed!")

def _set_module_as_default(self, fake=False):
"""
Sets the default module version except if we are in dry run
Expand Down
5 changes: 0 additions & 5 deletions easybuild/framework/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,6 @@ def sanity_check_step(self):
"""
Sanity check to run after installing extension
"""
res = (True, '')

if os.path.isdir(self.installdir):
change_dir(self.installdir)

Expand Down Expand Up @@ -330,9 +328,6 @@ def sanity_check_step(self):
fail_msg = 'command "%s" failed' % cmd
fail_msg += "; output:\n%s" % cmd_res.output.strip()
self.log.warning("Sanity check for '%s' extension failed: %s", self.name, fail_msg)
res = (False, fail_msg)
# keep track of all reasons of failure
# (only relevant when this extension is installed stand-alone via ExtensionEasyBlock)
self.sanity_check_fail_msgs.append(fail_msg)

return res
16 changes: 7 additions & 9 deletions easybuild/framework/extensioneasyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ def extra_options(extra_vars=None):
def __init__(self, *args, **kwargs):
"""Initialize either as EasyBlock or as Extension."""

self.is_extension = False

if isinstance(args[0], EasyBlock):
# make sure that extra custom easyconfig parameters are known
extra_params = self.__class__.extra_options()
Expand Down Expand Up @@ -187,20 +185,20 @@ def sanity_check_step(self, exts_filter=None, custom_paths=None, custom_commands
self.log.info(info_msg)
trace_msg(info_msg)
# perform sanity check for stand-alone extension
(sanity_check_ok, fail_msg) = Extension.sanity_check_step(self)
Extension.sanity_check_step(self)
else:
# perform single sanity check for extension
(sanity_check_ok, fail_msg) = Extension.sanity_check_step(self)
Extension.sanity_check_step(self)

if custom_paths or custom_commands or not self.is_extension:
super().sanity_check_step(custom_paths=custom_paths,
custom_commands=custom_commands,
extension=self.is_extension)
super().sanity_check_step(custom_paths=custom_paths,
custom_commands=custom_commands)

# pass or fail sanity check
if sanity_check_ok:
if not self.sanity_check_fail_msgs:
sanity_check_ok = True
self.log.info("Sanity check for %s successful!", self.name)
else:
sanity_check_ok = False
if not self.is_extension:
msg = "Sanity check for %s failed: %s" % (self.name, '; '.join(self.sanity_check_fail_msgs))
raise EasyBuildError(msg)
Expand Down
2 changes: 2 additions & 0 deletions easybuild/tools/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,8 @@ def load(self, modules, mod_paths=None, purge=False, init_env=None, allow_reload
:param init_env: original environment to restore after running 'module purge'
:param allow_reload: allow reloading an already loaded module
"""
if not any((modules, mod_paths, purge)):
return # Avoid costly module paths if nothing to do
if mod_paths is None:
mod_paths = []

Expand Down
Loading