From 4503a7f503460a686d80e282ebd8ddec3b86b953 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Sat, 13 Oct 2018 22:25:31 +0300 Subject: [PATCH] dvc: check if cache file inside dir was changed Fixes #1125 Signed-off-by: Ruslan Kuprieiev --- dvc/remote/local.py | 31 ++++++++++++++++++++++++++----- tests/test_checkout.py | 27 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/dvc/remote/local.py b/dvc/remote/local.py index b6c4aa19c..2effd5b2b 100644 --- a/dvc/remote/local.py +++ b/dvc/remote/local.py @@ -81,14 +81,35 @@ class RemoteLOCAL(RemoteBase): relpath = os.path.relpath(path, self.cache_dir) return os.path.dirname(relpath) + os.path.basename(relpath) - def changed_cache(self, md5): + def changed_cache_file(self, md5): cache = self.get(md5) if self.state.changed(cache, md5=md5): if os.path.exists(cache): - msg = 'Corrupted cache file {}' + msg = 'Corrupted cache file {}.' Logger.warn(msg.format(os.path.relpath(cache))) remove(cache) return True + return False + + def changed_cache(self, md5): + cache = self.get(md5) + clist = [(cache, md5)] + + while True: + if len(clist) == 0: + break + + cache, md5 = clist.pop() + if self.changed_cache_file(md5): + return True + + if not self.is_dir_cache(cache): + continue + + for entry in self.load_dir_cache(md5): + md5 = entry[self.PARAM_MD5] + cache = self.get(md5) + clist.append((cache, md5)) return False @@ -512,7 +533,7 @@ class RemoteLOCAL(RemoteBase): progress.update_target(title, 90, 100) - local_exists = [not self.changed_cache(md5) for md5 in md5s] + local_exists = [not self.changed_cache_file(md5) for md5 in md5s] progress.finish_target(title) @@ -539,7 +560,7 @@ class RemoteLOCAL(RemoteBase): names = [] # NOTE: filter files that are not corrupted for md5, name in grouped: - if self.changed_cache(md5): + if self.changed_cache_file(md5): md5s.append(md5) names.append(name) @@ -617,7 +638,7 @@ class RemoteLOCAL(RemoteBase): # NOTE: verifying that our cache is not corrupted def func(info): - return not self.changed_cache(info[self.PARAM_MD5]) + return not self.changed_cache_file(info[self.PARAM_MD5]) checksum_infos = list(filter(func, checksum_infos)) progress.update_target(title, 20, 100) diff --git a/tests/test_checkout.py b/tests/test_checkout.py index 1b15352c9..8df2c9d4d 100644 --- a/tests/test_checkout.py +++ b/tests/test_checkout.py @@ -6,6 +6,7 @@ import shutil import filecmp from dvc.main import main +from dvc.project import Project from tests.basic_env import TestDvc from tests.test_repro import TestRepro from dvc.stage import Stage @@ -65,6 +66,32 @@ class TestCheckoutCorruptedCacheFile(TestRepro): self.assertFalse(os.path.isfile(cache)) +class TestCheckoutCorruptedCacheDir(TestDvc): + def test(self): + # NOTE: using 'copy' so that cache and link don't have same inode + ret = main(['config', 'cache.type', 'copy']) + self.assertEqual(ret, 0) + + self.dvc = Project('.') + stages = self.dvc.add(self.DATA_DIR) + self.assertEqual(len(stages), 1) + self.assertEqual(len(stages[0].outs), 1) + out = stages[0].outs[0] + + # NOTE: modifying cache file for one of the files inside the directory + # to check if dvc will detect that the cache is corrupted. + entry = self.dvc.cache.local.load_dir_cache(out.md5)[0] + md5 = entry[self.dvc.cache.local.PARAM_MD5] + cache = self.dvc.cache.local.get(md5) + + with open(cache, 'w+') as fobj: + fobj.write('1') + + self.dvc.checkout() + + self.assertFalse(os.path.exists(cache)) + + class TestCmdCheckout(TestCheckout): def test(self): ret = main(['checkout'])