diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index d72ab4e9d3..57ea075f37 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -4668,13 +4668,27 @@ def test_cases_step(self): test_cmd = os.path.join(source_path, self.name, test) if os.path.exists(test_cmd): break - if not os.path.exists(test_cmd): - raise EasyBuildError(f"Test specifies invalid path: {test_cmd}") - + else: + test_cmd = test + if not os.path.exists(test_cmd): + raise EasyBuildError(f"Test specifies invalid path: {test_cmd}") + + if os.path.isfile(test_cmd): + original_perms = os.lstat(test_cmd).st_mode + if original_perms & stat.S_IEXEC: + original_perms = None + else: + adjust_permissions(test_cmd, stat.S_IEXEC, add=True, recursive=False) + else: + original_perms = None try: self.log.debug(f"Running test {test_cmd}") - run_shell_cmd(test_cmd) - except EasyBuildError as err: + try: + run_shell_cmd(test_cmd) + finally: + if original_perms is not None: + adjust_permissions(test_cmd, original_perms, relative=False) + except BaseException as err: raise EasyBuildError(f"Running test {test_cmd} failed: {err}") def update_config_template_run_step(self): diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 601b21b7f8..0daeaa5ed3 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -34,6 +34,7 @@ import os import re import shutil +import stat import sys import tempfile import textwrap @@ -52,8 +53,8 @@ from easybuild.tools import LooseVersion, config from easybuild.tools.build_log import EasyBuildError from easybuild.tools.config import get_module_syntax, update_build_option -from easybuild.tools.filetools import change_dir, copy_dir, copy_file, mkdir, read_file, remove_dir, remove_file -from easybuild.tools.filetools import symlink, verify_checksum, write_file +from easybuild.tools.filetools import adjust_permissions, change_dir, copy_dir, copy_file, mkdir, read_file +from easybuild.tools.filetools import remove_dir, remove_file, symlink, verify_checksum, write_file from easybuild.tools.module_generator import module_generator from easybuild.tools.modules import EnvironmentModules, Lmod, reset_module_caches from easybuild.tools.version import get_git_revision, this_is_easybuild @@ -1271,6 +1272,68 @@ def test_handle_iterate_opts(self): self.assertEqual(eb.cfg.iterating, False) self.assertEqual(eb.cfg['configopts'], ["--opt1 --anotheropt", "--opt2", "--opt3 --optbis"]) + def test_test_cases_step(self): + """Test test_cases_step""" + self.contents = '\n'.join([ + 'easyblock = "ConfigureMake"', + 'name = "pi"', + 'version = "3.14"', + 'homepage = "http://example.com"', + 'description = "test easyconfig"', + "toolchain = {'name': 'gompi', 'version': '2018a'}", + ]) + self.writeEC() + eb = EasyBlock(EasyConfig(self.eb_file)) + eb.cfg['tests'] = ['does-not-exist'] + self.assertRaisesRegex(EasyBuildError, 'invalid path: does-not-exist', eb.test_cases_step) + eb.cfg['tests'] = ['/abs/path/does-not-exist'] + self.assertRaisesRegex(EasyBuildError, 'invalid path: /abs/path/does-not-exist', eb.test_cases_step) + + mock_test_bin = os.path.join(self.test_prefix, 'pi', 'test_me') + os.environ['PATH'] += f':{os.path.dirname(mock_test_bin)}' + write_file(mock_test_bin, "#!/bin/bash\necho 'Test case success'") + + adjust_permissions(mock_test_bin, stat.S_IXUSR) + eb.cfg['tests'] = [os.path.basename(mock_test_bin)] + self.assertRaisesRegex(EasyBuildError, f'invalid path: {os.path.basename(mock_test_bin)}', eb.test_cases_step) + + init_config(args=[f"--sourcepath={self.test_prefix}"]) + write_file(eb.logfile, '') + eb.test_cases_step() + self.assertIn('Test case success', read_file(eb.logfile)) + + # Also works with non-executable file + perms = stat.S_IREAD + adjust_permissions(mock_test_bin, perms, relative=False) + eb.cfg['tests'] = [os.path.basename(mock_test_bin)] + write_file(eb.logfile, '') + eb.test_cases_step() + self.assertEqual(stat.S_IMODE(os.lstat(mock_test_bin).st_mode), perms) # Permissions unchanged + self.assertIn('Test case success', read_file(eb.logfile)) + + # Similar for absolute paths + eb.cfg['tests'] = [mock_test_bin] + write_file(eb.logfile, '') + eb.test_cases_step() + self.assertIn('Test case success', read_file(eb.logfile)) + + # Detect failure + adjust_permissions(mock_test_bin, stat.S_IWRITE) + mock_test_bin_fail = mock_test_bin + "_fail" + write_file(mock_test_bin_fail, "#!/bin/bash\necho 'Test case failure' && exit 1") + eb.cfg['tests'] = [mock_test_bin_fail] + write_file(eb.logfile, '') + self.assertRaisesRegex(EasyBuildError, f'Running test {mock_test_bin_fail} failed', eb.test_cases_step) + self.assertIn('Test case failure', read_file(eb.logfile)) + + # Multiple tests + eb.cfg['tests'] = [mock_test_bin, mock_test_bin_fail] + write_file(eb.logfile, '') + self.assertRaisesRegex(EasyBuildError, f'Running test {mock_test_bin_fail} failed', eb.test_cases_step) + log_txt = read_file(eb.logfile) + self.assertIn('Test case success', log_txt) + self.assertIn('Test case failure', log_txt) + def test_post_processing_step(self): """Test post_processing_step and deprecated post_install_step.""" init_config(build_options={'silent': True})