checkout/pull: report granular summary of changes (#10842)

This commit is contained in:
skshetry
2025-08-15 20:54:13 +05:45
committed by GitHub
parent 82efd3e780
commit e027bc6a87
11 changed files with 491 additions and 179 deletions
+259 -79
View File
@@ -36,7 +36,10 @@ def test_checkout(tmp_dir, dvc, copy_script):
remove(tmp_dir / "foo")
remove("data")
dvc.checkout(force=True)
assert dvc.checkout(force=True) == empty_checkout | {
"added": ["data" + os.sep, "foo"],
"stats": empty_stats | {"added": 2},
}
assert (tmp_dir / "foo").read_text() == "foo"
assert (tmp_dir / "data").read_text() == {"file": "file"}
@@ -65,6 +68,53 @@ def test_checkout_cli(tmp_dir, dvc, copy_script):
assert (tmp_dir / "data").read_text() == {"file": "file"}
@pytest.mark.parametrize(
"summary,expected_lines",
[
(
True,
[
"1 file modified, 3 files added and 1 file deleted",
],
),
(
False,
[
"M\tbar".expandtabs(),
"A\tdir".expandtabs() + os.sep,
"A\tfoo".expandtabs(),
"A\tlorem".expandtabs(),
"D\tipsum".expandtabs(),
],
),
],
)
def test_checkout_stats(tmp_dir, dvc, capsys, summary, expected_lines):
tmp_dir.dvc_gen(
{
"foo": "foo",
"bar": "bar",
"lorem": "lorem",
"dir": {"file": "file"},
"ipsum": "ipsum",
"dolor": "dolor",
}
)
for out in ["foo", "bar", "lorem", "dir"]:
remove(tmp_dir / out)
(tmp_dir / "ipsum.dvc").unlink()
(tmp_dir / "bar").write_text("foobar")
opts = ["--summary"] if summary else []
assert main(["checkout", "--force", *opts]) == 0
out, _ = capsys.readouterr()
assert out.splitlines() == expected_lines
main(["checkout"])
out, _ = capsys.readouterr()
assert not out
def test_remove_files_when_checkout(tmp_dir, dvc, scm):
# add the file into a separate branch
scm.checkout("branch", True)
@@ -167,8 +217,9 @@ def test_checkout_missing_md5_in_lock_file_for_outs_deps(tmp_dir, dvc, copy_scri
name="copy-file",
)
with pytest.raises(CheckoutError):
with pytest.raises(CheckoutError) as exc:
dvc.checkout(force=True)
assert exc.value.result == empty_checkout | {"failed": ["file1"]}
def test_checkout_empty_dir(tmp_dir, dvc):
@@ -179,8 +230,10 @@ def test_checkout_empty_dir(tmp_dir, dvc):
stage.outs[0].remove()
assert not empty_dir.exists()
stats = dvc.checkout(force=True)
assert stats["added"] == [os.path.join("empty_dir", "")]
assert dvc.checkout(force=True) == empty_checkout | {
"added": [os.path.join("empty_dir", "")],
"stats": empty_stats | {"added": 0},
}
assert empty_dir.is_dir()
assert not list(empty_dir.iterdir())
@@ -188,8 +241,7 @@ def test_checkout_empty_dir(tmp_dir, dvc):
def test_checkout_not_cached_file(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
dvc.run(cmd="cp foo bar", deps=["foo"], outs_no_cache=["bar"], name="copy-file")
stats = dvc.checkout(force=True)
assert not any(stats.values())
assert dvc.checkout(force=True) == empty_checkout
def test_checkout_with_deps_cli(tmp_dir, dvc, copy_script):
@@ -254,8 +306,7 @@ def test_checkout_recursive_not_directory(tmp_dir, dvc):
ret = main(["add", "foo"])
assert ret == 0
stats = dvc.checkout(targets=["foo.dvc"], recursive=True)
assert stats == {"added": [], "modified": [], "deleted": []}
assert dvc.checkout(targets=["foo.dvc"], recursive=True) == empty_checkout
def test_checkout_moved_cache_dir_with_symlinks(tmp_dir, dvc):
@@ -327,8 +378,7 @@ def test_checkout_relink(tmp_dir, dvc, link, link_test_func):
assert os.access(data_file, os.W_OK)
assert not link_test_func(data_file)
stats = dvc.checkout(["dir.dvc"], relink=True)
assert stats == empty_checkout
assert dvc.checkout(["dir.dvc"], relink=True) == empty_checkout
assert link_test_func(data_file)
# NOTE: Windows symlink perms don't propagate to the target and
@@ -344,12 +394,15 @@ def test_checkout_relink(tmp_dir, dvc, link, link_test_func):
def test_partial_checkout(tmp_dir, dvc, target):
tmp_dir.dvc_gen({"dir": {"subdir": {"file": "file"}, "other": "other"}})
shutil.rmtree("dir")
stats = dvc.checkout([target])
assert stats["added"] == ["dir" + os.sep]
assert dvc.checkout([target]) == empty_checkout | {
"added": ["dir" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert list(walk_files("dir")) == [os.path.join("dir", "subdir", "file")]
empty_checkout = {"added": [], "deleted": [], "modified": []}
empty_stats = {"added": 0, "deleted": 0, "modified": 0}
empty_checkout = {"added": [], "deleted": [], "modified": [], "stats": empty_stats}
def test_stats_on_empty_checkout(tmp_dir, dvc, scm):
@@ -372,11 +425,17 @@ def test_stats_on_checkout(tmp_dir, dvc, scm):
)
scm.checkout("HEAD~")
stats = dvc.checkout()
assert set(stats["deleted"]) == {"dir" + os.sep, "foo", "bar"}
assert stats == empty_checkout | {
"deleted": ["dir" + os.sep, "bar", "foo"],
"stats": empty_stats | {"deleted": 3},
}
scm.checkout("-")
stats = dvc.checkout()
assert set(stats["added"]) == {"bar", "dir" + os.sep, "foo"}
assert stats == empty_checkout | {
"added": ["dir" + os.sep, "bar", "foo"],
"stats": empty_stats | {"added": 4},
}
tmp_dir.gen({"lorem": "lorem", "bar": "new bar", "dir2": {"file": "file"}})
(tmp_dir / "foo").unlink()
@@ -385,18 +444,23 @@ def test_stats_on_checkout(tmp_dir, dvc, scm):
scm.checkout("HEAD~")
stats = dvc.checkout()
assert set(stats["modified"]) == {"bar"}
assert set(stats["added"]) == {"foo"}
assert set(stats["deleted"]) == {"lorem", "dir2" + os.sep}
assert stats == empty_checkout | {
"modified": ["bar"],
"added": ["foo"],
"deleted": ["dir2" + os.sep, "lorem"],
"stats": {"modified": 1, "added": 1, "deleted": 2},
}
scm.checkout("-")
stats = dvc.checkout()
assert set(stats["modified"]) == {"bar"}
assert set(stats["added"]) == {"dir2" + os.sep, "lorem"}
assert set(stats["deleted"]) == {"foo"}
assert stats == empty_checkout | {
"modified": ["bar"],
"added": ["dir2" + os.sep, "lorem"],
"deleted": ["foo"],
"stats": {"modified": 1, "added": 2, "deleted": 1},
}
@pytest.mark.xfail(reason="values relpath")
def test_checkout_stats_on_failure(tmp_dir, dvc, scm):
tmp_dir.dvc_gen(
{"foo": "foo", "dir": {"subdir": {"file": "file"}}, "other": "other"},
@@ -405,20 +469,20 @@ def test_checkout_stats_on_failure(tmp_dir, dvc, scm):
stage = load_file(dvc, "foo.dvc").stage
tmp_dir.dvc_gen({"foo": "foobar", "other": "other other"}, commit="second")
# corrupt cache
# remove object from cache
cache = stage.outs[0].cache_path
os.chmod(cache, 0o644)
with open(cache, "a", encoding="utf-8") as fd:
fd.write("destroy cache")
remove(cache)
(tmp_dir / "foo").unlink()
scm.checkout("HEAD~")
with pytest.raises(CheckoutError) as exc:
dvc.checkout(force=True)
assert exc.value.stats == {
**empty_checkout,
assert exc.value.result == empty_checkout | {
"failed": ["foo"],
"modified": ["other"],
"stats": empty_stats | {"modified": 1},
}
@@ -431,11 +495,19 @@ def test_stats_on_added_file_from_tracked_dir(tmp_dir, dvc, scm):
tmp_dir.gen("dir/subdir/newfile", "newfile")
tmp_dir.dvc_add("dir", commit="add newfile")
scm.checkout("HEAD~")
assert dvc.checkout() == {**empty_checkout, "modified": ["dir" + os.sep]}
assert dvc.checkout() == {
**empty_checkout,
"modified": ["dir" + os.sep],
"stats": empty_stats | {"deleted": 1},
}
assert dvc.checkout() == empty_checkout
scm.checkout("-")
assert dvc.checkout() == {**empty_checkout, "modified": ["dir" + os.sep]}
assert dvc.checkout() == {
**empty_checkout,
"modified": ["dir" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert dvc.checkout() == empty_checkout
@@ -448,11 +520,19 @@ def test_stats_on_updated_file_from_tracked_dir(tmp_dir, dvc, scm):
tmp_dir.gen("dir/subdir/file", "what file?")
tmp_dir.dvc_add("dir", commit="update file")
scm.checkout("HEAD~")
assert dvc.checkout() == {**empty_checkout, "modified": ["dir" + os.sep]}
assert dvc.checkout() == {
**empty_checkout,
"modified": ["dir" + os.sep],
"stats": empty_stats | {"modified": 1},
}
assert dvc.checkout() == empty_checkout
scm.checkout("-")
assert dvc.checkout() == {**empty_checkout, "modified": ["dir" + os.sep]}
assert dvc.checkout() == {
**empty_checkout,
"modified": ["dir" + os.sep],
"stats": empty_stats | {"modified": 1},
}
assert dvc.checkout() == empty_checkout
@@ -465,11 +545,19 @@ def test_stats_on_removed_file_from_tracked_dir(tmp_dir, dvc, scm):
(tmp_dir / "dir" / "subdir" / "file").unlink()
tmp_dir.dvc_add("dir", commit="removed file from subdir")
scm.checkout("HEAD~")
assert dvc.checkout() == {**empty_checkout, "modified": ["dir" + os.sep]}
assert dvc.checkout() == {
**empty_checkout,
"modified": ["dir" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert dvc.checkout() == empty_checkout
scm.checkout("-")
assert dvc.checkout() == {**empty_checkout, "modified": ["dir" + os.sep]}
assert dvc.checkout() == {
**empty_checkout,
"modified": ["dir" + os.sep],
"stats": empty_stats | {"deleted": 2},
}
assert dvc.checkout() == empty_checkout
@@ -511,7 +599,11 @@ def test_checkout_with_relink_existing(tmp_dir, dvc, link):
dvc.cache.local.cache_types = [link]
stats = dvc.checkout(relink=True)
assert stats == {**empty_checkout, "added": ["foo"]}
assert stats == {
**empty_checkout,
"added": ["foo"],
"stats": empty_stats | {"added": 1},
}
def test_checkout_with_deps(tmp_dir, dvc):
@@ -522,11 +614,19 @@ def test_checkout_with_deps(tmp_dir, dvc):
(tmp_dir / "foo").unlink()
stats = dvc.checkout(["copy-file"], with_deps=False)
assert stats == {**empty_checkout, "added": ["bar"]}
assert stats == {
**empty_checkout,
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "bar").unlink()
stats = dvc.checkout(["copy-file"], with_deps=True)
assert set(stats["added"]) == {"foo", "bar"}
assert stats == {
**empty_checkout,
"added": ["bar", "foo"],
"stats": empty_stats | {"added": 2},
}
def test_checkout_recursive(tmp_dir, dvc):
@@ -537,9 +637,9 @@ def test_checkout_recursive(tmp_dir, dvc):
(tmp_dir / "dir" / "bar").unlink()
stats = dvc.checkout(["dir"], recursive=True)
assert set(stats["added"]) == {
os.path.join("dir", "foo"),
os.path.join("dir", "bar"),
assert stats == empty_checkout | {
"added": [os.path.join("dir", "bar"), os.path.join("dir", "foo")],
"stats": empty_stats | {"added": 2},
}
@@ -550,30 +650,51 @@ def test_checkouts_with_different_addressing(tmp_dir, dvc, run_copy):
(tmp_dir / "bar").unlink()
(tmp_dir / "ipsum").unlink()
assert set(dvc.checkout(PROJECT_FILE)["added"]) == {"bar", "ipsum"}
assert dvc.checkout(PROJECT_FILE) == empty_checkout | {
"added": ["bar", "ipsum"],
"stats": empty_stats | {"added": 2},
}
(tmp_dir / "bar").unlink()
(tmp_dir / "ipsum").unlink()
assert set(dvc.checkout(":")["added"]) == {"bar", "ipsum"}
assert dvc.checkout(":") == empty_checkout | {
"added": ["bar", "ipsum"],
"stats": empty_stats | {"added": 2},
}
(tmp_dir / "bar").unlink()
assert dvc.checkout("copy-foo-bar")["added"] == ["bar"]
assert dvc.checkout("copy-foo-bar") == empty_checkout | {
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "bar").unlink()
assert dvc.checkout("dvc.yaml:copy-foo-bar")["added"] == ["bar"]
assert dvc.checkout("dvc.yaml:copy-foo-bar") == empty_checkout | {
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "bar").unlink()
assert dvc.checkout(":copy-foo-bar")["added"] == ["bar"]
assert dvc.checkout(":copy-foo-bar") == empty_checkout | {
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "bar").unlink()
(tmp_dir / "data").mkdir()
with (tmp_dir / "data").chdir():
assert dvc.checkout(relpath(tmp_dir / "dvc.yaml") + ":copy-foo-bar")[
"added"
] == [relpath(tmp_dir / "bar")]
assert dvc.checkout(
relpath(tmp_dir / "dvc.yaml") + ":copy-foo-bar"
) == empty_checkout | {
"added": [relpath(tmp_dir / "bar")],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "bar").unlink()
assert dvc.checkout("bar")["added"] == ["bar"]
assert dvc.checkout("bar") == empty_checkout | {
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
def test_checkouts_on_same_stage_name_and_output_name(tmp_dir, dvc, run_copy):
@@ -583,8 +704,14 @@ def test_checkouts_on_same_stage_name_and_output_name(tmp_dir, dvc, run_copy):
(tmp_dir / "bar").unlink()
(tmp_dir / "copy-foo-bar").unlink()
assert dvc.checkout("copy-foo-bar")["added"] == ["bar"]
assert dvc.checkout("./copy-foo-bar")["added"] == ["copy-foo-bar"]
assert dvc.checkout("copy-foo-bar") == empty_checkout | {
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
assert dvc.checkout("./copy-foo-bar") == empty_checkout | {
"added": ["copy-foo-bar"],
"stats": empty_stats | {"added": 1},
}
def test_checkouts_for_pipeline_tracked_outs(tmp_dir, dvc, scm, run_copy):
@@ -595,20 +722,35 @@ def test_checkouts_for_pipeline_tracked_outs(tmp_dir, dvc, scm, run_copy):
for out in ["bar", "ipsum"]:
(tmp_dir / out).unlink()
assert dvc.checkout(["bar"])["added"] == ["bar"]
assert dvc.checkout(["bar"]) == empty_checkout | {
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "bar").unlink()
assert set(dvc.checkout([PROJECT_FILE])["added"]) == {"bar", "ipsum"}
assert dvc.checkout([PROJECT_FILE]) == empty_checkout | {
"added": ["bar", "ipsum"],
"stats": empty_stats | {"added": 2},
}
for out in ["bar", "ipsum"]:
(tmp_dir / out).unlink()
assert set(dvc.checkout([stage1.addressing])["added"]) == {"bar"}
assert dvc.checkout([stage1.addressing]) == empty_checkout | {
"added": ["bar"],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "bar").unlink()
assert set(dvc.checkout([stage2.addressing])["added"]) == {"ipsum"}
assert dvc.checkout([stage2.addressing]) == empty_checkout | {
"added": ["ipsum"],
"stats": empty_stats | {"added": 1},
}
(tmp_dir / "ipsum").unlink()
assert set(dvc.checkout()["added"]) == {"bar", "ipsum"}
assert dvc.checkout() == empty_checkout | {
"added": ["bar", "ipsum"],
"stats": empty_stats | {"added": 2},
}
def test_checkout_executable(tmp_dir, dvc):
@@ -618,7 +760,10 @@ def test_checkout_executable(tmp_dir, dvc):
contents["outs"][0]["isexec"] = True
(tmp_dir / "foo.dvc").dump(contents)
dvc.checkout("foo")
assert dvc.checkout("foo") == empty_checkout | {
"modified": ["foo"],
"stats": empty_stats | {"modified": 1},
}
isexec = os.stat("foo").st_mode & stat.S_IEXEC
if os.name == "nt":
@@ -634,13 +779,22 @@ def test_checkout_partial(tmp_dir, dvc):
data_dir = tmp_dir / "data"
shutil.rmtree(data_dir)
dvc.checkout(str(data_dir / "foo"))
assert dvc.checkout(str(data_dir / "foo")) == empty_checkout | {
"added": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert data_dir.read_text() == {"foo": "foo"}
dvc.checkout(str(data_dir / "sub_dir" / "baz"))
assert dvc.checkout(str(data_dir / "sub_dir" / "baz")) == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert data_dir.read_text() == {"foo": "foo", "sub_dir": {"baz": "baz"}}
dvc.checkout(str(data_dir / "bar"))
assert dvc.checkout(str(data_dir / "bar")) == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert data_dir.read_text() == {
"foo": "foo",
"bar": "bar",
@@ -664,36 +818,45 @@ def test_checkout_partial_unchanged(tmp_dir, dvc):
sub_dir_file = sub_dir / "baz"
# Nothing changed, nothing added/deleted/modified
stats = dvc.checkout(str(bar))
assert not any(stats.values())
assert dvc.checkout(str(bar)) == empty_checkout
# Irrelevant file changed, still nothing added/deleted/modified
foo.unlink()
stats = dvc.checkout(str(bar))
assert not any(stats.values())
assert dvc.checkout(str(bar)) == empty_checkout
# Relevant change, one modified
bar.unlink()
stats = dvc.checkout(str(bar))
assert len(stats["modified"]) == 1
assert stats == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
# No changes inside data/sub
stats = dvc.checkout(str(sub_dir))
assert not any(stats.values())
assert dvc.checkout(str(sub_dir)) == empty_checkout
# Relevant change, one modified
sub_dir_file.unlink()
stats = dvc.checkout(str(sub_dir))
assert stats == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert len(stats["modified"]) == 1
stats = dvc.checkout(str(data_dir / "empty_sub_dir"))
assert stats == {**empty_checkout, "modified": ["data" + os.sep]}
assert stats == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"deleted": 1},
}
dvc.checkout(str(data_dir))
assert dvc.checkout(str(data_dir)) == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
# Everything is in place, no action taken
stats = dvc.checkout(str(data_dir))
assert not any(stats.values())
assert dvc.checkout(str(data_dir)) == empty_checkout
def test_checkout_partial_subdir(tmp_dir, dvc):
@@ -704,14 +867,20 @@ def test_checkout_partial_subdir(tmp_dir, dvc):
sub_dir_bar = sub_dir / "baz"
shutil.rmtree(sub_dir)
dvc.checkout(str(sub_dir))
assert dvc.checkout(str(sub_dir)) == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"added": 2},
}
assert data_dir.read_text() == {
"foo": "foo",
"sub_dir": {"bar": "bar", "baz": "baz"},
}
sub_dir_bar.unlink()
dvc.checkout(str(sub_dir_bar))
assert dvc.checkout(str(sub_dir_bar)) == empty_checkout | {
"modified": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert data_dir.read_text() == {
"foo": "foo",
"sub_dir": {"bar": "bar", "baz": "baz"},
@@ -721,12 +890,14 @@ def test_checkout_partial_subdir(tmp_dir, dvc):
def test_checkout_file(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
stats = dvc.checkout("foo")
assert not any(stats.values())
assert dvc.checkout("foo") == empty_checkout
os.unlink("foo")
stats = dvc.checkout("foo")
assert stats["added"] == ["foo"]
assert stats == empty_checkout | {
"added": ["foo"],
"stats": empty_stats | {"added": 1},
}
def test_checkout_dir_compat(tmp_dir, dvc):
@@ -743,7 +914,10 @@ def test_checkout_dir_compat(tmp_dir, dvc):
),
)
remove("data")
dvc.checkout()
assert dvc.checkout() == empty_checkout | {
"added": ["data" + os.sep],
"stats": empty_stats | {"added": 1},
}
assert (tmp_dir / "data").read_text() == {"foo": "foo"}
@@ -751,7 +925,10 @@ def test_checkout_cleanup_properly_on_untracked_nested_directories(tmp_dir, scm,
tmp_dir.dvc_gen({"datasets": {"dir1": {"file1": "file1"}}})
tmp_dir.gen({"datasets": {"dir2": {"dir3": {"file2": "file2"}}}})
dvc.checkout(force=True)
assert dvc.checkout(force=True) == empty_checkout | {
"modified": ["datasets" + os.sep],
"stats": empty_stats | {"deleted": 3},
}
assert (tmp_dir / "datasets").read_text() == {"dir1": {"file1": "file1"}}
@@ -766,7 +943,10 @@ def test_checkout_loads_specific_file(tmp_dir, dvc, mocker):
f = SingleStageFile(dvc, "foo.dvc")
spy = mocker.spy(FileMixin, "_load")
assert dvc.checkout("foo.dvc") == {"added": ["foo"], "deleted": [], "modified": []}
assert dvc.checkout("foo.dvc") == empty_checkout | {
"added": ["foo"],
"stats": empty_stats | {"added": 1},
}
spy.assert_called_with(f)
assert (tmp_dir / "foo").exists()
+128 -52
View File
@@ -17,9 +17,13 @@ from dvc.utils.fs import remove
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.db.local import LocalHashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from tests.func.test_checkout import empty_checkout, empty_stats
empty_stats = empty_stats | {"fetched": 0}
empty_pull = empty_checkout | {"stats": empty_stats}
def test_cloud_cli(tmp_dir, dvc, remote, mocker): # noqa: PLR0915
def test_cloud_cli(tmp_dir, dvc, capsys, remote, mocker): # noqa: PLR0915
jobs = 2
args = ["-v", "-j", str(jobs)]
@@ -42,6 +46,7 @@ def test_cloud_cli(tmp_dir, dvc, remote, mocker): # noqa: PLR0915
oids_exist = mocker.spy(LocalHashFileDB, "oids_exist")
assert main(["push", *args]) == 0
assert capsys.readouterr().out == "5 files pushed\n"
assert os.path.exists(cache)
assert os.path.isfile(cache)
assert os.path.isfile(cache_dir)
@@ -54,6 +59,7 @@ def test_cloud_cli(tmp_dir, dvc, remote, mocker): # noqa: PLR0915
oids_exist.reset_mock()
assert main(["fetch", *args]) == 0
assert capsys.readouterr().out == "5 files fetched\n"
assert os.path.exists(cache)
assert os.path.isfile(cache)
assert os.path.isfile(cache_dir)
@@ -65,6 +71,7 @@ def test_cloud_cli(tmp_dir, dvc, remote, mocker): # noqa: PLR0915
oids_exist.reset_mock()
assert main(["pull", *args]) == 0
assert capsys.readouterr().out == "Everything is up to date.\n"
assert os.path.exists(cache)
assert os.path.isfile(cache)
assert os.path.isfile(cache_dir)
@@ -93,6 +100,7 @@ def test_cloud_cli(tmp_dir, dvc, remote, mocker): # noqa: PLR0915
shutil.move(dvc.cache.local.path, dvc.cache.local.path + ".back")
assert main(["fetch", *args]) == 0
assert capsys.readouterr().out == "5 files fetched\n"
assert oids_exist.called
assert all(
@@ -101,6 +109,7 @@ def test_cloud_cli(tmp_dir, dvc, remote, mocker): # noqa: PLR0915
oids_exist.reset_mock()
assert main(["pull", "-f", *args]) == 0
assert capsys.readouterr().out == "Everything is up to date.\n"
assert os.path.exists(cache)
assert os.path.isfile(cache)
assert os.path.isfile(cache_dir)
@@ -195,7 +204,11 @@ def test_verify_hashes(tmp_dir, scm, dvc, mocker, tmp_path_factory, local_remote
hash_spy = mocker.spy(dvc_data.hashfile.hash, "file_md5")
dvc.pull()
assert dvc.pull() == empty_pull | {
"added": ["dir" + os.sep, "file"],
"stats": empty_stats | {"fetched": 3, "added": 2},
}
# NOTE: 2 are for index.data_tree building
assert hash_spy.call_count == 3
@@ -205,11 +218,11 @@ def test_verify_hashes(tmp_dir, scm, dvc, mocker, tmp_path_factory, local_remote
with dvc.config.edit() as conf:
conf["remote"]["upstream"]["verify"] = True
dvc.pull()
assert dvc.pull() == empty_pull | {"stats": empty_stats | {"fetched": 3}}
assert hash_spy.call_count == 10
@pytest.mark.flaky(reruns=3)
# @pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize("erepo_type", ["git_dir", "erepo_dir"])
def test_pull_git_imports(request, tmp_dir, dvc, scm, erepo_type):
erepo = request.getfixturevalue(erepo_type)
@@ -220,7 +233,7 @@ def test_pull_git_imports(request, tmp_dir, dvc, scm, erepo_type):
dvc.imp(os.fspath(erepo), "foo")
dvc.imp(os.fspath(erepo), "dir", out="new_dir", rev="HEAD~")
assert dvc.pull()["fetched"] == 0
assert dvc.pull() == empty_pull
for item in ["foo", "new_dir"]:
remove(item)
@@ -228,7 +241,10 @@ def test_pull_git_imports(request, tmp_dir, dvc, scm, erepo_type):
os.makedirs(dvc.cache.local.path, exist_ok=True)
clean_repos()
assert dvc.pull(force=True)["fetched"] == 2
assert dvc.pull(force=True) == empty_pull | {
"added": ["new_dir" + os.sep, "foo"],
"stats": empty_stats | {"fetched": 2, "added": 2},
}
assert (tmp_dir / "foo").exists()
assert (tmp_dir / "foo").read_text() == "foo"
@@ -248,11 +264,14 @@ def test_pull_external_dvc_imports(tmp_dir, dvc, scm, erepo_dir):
dvc.imp(os.fspath(erepo_dir), "foo")
dvc.imp(os.fspath(erepo_dir), "dir", out="new_dir", rev="HEAD~")
assert dvc.pull()["fetched"] == 0
assert dvc.pull() == empty_pull
clean(["foo", "new_dir"], dvc)
assert dvc.pull(force=True)["fetched"] == 2
assert dvc.pull(force=True) == empty_pull | {
"added": ["new_dir" + os.sep, "foo"],
"stats": empty_stats | {"fetched": 2, "added": 2},
}
assert (tmp_dir / "foo").exists()
assert (tmp_dir / "foo").read_text() == "foo"
@@ -267,7 +286,10 @@ def test_pull_partial_import(tmp_dir, dvc, local_workspace):
stage = dvc.imp_url("remote://workspace/file", os.fspath(dst), no_download=True)
result = dvc.pull("file")
assert result["fetched"] == 1
assert result == empty_checkout | {
"added": ["file"],
"stats": empty_stats | {"fetched": 1, "added": 1},
}
assert dst.exists()
assert stage.outs[0].get_hash().value == "d10b4c3ff123b26dc068d43a8bef2d23"
@@ -279,8 +301,9 @@ def test_pull_partial_import_missing(tmp_dir, dvc, local_workspace):
dvc.imp_url("remote://workspace/file", os.fspath(dst), no_download=True)
(local_workspace / "file").unlink()
with pytest.raises(CheckoutError):
with pytest.raises(CheckoutError) as exc:
dvc.pull("file")
assert exc.value.result == empty_pull | {"failed": ["file"]}
assert not dst.exists()
@@ -290,8 +313,9 @@ def test_pull_partial_import_modified(tmp_dir, dvc, local_workspace):
dvc.imp_url("remote://workspace/file", os.fspath(dst), no_download=True)
local_workspace.gen("file", "updated file content")
with pytest.raises(CheckoutError):
with pytest.raises(CheckoutError) as exc:
dvc.pull("file")
assert exc.value.result == empty_pull | {"failed": ["file"]}
assert not dst.exists()
@@ -309,7 +333,10 @@ def test_pull_external_dvc_imports_mixed(tmp_dir, dvc, scm, erepo_dir, local_rem
clean(["foo", "bar"], dvc)
assert dvc.pull()["fetched"] == 2
assert dvc.pull() == empty_pull | {
"added": ["bar", "foo"],
"stats": empty_stats | {"fetched": 2, "added": 2},
}
assert (tmp_dir / "foo").read_text() == "foo"
assert (tmp_dir / "bar").read_text() == "bar"
@@ -330,34 +357,44 @@ def recurse_list_dir(d):
def test_dvc_pull_pipeline_stages(tmp_dir, dvc, run_copy, local_remote):
(stage0,) = tmp_dir.dvc_gen("foo", "foo")
stage1 = run_copy("foo", "bar", name="copy-foo-bar")
stage2 = run_copy("bar", "foobar", name="copy-bar-foobar")
(stage0,) = tmp_dir.dvc_gen("bar", "bar")
stage1 = run_copy("bar", "foo", name="copy-bar-foo")
stage2 = run_copy("foo", "foobar", name="copy-foo-foobar")
dvc.push()
outs = ["foo", "bar", "foobar"]
outs = ["bar", "foo", "foobar"]
clean(outs, dvc)
dvc.pull()
assert dvc.pull() == empty_pull | {
"added": outs,
"stats": empty_stats | {"fetched": 1, "added": 3},
}
assert all((tmp_dir / file).exists() for file in outs)
for out, stage in zip(outs, [stage0, stage1, stage2]):
for target in [stage.addressing, out]:
clean(outs, dvc)
stats = dvc.pull([target])
assert stats["fetched"] == 1
assert stats["added"] == [out]
assert stats == empty_pull | {
"added": [out],
"stats": empty_stats | {"fetched": 1, "added": 1},
}
assert os.path.exists(out)
assert not any(os.path.exists(out) for out in set(outs) - {out})
clean(outs, dvc)
stats = dvc.pull([stage2.addressing], with_deps=True)
assert len(stats["added"]) == 3
assert set(stats["added"]) == set(outs)
assert stats == empty_pull | {
"added": outs,
"stats": empty_stats | {"fetched": 1, "added": 3},
}
clean(outs, dvc)
stats = dvc.pull([os.curdir], recursive=True)
assert set(stats["added"]) == set(outs)
assert stats == empty_pull | {
"added": outs,
"stats": empty_stats | {"fetched": 1, "added": 3},
}
def test_pipeline_file_target_ops(tmp_dir, dvc, run_copy, local_remote):
@@ -374,7 +411,7 @@ def test_pipeline_file_target_ops(tmp_dir, dvc, run_copy, local_remote):
remove(dvc.stage_cache.cache_dir)
dvc.push()
assert dvc.push() == 3
outs = ["foo", "lorem", "ipsum", "baz", "lorem2"]
@@ -382,20 +419,26 @@ def test_pipeline_file_target_ops(tmp_dir, dvc, run_copy, local_remote):
assert len(recurse_list_dir(path)) == 3
clean(outs, dvc)
assert set(dvc.pull(["dvc.yaml"])["added"]) == {"lorem2", "baz"}
assert dvc.pull(["dvc.yaml"]) == empty_pull | {
"added": ["baz", "lorem2"],
"stats": empty_stats | {"fetched": 2, "added": 2},
}
clean(outs, dvc)
assert set(dvc.pull()["added"]) == set(outs)
assert dvc.pull() == empty_pull | {
"added": ["baz", "foo", "ipsum", "lorem", "lorem2"],
"stats": empty_stats | {"fetched": 3, "added": 5},
}
# clean everything in remote and push
from dvc.testing.tmp_dir import TmpDir
clean(TmpDir(path).iterdir())
dvc.push(["dvc.yaml:copy-ipsum-baz"])
assert dvc.push(["dvc.yaml:copy-ipsum-baz"]) == 1
assert len(recurse_list_dir(path)) == 1
clean(TmpDir(path).iterdir())
dvc.push(["dvc.yaml"])
assert dvc.push(["dvc.yaml"]) == 2
assert len(recurse_list_dir(path)) == 2
with pytest.raises(StageNotFound):
@@ -440,23 +483,36 @@ def test_fetch_stats(tmp_dir, dvc, fs, msg, capsys, local_remote):
def test_pull_stats(tmp_dir, dvc, capsys, local_remote):
tmp_dir.dvc_gen({"foo": "foo", "bar": "bar"})
tmp_dir.dvc_gen(
{
"foo": "foo",
"bar": "bar",
"lorem": "lorem",
"dir": {"file": "file"},
"ipsum": "ipsum",
"dolor": "dolor",
}
)
dvc.push()
clean(["foo", "bar"], dvc)
clean(["foo", "bar", "dir", "lorem"], dvc)
(tmp_dir / "ipsum.dvc").unlink()
(tmp_dir / "bar").write_text("foobar")
assert main(["pull", "--force"]) == 0
out, _ = capsys.readouterr()
assert "M\tbar".expandtabs() in out
assert "A\tfoo".expandtabs() in out
assert "2 files fetched" in out
assert "1 file added" in out
assert "1 file modified" in out
assert out.splitlines() == [
"M\tbar".expandtabs(),
"A\tdir".expandtabs() + os.sep,
"A\tfoo".expandtabs(),
"A\tlorem".expandtabs(),
"D\tipsum".expandtabs(),
"6 files fetched, 1 file modified, 3 files added and 1 file deleted",
]
main(["pull"])
out, _ = capsys.readouterr()
assert "Everything is up to date." in out
assert out == "Everything is up to date.\n"
@pytest.mark.parametrize(
@@ -475,26 +531,32 @@ def test_push_pull_all(tmp_dir, scm, dvc, local_remote, key, expected):
assert dvc.push(**{key: True}) == expected
clean(["foo", "bar", "baz"], dvc)
assert dvc.pull(**{key: True})["fetched"] == expected
assert dvc.pull(**{key: True}) == empty_pull | {
"added": ["bar", "foo"],
"stats": empty_stats | {"fetched": expected, "added": 2},
}
def test_push_pull_fetch_pipeline_stages(tmp_dir, dvc, run_copy, local_remote):
tmp_dir.dvc_gen("foo", "foo")
run_copy("foo", "bar", name="copy-foo-bar")
dvc.push("copy-foo-bar")
assert dvc.push("copy-foo-bar") == 1
assert len(recurse_list_dir(local_remote.url)) == 1
# pushing everything so as we can check pull/fetch only downloads
# from specified targets
dvc.push()
assert dvc.push() == 0
clean(["foo", "bar"], dvc)
dvc.pull("copy-foo-bar")
assert dvc.pull("copy-foo-bar") == empty_pull | {
"added": ["bar"],
"stats": empty_stats | {"fetched": 1, "added": 1},
}
assert (tmp_dir / "bar").exists()
assert len(recurse_list_dir(dvc.cache.local.path)) == 1
clean(["bar"], dvc)
dvc.fetch("copy-foo-bar")
assert dvc.fetch("copy-foo-bar") == 1
assert len(recurse_list_dir(dvc.cache.local.path)) == 1
@@ -505,7 +567,10 @@ def test_pull_partial(tmp_dir, dvc, local_remote):
clean(["foo"], dvc)
stats = dvc.pull(os.path.join("foo", "bar"))
assert stats["fetched"] == 2
assert stats == empty_pull | {
"added": [os.path.join("foo", "")],
"stats": empty_stats | {"fetched": 2, "added": 1},
}
assert (tmp_dir / "foo").read_text() == {"bar": {"baz": "baz"}}
@@ -540,7 +605,10 @@ def test_output_remote(tmp_dir, dvc, make_remote):
clean(["foo", "bar", "data"], dvc)
dvc.pull()
assert dvc.pull() == empty_pull | {
"added": ["data" + os.sep, "bar", "foo"],
"stats": empty_stats | {"fetched": 5, "added": 4},
}
assert set(dvc.cache.local.all()) == {
"37b51d194a7513e45b56f6524f2d51f2",
@@ -573,7 +641,10 @@ def test_target_remote(tmp_dir, dvc, make_remote):
clean(["foo", "data"], dvc)
dvc.pull(remote="myremote")
assert dvc.pull(remote="myremote") == empty_pull | {
"added": ["data" + os.sep, "foo"],
"stats": empty_stats | {"fetched": 4, "added": 3},
}
assert set(dvc.cache.local.all()) == {
"acbd18db4cc2f85cedef654fccc4a4d8",
@@ -630,7 +701,10 @@ def test_output_target_remote(tmp_dir, dvc, make_remote):
clean(["foo", "bar", "data"], dvc)
# pull foo and data from for_foo remote
dvc.pull(remote="for_foo", allow_missing=True)
assert dvc.pull(remote="for_foo", allow_missing=True) == empty_pull | {
"added": ["data" + os.sep, "foo"],
"stats": empty_stats | {"fetched": 4, "added": 3},
}
assert set(dvc.cache.local.all()) == expected
@@ -638,15 +712,19 @@ def test_output_target_remote(tmp_dir, dvc, make_remote):
def test_pull_allow_missing(tmp_dir, dvc, local_remote):
dvc.stage.add(name="bar", outs=["bar"], cmd="echo bar > bar")
with pytest.raises(CheckoutError):
with pytest.raises(CheckoutError) as exc:
dvc.pull()
assert exc.value.result == empty_pull | {"failed": ["bar"]}
tmp_dir.dvc_gen("foo", "foo")
dvc.push()
clean(["foo"], dvc)
stats = dvc.pull(allow_missing=True)
assert stats["fetched"] == 1
assert stats == empty_pull | {
"added": ["foo"],
"stats": empty_stats | {"fetched": 1, "added": 1},
}
def test_pull_granular_excluding_import_that_cannot_be_pulled(
@@ -668,11 +746,9 @@ def test_pull_granular_excluding_import_that_cannot_be_pulled(
shutil.rmtree("dir")
dvc.cache.local.clear()
assert dvc.pull(stage.addressing) == {
assert dvc.pull(stage.addressing) == empty_pull | {
"added": [join("dir", "")],
"deleted": [],
"modified": [],
"fetched": 3,
"stats": empty_stats | {"added": 2, "fetched": 3},
}
with pytest.raises(CloneError, match="SCM error"):
@@ -704,8 +780,8 @@ def test_loads_single_file(tmp_dir, dvc, local_remote, mocker):
assert dvc.pull("foo.dvc") == {
"added": ["foo"],
"deleted": [],
"fetched": 1,
"modified": [],
"stats": {"added": 1, "deleted": 0, "modified": 0, "fetched": 1},
}
spy.assert_called_with(foo_dvcfile)
assert (tmp_dir / "foo").exists()
+6 -1
View File
@@ -20,7 +20,12 @@ def test_push_pull(tmp_dir, dvc, erepo_dir, run_copy, local_remote):
erepo_dir.add_remote(config=local_remote.config)
with erepo_dir.chdir():
assert not os.path.exists(erepo_dir.dvc.stage_cache.cache_dir)
assert erepo_dir.dvc.pull(run_cache=True)["fetched"] == 0
assert erepo_dir.dvc.pull(run_cache=True) == {
"added": [],
"deleted": [],
"modified": [],
"stats": {"fetched": 0, "added": 0, "deleted": 0, "modified": 0},
}
assert os.listdir(erepo_dir.dvc.stage_cache.cache_dir)
+7 -1
View File
@@ -1,3 +1,4 @@
import os
import shutil
from os.path import join
@@ -180,7 +181,12 @@ def test_partial_checkout_and_update(M, tmp_dir, dvc, remote):
dvc.cache.local.clear()
shutil.rmtree("dir")
assert dvc.pull("dir/subdir") == M.dict(added=[join("dir", "")], fetched=11)
assert dvc.pull("dir/subdir") == {
"added": ["dir" + os.sep],
"deleted": [],
"modified": [],
"stats": {"added": 10, "deleted": 0, "fetched": 11, "modified": 0},
}
assert (tmp_dir / "dir").read_text() == {"subdir": dir1}
tmp_dir.gen({"dir": {"subdir": {"file": "file"}}})