From bfb7ccdb8ea17a8661be5d112b066c3e3a8fe292 Mon Sep 17 00:00:00 2001 From: Jason Boutte Date: Thu, 9 Apr 2026 13:37:36 -0700 Subject: [PATCH 1/6] chore: remove scripts_regression_tests.py in favor of pytest --- CIME/tests/scripts_regression_tests.py | 295 ------------------ conftest.py | 170 +++++++++- .../fortran-unit-testing.rst | 4 +- doc/source/contributing-guide.rst | 45 +-- 4 files changed, 172 insertions(+), 342 deletions(-) delete mode 100755 CIME/tests/scripts_regression_tests.py diff --git a/CIME/tests/scripts_regression_tests.py b/CIME/tests/scripts_regression_tests.py deleted file mode 100755 index 66f4c015298..00000000000 --- a/CIME/tests/scripts_regression_tests.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python3 - -""" -Script containing CIME python regression test suite. This suite should be run -to confirm overall CIME correctness. -""" - -import glob, os, re, shutil, signal, sys, tempfile, threading, time, logging, unittest, getpass, filecmp, time, atexit, functools - -CIMEROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -sys.path.insert(0, CIMEROOT) - -from xml.etree.ElementTree import ParseError - -import subprocess, argparse - -subprocess.call('/bin/rm -f $(find . -name "*.pyc")', shell=True, cwd=CIMEROOT) -import stat as osstat - -import collections - -from CIME.utils import ( - run_cmd, - run_cmd_no_fail, - get_lids, - get_current_commit, - safe_copy, - CIMEError, - get_cime_root, - get_src_root, - Timeout, - import_from_file, - get_model, -) -import CIME.test_scheduler, CIME.wait_for_tests -from CIME import get_tests -from CIME.test_scheduler import TestScheduler -from CIME.XML.env_run import EnvRun -from CIME.XML.machines import Machines -from CIME.XML.files import Files -from CIME.case import Case -from CIME.code_checker import check_code, get_all_checkable_files -from CIME.test_status import * -from CIME.provenance import get_test_success, save_test_success -from CIME import utils -from CIME.tests.base import BaseTestCase -from CIME.config import Config - -os.environ["CIME_GLOBAL_WALLTIME"] = "0:05:00" - -TEST_RESULT = None - - -def write_provenance_info(machine, test_compiler, test_mpilib, test_root): - curr_commit = get_current_commit(repo=CIMEROOT) - logging.info("Testing commit %s" % curr_commit) - cime_model = get_model() - logging.info("Using cime_model = %s" % cime_model) - logging.info("Testing machine = %s" % machine.get_machine_name()) - if test_compiler is not None: - logging.info("Testing compiler = %s" % test_compiler) - if test_mpilib is not None: - logging.info("Testing mpilib = %s" % test_mpilib) - logging.info("Test root: %s" % test_root) - logging.info("Test driver: %s" % CIME.utils.get_cime_default_driver()) - logging.info("Python version {}\n".format(sys.version)) - - -def cleanup(test_root): - if ( - os.path.exists(test_root) - and TEST_RESULT is not None - and TEST_RESULT.wasSuccessful() - ): - testreporter = os.path.join(test_root, "testreporter") - files = os.listdir(test_root) - if len(files) == 1 and os.path.isfile(testreporter): - os.unlink(testreporter) - if not os.listdir(test_root): - print("All pass, removing directory:", test_root) - os.rmdir(test_root) - - -def setup_arguments(parser): - parser.add_argument( - "--fast", - action="store_true", - help="Skip full system tests, which saves a lot of time", - ) - - parser.add_argument( - "--no-batch", - action="store_true", - help="Do not submit jobs to batch system, run locally." - " If false, will default to machine setting.", - ) - - parser.add_argument( - "--no-fortran-run", - action="store_true", - help="Do not run any fortran jobs. Implies --fast" " Used for github actions", - ) - - parser.add_argument( - "--no-cmake", action="store_true", help="Do not run cmake tests" - ) - - parser.add_argument( - "--no-teardown", - action="store_true", - help="Do not delete directories left behind by testing", - ) - - parser.add_argument( - "--machine", help="Select a specific machine setting for cime", default=None - ) - - parser.add_argument( - "--compiler", help="Select a specific compiler setting for cime", default=None - ) - - parser.add_argument( - "--mpilib", help="Select a specific compiler setting for cime", default=None - ) - - parser.add_argument( - "--test-root", - help="Select a specific test root for all cases created by the testing", - default=None, - ) - - parser.add_argument( - "--timeout", - type=int, - help="Select a specific timeout for all tests", - default=None, - ) - - -def configure_tests( - timeout, - no_fortran_run, - fast, - no_batch, - no_cmake, - no_teardown, - machine, - compiler, - mpilib, - test_root, - **kwargs -): - config = CIME.utils.get_cime_config() - - customize_path = os.path.join(utils.get_src_root(), "cime_config", "customize") - Config.load(customize_path) - - if timeout: - BaseTestCase.GLOBAL_TIMEOUT = str(timeout) - - BaseTestCase.NO_FORTRAN_RUN = no_fortran_run or False - BaseTestCase.FAST_ONLY = fast or no_fortran_run - BaseTestCase.NO_BATCH = no_batch or False - BaseTestCase.NO_CMAKE = no_cmake or False - BaseTestCase.NO_TEARDOWN = no_teardown or False - - # make sure we have default values - MACHINE = None - TEST_COMPILER = None - TEST_MPILIB = None - - if machine is not None: - MACHINE = Machines(machine=machine) - os.environ["CIME_MACHINE"] = machine - elif "CIME_MACHINE" in os.environ: - MACHINE = Machines(machine=os.environ["CIME_MACHINE"]) - elif config.has_option("create_test", "MACHINE"): - MACHINE = Machines(machine=config.get("create_test", "MACHINE")) - elif config.has_option("main", "MACHINE"): - MACHINE = Machines(machine=config.get("main", "MACHINE")) - else: - MACHINE = Machines() - - BaseTestCase.MACHINE = MACHINE - - if compiler is not None: - TEST_COMPILER = compiler - elif config.has_option("create_test", "COMPILER"): - TEST_COMPILER = config.get("create_test", "COMPILER") - elif config.has_option("main", "COMPILER"): - TEST_COMPILER = config.get("main", "COMPILER") - - BaseTestCase.TEST_COMPILER = TEST_COMPILER - - if mpilib is not None: - TEST_MPILIB = mpilib - elif config.has_option("create_test", "MPILIB"): - TEST_MPILIB = config.get("create_test", "MPILIB") - elif config.has_option("main", "MPILIB"): - TEST_MPILIB = config.get("main", "MPILIB") - - BaseTestCase.TEST_MPILIB = TEST_MPILIB - - if test_root is not None: - TEST_ROOT = test_root - elif config.has_option("create_test", "TEST_ROOT"): - TEST_ROOT = config.get("create_test", "TEST_ROOT") - else: - TEST_ROOT = os.path.join( - MACHINE.get_value("CIME_OUTPUT_ROOT"), - "scripts_regression_test.%s" % CIME.utils.get_timestamp(), - ) - - BaseTestCase.TEST_ROOT = TEST_ROOT - - write_provenance_info(MACHINE, TEST_COMPILER, TEST_MPILIB, TEST_ROOT) - - atexit.register(functools.partial(cleanup, TEST_ROOT)) - - -def _main_func(description): - help_str = """ -{0} [TEST] [TEST] -OR -{0} --help - -\033[1mEXAMPLES:\033[0m - \033[1;32m# Run the full suite \033[0m - > {0} - - \033[1;32m# Run single test file (with or without extension) \033[0m - > {0} test_unit_doctest - - \033[1;32m# Run single test class from a test file \033[0m - > {0} test_unit_doctest.TestDocs - - \033[1;32m# Run single test case from a test class \033[0m - > {0} test_unit_doctest.TestDocs.test_lib_docs -""".format( - os.path.basename(sys.argv[0]) - ) - - parser = argparse.ArgumentParser( - usage=help_str, - description=description, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - setup_arguments(parser) - - parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") - - parser.add_argument("--debug", action="store_true", help="Enable debug logging") - - parser.add_argument("--silent", action="store_true", help="Disable all logging") - - parser.add_argument( - "tests", nargs="*", help="Specific tests to run e.g. test_unit*" - ) - - ns, args = parser.parse_known_args() - - # Now set the sys.argv to the unittest_args (leaving sys.argv[0] alone) - sys.argv[1:] = args - - utils.configure_logging(ns.verbose, ns.debug, ns.silent) - - configure_tests(**vars(ns)) - - os.chdir(CIMEROOT) - - if len(ns.tests) == 0: - test_root = os.path.join(CIMEROOT, "CIME", "tests") - - test_suite = unittest.defaultTestLoader.discover(test_root) - else: - # Fixes handling shell expansion e.g. test_unit_*, by removing python extension - tests = [x.replace(".py", "").replace("/", ".") for x in ns.tests] - - # Try to load tests by just names - test_suite = unittest.defaultTestLoader.loadTestsFromNames(tests) - - test_runner = unittest.TextTestRunner(verbosity=2) - - global TEST_RESULT - - TEST_RESULT = test_runner.run(test_suite) - - # Implements same behavior as unittesst.main - # https://github.com/python/cpython/blob/b6d68aa08baebb753534a26d537ac3c0d2c21c79/Lib/unittest/main.py#L272-L273 - sys.exit(not TEST_RESULT.wasSuccessful()) - - -if __name__ == "__main__": - _main_func(__doc__) diff --git a/conftest.py b/conftest.py index fcee11dfa00..1e6b6583825 100644 --- a/conftest.py +++ b/conftest.py @@ -8,7 +8,8 @@ from CIME import utils from CIME.config import Config -from CIME.tests import scripts_regression_tests +from CIME.XML.machines import Machines +from CIME.tests.base import BaseTestCase os.environ["CIME_GLOBAL_WALLTIME"] = "0:05:00" @@ -18,7 +19,7 @@ def pytest_addoption(parser): # pytest's addoption has same signature as add_argument setattr(parser, "add_argument", parser.addoption) - scripts_regression_tests.setup_arguments(parser) + setup_arguments(parser) # verbose and debug flags already exist parser.addoption("--silent", action="store_true", help="Disable all logging") @@ -29,7 +30,7 @@ def pytest_configure(config): utils.configure_logging(kwargs["verbose"], kwargs["debug"], kwargs["silent"]) - scripts_regression_tests.configure_tests(**kwargs) + configure_tests(**kwargs) @pytest.fixture(scope="module", autouse=True) @@ -46,3 +47,166 @@ def setup(pytestconfig): # ensure GLOABL is reset utils.GLOBAL = {} + +def setup_arguments(parser): + parser.add_argument( + "--fast", + action="store_true", + help="Skip full system tests, which saves a lot of time", + ) + + parser.add_argument( + "--no-batch", + action="store_true", + help="Do not submit jobs to batch system, run locally." + " If false, will default to machine setting.", + ) + + parser.add_argument( + "--no-fortran-run", + action="store_true", + help="Do not run any fortran jobs. Implies --fast" " Used for github actions", + ) + + parser.add_argument( + "--no-cmake", action="store_true", help="Do not run cmake tests" + ) + + parser.add_argument( + "--no-teardown", + action="store_true", + help="Do not delete directories left behind by testing", + ) + + parser.add_argument( + "--machine", help="Select a specific machine setting for cime", default=None + ) + + parser.add_argument( + "--compiler", help="Select a specific compiler setting for cime", default=None + ) + + parser.add_argument( + "--mpilib", help="Select a specific compiler setting for cime", default=None + ) + + parser.add_argument( + "--test-root", + help="Select a specific test root for all cases created by the testing", + default=None, + ) + + parser.add_argument( + "--timeout", + type=int, + help="Select a specific timeout for all tests", + default=None, + ) + +def configure_tests( + timeout, + no_fortran_run, + fast, + no_batch, + no_cmake, + no_teardown, + machine, + compiler, + mpilib, + test_root, + **kwargs +): + config = utils.get_cime_config() + + customize_path = os.path.join(utils.get_src_root(), "cime_config", "customize") + Config.load(customize_path) + + if timeout: + BaseTestCase.GLOBAL_TIMEOUT = str(timeout) + + BaseTestCase.NO_FORTRAN_RUN = no_fortran_run or False + BaseTestCase.FAST_ONLY = fast or no_fortran_run + BaseTestCase.NO_BATCH = no_batch or False + BaseTestCase.NO_CMAKE = no_cmake or False + BaseTestCase.NO_TEARDOWN = no_teardown or False + + # make sure we have default values + MACHINE = None + TEST_COMPILER = None + TEST_MPILIB = None + + if machine is not None: + MACHINE = Machines(machine=machine) + os.environ["CIME_MACHINE"] = machine + elif "CIME_MACHINE" in os.environ: + MACHINE = Machines(machine=os.environ["CIME_MACHINE"]) + elif config.has_option("create_test", "MACHINE"): + MACHINE = Machines(machine=config.get("create_test", "MACHINE")) + elif config.has_option("main", "MACHINE"): + MACHINE = Machines(machine=config.get("main", "MACHINE")) + else: + MACHINE = Machines() + + BaseTestCase.MACHINE = MACHINE + + if compiler is not None: + TEST_COMPILER = compiler + elif config.has_option("create_test", "COMPILER"): + TEST_COMPILER = config.get("create_test", "COMPILER") + elif config.has_option("main", "COMPILER"): + TEST_COMPILER = config.get("main", "COMPILER") + + BaseTestCase.TEST_COMPILER = TEST_COMPILER + + if mpilib is not None: + TEST_MPILIB = mpilib + elif config.has_option("create_test", "MPILIB"): + TEST_MPILIB = config.get("create_test", "MPILIB") + elif config.has_option("main", "MPILIB"): + TEST_MPILIB = config.get("main", "MPILIB") + + BaseTestCase.TEST_MPILIB = TEST_MPILIB + + if test_root is not None: + TEST_ROOT = test_root + elif config.has_option("create_test", "TEST_ROOT"): + TEST_ROOT = config.get("create_test", "TEST_ROOT") + else: + TEST_ROOT = os.path.join( + MACHINE.get_value("CIME_OUTPUT_ROOT"), + "scripts_regression_test.%s" % CIME.utils.get_timestamp(), + ) + + BaseTestCase.TEST_ROOT = TEST_ROOT + + write_provenance_info(MACHINE, TEST_COMPILER, TEST_MPILIB, TEST_ROOT) + + atexit.register(functools.partial(cleanup, TEST_ROOT)) + +def write_provenance_info(machine, test_compiler, test_mpilib, test_root): + curr_commit = get_current_commit(repo=CIMEROOT) + logging.info("Testing commit %s" % curr_commit) + cime_model = get_model() + logging.info("Using cime_model = %s" % cime_model) + logging.info("Testing machine = %s" % machine.get_machine_name()) + if test_compiler is not None: + logging.info("Testing compiler = %s" % test_compiler) + if test_mpilib is not None: + logging.info("Testing mpilib = %s" % test_mpilib) + logging.info("Test root: %s" % test_root) + logging.info("Test driver: %s" % CIME.utils.get_cime_default_driver()) + logging.info("Python version {}\n".format(sys.version)) + +def cleanup(test_root): + if ( + os.path.exists(test_root) + and TEST_RESULT is not None + and TEST_RESULT.wasSuccessful() + ): + testreporter = os.path.join(test_root, "testreporter") + files = os.listdir(test_root) + if len(files) == 1 and os.path.isfile(testreporter): + os.unlink(testreporter) + if not os.listdir(test_root): + print("All pass, removing directory:", test_root) + os.rmdir(test_root) diff --git a/doc/source/ccs/model-configuration/fortran-unit-testing.rst b/doc/source/ccs/model-configuration/fortran-unit-testing.rst index 336ef600933..6c09bb6f017 100644 --- a/doc/source/ccs/model-configuration/fortran-unit-testing.rst +++ b/doc/source/ccs/model-configuration/fortran-unit-testing.rst @@ -91,7 +91,7 @@ You will also see a final message like this: 100% tests passed, 0 tests failed out of 17 -These unit tests are run automatically as part of **scripts_regression_tests** on machines that have a serial build of pFUnit available for the default compiler. +These unit tests are run automatically as part of **pytest** on machines that have a serial build of pFUnit available for the default compiler. .. _adding_machine_support: @@ -109,7 +109,7 @@ Building pFUnit ~~~~~~~~~~~~~~~ Follow the instructions below to build pFUnit using the default compiler on your machine. -That is the default for **run_tests.py** and that is required for **scripts_regression_tests.py** to run the unit tests on your machine. +That is the default for **run_tests.py** and is required for pytest to run the unit tests on your machine. For the CMake step, we typically build with ``-DSKIP_MPI=YES``, ``-DSKIP_OPENMP=YES`` and ``-DCMAKE_INSTALL_PREFIX`` set to the directory where you want pFUnit to be installed. (At this time, no unit tests require parallel support, so we build without MPI support to keep things simple.) Optionally, you can also provide pFUnit builds with other supported compilers on your machine. diff --git a/doc/source/contributing-guide.rst b/doc/source/contributing-guide.rst index e43b1785f92..818551c1277 100644 --- a/doc/source/contributing-guide.rst +++ b/doc/source/contributing-guide.rst @@ -27,22 +27,17 @@ The `unit` category covers doctests and unit tests, while the `sys` category cov How to run the tests ``````````````````````` -There are two possible methods to run these tests. - .. warning:: - scripts_regression_tests.py is deprecated and will be removed in the future. + The legacy `scripts_regression_tests.py` entry point has been replaced by `pytest`. -pytest -:::::: -CIME supports running tests using `pytest`. By using `pytest` coverage reports are automatically generated. `pytest` supports all the same arguments as `scripts_regression_tests.py`, see `--help` for details. +CIME supports running tests using `pytest`. By using `pytest` coverage reports are automatically generated. See `--help` for details. To get started install `pytest` and `pytest-cov`. .. code-block:: bash pip install -r test-requirements.txt - pip install pytest pytest-cov Examples ........ @@ -56,7 +51,7 @@ Running only ``sys`` tests, ``sys`` can be replaced with ``unit`` to run only un .. code-block:: bash - pytest CIME/tests/test_sys.* + pytest CIME/tests/test_sys* Running a specific test case. @@ -70,39 +65,6 @@ A specific test can be run with the following. pytest CIME/tests/test_unit_case.py::TestCaseSubmit::test_check_case - -scripts_regression_tests.py -::::::::::::::::::::::::::: -The `scripts_regression_tests.py` script is located under `CIME/tests`. - -You can pass either the module name or the file path of a test. - -Examples -........ -Running all the ``sys`` and ``unit`` tests. - -.. code-block:: bash - - python CIME/tests/scripts_regression_tests.py - -Running only ``sys`` tests, ``sys`` can be replaced with ``unit`` to run only unit testing. - -.. code-block:: bash - - python CIME/tests/scripts_regression_tests.py CIME/tests/test_sys* - -Runnig a specific test case. - -.. code-block:: bash - - python CIME/tests/scripts_regression_tests.py CIME.tests.test_unit_case - -A specific test can be run with the following. - -.. code-block:: bash - - python CIME/tests/scripts_regression_tests.py CIME.tests.test_unit_case.TestCaseSubmit.test_check_case - Code Quality ------------ To ensure code quality we require all code to be linted by `pylint` and formatted using `black`. We run a few other tools to check XML formatting, ending files with newlines and trailing white spaces. @@ -183,4 +145,3 @@ You can even run CIME or testing without a shell. .. code-block:: bash docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/E3SM/cime ghcr.io/esmci/cime:latest ./scripts/create_test SMS.f19_g16.S - From 4bdbf9f76cab59a3a466c80dc9db9b58339bf8e1 Mon Sep 17 00:00:00 2001 From: Jason Boutte Date: Thu, 9 Apr 2026 13:46:04 -0700 Subject: [PATCH 2/6] chore: remove code_checker.py in favor of pre-commit pylint --- .pre-commit-config.yaml | 2 +- CIME/code_checker.py | 200 ---------------------------------------- doc/source/api.rst | 1 - 3 files changed, 1 insertion(+), 202 deletions(-) delete mode 100644 CIME/code_checker.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 442206da5c2..8ada08403f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,4 +24,4 @@ repos: args: - --disable=I,C,R,logging-not-lazy,wildcard-import,unused-wildcard-import,fixme,broad-except,bare-except,eval-used,exec-used,global-statement,logging-format-interpolation,no-name-in-module,arguments-renamed,unspecified-encoding,protected-access,import-error,no-member,logging-fstring-interpolation files: CIME - exclude: CIME/(tests|Tools|code_checker.py) + exclude: CIME/(tests|Tools) diff --git a/CIME/code_checker.py b/CIME/code_checker.py deleted file mode 100644 index 08a5a021b0f..00000000000 --- a/CIME/code_checker.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -Libraries for checking python code with pylint -""" - -import os -import json -from shutil import which - -from CIME.XML.standard_module_setup import * - -from CIME.utils import ( - run_cmd, - run_cmd_no_fail, - expect, - get_cime_root, - get_src_root, - is_python_executable, - get_cime_default_driver, -) - -from multiprocessing.dummy import Pool as ThreadPool - -logger = logging.getLogger(__name__) - - -############################################################################### -def _run_pylint(all_files, interactive): - ############################################################################### - pylint = which("pylint") - - cmd_options = ( - " --disable=I,C,R,logging-not-lazy,wildcard-import,unused-wildcard-import" - ) - cmd_options += ( - ",fixme,broad-except,bare-except,eval-used,exec-used,global-statement" - ) - cmd_options += ",logging-format-interpolation,no-name-in-module,arguments-renamed" - cmd_options += " -j 0 -f json" - cimeroot = get_cime_root() - srcroot = get_src_root() - - # if "scripts/Tools" in on_file: - # cmd_options +=",relative-import" - - # add init-hook option - cmd_options += ( - ' --init-hook=\'import sys; sys.path.extend(("%s","%s","%s","%s"))\'' - % ( - os.path.join(cimeroot, "CIME"), - os.path.join(cimeroot, "CIME", "Tools"), - os.path.join(cimeroot, "scripts", "fortran_unit_testing", "python"), - os.path.join(srcroot, "components", "cmeps", "cime_config", "runseq"), - ) - ) - - files = " ".join(all_files) - cmd = "%s %s %s" % (pylint, cmd_options, files) - logger.debug("pylint command is %s" % cmd) - stat, out, err = run_cmd(cmd, verbose=False, from_dir=cimeroot) - - data = json.loads(out) - - result = {} - - for item in data: - if item["type"] != "error": - continue - - path = item["path"] - message = item["message"] - line = item["line"] - - if path in result: - result[path].append(f"{message}:{line}") - else: - result[path] = [ - message, - ] - - for k in result.keys(): - result[k] = "\n".join(set(result[k])) - - return result - - -############################################################################### -def _matches(file_path, file_ends): - ############################################################################### - for file_end in file_ends: - if file_path.endswith(file_end): - return True - - return False - - -############################################################################### -def _should_pylint_skip(filepath): - ############################################################################### - # TODO - get rid of this - list_of_directories_to_ignore = ( - "xmlconvertors", - "pointclm", - "point_clm", - "tools", - "machines", - "apidocs", - "doc", - ) - for dir_to_skip in list_of_directories_to_ignore: - if dir_to_skip + "/" in filepath: - return True - # intended to be temporary, file needs update - if filepath.endswith("archive_metadata") or filepath.endswith("pgn.py"): - return True - - return False - - -############################################################################### -def get_all_checkable_files(): - ############################################################################### - cimeroot = get_cime_root() - all_git_files = run_cmd_no_fail( - "git ls-files", from_dir=cimeroot, verbose=False - ).splitlines() - if get_cime_default_driver() == "nuopc": - srcroot = get_src_root() - nuopc_git_files = [] - try: - nuopc_git_files = run_cmd_no_fail( - "git ls-files", - from_dir=os.path.join(srcroot, "components", "cmeps"), - verbose=False, - ).splitlines() - except: - logger.warning("No nuopc driver found in source") - all_git_files.extend( - [ - os.path.join(srcroot, "components", "cmeps", _file) - for _file in nuopc_git_files - ] - ) - files_to_test = [ - item - for item in all_git_files - if ( - (item.endswith(".py") or is_python_executable(os.path.join(cimeroot, item))) - and not _should_pylint_skip(item) - ) - ] - - return files_to_test - - -############################################################################### -def check_code(files, num_procs=10, interactive=False): - ############################################################################### - """ - Check all python files in the given directory - - Returns True if all files had no problems - """ - # Get list of files to check, we look to see if user-provided file argument - # is a valid file, if not, we search the repo for a file with similar name. - files_to_check = [] - if files: - repo_files = get_all_checkable_files() - for filearg in files: - if os.path.exists(filearg): - files_to_check.append(os.path.abspath(filearg)) - else: - found = False - for repo_file in repo_files: - if repo_file.endswith(filearg): - found = True - files_to_check.append(repo_file) # could have multiple matches - - if not found: - logger.warning( - "Could not find file matching argument '%s'" % filearg - ) - else: - # Check every python file - files_to_check = get_all_checkable_files() - - expect(len(files_to_check) > 0, "No matching files found") - - # No point in using more threads than files - # if len(files_to_check) < num_procs: - # num_procs = len(files_to_check) - - results = _run_pylint(files_to_check, interactive) - - return results - - # pool = ThreadPool(num_procs) - # results = pool.map(lambda x : _run_pylint(x, interactive), files_to_check) - # pool.close() - # pool.join() - # return dict(results) diff --git a/doc/source/api.rst b/doc/source/api.rst index f47a2b88299..61ae26cc50d 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -23,7 +23,6 @@ API build buildlib buildnml - code_checker compare_namelists compare_test_results config From a025ea620c20d222324a455f509f83707ac23229 Mon Sep 17 00:00:00 2001 From: Jason Boutte Date: Thu, 9 Apr 2026 13:46:24 -0700 Subject: [PATCH 3/6] chore: clean up the contributing guide --- doc/source/contributing-guide.rst | 50 ++++++++++++------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/doc/source/contributing-guide.rst b/doc/source/contributing-guide.rst index 818551c1277..85e6fdbc198 100644 --- a/doc/source/contributing-guide.rst +++ b/doc/source/contributing-guide.rst @@ -31,35 +31,33 @@ How to run the tests The legacy `scripts_regression_tests.py` entry point has been replaced by `pytest`. -CIME supports running tests using `pytest`. By using `pytest` coverage reports are automatically generated. See `--help` for details. - -To get started install `pytest` and `pytest-cov`. +CIME supports running tests using `pytest`. By using `pytest` coverage reports are automatically generated. Install the test requirements, which include `pytest` and `pytest-cov`: .. code-block:: bash pip install -r test-requirements.txt -Examples -........ -Running all the ``sys`` and ``unit`` tests. +Common examples +............... +Run all ``sys`` and ``unit`` tests. .. code-block:: bash pytest -Running only ``sys`` tests, ``sys`` can be replaced with ``unit`` to run only unit testing. +Run only ``sys`` tests. Replace ``sys`` with ``unit`` to run only unit tests. .. code-block:: bash pytest CIME/tests/test_sys* -Running a specific test case. +Run a specific test case. .. code-block:: bash pytest CIME/tests/test_unit_case.py -A specific test can be run with the following. +Run a specific test method. .. code-block:: bash @@ -69,9 +67,9 @@ Code Quality ------------ To ensure code quality we require all code to be linted by `pylint` and formatted using `black`. We run a few other tools to check XML formatting, ending files with newlines and trailing white spaces. -To ensure consistency when running these checks we require the use of [`pre-commit`](https://pre-commit.com/). +To ensure consistency when running these checks, we require [`pre-commit`](https://pre-commit.com/). -Our GitHub actions will lint and check the format of each PR but will not automatically fix any issues. It's up to the developer to resolve linting and formatting issues. We encourage installing `pre-commit`'s [Git hooks](#installing-git-hook-scripts) that will run these checks before code can be committed. +GitHub Actions lint and check the format of each PR, but they do not automatically fix issues. Installing the `pre-commit` [Git hooks](#installing-git-hook-scripts) runs those checks before each commit. Installing pre-commit ````````````````````` @@ -97,51 +95,41 @@ If you install these scripts then `pre-commit` will automatically run on `git co Docker container ---------------- -CIME provides a container that the CI uses to run all the testing. This container - -can also be used to test locally providing a reproducible environment. The - -compiler is ``GNU`` and the MPI implementation is ``OpenMPI``. +CIME provides a container that CI uses to run tests. You can also use it locally for a reproducible environment. The compiler is ``GNU`` and the MPI implementation is ``OpenMPI``. -The image can be pulled from ``ghcr.io``. +The image can be pulled from ``ghcr.io`` or built locally. For local builds, set the build context to the root of the CIME repository. .. code-block:: bash docker pull ghcr.io/esmci/cime:latest -or can be built locally. The build context needs to be set to the root of the CIME repository. - -.. code-block:: bash - docker build -t ghcr.io/esmci/cime:latest -f docker/Dockerfile . Running ``````` -The container does not provide any source, as such you will need to bind -mount the model+cime directory and define which model is being used. The -following example assumes the model is checked out in ``$SRC_PATH``. +The container does not include source code, so bind mount the model checkout and choose the model being used. The following example assumes the model is checked out in ``$SRC_PATH``. .. code-block:: bash - docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/E3SM/cime ghcr.io/esmci/cime:latest bash + docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/model/cime ghcr.io/esmci/cime:latest bash This example will drop into a shell where CIME commands or tests can be run. The options are broken down below. - ``--hostname docker`` is required to tell CIME which machine definition to use. - ``-e CIME_MODEL=e3sm`` defines the model. -- ``-v ${SRC_PATH}:/root/E3SM`` passes through the model source. -- ``-v ./storage:/root/storage`` persist all data; cases, baselines, archive, inputdata. the bind mounts can be broken out if you only want to persist certain input/outputs. -- ``-w /root/E3SM/cime`` set the current working directory to CIME's root. +- ``-v ${SRC_PATH}:/root/model`` passes through the model source. +- ``-v ./storage:/root/storage`` persists data such as cases, baselines, archive, and inputdata. You can split the bind mounts if you only want to keep specific inputs or outputs. +- ``-w /root/model/cime`` sets the current working directory to CIME's root. - ``ghcr.io/esmci/cime:latest`` container image. - ``bash`` the command to run in the container. -You can even run CIME or testing without a shell. +You can also run CIME commands or tests without opening a shell. .. code-block:: bash - docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/E3SM/cime ghcr.io/esmci/cime:latest pytest CIME/tests/test_unit* + docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/model/cime ghcr.io/esmci/cime:latest pytest CIME/tests/test_unit* .. code-block:: bash - docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/E3SM/cime ghcr.io/esmci/cime:latest ./scripts/create_test SMS.f19_g16.S + docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/model/cime ghcr.io/esmci/cime:latest ./scripts/create_test SMS.f19_g16.S From a45e2500d0f68623efbae9b33d20c009ef2886d9 Mon Sep 17 00:00:00 2001 From: Jason Boutte Date: Thu, 9 Apr 2026 13:50:41 -0700 Subject: [PATCH 4/6] chore: clean up contributing entrypoint --- CONTRIBUTING.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 88207281515..21b67dcd713 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,13 +20,12 @@ For more information on contributing to open source projects, is a great starting point. Also, checkout the [Zen of Scientific Software Maintenance](https://jrleeman.github.io/ScientificSoftwareMaintenance/) for some guiding principles on how to create high quality scientific software contributions. -The canonical, detailed contributing guide for this repository is included in the source tree at `doc/source/contributing-guide.rst`; please consult that file as the single source of truth for developer workflows, testing, and container usage. +The canonical, detailed contributing guide for this repository is included in the source tree at `doc/source/contributing-guide.rst`; please consult that file as the single source of truth for developer workflows, testing, container usage, and code quality. ## Getting Started Interested in helping extend CIME? Have code from your research that you believe others will -find useful? Have a few minutes to tackle an issue? This guide will get you set up to contribute -to CIME. +find useful? Have a few minutes to tackle an issue? Start here, then use the detailed guide for the workflow you need. ## What Can I Do? * Tackle any unassigned [issues](https://github.com/ESMCI/CIME/issues) you wish! @@ -69,13 +68,7 @@ You will need to initialize and update submodules: cd CIME git submodule update --init --recursive -From here you can edit the code and run the unit tests following this [guide](https://esmci.github.io/cime/versions/master/html/contributing-guide.html#pytest). - -When running the ``unit`` tests you can specify any valid machine e.g. docker and the tests will run. - -If you need to run the ``system`` tests you will need to have your respective model checked out and on a supported machine. Alternatively you can use CIME [container](https://esmci.github.io/cime/versions/master/html/contributing-guide.html#docker-container) which is used in our GitHub CI testing. - -Before creating your PR you will need to run the code quality checkers; see this [guide](https://esmci.github.io/cime/versions/master/html/contributing-guide.html#code-quality). +From here you can edit the code and follow the detailed guide for [testing](https://esmci.github.io/cime/versions/master/html/contributing-guide.html#contributing-guide-running-tests), [code quality](https://esmci.github.io/cime/versions/master/html/contributing-guide.html#code-quality), and the [Docker container workflow](https://esmci.github.io/cime/versions/master/html/contributing-guide.html#docker-container). Push to your fork and [submit a pull request][pr]. From 81d1e1cd5250f61728ae43380607229684d70a9b Mon Sep 17 00:00:00 2001 From: Jason Boutte Date: Tue, 12 May 2026 15:16:57 -0700 Subject: [PATCH 5/6] fix: add missing imports and fix undefined references in conftest.py - Add missing imports: atexit, functools, logging, get_current_commit, get_model - Fix CIME.utils references to use already-imported utils module - Add TEST_RESULT module-level variable to prevent NameError in cleanup() --- conftest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 1e6b6583825..2e9c6713424 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,6 @@ +import atexit +import functools +import logging import os import sys @@ -8,11 +11,14 @@ from CIME import utils from CIME.config import Config +from CIME.utils import get_current_commit, get_model from CIME.XML.machines import Machines from CIME.tests.base import BaseTestCase os.environ["CIME_GLOBAL_WALLTIME"] = "0:05:00" +TEST_RESULT = None + def pytest_addoption(parser): # set addoption as add_argument to use common argument setup @@ -174,7 +180,7 @@ def configure_tests( else: TEST_ROOT = os.path.join( MACHINE.get_value("CIME_OUTPUT_ROOT"), - "scripts_regression_test.%s" % CIME.utils.get_timestamp(), + "scripts_regression_test.%s" % utils.get_timestamp(), ) BaseTestCase.TEST_ROOT = TEST_ROOT @@ -194,7 +200,7 @@ def write_provenance_info(machine, test_compiler, test_mpilib, test_root): if test_mpilib is not None: logging.info("Testing mpilib = %s" % test_mpilib) logging.info("Test root: %s" % test_root) - logging.info("Test driver: %s" % CIME.utils.get_cime_default_driver()) + logging.info("Test driver: %s" % utils.get_cime_default_driver()) logging.info("Python version {}\n".format(sys.version)) def cleanup(test_root): From 5d72745e549644666b0114a3dc2cc832485324d5 Mon Sep 17 00:00:00 2001 From: Jason Boutte Date: Thu, 14 May 2026 21:17:17 -0700 Subject: [PATCH 6/6] fix: permissions for podman --- doc/source/contributing-guide.rst | 22 +++++++++++++++++++++- docker/README.md | 12 +++++++----- docker/entrypoint.sh | 23 ++++++++++++++++------- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/doc/source/contributing-guide.rst b/doc/source/contributing-guide.rst index 85e6fdbc198..e6546d94bef 100644 --- a/doc/source/contributing-guide.rst +++ b/doc/source/contributing-guide.rst @@ -119,7 +119,7 @@ The options are broken down below. - ``--hostname docker`` is required to tell CIME which machine definition to use. - ``-e CIME_MODEL=e3sm`` defines the model. - ``-v ${SRC_PATH}:/root/model`` passes through the model source. -- ``-v ./storage:/root/storage`` persists data such as cases, baselines, archive, and inputdata. You can split the bind mounts if you only want to keep specific inputs or outputs. +- ``-v ./storage:/root/storage`` persists data such as cases, baselines, archive, and inputdata. Files are created with world-readable permissions so they can be accessed from the host in real-time. - ``-w /root/model/cime`` sets the current working directory to CIME's root. - ``ghcr.io/esmci/cime:latest`` container image. - ``bash`` the command to run in the container. @@ -133,3 +133,23 @@ You can also run CIME commands or tests without opening a shell. .. code-block:: bash docker run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/model/cime ghcr.io/esmci/cime:latest ./scripts/create_test SMS.f19_g16.S + +Using Podman +```````````` +Podman can be used as a drop-in replacement for Docker. Use ``podman unshare`` to run commands within Podman's user namespace, allowing access to files created in bind mounts. + +.. code-block:: bash + + podman run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/model/cime ghcr.io/esmci/cime:latest bash + +Run tests directly: + +.. code-block:: bash + + podman run -it --rm --hostname docker -e CIME_MODEL=e3sm -v ${SRC_PATH}:/root/model -v ./storage:/root/storage -w /root/model/cime ghcr.io/esmci/cime:latest pytest CIME/tests/test_unit* + +To access files in ``./storage`` from the host while the container is running, use ``podman unshare``: + +.. code-block:: bash + + podman unshare ls -la ./storage diff --git a/docker/README.md b/docker/README.md index d1e5ad53608..a3de924b960 100644 --- a/docker/README.md +++ b/docker/README.md @@ -89,16 +89,18 @@ Environment variables to modify the container environment. The `config_machines.xml` definition as been setup to provided persistance for inputdata, cases, archives and tools. The following paths can be mounted as volumes to provide persistance. -* /storage/inputdata -* /storage/cases -* /storage/archives -* /storage/tools +* /root/storage/inputdata +* /root/storage/cases +* /root/storage/archive +* /root/storage/tools + +Files created in the storage directory are world-readable, allowing real-time access from the host while the container is running. ```bash docker run -it -v {hostpath}:{container_path} cime:latest bash e.g. -docker run -it -v ${PWD}/data-cache:/storage/inputdata cime:latest bash +docker run -it -v ${PWD}/data-cache:/root/storage/inputdata cime:latest bash ``` It's also possible to persist the source git repositories. diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index d11c2373484..1dc694875cc 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,12 +1,21 @@ #!/bin/bash -# Set up basic user, logname, and default group/user IDs +# Use fixed paths for container resources, regardless of user namespace mapping +# This ensures tools work with both Docker (root) and Podman (--userns=keep-id) +CONTAINER_HOME="/root" +export HOME="${CONTAINER_HOME}" export USER="$(id -nu)" export LOGNAME="${USER}" -# Set static home path where .cime exists and container entrypoint options SKIP_ENTRYPOINT="${SKIP_ENTRYPOINT:-false}" -STORAGE_DIR="${HOME}/storage" +STORAGE_DIR="${CONTAINER_HOME}/storage" + +# Make files in storage directory accessible from host in real-time +# Set permissive umask so all new files are world-readable/writable +if [[ -d "${STORAGE_DIR}" ]]; then + umask 000 + chmod -R a+rwX "${STORAGE_DIR}" 2>/dev/null || true +fi # Build the cprnc tool from CIME sources function build_cprnc() { @@ -53,9 +62,9 @@ function download_input_data() { # Link correct config_machines file based on CIME_MODEL, also set ESMFMKFILE for cesm function link_config_machines() { if [[ "${CIME_MODEL}" == "e3sm" ]]; then - ln -sf "${HOME}/.cime/config_machines.v2.xml" "${HOME}/.cime/config_machines.xml" + ln -sf "${CONTAINER_HOME}/.cime/config_machines.v2.xml" "${CONTAINER_HOME}/.cime/config_machines.xml" elif [[ "${CIME_MODEL}" == "cesm" ]]; then - ln -sf "${HOME}/.cime/config_machines.v3.xml" "${HOME}/.cime/config_machines.xml" + ln -sf "${CONTAINER_HOME}/.cime/config_machines.v3.xml" "${CONTAINER_HOME}/.cime/config_machines.xml" fi } @@ -76,8 +85,8 @@ if [[ -e "${PWD}/.git" ]]; then fi if [[ "${CI:-false}" == "false" ]] && [[ "${SKIP_ENTRYPOINT}" == "false" ]]; then - source ${HOME}/.local/bin/env - source ${HOME}/.venv/bin/activate + source ${CONTAINER_HOME}/.local/bin/env + source ${CONTAINER_HOME}/.venv/bin/activate fi if [[ "${SKIP_ENTRYPOINT}" == "false" ]]; then