diff --git a/.github/workflows/test_fast.yml b/.github/workflows/test_fast.yml index 5b4329842af..c785b33f98f 100644 --- a/.github/workflows/test_fast.yml +++ b/.github/workflows/test_fast.yml @@ -66,6 +66,9 @@ jobs: run: | pip install ."[all]" + - name: Configure git # Needed by the odd test + uses: cylc/release-actions/configure-git@v1 + - name: style run: | pycodestyle diff --git a/.github/workflows/test_functional.yml b/.github/workflows/test_functional.yml index c1b18d5b588..f10f93b7b45 100644 --- a/.github/workflows/test_functional.yml +++ b/.github/workflows/test_functional.yml @@ -159,6 +159,9 @@ jobs: sleep 1 ssh -vv _remote_background_indep_poll hostname + - name: Configure git # Needed by the odd test + uses: cylc/release-actions/configure-git@v1 + - name: Filter Tests env: # NOTE: we only want the CHUNK set in this step else we will diff --git a/CHANGES.md b/CHANGES.md index da7a1a4d3e7..c2b5015c49e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,10 @@ ones in. --> ------------------------------------------------------------------------------- ## __cylc-8.0b2 (Released 2021-??-??)__ +Third beta release of Cylc 8. + +(See note on cylc-8 backward-incompatible changes, above) + ### Enhancements [#4174](https://github.com/cylc/cylc-flow/pull/4174) - Terminology: replace @@ -60,6 +64,9 @@ configuration items from `global.cylc[platforms][]`: `run directory`, `work directory` and `suite definition directory`. This functionality is now provided by `[symlink dirs]`. +[#4142](https://github.com/cylc/cylc-flow/pull/4142) - Record source directory +version control information on installation of a workflow. + ### Fixes [#4180](https://github.com/cylc/cylc-flow/pull/4180) - Fix bug where installing @@ -71,6 +78,10 @@ of other, small bugs. ------------------------------------------------------------------------------- ## __cylc-8.0b1 (Released 2021-04-21)__ +Second beta release of Cylc 8. + +(See note on cylc-8 backward-incompatible changes, above) + ### Enhancements [#4154](https://github.com/cylc/cylc-flow/pull/4154) - diff --git a/cylc/flow/install_plugins/__init__.py b/cylc/flow/install_plugins/__init__.py new file mode 100644 index 00000000000..3c762247838 --- /dev/null +++ b/cylc/flow/install_plugins/__init__.py @@ -0,0 +1,32 @@ +# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE. +# Copyright (C) NIWA & British Crown (Met Office) & Contributors. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Plugins for running Python code before and after installation of a workflow. + +Built In Plugins +---------------- + +Cylc Flow provides the following pre-configure and post-install plugins: + +.. autosummary:: + :toctree: built-in + :template: docstring_only.rst + + cylc.flow.install_plugins.log_vc_info + +.. Note: Autosummary generates files in this directory, these are cleaned + up by `make clean`. +""" diff --git a/cylc/flow/install_plugins/log_vc_info.py b/cylc/flow/install_plugins/log_vc_info.py new file mode 100644 index 00000000000..70058802963 --- /dev/null +++ b/cylc/flow/install_plugins/log_vc_info.py @@ -0,0 +1,317 @@ +# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE. +# Copyright (C) NIWA & British Crown (Met Office) & Contributors. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Record version control information to the workflow log directory on +installation. + +If the workflow source directory is a supported repository/working copy +(git or svn), information about the working copy will be saved in +``/log/version/vcs.conf``. + +An example of this information for a git repo: + +.. code-block:: cylc + + version control system = "git" + repository version = "2.8.0-dirty" + commit = "e5dc6573dd70cabd8f973d1535c17c29c026d553" + working copy root path = "~/cylc-src/my-workflow-git" + status = \"\"\" + M flow.cylc + \"\"\" + +And for an svn working copy: + +.. code-block:: cylc + + version control system = "svn" + working copy root path = "~/cylc-src/my-workflow-svn" + url = "file:///home/my-workflow-svn/trunk" + repository uuid = "219f5687-8eb8-44b1-beb6-e8220fa964d3" + revision = "14" + status = \"\"\" + M flow.cylc + \"\"\" + + +Any uncommitted changes will also be saved as a diff in +``/log/version/uncommitted.diff``. (Note that git does not include +untracked files in the diff.) +""" + +from collections import OrderedDict +from pathlib import Path +from subprocess import Popen, DEVNULL, PIPE +from typing import Dict, Iterable, List, Optional, TYPE_CHECKING, Union + +from cylc.flow import LOG +from cylc.flow.exceptions import CylcError +from cylc.flow.workflow_files import WorkflowFiles + +if TYPE_CHECKING: + from optparse import Values + + +SVN = 'svn' +GIT = 'git' + +INFO_COMMANDS: Dict[str, List[str]] = { + SVN: ['info', '--non-interactive'], + GIT: ['describe', '--always', '--dirty'] +} + +# git ['show', '--quiet', '--format=short'], + +STATUS_COMMANDS: Dict[str, List[str]] = { + SVN: ['status', '--non-interactive'], + GIT: ['status', '--short'] +} + +DIFF_COMMANDS: Dict[str, List[str]] = { + SVN: ['diff', '--internal-diff', '--non-interactive'], + GIT: ['diff', 'HEAD'] + # ['diff', '--no-index', '/dev/null', '{0}'] # untracked files +} + +GIT_REV_PARSE_COMMAND: List[str] = ['rev-parse', 'HEAD'] + +NOT_REPO_ERRS: Dict[str, List[str]] = { + SVN: ['svn: e155007:', + 'svn: warning: w155007:'], + GIT: ['fatal: not a git repository', + 'warning: not a git repository'] +} + +NO_BASE_ERRS: Dict[str, List[str]] = { + SVN: [], # Not possible for svn working copy to have no base commit? + GIT: ['fatal: bad revision \'head\'', + 'fatal: ambiguous argument \'head\': unknown revision'] +} + +SVN_INFO_KEYS: List[str] = [ + 'revision', 'url', 'working copy root path', 'repository uuid' +] + + +LOG_VERSION_DIR = Path(WorkflowFiles.LOG_DIR, 'version') + + +class VCSNotInstalledError(CylcError): + """Exception to be raised if an attempted VCS command is not installed. + + Args: + vcs: The version control system command. + exc: The exception that was raised when attempting to run the command. + """ + def __init__(self, vcs: str, exc: Exception) -> None: + self.vcs = vcs + self.exc = exc + + def __str__(self) -> str: + return f"{self.vcs} does not appear to be installed ({self.exc})" + + +class VCSMissingBaseError(CylcError): + """Exception to be raised if a repository is missing a base commit. + + Args: + vcs: The version control system command. + repo_path: The path to the working copy. + """ + def __init__(self, vcs: str, repo_path: Union[Path, str]) -> None: + self.vcs = vcs + self.path = repo_path + + def __str__(self) -> str: + return f"{self.vcs} repository at {self.path} is missing a base commit" + + +def get_vc_info(path: Union[Path, str]) -> Optional['OrderedDict[str, str]']: + """Return the version control information for a repository, given its path. + """ + info = OrderedDict() + missing_base = False + for vcs, args in INFO_COMMANDS.items(): + try: + out = _run_cmd(vcs, args, cwd=path) + except VCSNotInstalledError as exc: + LOG.debug(exc) + continue + except VCSMissingBaseError as exc: + missing_base = True + LOG.debug(exc) + except OSError as exc: + if any(exc.strerror.lower().startswith(err) + for err in NOT_REPO_ERRS[vcs]): + LOG.debug(f"Source dir {path} is not a {vcs} repository") + continue + else: + raise exc + + info['version control system'] = vcs + if vcs == SVN: + info.update(_parse_svn_info(out)) + elif vcs == GIT: + if not missing_base: + info['repository version'] = out.splitlines()[0] + info['commit'] = _get_git_commit(path) + info['working copy root path'] = str(path) + info['status'] = get_status(vcs, path) + + LOG.debug(f"{vcs} repository detected") + return info + + return None + + +def _run_cmd(vcs: str, args: Iterable[str], cwd: Union[Path, str]) -> str: + """Run a command, return stdout. + + Args: + vcs: The version control system. + args: The args to pass to the version control command. + cwd: Directory to run the command in. + + Raises: + OSError: with stderr if non-zero return code. + """ + cmd = [vcs, *args] + try: + proc = Popen( + cmd, cwd=cwd, stdin=DEVNULL, stdout=PIPE, stderr=PIPE, text=True) + except FileNotFoundError as exc: + # This will only be raised if the VCS command is not installed, + # otherwise Popen() will succeed with a non-zero return code + raise VCSNotInstalledError(vcs, exc) + ret_code = proc.wait() + out, err = proc.communicate() + if ret_code: + if any(err.lower().startswith(msg) + for msg in NO_BASE_ERRS[vcs]): + # No base commit in repo + raise VCSMissingBaseError(vcs, cwd) + raise OSError(ret_code, err) + return out + + +def write_vc_info( + info: 'OrderedDict[str, str]', run_dir: Union[Path, str] +) -> None: + """Write version control info to the workflow's vcs log dir. + + Args: + info: The vcs info. + run_dir: The workflow run directory. + """ + if not info: + raise ValueError("Nothing to write") + info_file = Path(run_dir, LOG_VERSION_DIR, 'vcs.conf') + info_file.parent.mkdir(exist_ok=True) + with open(info_file, 'w') as f: + for key, value in info.items(): + if key == 'status': + f.write(f"{key} = \"\"\"\n") + f.write(f"{value}\n") + f.write("\"\"\"\n") + else: + f.write(f"{key} = \"{value}\"\n") + + +def _get_git_commit(path: Union[Path, str]) -> str: + """Return the hash of the HEAD of the repo at path.""" + args = GIT_REV_PARSE_COMMAND + return _run_cmd(GIT, args, cwd=path).splitlines()[0] + + +def get_status(vcs: str, path: Union[Path, str]) -> str: + """Return the short status of a repo. + + Args: + vcs: The version control system. + path: The path to the repository. + """ + args = STATUS_COMMANDS[vcs] + return _run_cmd(vcs, args, cwd=path).rstrip('\n') + + +def _parse_svn_info(info_text: str) -> 'OrderedDict[str, str]': + """Return OrderedDict of certain info parsed from svn info raw output.""" + ret = OrderedDict() + for line in info_text.splitlines(): + if line: + key, value = (ln.strip() for ln in line.split(':', 1)) + key = key.lower() + if key in SVN_INFO_KEYS: + ret[key] = value + return ret + + +def get_diff(vcs: str, path: Union[Path, str]) -> Optional[str]: + """Return the diff of uncommitted changes for a repository. + + Args: + vcs: The version control system. + path: The path to the repo. + """ + args = DIFF_COMMANDS[vcs] + try: + diff = _run_cmd(vcs, args, cwd=path) + except (VCSNotInstalledError, VCSMissingBaseError): + return None + header = ( + "# Auto-generated diff of uncommitted changes in the Cylc " + "workflow repository:\n" + f"# {path}") + return f"{header}\n{diff}" + + +def write_diff(diff: str, run_dir: Union[Path, str]) -> None: + """Write a diff to the workflow's vcs log dir. + + Args: + diff: The diff. + run_dir: The workflow run directory. + """ + diff_file = Path(run_dir, LOG_VERSION_DIR, 'uncommitted.diff') + diff_file.parent.mkdir(exist_ok=True) + with open(diff_file, 'w') as f: + f.write(diff) + + +# Entry point: +def main( + srcdir: Union[Path, str], opts: 'Values', rundir: Union[Path, str] +) -> bool: + """Entry point for this plugin. Write version control info and any + uncommmited diff to the workflow log dir. + + Args: + srcdir: Workflow source dir for cylc install. + opts: CLI options (requirement for post_install entry point, but + not used here) + rundir: Workflow run dir. + + Return True if source dir is a supported repo, else False. + """ + vc_info = get_vc_info(srcdir) + if vc_info is None: + return False + vcs = vc_info['version control system'] + diff = get_diff(vcs, srcdir) + write_vc_info(vc_info, rundir) + if diff is not None: + write_diff(diff, rundir) + return True diff --git a/cylc/flow/main_loop/__init__.py b/cylc/flow/main_loop/__init__.py index 44a6cd2b35b..3481da87714 100644 --- a/cylc/flow/main_loop/__init__.py +++ b/cylc/flow/main_loop/__init__.py @@ -39,7 +39,7 @@ Main loop plugins can be activated either by: -* Using the ``-main-loop`` option with ``cylc play`` e.g: +* Using the ``--main-loop`` option with ``cylc play`` e.g: .. code-block:: console diff --git a/setup.cfg b/setup.cfg index ccc9aaf29c5..1a4d5d8805d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -124,6 +124,7 @@ cylc.main_loop = log_memory = cylc.flow.main_loop.log_memory prune_flow_labels = cylc.flow.main_loop.prune_flow_labels # NOTE: all entry points should be listed here even if Cylc Flow does not -# provide any implementations to make entry point scraping easier +# provide any implementations, to make entry point scraping easier cylc.pre_configure = cylc.post_install = + log_vc_info = cylc.flow.install_plugins.log_vc_info:main diff --git a/tests/functional/lib/bash/test_header b/tests/functional/lib/bash/test_header index 41becf231ff..f97f7dce120 100644 --- a/tests/functional/lib/bash/test_header +++ b/tests/functional/lib/bash/test_header @@ -124,7 +124,7 @@ # Install a reference workflow using `install_workflow`, run a validation # test on the workflow and run the reference workflow with `workflow_run_ok`. # Expect 2 OK tests. -# +# # create_test_global_config [PRE [POST]] # Create a new global config file $PWD/etc from global-tests.cylc # with PRE and POST pre- and ap-pended (PRE for e.g. jinja2 shebang). @@ -501,7 +501,7 @@ log_scan () { make_rnd_workflow() { # Create a randomly-named workflow source directory. # Define its run directory. - RND_WORKFLOW_NAME=x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6) + RND_WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" RND_WORKFLOW_SOURCE="$PWD/${RND_WORKFLOW_NAME}" mkdir -p "${RND_WORKFLOW_SOURCE}" touch "${RND_WORKFLOW_SOURCE}/flow.cylc" @@ -566,6 +566,10 @@ purge () { bash -c "$CMD" 2>/dev/null || true } purge_rnd_workflow() { + if ((FAILURES != 0)); then + # if tests have failed then don't clean up + return 0 + fi # Remove the workflow source created by make_rnd_workflow(). # And remove its run-directory too. rm -rf "${RND_WORKFLOW_SOURCE}" diff --git a/tests/functional/post-install/00-log-vc-info.t b/tests/functional/post-install/00-log-vc-info.t new file mode 100644 index 00000000000..c8d8e1a89df --- /dev/null +++ b/tests/functional/post-install/00-log-vc-info.t @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE. +# Copyright (C) NIWA & British Crown (Met Office) & Contributors. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#------------------------------------------------------------------------------- +# Test logging of source dir version control information occurs post install + +. "$(dirname "$0")/test_header" +if ! command -v 'git' > /dev/null; then + skip_all 'git not installed' +fi +set_test_number 4 + +make_rnd_workflow +cd "${RND_WORKFLOW_SOURCE}" || exit 1 +cat > 'flow.cylc' << __FLOW__ +[scheduling] + [[graph]] + R1 = foo +__FLOW__ + +git init +git add 'flow.cylc' +git commit -am 'Initial commit' + +run_ok "${TEST_NAME_BASE}-install" cylc install + +VCS_INFO_FILE="${RND_WORKFLOW_RUNDIR}/runN/log/version/vcs.conf" +exists_ok "$VCS_INFO_FILE" +# Basic check, unit tests cover this in more detail: +contains_ok "$VCS_INFO_FILE" <<< 'version control system = "git"' + +DIFF_FILE="${RND_WORKFLOW_RUNDIR}/runN/log/version/uncommitted.diff" +exists_ok "$DIFF_FILE" # Expected to be empty but should exist + +purge_rnd_workflow diff --git a/tests/functional/post-install/test_header b/tests/functional/post-install/test_header new file mode 120000 index 00000000000..90bd5a36f92 --- /dev/null +++ b/tests/functional/post-install/test_header @@ -0,0 +1 @@ +../lib/bash/test_header \ No newline at end of file diff --git a/tests/unit/post_install/test_log_vc_info.py b/tests/unit/post_install/test_log_vc_info.py new file mode 100644 index 00000000000..987e379853d --- /dev/null +++ b/tests/unit/post_install/test_log_vc_info.py @@ -0,0 +1,229 @@ +# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE. +# Copyright (C) NIWA & British Crown (Met Office) & Contributors. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from pathlib import Path +import pytest +from pytest import MonkeyPatch, TempPathFactory +import shutil +import subprocess +from typing import Any, Tuple +from unittest.mock import Mock + +from cylc.flow.install_plugins.log_vc_info import ( + get_diff, _get_git_commit, get_status, get_vc_info, main +) + +Fixture = Any + + +BASIC_FLOW_1 = """ +[scheduling] + [[graph]] + R1 = foo +""" + +BASIC_FLOW_2 = """ +[scheduling] + [[graph]] + R1 = bar +""" + + +require_git = pytest.mark.skipif( + shutil.which('git') is None, + reason="git is not installed" +) + +require_svn = pytest.mark.skipif( + shutil.which('svn') is None, + reason="svn is not installed" +) + + +@pytest.fixture(scope='module') +def git_source_repo(tmp_path_factory: TempPathFactory) -> Tuple[str, str]: + """Init a git repo for a workflow source dir. + + The repo has a flow.cylc file with uncommitted changes. This dir is reused + by all tests requesting it in this module. + + Returns (source_dir_path, commit_hash) + """ + source_dir: Path = tmp_path_factory.getbasetemp().joinpath('git_repo') + source_dir.mkdir() + subprocess.run(['git', 'init'], cwd=source_dir, check=True) + flow_file = source_dir.joinpath('flow.cylc') + flow_file.write_text(BASIC_FLOW_1) + subprocess.run(['git', 'add', '-A'], cwd=source_dir, check=True) + subprocess.run( + ['git', 'commit', '-am', '"Initial commit"'], + cwd=source_dir, check=True, capture_output=True) + # Overwrite file to introduce uncommitted changes: + flow_file.write_text(BASIC_FLOW_2) + commit_sha = subprocess.run( + ['git', 'rev-parse', 'HEAD'], + cwd=source_dir, check=True, capture_output=True, text=True + ).stdout.splitlines()[0] + return (str(source_dir), commit_sha) + + +@pytest.fixture(scope='module') +def svn_source_repo(tmp_path_factory: TempPathFactory) -> Tuple[str, str, str]: + """Init an svn repo & working copy for a workflow source dir. + + The working copy has a flow.cylc file with uncommitted changes. This dir + is reused by all tests requesting it in this module. + + Returns (source_dir_path, repository_UUID, repository_path) + """ + tmp_path: Path = tmp_path_factory.getbasetemp() + repo = tmp_path.joinpath('svn_repo') + subprocess.run( + ['svnadmin', 'create', 'svn_repo'], cwd=tmp_path, check=True) + uuid = subprocess.run( + ['svnlook', 'uuid', repo], + check=True, capture_output=True, text=True + ).stdout.splitlines()[0] + project_dir = tmp_path.joinpath('project') + project_dir.mkdir() + project_dir.joinpath('flow.cylc').write_text(BASIC_FLOW_1) + subprocess.run( + ['svn', 'import', project_dir, f'file://{repo}/project/trunk', + '-m', '"Initial import"'], check=True) + source_dir = tmp_path.joinpath('svn_working_copy') + subprocess.run( + ['svn', 'checkout', f'file://{repo}/project/trunk', source_dir], + check=True) + + flow_file = source_dir.joinpath('flow.cylc') + # Overwrite file to introduce uncommitted changes: + flow_file.write_text(BASIC_FLOW_2) + + return (str(source_dir), uuid, str(repo)) + + +@require_git +def test_get_git_commit(git_source_repo: Tuple[str, str]): + """Test get_git_commit()""" + source_dir, commit_sha = git_source_repo + assert _get_git_commit(source_dir) == commit_sha + + +@require_git +def test_get_status_git(git_source_repo: Tuple[str, str]): + """Test get_status() for a git repo""" + source_dir, commit_sha = git_source_repo + assert get_status('git', source_dir) == " M flow.cylc" + + +@require_git +def test_get_vc_info_git(git_source_repo: Tuple[str, str]): + """Test get_vc_info() for a git repo""" + source_dir, commit_sha = git_source_repo + vc_info = get_vc_info(source_dir) + assert vc_info is not None + expected = [ + ('version control system', "git"), + ('repository version', f"{commit_sha[:7]}-dirty"), + ('commit', commit_sha), + ('working copy root path', source_dir), + ('status', " M flow.cylc") + ] + assert list(vc_info.items()) == expected + + +@require_git +def test_get_diff_git(git_source_repo: Tuple[str, str]): + """Test get_diff() for a git repo""" + source_dir, commit_sha = git_source_repo + diff = get_diff('git', source_dir) + assert diff is not None + diff_lines = diff.splitlines() + for line in ("diff --git a/flow.cylc b/flow.cylc", + "- R1 = foo", + "+ R1 = bar"): + assert line in diff_lines + + +@require_svn +def test_get_vc_info_svn(svn_source_repo: Tuple[str, str, str]): + """Test get_vc_info() for an svn working copy""" + source_dir, uuid, repo_path = svn_source_repo + vc_info = get_vc_info(source_dir) + assert vc_info is not None + expected = [ + ('version control system', "svn"), + ('working copy root path', str(source_dir)), + ('url', f"file://{repo_path}/project/trunk"), + ('repository uuid', uuid), + ('revision', "1"), + ('status', "M flow.cylc") + ] + assert list(vc_info.items()) == expected + + +@require_svn +def test_get_diff_svn(svn_source_repo: Tuple[str, str, str]): + """Test get_diff() for an svn working copy""" + source_dir, uuid, repo_path = svn_source_repo + diff = get_diff('svn', source_dir) + assert diff is not None + diff_lines = diff.splitlines() + for line in ("--- flow.cylc (revision 1)", + "+++ flow.cylc (working copy)", + "- R1 = foo", + "+ R1 = bar"): + assert line in diff_lines + + +def test_not_repo(tmp_path: Path, monkeypatch: MonkeyPatch): + """Test get_vc_info() and main() for a dir that is not a supported repo""" + source_dir = Path(tmp_path, 'git_repo') + source_dir.mkdir() + flow_file = source_dir.joinpath('flow.cylc') + flow_file.write_text(BASIC_FLOW_1) + mock_write_vc_info = Mock() + monkeypatch.setattr('cylc.flow.install_plugins.log_vc_info.write_vc_info', + mock_write_vc_info) + mock_write_diff = Mock() + monkeypatch.setattr('cylc.flow.install_plugins.log_vc_info.write_diff', + mock_write_diff) + + assert get_vc_info(source_dir) is None + assert main(source_dir, None, None) is False # type: ignore + assert mock_write_vc_info.called is False + assert mock_write_diff.called is False + + +@require_git +def test_no_base_commit_git(tmp_path: Path): + """Test get_vc_info() and get_diff() for a recently init'd git source dir + that does not have a base commit yet.""" + source_dir = Path(tmp_path, 'new_git_repo') + source_dir.mkdir() + subprocess.run(['git', 'init'], cwd=source_dir, check=True) + flow_file = source_dir.joinpath('flow.cylc') + flow_file.write_text(BASIC_FLOW_1) + + vc_info = get_vc_info(source_dir) + assert vc_info is not None + expected = [ + ('version control system', "git"), + ('working copy root path', str(source_dir)), + ('status', "?? flow.cylc") + ] + assert list(vc_info.items()) == expected + assert get_diff('git', source_dir) is None