diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index bd2371f3b6..fd3423739f 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -240,22 +240,29 @@ jobs: # create file owned by root but writable by anyone (used by test_copy_file) sudo touch /tmp/file_to_overwrite_for_easybuild_test_copy_file.txt sudo chmod o+w /tmp/file_to_overwrite_for_easybuild_test_copy_file.txt + + if [ "$GITHUB_REPOSITORY_OWNER" == 'easybuilders' ] || [ '${{github.event.pull_request.head.repo.owner}}' == 'easybuilders' ]; then + FORCE_EB_GITHUB_TESTS=1 + else + echo "Not force-enabling Github tests" + echo "\$GITHUB_EVENT_NAME=GITHUB_EVENT_NAME" + echo "\$GITHUB_REPOSITORY_OWNER=$GITHUB_REPOSITORY_OWNER" + echo "PR HEAD repo owner=${{github.event.pull_request.head.repo.owner}}" + fi + # run test suite python -O -m test.framework.suite 2>&1 | tee test_framework_suite.log # try and make sure output of running tests is clean (no printed messages/warnings) - IGNORE_PATTERNS="no GitHub token available" - IGNORE_PATTERNS+="|skipping SvnRepository test" - IGNORE_PATTERNS+="|requires Lmod as modules tool" + IGNORE_PATTERNS="^[s.]*$" # Only PASS (.) and SKIP (s) status on line IGNORE_PATTERNS+="|stty: 'standard input': Inappropriate ioctl for device" IGNORE_PATTERNS+="|CryptographyDeprecationWarning: Python 3.7" IGNORE_PATTERNS+="|from cryptography.* import " IGNORE_PATTERNS+="|Blowfish" - IGNORE_PATTERNS+="|GC3Pie not available, skipping test" IGNORE_PATTERNS+="|CryptographyDeprecationWarning: TripleDES has been moved" IGNORE_PATTERNS+="|algorithms.TripleDES" - # ignore lines with only successful ('.') and skipped ('s') tests - IGNORE_PATTERNS+="|^[\.s]+$" + + TEST_OUTPUT=$(sed -n '/------------------------------/q;p' test_framework_suite.log) # Everything up to the result divider # '|| true' is needed to avoid that GitHub Actions stops the job on non-zero exit of grep (i.e. when there are no matches) - PRINTED_MSG=$(egrep -v "${IGNORE_PATTERNS}" test_framework_suite.log | grep '\.\n*[A-Za-z]' || true) - test "x$PRINTED_MSG" = "x" || (echo "ERROR: Found printed messages in output of test suite" && echo "${PRINTED_MSG}" && exit 1) + PRINTED_MSG=$(echo "$TEST_OUTPUT" | egrep -v "${IGNORE_PATTERNS}" || true) + [[ -z $PRINTED_MSG ]] || (echo "ERROR: Found printed messages in output of test suite" && echo "${PRINTED_MSG}" && exit 1) done diff --git a/test/framework/easyconfig.py b/test/framework/easyconfig.py index 1f5067b804..fd4728c011 100644 --- a/test/framework/easyconfig.py +++ b/test/framework/easyconfig.py @@ -45,7 +45,6 @@ import easybuild.tools.build_log import easybuild.framework.easyconfig as easyconfig -import easybuild.tools.github as gh import easybuild.tools.systemtools as st from easybuild.framework.easyblock import EasyBlock from easybuild.framework.easyconfig.constants import EXTERNAL_MODULE_MARKER @@ -79,13 +78,9 @@ from easybuild.tools.toolchain.utilities import search_toolchain from easybuild.tools.utilities import quote_str, quote_py_str -from test.framework.github import GITHUB_TEST_ACCOUNT -from test.framework.utilities import find_full_path +from test.framework.github import requires_github_token +from test.framework.utilities import find_full_path, requires_autopep8, requires_pycodestyle, requires_pygraph -try: - import pycodestyle # noqa # pylint:disable=unused-import -except ImportError: - pass EXPECTED_DOTTXT_TOY_DEPS = """digraph graphname { toy; @@ -112,9 +107,6 @@ def setUp(self): if os.path.exists(self.eb_file): os.remove(self.eb_file) - github_token = gh.fetch_github_token(GITHUB_TEST_ACCOUNT) - self.skip_github_tests = github_token is None and os.getenv('FORCE_EB_GITHUB_TESTS') is None - self.orig_easyconfig_DEPRECATED_EASYCONFIG_PARAMETERS = easyconfig.easyconfig.DEPRECATED_EASYCONFIG_PARAMETERS self.orig_easyconfig_DEPRECATED_EASYCONFIG_TEMPLATES = easyconfig.easyconfig.DEPRECATED_EASYCONFIG_TEMPLATES self.orig_easyconfig_ALTERNATIVE_EASYCONFIG_PARAMETERS = easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_PARAMETERS @@ -2779,25 +2771,18 @@ def test_dump_order(self): 'foo_extra1', '', 'moduleclass', ''] self.assertEqual(param_regex.findall(ectxt), expected) + @requires_autopep8() def test_dump_autopep8(self): """Test dump() with autopep8 usage enabled (only if autopep8 is available).""" - try: - import autopep8 # noqa # pylint:disable=unused-import - except ImportError: - print("Skipping test_dump_autopep8, since autopep8 is not available") - return os.environ['EASYBUILD_DUMP_AUTOPEP8'] = '1' init_config() self.test_dump() del os.environ['EASYBUILD_DUMP_AUTOPEP8'] + @requires_pycodestyle() def test_dump_extra(self): """Test EasyConfig's dump() method for files containing extra values""" - if 'pycodestyle' not in sys.modules: - print("Skipping test_dump_extra pycodestyle is not available") - return - rawtxt = '\n'.join([ "easyblock = 'EB_foo'", '', @@ -2834,13 +2819,10 @@ def test_dump_extra(self): check_easyconfigs_style([testec]) + @requires_pycodestyle() def test_dump_template(self): """ Test EasyConfig's dump() method for files containing templates""" - if 'pycodestyle' not in sys.modules: - print("Skipping test_dump_template pycodestyle is not available") - return - rawtxt = '\n'.join([ "easyblock = 'EB_foo'", '', @@ -2923,13 +2905,10 @@ def test_dump_template(self): check_easyconfigs_style([testec]) + @requires_pycodestyle() def test_dump_comments(self): """ Test dump() method for files containing comments """ - if 'pycodestyle' not in sys.modules: - print("Skipping test_dump_comments pycodestyle is not available") - return - rawtxt = '\n'.join([ "# #", "# some header comment", @@ -3278,13 +3257,9 @@ def test_to_template_str(self): res = "sanity_check_paths = {\n 'files': [],\n 'dirs': ['lib/python%(pyshortver)s/site-packages'],\n}" self.assertEqual(to_template_str('sanity_check_paths', test_input, templ_const, templ_val), res) + @requires_pygraph() def test_dep_graph(self): """Test for dep_graph.""" - try: - import pygraph # noqa # pylint:disable=unused-import - except ImportError: - print("Skipping test_dep_graph, since pygraph is not available") - return test_easyconfigs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs') build_options = { @@ -3315,52 +3290,47 @@ def test_dep_graph(self): ordered_expected = '\n'.join(sorted(EXPECTED_DOTTXT_TOY_DEPS.split('\n'))) self.assertEqual(ordered_dottxt, ordered_expected) + @requires_pygraph() def test_dep_graph_multi_deps(self): """ Test for dep_graph using easyconfig that uses multi_deps. """ - try: - import pygraph # noqa # pylint:disable=unused-import - - test_easyconfigs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs') - build_options = { - 'external_modules_metadata': ConfigObj(), - 'valid_module_classes': module_classes(), - 'robot_path': [test_easyconfigs], - 'silent': True, - } - init_config(build_options=build_options) - - toy_ec = os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb') - toy_ec_txt = read_file(toy_ec) + test_easyconfigs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs') + build_options = { + 'external_modules_metadata': ConfigObj(), + 'valid_module_classes': module_classes(), + 'robot_path': [test_easyconfigs], + 'silent': True, + } + init_config(build_options=build_options) - test_ec = os.path.join(self.test_prefix, 'test.eb') - test_ec_txt = toy_ec_txt + "\nmulti_deps = {'GCC': ['4.6.3', '4.8.3', '7.3.0-2.30']}" - write_file(test_ec, test_ec_txt) + toy_ec = os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb') + toy_ec_txt = read_file(toy_ec) - ec_files = [(test_ec, False)] - ecs, _ = parse_easyconfigs(ec_files) + test_ec = os.path.join(self.test_prefix, 'test.eb') + test_ec_txt = toy_ec_txt + "\nmulti_deps = {'GCC': ['4.6.3', '4.8.3', '7.3.0-2.30']}" + write_file(test_ec, test_ec_txt) - dot_file = os.path.join(self.test_prefix, 'test.dot') - ordered_ecs = resolve_dependencies(ecs, self.modtool, retain_all_deps=True) - dep_graph(dot_file, ordered_ecs) + ec_files = [(test_ec, False)] + ecs, _ = parse_easyconfigs(ec_files) - # hard check for expect .dot file contents - # 3 nodes should be there: 'GCC/6.4.0-2.28 (EXT)', 'toy', and 'intel/2018a' - # and 2 edges: 'toy -> intel' and 'toy -> "GCC/6.4.0-2.28 (EXT)"' - dottxt = read_file(dot_file) + dot_file = os.path.join(self.test_prefix, 'test.dot') + ordered_ecs = resolve_dependencies(ecs, self.modtool, retain_all_deps=True) + dep_graph(dot_file, ordered_ecs) - self.assertTrue(dottxt.startswith('digraph graphname {')) + # hard check for expect .dot file contents + # 3 nodes should be there: 'GCC/6.4.0-2.28 (EXT)', 'toy', and 'intel/2018a' + # and 2 edges: 'toy -> intel' and 'toy -> "GCC/6.4.0-2.28 (EXT)"' + dottxt = read_file(dot_file) - # just check for toy -> GCC deps - # don't bother doing full output check - # (different order for fields depending on Python version makes that tricky) - for gccver in ['4.6.3', '4.8.3', '7.3.0-2.30']: - self.assertTrue('"GCC/%s";' % gccver in dottxt) - self.assertTrue('"toy/0.0" -> "GCC/%s"' % gccver in dottxt) + self.assertTrue(dottxt.startswith('digraph graphname {')) - except ImportError: - print("Skipping test_dep_graph, since pygraph is not available") + # just check for toy -> GCC deps + # don't bother doing full output check + # (different order for fields depending on Python version makes that tricky) + for gccver in ['4.6.3', '4.8.3', '7.3.0-2.30']: + self.assertTrue('"GCC/%s";' % gccver in dottxt) + self.assertTrue('"toy/0.0" -> "GCC/%s"' % gccver in dottxt) def test_ActiveMNS_singleton(self): """Make sure ActiveMNS is a singleton class.""" @@ -4911,10 +4881,11 @@ def test_det_copy_ec_specs(self): self.assertEqual(paths, args[:-1]) self.assertEqual(target_path, args[-1]) - if self.skip_github_tests: - print("Skipping test_det_copy_ec_specs using --from-pr, no GitHub token available?") - return + @requires_github_token() + def test_det_copy_ec_specs_from_pr(self): + """Test det_copy_ec_specs function with --from-pr.""" + cwd = os.getcwd() # use fixed PR (speeds up the test due to caching in fetch_files_from_pr; # see https://github.com/easybuilders/easybuild-easyconfigs/pull/22345 from_pr = 22345 diff --git a/test/framework/github.py b/test/framework/github.py index 6d88b90924..37e56e683a 100644 --- a/test/framework/github.py +++ b/test/framework/github.py @@ -37,7 +37,8 @@ import textwrap import unittest from string import ascii_letters -from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config +from test.framework.utilities import (EnhancedTestCase, TestLoaderFiltered, init_config, + skip_never, skip_silentCI_unless) from time import gmtime from unittest import TextTestRunner from urllib.request import HTTPError, URLError @@ -74,14 +75,13 @@ def requires_github_access(): - """Silently skip for pull requests unless $FORCE_EB_GITHUB_TESTS is set + """Skip for pull requests unless $FORCE_EB_GITHUB_TESTS is set Useful when the test uses e.g. `git` commands to download from Github and would run into rate limits """ - return unittest.skipUnless( - os.environ.get('FORCE_EB_GITHUB_TESTS', '0') != '0' or os.getenv('GITHUB_EVENT_NAME') != 'pull_request', - "Skipping test requiring GitHub access" - ) + if 'FORCE_EB_GITHUB_TESTS' in os.environ: + return skip_never + return skip_silentCI_unless(os.getenv('GITHUB_EVENT_NAME') != 'pull_request', "Requires GitHub access") def ignore_rate_limit_in_pr(test_item): @@ -100,6 +100,18 @@ def skip_wrapper(self, *args, **kwargs): return skip_wrapper +def requires_github_token(): + """Require a github token to be available unless $FORCE_EB_GITHUB_TESTS is set""" + if 'FORCE_EB_GITHUB_TESTS' in os.environ: + return skip_never + github_token = gh.fetch_github_token(GITHUB_TEST_ACCOUNT) + if os.getenv('GITHUB_EVENT_NAME') == 'pull_request': + # For pull requests silently skip if no token is available as that is expected + return skip_silentCI_unless(github_token, "No GitHub token available") + else: + return unittest.skipUnless(github_token, "No GitHub token available") + + class GithubTest(EnhancedTestCase): """ small test for The github package This should not be to much, since there is an hourly limit of request @@ -118,8 +130,6 @@ def setUp(self): self.ghfs = gh.Githubfs(GITHUB_USER, GITHUB_REPO, GITHUB_BRANCH, username, None, token) - self.skip_github_tests = self.github_token is None and os.getenv('FORCE_EB_GITHUB_TESTS') is None - self.orig_testing_create_gist = easybuild.tools.testing.create_gist def tearDown(self): @@ -198,12 +208,9 @@ def test_github_pick_default_branch(self): self.assertEqual(pick_default_branch('easybuilders'), 'main') self.assertEqual(pick_default_branch('foobar'), 'master') + @requires_github_token() def test_github_walk(self): """test the gitubfs walk function""" - if self.skip_github_tests: - print("Skipping test_walk, no GitHub token available?") - return - try: expected = [ (None, ['a_directory', 'second_dir'], ['README.md']), @@ -214,23 +221,17 @@ def test_github_walk(self): except IOError: pass + @requires_github_token() def test_github_read_api(self): """Test the githubfs read function""" - if self.skip_github_tests: - print("Skipping test_read_api, no GitHub token available?") - return - try: self.assertEqual(self.ghfs.read("a_directory/a_file.txt").strip(), b"this is a line of text") except IOError: pass + @requires_github_token() def test_github_read(self): """Test the githubfs read function without using the api""" - if self.skip_github_tests: - print("Skipping test_read, no GitHub token available?") - return - try: with self.mocked_stdout_stderr(): fp = self.ghfs.read("a_directory/a_file.txt", api=False) @@ -239,12 +240,9 @@ def test_github_read(self): except (IOError, OSError): pass + @requires_github_token() def test_github_add_pr_labels(self): """Test add_pr_labels function.""" - if self.skip_github_tests: - print("Skipping test_add_pr_labels, no GitHub token available?") - return - build_options = { 'pr_target_account': GITHUB_USER, 'pr_target_repo': GITHUB_EASYBLOCKS_REPO, @@ -277,12 +275,9 @@ def test_github_add_pr_labels(self): self.mock_stderr(False) self.assertIn("Could not determine any missing labels for PR #22088", stdout) + @requires_github_token() def test_github_fetch_pr_data(self): """Test fetch_pr_data function.""" - if self.skip_github_tests: - print("Skipping test_fetch_pr_data, no GitHub token available?") - return - pr_data, _ = gh.fetch_pr_data(1, GITHUB_USER, GITHUB_REPO, GITHUB_TEST_ACCOUNT) self.assertEqual(pr_data['number'], 1) @@ -299,12 +294,9 @@ def test_github_fetch_pr_data(self): self.assertEqual(pr_data['reviews'][0]['user']['login'], 'boegel') self.assertEqual(pr_data['status_last_commit'], None) + @requires_github_token() def test_github_list_prs(self): """Test list_prs function.""" - if self.skip_github_tests: - print("Skipping test_list_prs, no GitHub token available?") - return - parameters = ('closed', 'created', 'asc') init_config(build_options={'pr_target_account': GITHUB_USER, @@ -321,12 +313,9 @@ def test_github_list_prs(self): self.assertEqual(expected, output) + @requires_github_token() def test_github_reasons_for_closing(self): """Test reasons_for_closing function.""" - if self.skip_github_tests: - print("Skipping test_reasons_for_closing, no GitHub token available?") - return - repo_owner = gh.GITHUB_EB_MAIN repo_name = gh.GITHUB_EASYCONFIGS_REPO @@ -360,12 +349,9 @@ def test_github_reasons_for_closing(self): for pattern in patterns: self.assertIn(pattern, stdout) + @requires_github_token() def test_github_close_pr(self): """Test close_pr function.""" - if self.skip_github_tests: - print("Skipping test_close_pr, no GitHub token available?") - return - build_options = { 'dry_run': True, 'github_user': GITHUB_TEST_ACCOUNT, @@ -405,12 +391,9 @@ def test_github_close_pr(self): for pattern in patterns: self.assertIn(pattern, stdout) + @requires_github_token() def test_github_fetch_easyblocks_from_pr(self): """Test fetch_easyblocks_from_pr function.""" - if self.skip_github_tests: - print("Skipping test_fetch_easyblocks_from_pr, no GitHub token available?") - return - init_config(build_options={ 'pr_target_account': gh.GITHUB_EB_MAIN, }) @@ -433,12 +416,9 @@ def test_github_fetch_easyblocks_from_pr(self): except URLError as err: print("Ignoring URLError '%s' in test_fetch_easyblocks_from_pr" % err) + @requires_github_token() def test_github_fetch_easyconfigs_from_pr(self): """Test fetch_easyconfigs_from_pr function.""" - if self.skip_github_tests: - print("Skipping test_fetch_easyconfigs_from_pr, no GitHub token available?") - return - init_config(build_options={ 'pr_target_account': gh.GITHUB_EB_MAIN, }) @@ -471,12 +451,9 @@ def test_github_fetch_easyconfigs_from_pr(self): except URLError as err: print("Ignoring URLError '%s' in test_fetch_easyconfigs_from_pr" % err) + @requires_github_token() def test_github_fetch_files_from_pr_cache(self): """Test caching for fetch_files_from_pr.""" - if self.skip_github_tests: - print("Skipping test_fetch_files_from_pr_cache, no GitHub token available?") - return - init_config(build_options={ 'pr_target_account': gh.GITHUB_EB_MAIN, }) @@ -588,24 +565,18 @@ def test_fetch_easyconfigs_from_commit(self): self.assertEqual(len(res), 1) self.assertIn("v4.9.0 (30 December 2023)", read_file(res[0])) + @requires_github_token() def test_github_fetch_latest_commit_sha(self): """Test fetch_latest_commit_sha function.""" - if self.skip_github_tests: - print("Skipping test_fetch_latest_commit_sha, no GitHub token available?") - return - sha = gh.fetch_latest_commit_sha('easybuild-framework', 'easybuilders', github_user=GITHUB_TEST_ACCOUNT) self.assertTrue(re.match('^[0-9a-f]{40}$', sha)) sha = gh.fetch_latest_commit_sha('easybuild-easyblocks', 'easybuilders', github_user=GITHUB_TEST_ACCOUNT, branch='develop') self.assertTrue(re.match('^[0-9a-f]{40}$', sha)) + @requires_github_token() def test_github_download_repo(self): """Test download_repo function.""" - if self.skip_github_tests: - print("Skipping test_download_repo, no GitHub token available?") - return - cwd = os.getcwd() self.mock_stdout(True) @@ -672,16 +643,11 @@ def test_github_download_repo_commit(self): self.assertErrorRegex(EasyBuildError, "Failed to download tarball .* commit", gh.download_repo, path=self.test_prefix, commit='0000000000000000000000000000000000000000') + @unittest.skipIf(not HAVE_KEYRING, "keyring module not available") + @requires_github_token() def test_install_github_token(self): """Test for install_github_token function.""" - if self.skip_github_tests: - print("Skipping test_install_github_token, no GitHub token available?") - return - - if not HAVE_KEYRING: - print("Skipping test_install_github_token, keyring module not available") - return - + random_user = ''.join(random.choice(ascii_letters) for _ in range(10)) random_user = ''.join(random.choice(ascii_letters) for _ in range(10)) self.assertEqual(gh.fetch_github_token(random_user), None) @@ -712,16 +678,10 @@ def fake_getpass(*args, **kwargs): self.assertTrue(token_installed) self.assertTrue(token == self.github_token) + @unittest.skipIf(not HAVE_KEYRING, "keyring module not available") + @requires_github_token() def test_validate_github_token(self): """Test for validate_github_token function.""" - if self.skip_github_tests: - print("Skipping test_validate_github_token, no GitHub token available?") - return - - if not HAVE_KEYRING: - print("Skipping test_validate_github_token, keyring module not available") - return - self.assertTrue(gh.validate_github_token(self.github_token, GITHUB_TEST_ACCOUNT)) # if a token in the old format is available, test with that too @@ -734,11 +694,9 @@ def test_validate_github_token(self): if finegrained_token: self.assertTrue(gh.validate_github_token(finegrained_token, GITHUB_TEST_ACCOUNT)) + @requires_github_token() def test_github_find_easybuild_easyconfig(self): """Test for find_easybuild_easyconfig function""" - if self.skip_github_tests: - print("Skipping test_find_easybuild_easyconfig, no GitHub token available?") - return with self.mocked_stdout_stderr(): path = gh.find_easybuild_easyconfig(github_user=GITHUB_TEST_ACCOUNT) expected = os.path.join('e', 'EasyBuild', r'EasyBuild-[1-9]+\.[0-9]+\.[0-9]+\.eb') @@ -780,13 +738,9 @@ def test_github_find_patches(self): self.assertEqual(gh.find_software_name_for_patch('test.patch', [self.test_prefix]), None) self.mock_stdout(False) + @requires_github_token() def test_github_det_commit_status(self): """Test det_commit_status function.""" - - if self.skip_github_tests: - print("Skipping test_det_commit_status, no GitHub token available?") - return - # ancient commit, from Jenkins era, no commit status available anymore commit_sha = 'ec5d6f7191676a86a18404616691796a352c5f1d' res = gh.det_commit_status('easybuilders', 'easybuild-easyconfigs', commit_sha, GITHUB_TEST_ACCOUNT) @@ -1084,12 +1038,9 @@ def test_github_det_patch_specs(self): self.assertEqual([i[0] for i in res], patch_paths) self.assertEqual([i[1] for i in res], ['A'] + ['patched_bundle'] * 4) + @requires_github_token() def test_github_restclient(self): """Test use of RestClient.""" - if self.skip_github_tests: - print("Skipping test_restclient, no GitHub token available?") - return - client = RestClient('https://api.github.com', username=GITHUB_TEST_ACCOUNT, token=self.github_token) status, body = client.repos['easybuilders']['testrepository'].contents.a_directory['a_file.txt'].get() @@ -1119,24 +1070,18 @@ def test_github_restclient(self): httperror_hit = True self.assertTrue(httperror_hit, "expected HTTPError not encountered") + @requires_github_token() def test_github_create_delete_gist(self): """Test create_gist and delete_gist.""" - if self.skip_github_tests: - print("Skipping test_restclient, no GitHub token available?") - return - test_txt = "This is just a test." gist_url = gh.create_gist(test_txt, 'test.txt', github_user=GITHUB_TEST_ACCOUNT, github_token=self.github_token) gist_id = gist_url.split('/')[-1] gh.delete_gist(gist_id, github_user=GITHUB_TEST_ACCOUNT, github_token=self.github_token) + @requires_github_token() def test_github_det_account_repo_branch_for_pr(self): """Test det_account_branch_for_pr.""" - if self.skip_github_tests: - print("Skipping test_det_account_branch_for_pr, no GitHub token available?") - return - init_config(build_options={ 'pr_target_account': 'easybuilders', 'pr_target_repo': 'easybuild-easyconfigs', @@ -1251,12 +1196,9 @@ def test_push_branch_to_github(self): regex = re.compile(pattern) self.assertTrue(regex.match(stdout.strip()), "Pattern '%s' doesn't match: %s" % (regex.pattern, stdout)) + @requires_github_token() def test_github_pr_test_report(self): """Test for post_pr_test_report function.""" - if self.skip_github_tests: - print("Skipping test_post_pr_test_report, no GitHub token available?") - return - init_config(build_options={ 'dry_run': True, 'github_user': GITHUB_TEST_ACCOUNT, diff --git a/test/framework/modules.py b/test/framework/modules.py index b69bb69c3e..eb3743f90b 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -850,43 +850,44 @@ def test_path_to_top_of_module_tree_hierarchical_mns(self): def test_path_to_top_of_module_tree_lua(self): """Test path_to_top_of_module_tree function on modules in Lua syntax.""" - if isinstance(self.modtool, Lmod): - orig_modulepath = os.environ.get('MODULEPATH') - self.modtool.unuse(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'modules')) - curr_modulepath = os.environ.get('MODULEPATH') - error_msg = "Incorrect $MODULEPATH value after unuse: %s (orig: %s)" % (curr_modulepath, orig_modulepath) - self.assertEqual(curr_modulepath, None, error_msg) + if not isinstance(self.modtool, Lmod): + if os.environ.get('TEST_EASYBUILD_MODULES_TOOL') != 'Lmod': + return # Treat as success as nothing to do + self.skipTest("Requires Lmod as modules tool") - top_moddir = os.path.join(self.test_prefix, 'test_modules') - core_dir = os.path.join(top_moddir, 'Core') - mkdir(core_dir, parents=True) - self.modtool.use(core_dir) - self.assertTrue(os.path.samefile(os.environ.get('MODULEPATH'), core_dir)) + orig_modulepath = os.environ.get('MODULEPATH') + self.modtool.unuse(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'modules')) + curr_modulepath = os.environ.get('MODULEPATH') + error_msg = "Incorrect $MODULEPATH value after unuse: %s (orig: %s)" % (curr_modulepath, orig_modulepath) + self.assertEqual(curr_modulepath, None, error_msg) - # install toy modules in Lua syntax that are sufficient to test path_to_top_of_module_tree with - intel_mod_dir = os.path.join(top_moddir, 'Compiler', 'intel', '2016') - intel_mod = 'prepend_path("MODULEPATH", "%s")\n' % intel_mod_dir - write_file(os.path.join(core_dir, 'intel', '2016.lua'), intel_mod) + top_moddir = os.path.join(self.test_prefix, 'test_modules') + core_dir = os.path.join(top_moddir, 'Core') + mkdir(core_dir, parents=True) + self.modtool.use(core_dir) + self.assertTrue(os.path.samefile(os.environ.get('MODULEPATH'), core_dir)) - impi_mod_dir = os.path.join(top_moddir, 'MPI', 'intel', '2016', 'impi', '2016') - impi_mod = 'prepend_path("MODULEPATH", "%s")\n' % impi_mod_dir - write_file(os.path.join(intel_mod_dir, 'impi', '2016.lua'), impi_mod) + # install toy modules in Lua syntax that are sufficient to test path_to_top_of_module_tree with + intel_mod_dir = os.path.join(top_moddir, 'Compiler', 'intel', '2016') + intel_mod = 'prepend_path("MODULEPATH", "%s")\n' % intel_mod_dir + write_file(os.path.join(core_dir, 'intel', '2016.lua'), intel_mod) - imkl_mod = 'io.stderr:write("Hi from the imkl module")\n' - write_file(os.path.join(impi_mod_dir, 'imkl', '2016.lua'), imkl_mod) + impi_mod_dir = os.path.join(top_moddir, 'MPI', 'intel', '2016', 'impi', '2016') + impi_mod = 'prepend_path("MODULEPATH", "%s")\n' % impi_mod_dir + write_file(os.path.join(intel_mod_dir, 'impi', '2016.lua'), impi_mod) - self.assertEqual(self.modtool.available(), ['intel/2016']) + imkl_mod = 'io.stderr:write("Hi from the imkl module")\n' + write_file(os.path.join(impi_mod_dir, 'imkl', '2016.lua'), imkl_mod) - imkl_deps = ['intel/2016', 'impi/2016'] + self.assertEqual(self.modtool.available(), ['intel/2016']) - # modules that compose toolchain are expected to be loaded - self.modtool.load(imkl_deps) + imkl_deps = ['intel/2016', 'impi/2016'] - res = self.modtool.path_to_top_of_module_tree(core_dir, 'imkl/2016', impi_mod_dir, imkl_deps) - self.assertEqual(res, ['impi/2016', 'intel/2016']) + # modules that compose toolchain are expected to be loaded + self.modtool.load(imkl_deps) - else: - print("Skipping test_path_to_top_of_module_tree_lua, requires Lmod as modules tool") + res = self.modtool.path_to_top_of_module_tree(core_dir, 'imkl/2016', impi_mod_dir, imkl_deps) + self.assertEqual(res, ['impi/2016', 'intel/2016']) def test_interpret_raw_path_lua(self): """Test interpret_raw_path_lua method""" diff --git a/test/framework/options.py b/test/framework/options.py index 9727d3354c..010710098c 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -66,14 +66,8 @@ from easybuild.tools.run import run_shell_cmd from easybuild.tools.systemtools import DARWIN, HAVE_ARCHSPEC, get_os_type from easybuild.tools.version import VERSION -from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, cleanup, init_config -from test.framework.github import ignore_rate_limit_in_pr - -try: - import pycodestyle # noqa -except ImportError: - pass - +from test.framework.github import ignore_rate_limit_in_pr, requires_github_token +from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, cleanup, init_config, requires_pycodestyle EXTERNAL_MODULES_METADATA = """[foobar/1.2.3] name = foo, bar @@ -106,7 +100,6 @@ class CommandLineOptionsTest(EnhancedTestCase): def setUp(self): """Set up test.""" super().setUp() - self.github_token = fetch_github_token(GITHUB_TEST_ACCOUNT) self.orig_terminal_supports_colors = easybuild.tools.options.terminal_supports_colors self.orig_os_getuid = easybuild.main.os.getuid @@ -1403,12 +1396,9 @@ def check_copied_files(): with self.mocked_stdout_stderr(): self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True) + @requires_github_token() def test_github_copy_ec_from_pr(self): """Test combination of --copy-ec with --from-pr.""" - if self.github_token is None: - print("Skipping test_copy_ec_from_pr, no GitHub token available?") - return - test_working_dir = os.path.join(self.test_prefix, 'test_working_dir') mkdir(test_working_dir) test_target_dir = os.path.join(self.test_prefix, 'test_target_dir') @@ -2027,12 +2017,9 @@ def test_dry_run_categorized(self): if os.path.exists(dummylogfn): os.remove(dummylogfn) + @requires_github_token() def test_github_from_pr(self): """Test fetching easyconfigs from a PR.""" - if self.github_token is None: - print("Skipping test_from_pr, no GitHub token available?") - return - fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log') os.close(fd) @@ -2096,12 +2083,9 @@ def test_github_from_pr(self): except URLError as err: print("Ignoring URLError '%s' in test_from_pr" % err) + @requires_github_token() def test_github_from_pr_token_log(self): """Check that --from-pr doesn't leak GitHub token in log.""" - if self.github_token is None: - print("Skipping test_from_pr_token_log, no GitHub token available?") - return - fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log') os.close(fd) @@ -2114,24 +2098,22 @@ def test_github_from_pr_token_log(self): '--robot=%s' % os.path.join(os.path.dirname(__file__), 'easyconfigs'), '--github-user=%s' % GITHUB_TEST_ACCOUNT, # a GitHub token should be available for this user ] + github_token = fetch_github_token(GITHUB_TEST_ACCOUNT) try: with self.mocked_stdout_stderr(): outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True) stdout = self.get_stdout() stderr = self.get_stderr() - self.assertNotIn(self.github_token, outtxt) - self.assertNotIn(self.github_token, stdout) - self.assertNotIn(self.github_token, stderr) + self.assertNotIn(github_token, outtxt) + self.assertNotIn(github_token, stdout) + self.assertNotIn(github_token, stderr) except URLError as err: print("Ignoring URLError '%s' in test_from_pr" % err) + @requires_github_token() def test_github_from_pr_listed_ecs(self): """Test --from-pr in combination with specifying easyconfigs on the command line.""" - if self.github_token is None: - print("Skipping test_from_pr, no GitHub token available?") - return - fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log') os.close(fd) @@ -2179,12 +2161,9 @@ def test_github_from_pr_listed_ecs(self): except URLError as err: print("Ignoring URLError '%s' in test_from_pr" % err) + @requires_github_token() def test_github_from_pr_x(self): """Test combination of --from-pr with --extended-dry-run.""" - if self.github_token is None: - print("Skipping test_from_pr_x, no GitHub token available?") - return - fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log') os.close(fd) @@ -2215,6 +2194,7 @@ def test_github_from_pr_x(self): except URLError as err: print("Ignoring URLError '%s' in test_from_pr_x" % err) + @requires_github_token() def test_from_commit(self): """Test for --from-commit.""" # --from-commit does not involve using GitHub API, so no GitHub token required; @@ -2222,9 +2202,6 @@ def test_from_commit(self): # (see also https://github.blog/changelog/2025-05-08-updated-rate-limits-for-unauthenticated-requests/), # we only run this test when a GitHub token is available, # which is only the case for a limited number of test configurations (see .github/workflows/unit_tests.yml) - if self.github_token is None: - print("Skipping test_from_commit (no GitHub token available)") - return # easyconfigs commit to add EasyBuild-4.8.2.eb test_commit = '7c83a553950c233943c7b0189762f8c05cfea852' @@ -2304,6 +2281,7 @@ def test_from_commit(self): # must be run after test for --list-easyblocks, hence the '_xxx_' # cleaning up the imported easyblocks is quite difficult... + @requires_github_token() def test_xxx_include_easyblocks_from_commit(self): """Test for --include-easyblocks-from-commit.""" # --from-commit does not involve using GitHub API, so no GitHub token required; @@ -2311,9 +2289,6 @@ def test_xxx_include_easyblocks_from_commit(self): # (see also https://github.blog/changelog/2025-05-08-updated-rate-limits-for-unauthenticated-requests/), # we only run this test when a GitHub token is available, # which is only the case for a limited number of test configurations (see .github/workflows/unit_tests.yml) - if self.github_token is None: - print("Skipping test_xxx_include_easyblocks_from_commit (no GitHub token available)") - return orig_local_sys_path = sys.path[:] # easyblocks commit only touching Binary easyblock @@ -3866,12 +3841,9 @@ def test_xxx_include_generic_easyblocks(self): # must be run after test for --list-easyblocks, hence the '_xxx_' # cleaning up the imported easyblocks is quite difficult... + @requires_github_token() def test_github_xxx_include_easyblocks_from_pr(self): """Test --include-easyblocks-from-pr.""" - if self.github_token is None: - print("Skipping test_preview_pr, no GitHub token available?") - return - orig_local_sys_path = sys.path[:] fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log') @@ -4213,12 +4185,9 @@ def test_cleanup_tmpdir(self): tweaked_dir = os.path.join(tmpdir, tmpdir_files[0], 'tweaked_easyconfigs') self.assertExists(os.path.join(tweaked_dir, 'toy-1.0.eb')) + @requires_github_token() def test_github_preview_pr(self): """Test --preview-pr.""" - if self.github_token is None: - print("Skipping test_preview_pr, no GitHub token available?") - return - test_ecs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs') eb_file = os.path.join(test_ecs_path, 'b', 'bzip2', 'bzip2-1.0.6-GCC-4.9.2.eb') args = [ @@ -4232,12 +4201,9 @@ def test_github_preview_pr(self): txt = self.get_stdout() self.assertRegex(txt, r"^Comparing bzip2-1.0.6\S* with bzip2-1.0.8") + @requires_github_token() def test_github_review_pr(self): """Test --review-pr.""" - if self.github_token is None: - print("Skipping test_review_pr, no GitHub token available?") - return - # PR for bwidget 1.10.1 easyconfig, see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227 args = [ '--color=never', @@ -4484,12 +4450,9 @@ def _run_mock_eb(self, args, strip=False, **kwargs): stderr_txt = stderr_txt.strip() return stdout_txt, stderr_txt + @requires_github_token() def test_new_branch_github(self): """Test for --new-branch-github.""" - if self.github_token is None: - print("Skipping test_create_branch_github, no GitHub token available?") - return - topdir = os.path.dirname(os.path.abspath(__file__)) # test easyconfigs @@ -4560,12 +4523,9 @@ def test_new_branch_github(self): ] self.assert_multi_regex(regexs, txt) + @requires_github_token() def test_github_new_pr_from_branch(self): """Test --new-pr-from-branch.""" - if self.github_token is None: - print("Skipping test_github_new_pr_from_branch, no GitHub token available?") - return - # see https://github.com/boegel/easybuild-easyconfigs/tree/test_new_pr_from_branch_DO_NOT_REMOVE # branch created specifically for this test, # only adds toy-0.0.eb test easyconfig compared to central develop branch @@ -4600,12 +4560,9 @@ def test_github_new_pr_from_branch(self): ] self.assert_multi_regex(regexs, txt) + @requires_github_token() def test_update_branch_github(self): """Test --update-branch-github.""" - if self.github_token is None: - print("Skipping test_update_branch_github, no GitHub token available?") - return - topdir = os.path.dirname(os.path.abspath(__file__)) test_ecs = os.path.join(topdir, 'easyconfigs', 'test_ecs') toy_ec = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb') @@ -4630,12 +4587,9 @@ def test_update_branch_github(self): ] self.assert_multi_regex(regexs, txt) + @requires_github_token() def test_github_new_update_pr(self): """Test use of --new-pr (dry run only).""" - if self.github_token is None: - print("Skipping test_new_update_pr, no GitHub token available?") - return - # copy toy test easyconfig topdir = os.path.dirname(os.path.abspath(__file__)) test_ecs = os.path.join(topdir, 'easyconfigs', 'test_ecs') @@ -4857,13 +4811,9 @@ def test_github_new_update_pr(self): ] self.assert_multi_regex(regexs, txt, assert_true=False) + @requires_github_token() def test_github_new_pr_warning_missing_patch(self): """Test warning printed by --new-pr (dry run only) when a specified patch file could not be found.""" - - if self.github_token is None: - print("Skipping test_new_pr_warning_missing_patch, no GitHub token available?") - return - topdir = os.path.dirname(os.path.abspath(__file__)) test_ecs = os.path.join(topdir, 'easyconfigs', 'test_ecs') test_ec = os.path.join(self.test_prefix, 'test.eb') @@ -4899,12 +4849,9 @@ def test_github_new_pr_warning_missing_patch(self): self.assertRegex(stdout, new_pr_out_regex) self.assertRegex(stderr, warning_regex) + @requires_github_token() def test_github_sync_pr_with_develop(self): """Test use of --sync-pr-with-develop (dry run only).""" - if self.github_token is None: - print("Skipping test_sync_pr_with_develop, no GitHub token available?") - return - # use https://github.com/easybuilders/easybuild-easyconfigs/pull/9150, # which is a PR from boegel:develop to easybuilders:develop # (to sync 'develop' branch in boegel's fork with central develop branch); @@ -4928,12 +4875,9 @@ def test_github_sync_pr_with_develop(self): ]) self.assertTrue(re.match(pattern, txt), "Pattern '%s' doesn't match: %s" % (pattern, txt)) + @requires_github_token() def test_github_sync_branch_with_develop(self): """Test use of --sync-branch-with-develop (dry run only).""" - if self.github_token is None: - print("Skipping test_sync_pr_with_develop, no GitHub token available?") - return - # see https://github.com/boegel/easybuild-easyconfigs/tree/test_new_pr_from_branch_DO_NOT_REMOVE test_branch = 'test_new_pr_from_branch_DO_NOT_REMOVE' @@ -4957,12 +4901,9 @@ def test_github_sync_branch_with_develop(self): ]) self.assertTrue(re.match(pattern, stdout), "Pattern '%s' doesn't match: %s" % (pattern, stdout)) + @requires_github_token() def test_github_new_pr_python(self): """Check generated PR title for --new-pr on easyconfig that includes Python dependency.""" - if self.github_token is None: - print("Skipping test_new_pr_python, no GitHub token available?") - return - # copy toy test easyconfig test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs') toy_ec = os.path.join(self.test_prefix, 'toy.eb') @@ -5002,13 +4943,9 @@ def test_github_new_pr_python(self): regex = re.compile(r"^\* title: \"\{tools\}\[system/system\] toy v0.0\"$", re.M) self.assertRegex(txt, regex) + @requires_github_token() def test_github_new_pr_delete(self): """Test use of --new-pr to delete easyconfigs.""" - - if self.github_token is None: - print("Skipping test_new_pr_delete, no GitHub token available?") - return - ec_name = 'bzip2-1.0.8.eb' args = [ '--new-pr', @@ -5028,13 +4965,9 @@ def test_github_new_pr_delete(self): ] self.assert_multi_regex(regexs, txt) + @requires_github_token() def test_github_new_pr_dependencies(self): """Test use of --new-pr with automatic dependency lookup.""" - - if self.github_token is None: - print("Skipping test_new_pr_dependencies, no GitHub token available?") - return - foo_eb = '\n'.join([ 'easyblock = "ConfigureMake"', 'name = "foo"', @@ -5076,15 +5009,11 @@ def test_github_new_pr_dependencies(self): self.assert_multi_regex(regexs, txt) + @requires_github_token() def test_github_new_pr_easyblock(self): """ Test using --new-pr to open an easyblocks PR """ - - if self.github_token is None: - print("Skipping test_new_pr_easyblock, no GitHub token available?") - return - topdir = os.path.dirname(os.path.abspath(__file__)) toy_eb = os.path.join(topdir, 'sandbox', 'easybuild', 'easyblocks', 't', 'toy.py') self.assertExists(toy_eb) @@ -5105,13 +5034,10 @@ def test_github_new_pr_easyblock(self): ] self.assert_multi_regex(patterns, txt) + @requires_github_token() def test_github_merge_pr(self): """ Test use of --merge-pr (dry run)""" - if self.github_token is None: - print("Skipping test_merge_pr, no GitHub token available?") - return - # start by making sure --merge-pr without dry-run errors out for a closed PR args = [ '--merge-pr', @@ -5212,12 +5138,9 @@ def test_github_merge_pr(self): ]) self.assertIn(expected_stdout, stdout) + @requires_github_token() def test_github_empty_pr(self): """Test use of --new-pr (dry run only) with no changes""" - if self.github_token is None: - print("Skipping test_empty_pr, no GitHub token available?") - return - # get file from develop branch full_url = URL_SEPARATOR.join([GITHUB_RAW, GITHUB_EB_MAIN, GITHUB_EASYCONFIGS_REPO, 'develop/easybuild/easyconfigs/z/zlib/zlib-1.3.1-GCCcore-14.2.0.eb']) @@ -5673,13 +5596,14 @@ def test_zip_logs(self): def test_debug_lmod(self): """Test use of --debug-lmod.""" - if isinstance(self.modtool, Lmod): - init_config(build_options={'debug_lmod': True}) - out = self.modtool.run_module('avail', return_output=True) + if not isinstance(self.modtool, Lmod): + if os.environ.get('TEST_EASYBUILD_MODULES_TOOL') != 'Lmod': + return # Treat as success as nothing to do + self.skipTest("requires Lmod as modules tool") + init_config(build_options={'debug_lmod': True}) + out = self.modtool.run_module('avail', return_output=True) - self.assert_multi_regex([r"^Lmod version", r"^lmod\(--terse -D avail\)\{", ":avail"], out) - else: - print("Skipping test_debug_lmod, requires Lmod as modules tool") + self.assert_multi_regex([r"^Lmod version", r"^lmod\(--terse -D avail\)\{", ":avail"], out) def test_use_color(self): """Test use_color function.""" @@ -5879,12 +5803,9 @@ def test_parse_optarch(self): options.postprocess() self.assertEqual(options.options.optarch, optarch_parsed) + @requires_pycodestyle() def test_check_contrib_style(self): """Test style checks performed by --check-contrib + dedicated --check-style option.""" - if 'pycodestyle' not in sys.modules: - print("Skipping test_check_contrib_style pycodestyle is not available") - return - regex = re.compile(r"Running style check on 2 easyconfig\(s\)(.|\n)*>> All style checks PASSed!", re.M) args = [ '--check-style', @@ -5930,13 +5851,10 @@ def test_check_contrib_style(self): ] self.assert_multi_regex(patterns, stdout) + @requires_pycodestyle() def test_check_contrib_non_style(self): """Test non-style checks performed by --check-contrib.""" - if 'pycodestyle' not in sys.modules: - print("Skipping test_check_contrib_non_style pycodestyle is not available") - return - args = [ '--check-contrib', 'toy-0.0.eb', diff --git a/test/framework/parallelbuild.py b/test/framework/parallelbuild.py index 0c821dd882..1efbcf6d06 100644 --- a/test/framework/parallelbuild.py +++ b/test/framework/parallelbuild.py @@ -44,7 +44,7 @@ from easybuild.tools.options import parse_options from easybuild.tools.parallelbuild import build_easyconfigs_in_parallel, submit_jobs from easybuild.tools.robot import resolve_dependencies - +from test.framework.utilities import requires_GC3Pie # test GC3Pie configuration with large resource specs GC3PIE_LOCAL_CONFIGURATION = """[resource/ebtestlocalhost] @@ -215,14 +215,9 @@ def test_build_easyconfigs_in_parallel_pbs_python(self): pbs_python.PbsJob = pbs_python_PbsJob self.mock_stdout(False) + @requires_GC3Pie() def test_build_easyconfigs_in_parallel_gc3pie(self): """Test build_easyconfigs_in_parallel(), using GC3Pie with local config as backend for --job.""" - try: - import gc3libs # noqa (ignore unused import) - except ImportError: - print("GC3Pie not available, skipping test") - return - self.allow_deprecated_behaviour() # put GC3Pie config in place to use local host and fork/exec diff --git a/test/framework/repository.py b/test/framework/repository.py index 37f0d9a729..84f12aa5f2 100644 --- a/test/framework/repository.py +++ b/test/framework/repository.py @@ -44,6 +44,7 @@ from easybuild.tools.repository.repository import init_repository from easybuild.tools.run import run_shell_cmd from easybuild.tools.version import VERSION +from test.framework.utilities import requires_pysvn class RepositoryTest(EnhancedTestCase): @@ -113,15 +114,9 @@ def test_gitrepo(self): shutil.rmtree(repo.wc) shutil.rmtree(tmpdir) + @requires_pysvn() def test_svnrepo(self): """Test using SvnRepository.""" - # only run this test if pysvn Python module is available - try: - from pysvn import ClientError # noqa - except ImportError: - print("(skipping SvnRepository test)") - return - # GitHub also supports SVN test_repo_url = 'https://github.com/easybuilders/testrepository' diff --git a/test/framework/robot.py b/test/framework/robot.py index 895c92f834..ca71faf11b 100644 --- a/test/framework/robot.py +++ b/test/framework/robot.py @@ -51,10 +51,10 @@ from easybuild.tools.config import module_classes from easybuild.tools.configobj import ConfigObj from easybuild.tools.filetools import copy_file, mkdir, read_file, write_file -from easybuild.tools.github import fetch_github_token from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version from easybuild.tools.modules import invalidate_module_caches_for, reset_module_caches from easybuild.tools.robot import check_conflicts, det_robot_path, resolve_dependencies, search_easyconfigs +from test.framework.github import requires_github_token from test.framework.utilities import find_full_path @@ -113,7 +113,6 @@ def install_mock_module(self): def setUp(self): """Set up test.""" super().setUp() - self.github_token = fetch_github_token(GITHUB_TEST_ACCOUNT) self.orig_experimental = easybuild.framework.easyconfig.tools._log.experimental self.orig_modtool = self.modtool @@ -753,12 +752,9 @@ def test_github_det_easyconfig_paths_from_commit(self): regex = re.compile(r"^ \* \[.\] %s.*%s \(module: %s\)$" % (path_prefix, ec_fn, module), re.M) self.assertTrue(regex.search(outtxt), "Found pattern %s in %s" % (regex.pattern, outtxt)) + @requires_github_token() def test_github_det_easyconfig_paths_from_pr(self): """Test det_easyconfig_paths function, with --from-pr enabled as well.""" - if self.github_token is None: - print("Skipping test_from_pr, no GitHub token available?") - return - fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log') os.close(fd) diff --git a/test/framework/style.py b/test/framework/style.py index 92f588cf3b..81040617b8 100644 --- a/test/framework/style.py +++ b/test/framework/style.py @@ -31,26 +31,19 @@ import glob import os import sys -from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered +from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, requires_pycodestyle from unittest import TextTestRunner from easybuild.base import fancylogger from easybuild.framework.easyconfig.style import _eb_check_trailing_whitespace, check_easyconfigs_style -try: - import pycodestyle # noqa -except ImportError: - pass - class StyleTest(EnhancedTestCase): log = fancylogger.getLogger("StyleTest", fname=False) + @requires_pycodestyle() def test_style_conformance(self): """Check the easyconfigs for style""" - if 'pycodestyle' not in sys.modules: - print("Skipping test_style_conformance pycodestyle is not available") - return # all available easyconfig files test_easyconfigs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs') @@ -61,11 +54,9 @@ def test_style_conformance(self): self.assertEqual(result, 0, "No code style errors (and/or warnings) found.") + @requires_pycodestyle() def test_check_trailing_whitespace(self): """Test for trailing whitespace check.""" - if 'pycodestyle' not in sys.modules: - print("Skipping test_check_trailing_whitespace pycodestyle is not available") - return lines = [ "name = 'foo'", # no trailing whitespace diff --git a/test/framework/suite.py b/test/framework/suite.py index d9a2454b4e..d5a1cc8567 100755 --- a/test/framework/suite.py +++ b/test/framework/suite.py @@ -137,12 +137,29 @@ def run(self, *args, **kwargs): return res -def load_tests(loader, tests, pattern): +class SkipSummaryResult(unittest.TextTestResult): + """Decorator that prints skipped tests at the end of the run if in CI environment.""" + def __init__(self, stream, descriptions, verbosity, *args, **kwargs): + super().__init__(stream, descriptions, verbosity, *args, **kwargs) + self._verbosity = verbosity + + def stopTestRun(self): + super().stopTestRun() + if 'CI' in os.environ and self.skipped: + print('\n=== Skipped tests ===') + print('\n'.join(f'\t{test}: {reason}' for test, reason in self.skipped)) + + +class SkipSummaryRunner(unittest.TextTestRunner): + resultclass = SkipSummaryResult + + +def load_tests(loader, _tests, _pattern): return EasyBuildFrameworkTestSuite(loader) if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1].startswith('-'): - unittest.main() + unittest.main(testRunner=SkipSummaryRunner()) else: - unittest.TextTestRunner().run(EasyBuildFrameworkTestSuite(None)) + SkipSummaryRunner().run(EasyBuildFrameworkTestSuite(None)) diff --git a/test/framework/utilities.py b/test/framework/utilities.py index 1807085d75..e44a5cb12a 100644 --- a/test/framework/utilities.py +++ b/test/framework/utilities.py @@ -30,6 +30,7 @@ """ import copy import fileinput +import functools import os import re import shutil @@ -48,7 +49,7 @@ from easybuild.framework.easyconfig import easyconfig from easybuild.framework.easyblock import EasyBlock from easybuild.main import main -from easybuild.tools import config +from easybuild.tools import config, LooseVersion from easybuild.tools.config import GENERAL_CLASS, Singleton, module_classes from easybuild.tools.configobj import ConfigObj from easybuild.tools.environment import modify_env @@ -555,3 +556,87 @@ def find_full_path(base_path, trim=(lambda x: x)): break return full_path + + +def skip_silently(test_item): + """Decorator to turn a test into a no-op""" + @functools.wraps(test_item) + def skip_wrapper(*args, **kwargs): + return + return skip_wrapper + + +def skip_never(test_item): + """Decorator to not skip a test""" + return test_item + + +def skip_silentCI_unless(condition, reason): + """Decorator to skip a test if the condition is met. + + On CI the test is turned into a no-op to avoid any output.""" + if 'CI' in os.environ: + return skip_never if condition else skip_silently + else: + return unittest.skipUnless(condition, reason) + + +def requires_pycodestyle(): + try: + import pycodestyle # noqa + ok = True + except ImportError: + ok = False + return unittest.skipUnless(ok, "no pycodestyle available") + + +def requires_autopep8(): + try: + import autopep8 # noqa + ok = True + except ImportError: + ok = False + return unittest.skipUnless(ok, "autopep8 is not available") + + +def requires_GC3Pie(): + try: + import gc3libs # noqa + ok = True + except ImportError: + ok = False + if LooseVersion(sys.version) < '3.11': + return unittest.skipUnless(ok, "GC3Pie not available") + else: + # GC3Pie not available for Python 3.11 so silently skip: + # https://github.com/gc3pie/gc3pie/issues/674 + return skip_silentCI_unless(ok, "GC3Pie not available") + + +def requires_pygraph(): + try: + import pygraph # noqa + ok = True + except ImportError: + ok = False + return unittest.skipUnless(ok, "pygraph is not available") + + +def requires_pysvn(): + try: + from pysvn import ClientError # noqa + ok = True + except ImportError: + ok = False + # For CI skip silently, not easy enough to install, + # see https://github.com/leafvmaple/pysvn/issues/1 + return skip_silentCI_unless(ok, "PySVN is not available, use e.g. apt-get install python3-svn") + + +def requires_PyYAML(): + try: + import yaml # noqa + ok = True + except ImportError: + ok = False + return unittest.skipUnless(ok, "PyYAML is not available")