From 588c0beeb9775506e23bef7ae84e9819bc695104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saugat=20Pachhai=20=28=E0=A4=B8=E0=A5=8C=E0=A4=97=E0=A4=BE?= =?UTF-8?q?=E0=A4=A4=29?= Date: Wed, 8 Dec 2021 19:40:26 +0545 Subject: [PATCH] migrate functional tests --- tests/test_git.py | 215 +++++++++++++++++++++++++++++++++++++++++--- tests/test_noscm.py | 27 ++++++ tests/test_stash.py | 125 ++++++++++++++++++++++++++ 3 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 tests/test_noscm.py create mode 100644 tests/test_stash.py diff --git a/tests/test_git.py b/tests/test_git.py index 9b858704..3b699f4d 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -2,16 +2,19 @@ from typing import Iterator, Type import pytest +from git import Repo as GitPythonRepo from pytest_test_utils import TempDirFactory, TmpDir from pytest_test_utils.matchers import Matcher from scmrepo.exceptions import MergeConflictError, RevError, SCMError from scmrepo.git import Git -# pylint: disable=redefined-outer-name,unused-argument +# pylint: disable=redefined-outer-name,unused-argument,protected-access +backends = ["gitpython", "dulwich", "pygit2"] -@pytest.fixture(params=["gitpython", "dulwich", "pygit2"]) + +@pytest.fixture(params=backends) def git_backend(request) -> str: marker = request.node.get_closest_marker("skip_git_backend") to_skip = marker.args if marker else [] @@ -37,18 +40,58 @@ def remote_git_dir(tmp_dir_factory: TempDirFactory): return git_dir -@pytest.mark.parametrize("backend", ["gitpython", "dulwich", "pygit2"]) -def test_git_init(tmp_dir: TmpDir, backend: str): - Git.init(".", _backend=backend) +@pytest.fixture +def submodule_dir(tmp_dir: TmpDir, scm: Git): + scm.commit("init") + + subrepo = GitPythonRepo.init() + subrepo_path = "subrepo" + subrepo.create_submodule(subrepo_path, subrepo_path, subrepo.git_dir) + subrepo.close() + + yield tmp_dir / subrepo_path + + +def test_git_init(tmp_dir: TmpDir, git_backend: str): + Git.init(".", _backend=git_backend) assert (tmp_dir / ".git").is_dir() - Git(tmp_dir) + + for backend in backends: + Git(tmp_dir, backends=[backend]) -@pytest.mark.parametrize("backend", ["gitpython", "dulwich", "pygit2"]) -def test_git_init_bare(tmp_dir: TmpDir, backend: str): - Git.init(".", bare=True, _backend=backend) +def test_git_init_bare(tmp_dir: TmpDir, git_backend: str): + Git.init(".", bare=True, _backend=git_backend) assert list(tmp_dir.iterdir()) - Git(tmp_dir) + + for backend in backends: + Git(tmp_dir, backends=[backend]) + + +def test_git_submodule(submodule_dir: TmpDir, git_backend: str): + git = Git(backends=[git_backend]) + git.close() + + git = Git(submodule_dir, backends=[git_backend]) + git.close() + + +@pytest.mark.skip_git_backend("pygit2") +def test_commit(tmp_dir: TmpDir, scm: Git, git: Git): + tmp_dir.gen({"foo": "foo"}) + git.add(["foo"]) + git.commit("add") + assert "foo" in scm.gitpython.git.ls_files() + + +@pytest.mark.skip_git_backend("pygit2") +def test_commit_in_root_repo_with_submodule( + tmp_dir: TmpDir, scm: Git, git: Git, submodule_dir: TmpDir +): + tmp_dir.gen("foo", "foo") + git.add(["foo"]) + git.commit("add foo") + assert "foo" in scm.gitpython.git.ls_files() @pytest.mark.parametrize( @@ -151,6 +194,26 @@ def test_is_tracked_unicode(tmp_dir: TmpDir, scm: Git, git: Git): assert not git.is_tracked("ṳṋṭṝḁḉḵḗḋ") +@pytest.mark.skip_git_backend("pygit2") +def test_is_tracked_func(tmp_dir, scm, git): + tmp_dir.gen({"foo": "foo", "тест": "проверка"}) + scm.add(["foo", "тест"]) + + abs_foo = os.path.abspath("foo") + assert git.is_tracked(abs_foo) + assert git.is_tracked("foo") + assert git.is_tracked("тест") + + git.commit("add") + assert git.is_tracked(abs_foo) + assert git.is_tracked("foo") + + scm.gitpython.repo.index.remove(["foo"], working_tree=True) + assert not git.is_tracked(abs_foo) + assert not git.is_tracked("foo") + assert not git.is_tracked("not-existing-file") + + @pytest.mark.skip_git_backend("pygit2") def test_no_commits(tmp_dir: TmpDir, scm: Git, git: Git): assert git.no_commits @@ -595,7 +658,6 @@ def test_add(tmp_dir: TmpDir, scm: Git, git: Git): @pytest.mark.skip_git_backend("dulwich", "gitpython") -@pytest.mark.skipif(os.name != "nt", reason="Windows only") def test_checkout_subdir(tmp_dir: TmpDir, scm: Git, git: Git): tmp_dir.gen("foo", "foo") scm.add_commit("foo", message="init") @@ -634,3 +696,134 @@ def test_describe(tmp_dir: TmpDir, scm: Git, git: Git): scm.tag("tag") assert git.describe(rev_bar) == "refs/tags/tag" + + +def test_ignore(tmp_dir: TmpDir, scm: Git, git: Git): + file = os.fspath(tmp_dir / "foo") + + git.ignore(file) + assert (tmp_dir / ".gitignore").cat() == "/foo\n" + + git._reset() + git.ignore(file) + assert (tmp_dir / ".gitignore").cat() == "/foo\n" + + git._reset() + git.ignore_remove(file) + assert not (tmp_dir / ".gitignore").exists() + + +def test_ignored(tmp_dir: TmpDir, scm: Git, git: Git, git_backend: str): + if os.name == "nt" and git_backend == "pygit2": + pytest.skip() + + tmp_dir.gen({"dir1": {"file1.jpg": "cont", "file2.txt": "cont"}}) + tmp_dir.gen({".gitignore": "dir1/*.jpg"}) + + git._reset() + + assert git.is_ignored(tmp_dir / "dir1" / "file1.jpg") + assert not git.is_ignored(tmp_dir / "dir1" / "file2.txt") + + +@pytest.mark.skip_git_backend("pygit2", "gitpython") +def test_ignored_dir_unignored_subdirs(tmp_dir: TmpDir, scm: Git, git: Git): + tmp_dir.gen({".gitignore": "data/**\n!data/**/\n!data/**/*.csv"}) + scm.add([".gitignore"]) + tmp_dir.gen( + { + os.path.join("data", "raw", "tracked.csv"): "cont", + os.path.join("data", "raw", "not_tracked.json"): "cont", + } + ) + + git._reset() + + assert not git.is_ignored(tmp_dir / "data" / "raw" / "tracked.csv") + assert git.is_ignored(tmp_dir / "data" / "raw" / "not_tracked.json") + assert not git.is_ignored(tmp_dir / "data" / "raw" / "non_existent.csv") + assert git.is_ignored(tmp_dir / "data" / "raw" / "non_existent.json") + assert not git.is_ignored(tmp_dir / "data" / "non_existent.csv") + assert git.is_ignored(tmp_dir / "data" / "non_existent.json") + + assert not git.is_ignored(f"data{os.sep}") + # git check-ignore would now mark "data/raw" as ignored + # after detecting it's a directory in the file system; + # instead, we rely on the trailing separator to determine if handling a + # a directory - for consistency between existent and non-existent paths + assert git.is_ignored(os.path.join("data", "raw")) + assert not git.is_ignored(os.path.join("data", f"raw{os.sep}")) + + assert git.is_ignored(os.path.join("data", "non_existent")) + assert not git.is_ignored(os.path.join("data", f"non_existent{os.sep}")) + + +def test_get_gitignore(tmp_dir: TmpDir, scm: Git, git: Git): + tmp_dir.gen({"file1": "contents", "dir": {}}) + + data_dir = os.fspath(tmp_dir / "file1") + entry, gitignore = git._get_gitignore(data_dir) + assert entry == "/file1" + assert gitignore == os.fspath(tmp_dir / ".gitignore") + + data_dir = os.fspath(tmp_dir / "dir") + entry, gitignore = git._get_gitignore(data_dir) + + assert entry == "/dir" + assert gitignore == os.fspath(tmp_dir / ".gitignore") + + +def test_get_gitignore_symlink(tmp_dir: TmpDir, scm: Git, git: Git): + tmp_dir.gen({"dir": {"subdir": {"data": "contents"}}}) + link = tmp_dir / "link" + link.symlink_to(tmp_dir / "dir" / "subdir" / "data") + + entry, gitignore = git._get_gitignore(os.fspath(link)) + assert entry == "/link" + assert gitignore == os.fspath(tmp_dir / ".gitignore") + + +def test_get_gitignore_subdir(tmp_dir: TmpDir, scm: Git, git: Git): + tmp_dir.gen({"dir1": {"file1": "cont", "dir2": {}}}) + + data_dir = os.fspath(tmp_dir / "dir1" / "file1") + entry, gitignore = git._get_gitignore(data_dir) + assert entry == "/file1" + assert gitignore == os.fspath(tmp_dir / "dir1" / ".gitignore") + + data_dir = os.fspath(tmp_dir / "dir1" / "dir2") + entry, gitignore = git._get_gitignore(data_dir) + assert entry == "/dir2" + assert gitignore == os.fspath(tmp_dir / "dir1" / ".gitignore") + + +def test_gitignore_should_append_newline_to_gitignore( + tmp_dir: TmpDir, scm: Git, git: Git +): + tmp_dir.gen({"foo": "foo", "bar": "bar"}) + + bar_path = os.fspath(tmp_dir / "bar") + gitignore = tmp_dir / ".gitignore" + + gitignore.write_text("/foo") + assert not gitignore.read_text().endswith("\n") + + git.ignore(bar_path) + contents = gitignore.read_text() + assert gitignore.read_text().endswith("\n") + + assert contents.splitlines() == ["/foo", "/bar"] + + +@pytest.mark.skip_git_backend("dulwich") +def test_git_detach_head(tmp_dir: TmpDir, scm: Git, git: Git): + tmp_dir.gen({"file": "0"}) + scm.add_commit("file", message="init") + init_rev = scm.get_rev() + + with git.detach_head() as rev: + assert init_rev == rev + assert init_rev == (tmp_dir / ".git" / "HEAD").read_text().strip() + assert ( + tmp_dir / ".git" / "HEAD" + ).read_text().strip() == "ref: refs/heads/master" diff --git a/tests/test_noscm.py b/tests/test_noscm.py new file mode 100644 index 00000000..5afcab65 --- /dev/null +++ b/tests/test_noscm.py @@ -0,0 +1,27 @@ +from collections.abc import Callable + +import pytest +from pytest_test_utils import TmpDir +from pytest_test_utils.matchers import Matcher + +from scmrepo.git import Git +from scmrepo.noscm import NoSCM + + +def test_noscm(tmp_dir: TmpDir): + scm = NoSCM(tmp_dir) + scm.add("test") + + +def test_noscm_raises_exc_on_unimplemented_apis( + tmp_dir: TmpDir, matcher: Matcher +): + class Unimplemented(Exception): + pass + + scm = NoSCM(tmp_dir, _raise_not_implemented_as=Unimplemented) + assert scm._exc is Unimplemented # pylint: disable=protected-access + + assert Git.reset == matcher.instance_of(Callable) # type: ignore[arg-type] + with pytest.raises(Unimplemented): + assert scm.reset() diff --git a/tests/test_stash.py b/tests/test_stash.py new file mode 100644 index 00000000..ffb4865a --- /dev/null +++ b/tests/test_stash.py @@ -0,0 +1,125 @@ +import sys +from typing import Optional + +import pytest +from pytest_test_utils import TmpDir + +from scmrepo.git import Git, Stash + + +def test_git_stash_workspace(tmp_dir: TmpDir, scm: Git): + tmp_dir.gen({"file": "0"}) + scm.add_commit("file", message="init") + tmp_dir.gen("file", "1") + + with scm.stash_workspace(): + assert not scm.is_dirty() + assert (tmp_dir / "file").read_text() == "0" + assert scm.is_dirty() + assert (tmp_dir / "file").read_text() == "1" + + +@pytest.mark.parametrize( + "ref, include_untracked", + [ + (None, True), + (None, False), + ("refs/foo/stash", True), + ("refs/foo/stash", False), + ], +) +def test_git_stash_push( + tmp_dir: TmpDir, scm: Git, ref: Optional[str], include_untracked: bool +): + tmp_dir.gen({"file": "0"}) + scm.add_commit("file", message="init") + tmp_dir.gen({"file": "1", "untracked": "0"}) + + stash = Stash(scm, ref=ref) + rev = stash.push(include_untracked=include_untracked) + assert rev == scm.get_ref(stash.ref) + assert (tmp_dir / "file").read_text() == "0" + assert include_untracked != (tmp_dir / "untracked").exists() + assert len(stash) == 1 + + stash.apply(rev) + assert (tmp_dir / "file").read_text() == "1" + assert (tmp_dir / "untracked").read_text() == "0" + + parts = list(stash.ref.split("/")) + assert (tmp_dir / ".git").joinpath(*parts).exists() + assert (tmp_dir / ".git" / "logs").joinpath(*parts).exists() + + +@pytest.mark.parametrize("ref", [None, "refs/foo/stash"]) +def test_git_stash_drop(tmp_dir: TmpDir, scm: Git, ref: Optional[str]): + tmp_dir.gen({"file": "0"}) + scm.add_commit("file", message="init") + tmp_dir.gen("file", "1") + + stash = Stash(scm, ref=ref) + stash.push() + + tmp_dir.gen("file", "2") + expected = stash.push() + + stash.drop(1) + assert expected == scm.get_ref(stash.ref) + assert len(stash) == 1 + + +reason = """libgit2 stash_save() is flaky on linux when run inside pytest + https://github.com/iterative/dvc/pull/5286#issuecomment-792574294""" + + +@pytest.mark.parametrize( + "ref", + [ + pytest.param( + None, + marks=pytest.mark.xfail( + sys.platform == "linux", raises=AssertionError, reason=reason + ), + ), + "refs/foo/stash", + ], +) +def test_git_stash_pop(tmp_dir: TmpDir, scm: Git, ref: Optional[str]): + tmp_dir.gen({"file": "0"}) + scm.add_commit("file", message="init") + tmp_dir.gen("file", "1") + + stash = Stash(scm, ref=ref) + first = stash.push() + + tmp_dir.gen("file", "2") + second = stash.push() + + assert second == stash.pop() + assert len(stash) == 1 + assert first == scm.get_ref(stash.ref) + assert (tmp_dir / "file").read_text() == "2" + + +@pytest.mark.parametrize("ref", [None, "refs/foo/stash"]) +def test_git_stash_clear(tmp_dir: TmpDir, scm: Git, ref: Optional[str]): + tmp_dir.gen({"file": "0"}) + scm.add_commit("file", message="init") + tmp_dir.gen("file", "1") + + stash = Stash(scm, ref=ref) + stash.push() + + tmp_dir.gen("file", "2") + stash.push() + + stash.clear() + assert len(stash) == 0 + + parts = list(stash.ref.split("/")) + assert not (tmp_dir / ".git").joinpath(*parts).exists() + + reflog_file = (tmp_dir / ".git" / "logs").joinpath(*parts) + # NOTE: some backends will completely remove reflog file on clear, some + # will only truncate it, either case means an empty stash + assert not reflog_file.exists() or not reflog_file.cat()