diff --git a/benchcab/benchcab.py b/benchcab/benchcab.py index 141595d..f068304 100644 --- a/benchcab/benchcab.py +++ b/benchcab/benchcab.py @@ -28,7 +28,7 @@ from benchcab.utils.pbs import render_job_script from benchcab.utils.repo import create_repo from benchcab.utils.subprocess import SubprocessWrapper, SubprocessWrapperInterface -from benchcab.workdir import setup_fluxsite_directory_tree +from benchcab.workdir import clean_directory_tree, clean_cwd_logfiles, setup_fluxsite_directory_tree class Benchcab: @@ -308,6 +308,11 @@ def fluxsite(self, config_path: str, no_submit: bool, skip: list[str]): else: self.fluxsite_submit_job(config_path, skip) + def clean(self, config_path: str): + """Endpoint for cleaning runs uirectory ig.""" + clean_directory_tree() + clean_cwd_logfiles() + def spatial(self, config_path: str): """Endpoint for `benchcab spatial`.""" diff --git a/benchcab/cli.py b/benchcab/cli.py index 4e19d26..6dfec10 100644 --- a/benchcab/cli.py +++ b/benchcab/cli.py @@ -199,4 +199,15 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser: ) parser_spatial.set_defaults(func=app.spatial) + # subcommand: 'benchcab clean' + parser_spatial = subparsers.add_parser( + "clean", + parents=[args_help, args_subcommand], + help="Cleanup files created by running benchcab.", + description="""Removes src/ and runs/ directories, along with log files in the + project root directory.""", + add_help=False, + ) + parser_spatial.set_defaults(func=app.clean) + return main_parser diff --git a/benchcab/data/config-schema.yml b/benchcab/data/config-schema.yml index 280c560..2be535f 100644 --- a/benchcab/data/config-schema.yml +++ b/benchcab/data/config-schema.yml @@ -24,7 +24,7 @@ realisations: schema: git: type: "dict" - excludes: "svn" + excludes: ["svn", "local"] schema: branch: type: "string" @@ -37,13 +37,20 @@ realisations: required: false svn: type: "dict" - excludes: "git" + excludes: ["git", "local"] schema: branch_path: type: "string" required: true revision: type: "integer" + local: + type: "dict" + excludes: ["git", "svn"] + schema: + local_path: + type: "string" + required: true name: nullable: true type: "string" diff --git a/benchcab/model.py b/benchcab/model.py index 1bbf014..b0982b4 100644 --- a/benchcab/model.py +++ b/benchcab/model.py @@ -14,7 +14,7 @@ from benchcab.environment_modules import EnvironmentModules, EnvironmentModulesInterface from benchcab.utils import get_logger from benchcab.utils.fs import chdir, copy2, rename -from benchcab.utils.repo import GitRepo, Repo +from benchcab.utils.repo import GitRepo, LocalRepo, Repo from benchcab.utils.subprocess import SubprocessWrapper, SubprocessWrapperInterface @@ -62,7 +62,7 @@ def __init__( # TODO(Sean) we should not have to know whether `repo` is a `GitRepo` or # `SVNRepo`, we should only be working with the `Repo` interface. # See issue https://github.com/CABLE-LSM/benchcab/issues/210 - if isinstance(repo, GitRepo): + if isinstance(repo, (GitRepo, LocalRepo)): self.src_dir = Path("src") @property diff --git a/benchcab/utils/repo.py b/benchcab/utils/repo.py index 2f89172..fcf0a84 100644 --- a/benchcab/utils/repo.py +++ b/benchcab/utils/repo.py @@ -46,6 +46,49 @@ def get_branch_name(self) -> str: """ +class LocalRepo(Repo): + """Concrete implementation of the `Repo` class using local path backend.""" + + def __init__(self, local_path: str, path: str) -> None: + """Return a LocalRepo instance. + + Parameters + ---------- + path : str + Path of local CABLE branch + """ + self.name = Path(local_path).name + self.local_path = local_path + self.path = path / self.name if path.is_dir() else path + self.logger = get_logger() + + def checkout(self): + """Checkout the source code.""" + self.path.symlink_to(self.local_path) + self.logger.info(f"Created symlink from to {self.path} named {self.name}") + + def get_revision(self) -> str: + """Return the latest revision of the source code. + + Returns + ------- + str + Human readable string describing the latest revision. + + """ + return f"Using local CABLE branch: {self.name}" + + def get_branch_name(self) -> str: + """Return the branch name of the source code. + + Returns + ------- + str + Branch name of the source code. + + """ + return Path(self.path).absolute() + class GitRepo(Repo): """A concrete implementation of the `Repo` class using a Git backend. @@ -236,4 +279,6 @@ def create_repo(spec: dict, path: Path) -> Repo: return GitRepo(path=path, **spec["git"]) if "svn" in spec: return SVNRepo(svn_root=internal.CABLE_SVN_ROOT, path=path, **spec["svn"]) + if "local" in spec: + return LocalRepo(path=path, **spec["local"]) raise RepoSpecError diff --git a/benchcab/workdir.py b/benchcab/workdir.py index 27a0a1c..47e07cb 100644 --- a/benchcab/workdir.py +++ b/benchcab/workdir.py @@ -8,15 +8,23 @@ from benchcab import internal from benchcab.utils.fs import mkdir - def clean_directory_tree(): - """Remove pre-existing directories in current working directory.""" + """Remove pre-existing directories and log files in current working directory.""" if internal.SRC_DIR.exists(): + for realisation in internal.SRC_DIR.iterdir(): + if realisation.is_symlink(): + realisation.unlink() shutil.rmtree(internal.SRC_DIR) if internal.RUN_DIR.exists(): shutil.rmtree(internal.RUN_DIR) + +def clean_cwd_logfiles(): + for rev_log_file in internal.CWD.glob("rev_number-*.log"): + rev_log_file.unlink() + for pbs_job_file in internal.CWD.glob("benchcab_cable_qsub.sh*"): + pbs_job_file.unlink() def setup_fluxsite_directory_tree(): """Generate the directory structure used by `benchcab`.""" diff --git a/tests/test_workdir.py b/tests/test_workdir.py index 62dd710..53e97f0 100644 --- a/tests/test_workdir.py +++ b/tests/test_workdir.py @@ -6,11 +6,13 @@ """ from pathlib import Path +from benchcab import internal import pytest from benchcab.workdir import ( clean_directory_tree, + clean_cwd_logfiles, setup_fluxsite_directory_tree, ) @@ -37,8 +39,8 @@ def test_directory_structure_generated(self, fluxsite_directory_list): assert path.exists() -class TestCleanDirectoryTree: - """Tests for `clean_directory_tree()`.""" +class TestCleanFiles: + """Tests for `clean_directory_tree()` and `clean_cwd_logfiles).""" @pytest.mark.parametrize("test_path", [Path("runs"), Path("src")]) def test_clean_directory_tree(self, test_path): @@ -46,3 +48,13 @@ def test_clean_directory_tree(self, test_path): test_path.mkdir() clean_directory_tree() assert not test_path.exists() + + @pytest.mark.parametrize("test_path", [Path("rev_number-200.log"), Path("benchcab_cable_qsub.sh.o21871")]) + def test_clean_directory_tree(self, test_path: Path, monkeypatch): + """Success case: log files in project root directory do not exist after clean.""" + monkeypatch.setattr(internal, "CWD", Path.cwd()) + test_path.touch() + clean_cwd_logfiles() + + assert not test_path.exists() + \ No newline at end of file