diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 3b88c58436..162512b8b7 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -34,44 +34,23 @@ jobs: - ${{needs.setup.outputs.modulesTcl}} - ${{needs.setup.outputs.modules3}} - ${{needs.setup.outputs.modules4}} - module_syntax: [Lua, Tcl] lc_all: [""] - # don't test with Lua module syntax (only supported in Lmod) - exclude: - - modules_tool: ${{needs.setup.outputs.modulesTcl}} - module_syntax: Lua - - modules_tool: ${{needs.setup.outputs.modules3}} - module_syntax: Lua - - modules_tool: ${{needs.setup.outputs.modules4}} - module_syntax: Lua include: # Test different Python 3 versions with Lmod 8.x (with both Lua and Tcl module syntax) - python: 3.7 modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Lua - python: 3.8 modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Lua - - python: 3.8 - modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Tcl - python: 3.9 modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Lua - python: '3.10' modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Lua - - python: '3.11' - modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Lua - python: '3.11' modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Tcl # There may be encoding errors in Python 3 which are hidden when an UTF-8 encoding is set # Hence run the tests (again) with LC_ALL=C and Python 3.6 (or any < 3.7) - python: 3.6 modules_tool: ${{needs.setup.outputs.lmod8}} - module_syntax: Lua lc_all: C fail-fast: false steps: @@ -122,16 +101,11 @@ jobs: # and are only run after the PR gets merged GITHUB_TOKEN: ${{secrets.CI_UNIT_TESTS_GITHUB_TOKEN}} run: | - # don't install GitHub token when testing with Lmod 7.x or non-Lmod module tools, - # and only when testing with Lua as module syntax, - # to avoid hitting GitHub rate limit; + # don't install GitHub token when testing with Lmod 7.x or non-Lmod module tools, to avoid hitting GitHub rate limit; # tests that require a GitHub token are skipped automatically when no GitHub token is available - if [[ ! "${{matrix.modules_tool}}" =~ 'Lmod-7' ]] && [[ ! "${{matrix.modules_tool}}" =~ 'modules-' ]] && [[ "${{matrix.module_syntax}}" == 'Lua' ]]; then + if [[ ! "${{matrix.modules_tool}}" =~ 'Lmod-7' ]] && [[ ! "${{matrix.modules_tool}}" =~ 'modules-' ]]; then if [ ! -z $GITHUB_TOKEN ]; then - if [ "x${{matrix.python}}" == 'x2.6' ]; - then SET_KEYRING="keyring.set_keyring(keyring.backends.file.PlaintextKeyring())"; - else SET_KEYRING="import keyrings.alt.file; keyring.set_keyring(keyrings.alt.file.PlaintextKeyring())"; - fi; + SET_KEYRING="import keyrings.alt.file; keyring.set_keyring(keyrings.alt.file.PlaintextKeyring())"; python -c "import keyring; $SET_KEYRING; keyring.set_password('github_token', 'easybuild_test', '$GITHUB_TOKEN')"; fi echo "GitHub token installed!" @@ -169,8 +143,6 @@ jobs: - name: run test suite env: EB_VERBOSE: 1 - EASYBUILD_MODULE_SYNTAX: ${{matrix.module_syntax}} - TEST_EASYBUILD_MODULE_SYNTAX: ${{matrix.module_syntax}} LC_ALL: ${{matrix.lc_all}} run: | # run tests *outside* of checked out easybuild-framework directory, @@ -195,19 +167,32 @@ jobs: else export EASYBUILD_MODULES_TOOL=Lmod fi - export TEST_EASYBUILD_MODULES_TOOL=$EASYBUILD_MODULES_TOOL - eb --show-config - # gather some useful info on test system - eb --show-system-info - # check GitHub configuration - eb --check-github --github-user=easybuild_test - # 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 - # 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|skipping SvnRepository test|requires Lmod as modules tool|stty: 'standard input': Inappropriate ioctl for device|CryptographyDeprecationWarning: Python 3.[56]|from cryptography.* import |CryptographyDeprecationWarning: Python 2|Blowfish|GC3Pie not available, skipping test" - # '|| true' is needed to avoid that Travis 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) + export TEST_EASYBUILD_MODULES_TOOL=${EASYBUILD_MODULES_TOOL} + + # Run tests with LUA and Tcl module syntax (where supported) + for module_syntax in Lua Tcl; do + # Only Lmod supports Lua + if [[ "${module_syntax}" == "Lua" ]] && [[ "${EASYBUILD_MODULES_TOOL}" != "Lmod" ]]; then + echo "Not testing with '${module_syntax}' as module syntax with '${EASYBUILD_MODULES_TOOL}' as modules tool" + continue + fi + printf '\n\n=====================> Using $module_syntax module syntax <=====================\n\n' + export EASYBUILD_MODULE_SYNTAX="${module_syntax}" + export TEST_EASYBUILD_MODULE_SYNTAX="${EASYBUILD_MODULE_SYNTAX}" + + eb --show-config + # gather some useful info on test system + eb --show-system-info + # check GitHub configuration + eb --check-github --github-user=easybuild_test + # 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 + # 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|skipping SvnRepository test|requires Lmod as modules tool|stty: 'standard input': Inappropriate ioctl for device|CryptographyDeprecationWarning: Python 3.[56]|from cryptography.* import |CryptographyDeprecationWarning: Python 2|Blowfish|GC3Pie not available, skipping test" + # '|| 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) + done diff --git a/.github/workflows/unit_tests_python2.yml b/.github/workflows/unit_tests_python2.yml new file mode 100644 index 0000000000..1b921ee83c --- /dev/null +++ b/.github/workflows/unit_tests_python2.yml @@ -0,0 +1,81 @@ +# documentation: https://help.github.com/en/articles/workflow-syntax-for-github-actions +name: EasyBuild framework unit tests (python2) +on: [push, pull_request] + +permissions: + contents: read # to fetch code (actions/checkout) + +concurrency: + group: ${{format('{0}:{1}:{2}', github.repository, github.ref, github.workflow)}} + cancel-in-progress: true + +jobs: + test_python2: + runs-on: ubuntu-20.04 + container: + # CentOS 7.9 container that already includes Lmod & co, + # see https://github.com/easybuilders/easybuild-containers + image: ghcr.io/easybuilders/centos-7.9-amd64 + steps: + - uses: actions/checkout@v3 + + - name: install Python packages + run: | + # Python packages + python2 -V + python2 -m pip --version + python2 -m pip install --upgrade pip + python2 -m pip --version + # strip out GC3Pie since installation with ancient setuptools (0.9.8) fails + sed -i '/GC3Pie/d' requirements.txt + python2 -m pip install -r requirements.txt + # git config is required to make actual git commits (cfr. tests for GitRepository) + sudo -u easybuild git config --global user.name "GitHub Actions" + sudo -u easybuild git config --global user.email "actions@github.com" + sudo -u easybuild git config --get-regexp 'user.*' + + - name: install GitHub token (if available) + env: + # token (owned by @boegelbot) with gist permissions (required for some of the tests for GitHub integration); + # this token is not available in pull requests, so tests that require it are skipped in PRs, + # and are only run after the PR gets merged + GITHUB_TOKEN: ${{secrets.CI_UNIT_TESTS_GITHUB_TOKEN}} + run: | + # tests that require a GitHub token are skipped automatically when no GitHub token is available + if [ ! -z $GITHUB_TOKEN ]; then + sudo -u easybuild python2 -c "import keyring; import keyrings.alt.file; keyring.set_keyring(keyrings.alt.file.PlaintextKeyring()); keyring.set_password('github_token', 'easybuild_test', '$GITHUB_TOKEN')"; + echo "GitHub token installed!" + else + echo "Installation of GitHub token skipped!" + fi + + - name: install sources + run: | + # install from source distribution tarball, to test release as published on PyPI + python2 setup.py sdist + ls dist + export PREFIX=/tmp/$USER/$GITHUB_SHA + python2 -m pip install --prefix $PREFIX dist/easybuild-framework*tar.gz + + - name: run test suite + run: | + # run tests *outside* of checked out easybuild-framework directory, + # to ensure we're testing installed version (see previous step) + cd $HOME + # make sure 'eb' is available via $PATH, and that $PYTHONPATH is set (some tests expect that) + export PREFIX=/tmp/$USER/$GITHUB_SHA + ENV_CMDS="export PATH=$PREFIX/bin:$PATH; export PYTHONPATH=$PREFIX/lib/python2.7/site-packages:$PYTHONPATH" + ENV_CMDS="${ENV_CMDS}; export EB_VERBOSE=1; export EB_PYTHON=python2; export TEST_EASYBUILD_SILENCE_DEPRECATION_WARNINGS=python2" + # run EasyBuild command via (non-root) easybuild user + login shell + sudo -u easybuild bash -l -c "${ENV_CMDS}; module --version; eb --version" + # show active EasyBuild configuration + sudo -u easybuild bash -l -c "${ENV_CMDS}; eb --show-config" + # gather some useful info on test system + sudo -u easybuild bash -l -c "${ENV_CMDS}; eb --show-system-info" + # check GitHub configuration + sudo -u easybuild bash -l -c "${ENV_CMDS}; eb --check-github --github-user=easybuild_test" + # 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 + # run test suite (via easybuild user + login shell) + sudo -u easybuild bash -l -c "${ENV_CMDS}; python2 -O -m test.framework.suite" diff --git a/easybuild/framework/easyconfig/templates.py b/easybuild/framework/easyconfig/templates.py index 04ffdff1e7..0cd633bbcd 100644 --- a/easybuild/framework/easyconfig/templates.py +++ b/easybuild/framework/easyconfig/templates.py @@ -76,9 +76,9 @@ ('installdir', "Installation directory"), ('start_dir', "Directory in which the build process begins"), ] -# software names for which to define ver and shortver templates +# software names for which to define ver, majver and shortver templates TEMPLATE_SOFTWARE_VERSIONS = [ - # software name, prefix for *ver and *shortver + # software name, prefix for *ver, *majver and *shortver ('CUDA', 'cuda'), ('CUDAcore', 'cuda'), ('Java', 'java'), @@ -423,6 +423,7 @@ def template_documentation(): # step 2: add *ver/*shortver templates for software listed in TEMPLATE_SOFTWARE_VERSIONS doc.append("Template names/values for (short) software versions") for name, pref in TEMPLATE_SOFTWARE_VERSIONS: + doc.append("%s%%(%smajver)s: major version for %s" % (indent_l1, pref, name)) doc.append("%s%%(%sshortver)s: short version for %s (.)" % (indent_l1, pref, name)) doc.append("%s%%(%sver)s: full version for %s" % (indent_l1, pref, name)) diff --git a/easybuild/main.py b/easybuild/main.py index 343b8450d8..d6d5755e9a 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -42,6 +42,7 @@ import os import stat import sys +import tempfile import traceback # IMPORTANT this has to be the first easybuild import as it customises the logging @@ -253,8 +254,9 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state easyconfig._easyconfigs_cache.clear() easyconfig._easyconfig_files_cache.clear() - # restore environment + # restore environment and reset tempdir (to avoid tmpdir path getting progressively longer) restore_env(init_env) + tempfile.tempdir = None # If EasyConfig specific arguments were supplied in EasyStack file # merge arguments with original command line args diff --git a/easybuild/tools/docs.py b/easybuild/tools/docs.py index cf43f66bad..c6e1403ab5 100644 --- a/easybuild/tools/docs.py +++ b/easybuild/tools/docs.py @@ -441,6 +441,7 @@ def avail_easyconfig_templates_txt(): # step 2: add SOFTWARE_VERSIONS doc.append('Template names/values for (short) software versions') for name, pref in TEMPLATE_SOFTWARE_VERSIONS: + doc.append("%s%%(%smajver)s: major version for %s" % (INDENT_4SPACES, pref, name)) doc.append("%s%%(%sshortver)s: short version for %s (.)" % (INDENT_4SPACES, pref, name)) doc.append("%s%%(%sver)s: full version for %s" % (INDENT_4SPACES, pref, name)) doc.append('') @@ -495,8 +496,10 @@ def avail_easyconfig_templates_rst(): ver = [] ver_desc = [] for name, pref in TEMPLATE_SOFTWARE_VERSIONS: + ver.append('``%%(%smajver)s``' % pref) ver.append('``%%(%sshortver)s``' % pref) ver.append('``%%(%sver)s``' % pref) + ver_desc.append('major version for %s' % name) ver_desc.append('short version for %s (.)' % name) ver_desc.append('full version for %s' % name) table_values = [ver, ver_desc] @@ -558,8 +561,10 @@ def avail_easyconfig_templates_md(): ver = [] ver_desc = [] for name, pref in TEMPLATE_SOFTWARE_VERSIONS: + ver.append('``%%(%smajver)s``' % pref) ver.append('``%%(%sshortver)s``' % pref) ver.append('``%%(%sver)s``' % pref) + ver_desc.append('major version for %s' % name) ver_desc.append('short version for %s (``.``)' % name) ver_desc.append('full version for %s' % name) table_values = [ver, ver_desc] diff --git a/test/framework/docs.py b/test/framework/docs.py index 146d05b24c..84d862bd3c 100644 --- a/test/framework/docs.py +++ b/test/framework/docs.py @@ -785,6 +785,7 @@ def test_avail_easyconfig_templates(self): r"^Template names/values derived from easyconfig instance", r"^\s+%\(version_major\)s: Major version", r"^Template names/values for \(short\) software versions", + r"^\s+%\(pymajver\)s: major version for Python", r"^\s+%\(pyshortver\)s: short version for Python \(\.\)", r"^Template constants that can be used in easyconfigs", r"^\s+SOURCE_TAR_GZ: Source \.tar\.gz bundle \(%\(name\)s-%\(version\)s.tar.gz\)", diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 3e12bc0ed5..6ab65c933b 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -1527,6 +1527,7 @@ def test_fetch_sources(self): def test_download_instructions(self): """Test use of download_instructions easyconfig parameter.""" + orig_test_ec = '\n'.join([ "easyblock = 'ConfigureMake'", "name = 'software_with_missing_sources'", diff --git a/test/framework/easyconfig.py b/test/framework/easyconfig.py index 8c4c64e5b5..56e6a378bd 100644 --- a/test/framework/easyconfig.py +++ b/test/framework/easyconfig.py @@ -1294,7 +1294,7 @@ def test_templating_doc(self): # expected length: 1 per constant and 2 extra per constantgroup (title + empty line in between) temps = [ easyconfig.templates.TEMPLATE_NAMES_EASYCONFIG, - easyconfig.templates.TEMPLATE_SOFTWARE_VERSIONS * 2, + easyconfig.templates.TEMPLATE_SOFTWARE_VERSIONS * 3, easyconfig.templates.TEMPLATE_NAMES_CONFIG, easyconfig.templates.TEMPLATE_NAMES_LOWER, easyconfig.templates.TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, @@ -2140,12 +2140,15 @@ def eval_quoted_string(quoted_val, val): Helper function to sanity check we can use the quoted string in Python contexts. Returns the evaluated (i.e. unquoted) string """ - globals = dict() + scope = dict() try: - exec('res = %s' % quoted_val, globals) - except Exception as e: # pylint: disable=broad-except - self.fail('Failed to evaluate %s (from %s): %s' % (quoted_val, val, e)) - return globals['res'] + # this is needlessly complicated because we can't use 'exec' here without potentially running + # into a SyntaxError bug in old Python 2.7 versions (for example when running the tests in CentOS 7.9) + # cfr. https://stackoverflow.com/questions/4484872/why-doesnt-exec-work-in-a-function-with-a-subfunction + eval(compile('res = %s' % quoted_val, '', 'exec'), dict(), scope) + except Exception as err: # pylint: disable=broad-except + self.fail('Failed to evaluate %s (from %s): %s' % (quoted_val, val, err)) + return scope['res'] def assertEqual_unquoted(quoted_val, val): """Assert that evaluating the quoted_val yields the val""" diff --git a/test/framework/easystack.py b/test/framework/easystack.py index e131964df6..198350a0e7 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -31,6 +31,7 @@ import os import re import sys +import tempfile from unittest import TextTestRunner import easybuild.tools.build_log @@ -129,11 +130,22 @@ def test_easystack_invalid_key2(self): self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) def test_easystack_restore_env_after_each_build(self): - """Test that the build environment is reset for each easystack item""" + """Test that the build environment and tmpdir is reset for each easystack item""" + + orig_tmpdir_tempfile = tempfile.gettempdir() + orig_tmpdir_env = os.getenv('TMPDIR') + orig_tmpdir_tempfile_len = len(orig_tmpdir_env.split(os.path.sep)) + orig_tmpdir_env_len = len(orig_tmpdir_env.split(os.path.sep)) + test_es_txt = '\n'.join([ "easyconfigs:", " - toy-0.0-gompi-2018a.eb:", " - libtoy-0.0.eb:", + # also include a couple of easyconfigs for which a module is already available in test environment, + # see test/framework/modules + " - GCC-7.3.0-2.30", + " - FFTW-3.3.7-gompi-2018a", + " - foss-2018a", ]) test_es_path = os.path.join(self.test_prefix, 'test.yml') write_file(test_es_path, test_es_txt) @@ -145,10 +157,25 @@ def test_easystack_restore_env_after_each_build(self): ] self.mock_stdout(True) stdout = self.eb_main(args, do_build=True, raise_error=True) + stdout = self.eb_main(args, do_build=True, raise_error=True, reset_env=False, redo_init_config=False) self.mock_stdout(False) regex = re.compile(r"WARNING Loaded modules detected: \[.*gompi/2018.*\]\n") self.assertFalse(regex.search(stdout), "Pattern '%s' should not be found in: %s" % (regex.pattern, stdout)) + # temporary directory after run should be exactly 2 levels deeper than original one: + # - 1 level added by setting up configuration in EasyBuild main function + # - 1 extra level added by first re-configuration for easystack item + # (because $TMPDIR set by configuration done in main function is retained) + tmpdir_tempfile = tempfile.gettempdir() + tmpdir_env = os.getenv('TMPDIR') + tmpdir_tempfile_len = len(tmpdir_tempfile.split(os.path.sep)) + tmpdir_env_len = len(tmpdir_env.split(os.path.sep)) + + self.assertEqual(tmpdir_tempfile_len, orig_tmpdir_tempfile_len + 2) + self.assertEqual(tmpdir_env_len, orig_tmpdir_env_len + 2) + self.assertTrue(tmpdir_tempfile.startswith(orig_tmpdir_tempfile)) + self.assertTrue(tmpdir_env.startswith(orig_tmpdir_env)) + def test_missing_easyconfigs_key(self): """Test that EasyStack file that doesn't contain an EasyConfigs key will fail with sane error message""" topdir = os.path.dirname(os.path.abspath(__file__)) diff --git a/test/framework/options.py b/test/framework/options.py index 59a742ae7e..bde1e565c5 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -577,6 +577,7 @@ def run_test(fmt=None): pattern_lines = [ r'^``%\(version_major\)s``\s+Major version\s*$', r'^``%\(cudaver\)s``\s+full version for CUDA\s*$', + r'^``%\(cudamajver\)s``\s+major version for CUDA\s*$', r'^``%\(pyshortver\)s``\s+short version for Python \(.\)\s*$', r'^\* ``%\(name\)s``$', r'^``%\(namelower\)s``\s+lower case of value of name\s*$', @@ -588,6 +589,7 @@ def run_test(fmt=None): pattern_lines = [ r'^\s+%\(version_major\)s: Major version$', r'^\s+%\(cudaver\)s: full version for CUDA$', + r'^\s+%\(cudamajver\)s: major version for CUDA$', r'^\s+%\(pyshortver\)s: short version for Python \(.\)$', r'^\s+%\(name\)s$', r'^\s+%\(namelower\)s: lower case of value of name$', @@ -5329,7 +5331,7 @@ def test_debug_lmod(self): init_config(build_options={'debug_lmod': True}) out = self.modtool.run_module('avail', return_output=True) - for pattern in [r"^Lmod version", r"^lmod\(--terse -D avail\)\{", "Master:avail"]: + for pattern in [r"^Lmod version", r"^lmod\(--terse -D avail\)\{", ":avail"]: regex = re.compile(pattern, re.M) self.assertTrue(regex.search(out), "Pattern '%s' found in: %s" % (regex.pattern, out)) else: diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index f8280ba733..2c4bef191c 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -3653,7 +3653,7 @@ def __exit__(self, type, value, traceback): wait_matches = wait_regex.findall(stdout) # we can't rely on an exact number of 'waiting' messages, so let's go with a range... - self.assertIn(len(wait_matches), range(2, 5)) + self.assertIn(len(wait_matches), range(1, 5)) self.assertTrue(ok_regex.search(stdout), "Pattern '%s' found in: %s" % (ok_regex.pattern, stdout))