Repository: LUMC/pytest-workflow (language: python)

Issue:
pytest-workflow crashes when git submodules are missing
When using the `--git-aware` flag, pytest-workflow crashes with a wall of errors if your project has git submodules, but you haven't cloned the submodules yet. It is hard to tell from the error messages what is going on.

In my case, this error message is one of 13 errors, it appears this error is printed once for every test case that is defined.
```python
___________________________________________________________________________________ ERROR collecting tests/test_functional.yml ____________________________________________________________________________________
../../miniconda3/envs/HAMLET/lib/python3.10/site-packages/pytest_workflow/plugin.py:439: in collect
    workflow = self.queue_workflow()
../../miniconda3/envs/HAMLET/lib/python3.10/site-packages/pytest_workflow/plugin.py:397: in queue_workflow
    duplicate_tree(root_dir, tempdir,
../../miniconda3/envs/HAMLET/lib/python3.10/site-packages/pytest_workflow/util.py:161: in duplicate_tree
    copy(src_path, dest_path)
../../miniconda3/envs/HAMLET/lib/python3.10/shutil.py:434: in copy2
    copyfile(src, dst, follow_symlinks=follow_symlinks)
../../miniconda3/envs/HAMLET/lib/python3.10/shutil.py:254: in copyfile
    with open(src, 'rb') as fsrc:
E   IsADirectoryError: [Errno 21] Is a directory: '/home/user/devel/prod-test/hamlet'
```

Proposed solution: Check to see if all git submodules are present when using `--git--aware`, and immediately abort with one informative error message if they are missing.


Discussion / hints:
Looks like this will be a simple fix. `git submodule status` reports all submodules and adds a `-` in front of the commit if not initialized. The command can even be run as `git submodule status --recursive` so also nested submodules can be found.
The error can then suggest that the user run `git submodule update --init --recursive` which should solve all problems.

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 46a2bbb0..7c613953 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,7 +49,6 @@ jobs:
           - "3.9"
           - "3.10"
           - "3.11"
-    needs: lint
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Set up Python ${{ matrix.python-version }}
diff --git a/HISTORY.rst b/HISTORY.rst
index 03210de7..f51186a7 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -9,7 +9,11 @@ Changelog
 
 version 1.7.0-dev
 ---------------------------
-+ Skip tests searching in non existing file
++ When the ``--git-aware`` flag is used a submodule check is performed in order
+  to assert that all submodules are properly checked out. This prevents
+  unclear copying errors.
++ Tests for checking file content are now skipped when the file does not exist
+  in order to reduce visual clutter when reporting failing tests.
 + Test and support for Python 3.11.
 + Add ``--stderr-bytes`` or ``--sb`` option to change the maximum 
   number of bytes to display for the stderr and stdout on 


Test patch:
diff --git a/src/pytest_workflow/util.py b/src/pytest_workflow/util.py
index e208dc4d..94e2ffbf 100644
--- a/src/pytest_workflow/util.py
+++ b/src/pytest_workflow/util.py
@@ -48,7 +48,7 @@ def is_in_dir(child: Path, parent: Path, strict: bool = False) -> bool:
     return False
 
 
-def _run_command(*args):
+def _run_command(*args) -> str:
     """Run an external command and return the output"""
     result = subprocess.run(args,
                             stdout=subprocess.PIPE,
@@ -64,6 +64,19 @@ def git_root(path: Filepath) -> str:
     return output.strip()  # Remove trailing newline
 
 
+def git_check_submodules_cloned(path: Filepath):
+    output = _run_command("git", "-C", os.fspath(path), "submodule", "status",
+                          "--recursive")
+    for line in output.splitlines():
+        commit, path = line.strip().split(maxsplit=1)
+        if commit.startswith("-"):
+            raise RuntimeError(
+                f"Git submodule '{path}' was not cloned. Pytest-workflow "
+                f"cannot copy paths from non-existing submodules. Please "
+                f"clone all submodules using 'git submodule update --init "
+                f"--recursive'.")
+
+
 def git_ls_files(path: Filepath) -> List[str]:
     output = _run_command("git", "-C", os.fspath(path), "ls-files",
                           # Make sure submodules are included.
@@ -97,6 +110,7 @@ def _recurse_git_repository_tree(src: Filepath, dest: Filepath
     # A set of dirs we have already yielded. '' is the output of
     # os.path.dirname when the path is in the current directory.
     yielded_dirs: Set[str] = {''}
+    git_check_submodules_cloned(src)
     for path in git_ls_files(src):
         # git ls-files does not list directories. Yield parent first to prevent
         # creating files in non-existing directories. Also check if it is
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 5bbed249..098f1e67 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -22,7 +22,8 @@
 
 import pytest
 
-from pytest_workflow.util import duplicate_tree, file_md5sum, git_root, \
+from pytest_workflow.util import duplicate_tree, file_md5sum, \
+    git_check_submodules_cloned, git_root, \
     is_in_dir, link_tree, replace_whitespace
 
 WHITESPACE_TESTS = [
@@ -158,3 +159,45 @@ def test_file_md5sum(hash_file: Path):
     whole_file_md5 = hashlib.md5(hash_file.read_bytes()).hexdigest()
     per_line_md5 = file_md5sum(hash_file)
     assert whole_file_md5 == per_line_md5
+
+
+def create_git_repo(path):
+    dir = Path(path)
+    os.mkdir(dir)
+    file = dir / "README.md"
+    file.write_text("# My new project\n\nHello this project is awesome!\n")
+    subprocess.run(["git", "init", "-b", "main"], cwd=dir)
+    subprocess.run(["git", "config", "user.name", "A U Thor"], cwd=dir)
+    subprocess.run(["git", "config", "user.email", "author@example.com"],
+                   cwd=dir)
+    subprocess.run(["git", "add", "README.md"], cwd=dir)
+    subprocess.run(["git", "commit", "-m", "initial commit"], cwd=dir)
+
+
+def test_git_submodule_check(tmp_path):
+    bird_repo = tmp_path / "bird"
+    nest_repo = tmp_path / "nest"
+    create_git_repo(bird_repo)
+    create_git_repo(nest_repo)
+    # https://bugs.launchpad.net/ubuntu/+source/git/+bug/1993586
+    subprocess.run(["git", "-c", "protocol.file.allow=always",
+                    "submodule", "add", bird_repo.absolute()],
+                   cwd=nest_repo.absolute())
+    subprocess.run(["git", "commit", "-m", "add bird repo as a submodule"],
+                   cwd=nest_repo.absolute())
+    cloned_repo = tmp_path / "cloned"
+    subprocess.run(
+        # No recursive clone
+        ["git", "-c", "protocol.file.allow=always",
+         "clone", nest_repo.absolute(), cloned_repo.absolute()],
+        cwd=tmp_path
+    )
+    with pytest.raises(RuntimeError) as error:
+        git_check_submodules_cloned(cloned_repo)
+    # Error message should allow user to resolve the issue.
+    error.match("'git submodule update --init --recursive'")
+    subprocess.run(["git", "-c", "protocol.file.allow=always", "submodule",
+                    "update", "--init", "--recursive"],
+                   cwd=cloned_repo.absolute())
+    # Check error does not occur when issue resolved.
+    git_check_submodules_cloned(cloned_repo)