efiop: Windows is failing, will check it out tomorrow.
shcheklein: Please, update docs if needed after this change.
diff --git a/dvc/command/unprotect.py b/dvc/command/unprotect.py
index eb6dc8876..e9c5df81a 100644
--- a/dvc/command/unprotect.py
+++ b/dvc/command/unprotect.py
@@ -22,7 +22,10 @@ class CmdUnprotect(CmdBase):
 
 
 def add_parser(subparsers, parent_parser):
-    UNPROTECT_HELP = "Unprotect data files or directories."
+    UNPROTECT_HELP = (
+        "Unprotect tracked files or directories (when hardlinks or symlinks "
+        "have been enabled with `dvc config cache.type`)"
+    )
     unprotect_parser = subparsers.add_parser(
         "unprotect",
         parents=[parent_parser],
diff --git a/dvc/config.py b/dvc/config.py
index 0907c66f3..34addc400 100644
--- a/dvc/config.py
+++ b/dvc/config.py
@@ -97,7 +97,7 @@ REMOTE_COMMON = {
 }
 LOCAL_COMMON = {
     "type": supported_cache_type,
-    Optional("protected", default=False): Bool,
+    Optional("protected", default=False): Bool,  # obsoleted
     "shared": All(Lower, Choices("group")),
     Optional("slow_link_warning", default=True): Bool,
 }
diff --git a/dvc/remote/base.py b/dvc/remote/base.py
index c12f5df91..8ff3046cb 100644
--- a/dvc/remote/base.py
+++ b/dvc/remote/base.py
@@ -83,6 +83,9 @@ class RemoteBASE(object):
     DEFAULT_NO_TRAVERSE = True
     DEFAULT_VERIFY = False
 
+    CACHE_MODE = None
+    SHARED_MODE_MAP = {None: (None, None), "group": (None, None)}
+
     state = StateNoop()
 
     def __init__(self, repo, config):
@@ -90,12 +93,14 @@ class RemoteBASE(object):
 
         self._check_requires(config)
 
+        shared = config.get("shared")
+        self._file_mode, self._dir_mode = self.SHARED_MODE_MAP[shared]
+
         self.checksum_jobs = (
             config.get("checksum_jobs")
             or (self.repo and self.repo.config["core"].get("checksum_jobs"))
             or self.CHECKSUM_JOBS
         )
-        self.protected = False
         self.no_traverse = config.get("no_traverse", self.DEFAULT_NO_TRAVERSE)
         self.verify = config.get("verify", self.DEFAULT_VERIFY)
         self._dir_info = {}
@@ -221,7 +226,7 @@ class RemoteBASE(object):
         new_info = self.cache.checksum_to_path_info(checksum)
         if self.cache.changed_cache_file(checksum):
             self.cache.makedirs(new_info.parent)
-            self.cache.move(tmp_info, new_info)
+            self.cache.move(tmp_info, new_info, mode=self.CACHE_MODE)
 
         self.state.save(path_info, checksum)
         self.state.save(new_info, checksum)
@@ -409,15 +414,8 @@ class RemoteBASE(object):
 
         link_method(from_info, to_info)
 
-        if self.protected:
-            self.protect(to_info)
-
         logger.debug(
-            "Created %s'%s': %s -> %s",
-            "protected " if self.protected else "",
-            self.cache_types[0],
-            from_info,
-            to_info,
+            "Created '%s': %s -> %s", self.cache_types[0], from_info, to_info,
         )
 
     def _save_file(self, path_info, checksum, save_link=True):
@@ -425,14 +423,11 @@ class RemoteBASE(object):
 
         cache_info = self.checksum_to_path_info(checksum)
         if self.changed_cache(checksum):
-            self.move(path_info, cache_info)
+            self.move(path_info, cache_info, mode=self.CACHE_MODE)
             self.link(cache_info, path_info)
         elif self.iscopy(path_info) and self._cache_is_copy(path_info):
             # Default relink procedure involves unneeded copy
-            if self.protected:
-                self.protect(path_info)
-            else:
-                self.unprotect(path_info)
+            self.unprotect(path_info)
         else:
             self.remove(path_info)
             self.link(cache_info, path_info)
@@ -656,7 +651,8 @@ class RemoteBASE(object):
     def remove(self, path_info):
         raise RemoteActionNotImplemented("remove", self.scheme)
 
-    def move(self, from_info, to_info):
+    def move(self, from_info, to_info, mode=None):
+        assert mode is None
         self.copy(from_info, to_info)
         self.remove(from_info)
 
@@ -718,6 +714,9 @@ class RemoteBASE(object):
             removed = True
         return removed
 
+    def is_protected(self, path_info):
+        return False
+
     def changed_cache_file(self, checksum):
         """Compare the given checksum with the (corresponding) actual one.
 
@@ -730,7 +729,14 @@ class RemoteBASE(object):
 
         - Remove the file from cache if it doesn't match the actual checksum
         """
+
         cache_info = self.checksum_to_path_info(checksum)
+        if self.is_protected(cache_info):
+            logger.debug(
+                "Assuming '%s' is unchanged since it is read-only", cache_info
+            )
+            return False
+
         actual = self.get_checksum(cache_info)
 
         logger.debug(
@@ -744,6 +750,9 @@ class RemoteBASE(object):
             return True
 
         if actual.split(".")[0] == checksum.split(".")[0]:
+            # making cache file read-only so we don't need to check it
+            # next time
+            self.protect(cache_info)
             return False
 
         if self.exists(cache_info):
diff --git a/dvc/remote/local.py b/dvc/remote/local.py
index 3e62e0628..d1470620e 100644
--- a/dvc/remote/local.py
+++ b/dvc/remote/local.py
@@ -32,19 +32,11 @@ class RemoteLOCAL(RemoteBASE):
 
     DEFAULT_CACHE_TYPES = ["reflink", "copy"]
 
+    CACHE_MODE = 0o444
     SHARED_MODE_MAP = {None: (0o644, 0o755), "group": (0o664, 0o775)}
 
     def __init__(self, repo, config):
         super().__init__(repo, config)
-        self.protected = config.get("protected", False)
-
-        shared = config.get("shared")
-        self._file_mode, self._dir_mode = self.SHARED_MODE_MAP[shared]
-
-        if self.protected:
-            # cache files are set to be read-only for everyone
-            self._file_mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
-
         self.cache_dir = config.get("url")
         self._dir_info = {}
 
@@ -142,16 +134,17 @@ class RemoteLOCAL(RemoteBASE):
         if self.exists(path_info):
             remove(path_info.fspath)
 
-    def move(self, from_info, to_info):
+    def move(self, from_info, to_info, mode=None):
         if from_info.scheme != "local" or to_info.scheme != "local":
             raise NotImplementedError
 
         self.makedirs(to_info.parent)
 
-        if self.isfile(from_info):
-            mode = self._file_mode
-        else:
-            mode = self._dir_mode
+        if mode is None:
+            if self.isfile(from_info):
+                mode = self._file_mode
+            else:
+                mode = self._dir_mode
 
         move(from_info, to_info, mode=mode)
 
@@ -159,6 +152,7 @@ class RemoteLOCAL(RemoteBASE):
         tmp_info = to_info.parent / tmp_fname(to_info.name)
         try:
             System.copy(from_info, tmp_info)
+            os.chmod(fspath_py35(tmp_info), self._file_mode)
             os.rename(fspath_py35(tmp_info), fspath_py35(to_info))
         except Exception:
             self.remove(tmp_info)
@@ -202,9 +196,13 @@ class RemoteLOCAL(RemoteBASE):
     def is_hardlink(path_info):
         return System.is_hardlink(path_info)
 
-    @staticmethod
-    def reflink(from_info, to_info):
-        System.reflink(from_info, to_info)
+    def reflink(self, from_info, to_info):
+        tmp_info = to_info.parent / tmp_fname(to_info.name)
+        System.reflink(from_info, tmp_info)
+        # NOTE: reflink has its own separate inode, so you can set permissions
+        # that are different from the source.
+        os.chmod(fspath_py35(tmp_info), self._file_mode)
+        os.rename(fspath_py35(tmp_info), fspath_py35(to_info))
 
     def cache_exists(self, checksums, jobs=None, name=None):
         return [
@@ -402,8 +400,7 @@ class RemoteLOCAL(RemoteBASE):
             )
             logger.warning(msg)
 
-    @staticmethod
-    def _unprotect_file(path):
+    def _unprotect_file(self, path):
         if System.is_symlink(path) or System.is_hardlink(path):
             logger.debug("Unprotecting '{}'".format(path))
             tmp = os.path.join(os.path.dirname(path), "." + uuid())
@@ -423,13 +420,13 @@ class RemoteLOCAL(RemoteBASE):
                 "a symlink or a hardlink.".format(path)
             )
 
-        os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
+        os.chmod(path, self._file_mode)
 
     def _unprotect_dir(self, path):
         assert is_working_tree(self.repo.tree)
 
         for fname in self.repo.tree.walk_files(path):
-            RemoteLOCAL._unprotect_file(fname)
+            self._unprotect_file(fname)
 
     def unprotect(self, path_info):
         path = path_info.fspath
@@ -441,12 +438,11 @@ class RemoteLOCAL(RemoteBASE):
         if os.path.isdir(path):
             self._unprotect_dir(path)
         else:
-            RemoteLOCAL._unprotect_file(path)
+            self._unprotect_file(path)
 
-    @staticmethod
-    def protect(path_info):
+    def protect(self, path_info):
         path = fspath_py35(path_info)
-        mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
+        mode = self.CACHE_MODE
 
         try:
             os.chmod(path, mode)
@@ -519,3 +515,11 @@ class RemoteLOCAL(RemoteBASE):
             if self.is_dir_checksum(c):
                 unpacked.add(c + self.UNPACKED_DIR_SUFFIX)
         return unpacked
+
+    def is_protected(self, path_info):
+        if not self.exists(path_info):
+            return False
+
+        mode = os.stat(fspath_py35(path_info)).st_mode
+
+        return stat.S_IMODE(mode) == self.CACHE_MODE
diff --git a/dvc/remote/ssh/__init__.py b/dvc/remote/ssh/__init__.py
index a3773871f..09c23bc6b 100644
--- a/dvc/remote/ssh/__init__.py
+++ b/dvc/remote/ssh/__init__.py
@@ -209,7 +209,8 @@ class RemoteSSH(RemoteBASE):
         with self.ssh(path_info) as ssh:
             ssh.remove(path_info.path)
 
-    def move(self, from_info, to_info):
+    def move(self, from_info, to_info, mode=None):
+        assert mode is None
         if from_info.scheme != self.scheme or to_info.scheme != self.scheme:
             raise NotImplementedError
 

Pull extrememly slow on ~400GB of data with hot DVC cache
OS: Docker image based off of `tensorflow/tensorflow:latest-gpu-py3`, with Python 3.7.5 as the system Python.

Initial setup:
```
# python --version
Python 3.7.5
# python -m venv .profiling_venv
# source .profiling_venv/bin/activate
# pip install dvc yappi
...
# dvc --version
0.82.6
```

DVC cache configuration:
```
# dvc config cache.dir
ERROR: configuration error - config file error: section 'cache' doesn't exist
# dvc config --local cache.dir
ERROR: configuration error - config file error: section 'cache' doesn't exist
# dvc config --global cache.dir
ERROR: configuration error - config file error: section 'cache' doesn't exist
# dvc config --system cache.dir
/ssd/.../dvc/cache
# cat /etc/xdg/dvc/config
[cache]
dir = /ssd/.../dvc/cache
protected = true
type = "hardlink,symlink"
```
Please note that the DVC cache is hot. In other words, most if not all files for `dvc fetch` are present and up-to-date at `/ssd/...`.

Make a fresh clone and profile `dvc pull`:
```
# git clone ssh://... repo
...
# cd repo
# yappi -f callgrind -o dvc_pull.prof -s $(which dvc) pull
...
```
**This `dvc pull`, uninstrumented, usually takes 40+ minutes with a hot DVC cache.**

Count the number of DVC-tracked files (symlinks, see the above config) and the total size of the repo:
```
# find . -type l | wc -l
29003
# du -shL .
403G    .
```

Looking at the `dvc_pull.prof` (in KCachegrind) suggests that the bottleneck is the checksum process. The `file_md5` and `dos2unix` functions in `utils/__init__.py` appear particularly costly.

Is this a known issue? Would the primary authors of DVC entertain a more performant version of `file_md5` (perhaps written in C/C++ and without TQDM integration)?

[dvc_pull_prof.zip](https://github.com/iterative/dvc/files/4135825/dvc_pull_prof.zip)

![Screenshot_2020-01-30_14-02-20](https://user-images.githubusercontent.com/1828432/73481187-3401ad00-4393-11ea-8041-cb051c713be9.png)

diff --git a/tests/func/remote/test_local.py b/tests/func/remote/test_local.py
index cc0f7ded1..48c89c354 100644
--- a/tests/func/remote/test_local.py
+++ b/tests/func/remote/test_local.py
@@ -57,6 +57,7 @@ def test_dir_cache_changed_on_single_cache_file_modification(tmp_dir, dvc):
     assert os.path.exists(unpacked_dir)
 
     cache_file_path = dvc.cache.local.get(file_md5)
+    os.chmod(cache_file_path, 0o644)
     with open(cache_file_path, "a") as fobj:
         fobj.write("modification")
 
diff --git a/tests/func/test_add.py b/tests/func/test_add.py
index 9d966452d..6a23ee832 100644
--- a/tests/func/test_add.py
+++ b/tests/func/test_add.py
@@ -489,12 +489,12 @@ class TestAddUnprotected(TestDvc):
         ret = main(["config", "cache.type", "hardlink"])
         self.assertEqual(ret, 0)
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["add", self.FOO])
         self.assertEqual(ret, 0)
 
+        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertTrue(System.is_hardlink(self.FOO))
+
         ret = main(["unprotect", self.FOO])
         self.assertEqual(ret, 0)
 
@@ -561,7 +561,6 @@ def test_readding_dir_should_not_unprotect_all(tmp_dir, dvc, mocker):
     tmp_dir.gen("dir/data", "data")
 
     dvc.cache.local.cache_types = ["symlink"]
-    dvc.cache.local.protected = True
 
     dvc.add("dir")
     tmp_dir.gen("dir/new_file", "new_file_content")
@@ -618,7 +617,6 @@ def test_should_relink_on_repeated_add(
 @pytest.mark.parametrize("link", ["hardlink", "symlink", "copy"])
 def test_should_protect_on_repeated_add(link, tmp_dir, dvc):
     dvc.cache.local.cache_types = [link]
-    dvc.cache.local.protected = True
 
     tmp_dir.dvc_gen({"foo": "foo"})
 
@@ -626,7 +624,16 @@ def test_should_protect_on_repeated_add(link, tmp_dir, dvc):
 
     dvc.add("foo")
 
-    assert not os.access("foo", os.W_OK)
+    assert not os.access(
+        os.path.join(".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"),
+        os.W_OK,
+    )
+
+    # NOTE: Windows symlink perms don't propagate to the target
+    if link == "copy" or (link == "symlink" and os.name == "nt"):
+        assert os.access("foo", os.W_OK)
+    else:
+        assert not os.access("foo", os.W_OK)
 
 
 def test_escape_gitignore_entries(tmp_dir, scm, dvc):
diff --git a/tests/func/test_api.py b/tests/func/test_api.py
index 9a62edfff..00b0500ca 100644
--- a/tests/func/test_api.py
+++ b/tests/func/test_api.py
@@ -1,5 +1,4 @@
 import os
-import shutil
 
 import pytest
 
@@ -9,6 +8,8 @@ from dvc.compat import fspath
 from dvc.exceptions import FileMissingError
 from dvc.main import main
 from dvc.path_info import URLInfo
+from dvc.utils.fs import remove
+
 from tests.remotes import Azure, GCP, HDFS, Local, OSS, S3, SSH
 
 
@@ -65,7 +66,7 @@ def test_open(remote_url, tmp_dir, dvc):
     run_dvc("push")
 
     # Remove cache to force download
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     with api.open("foo") as fd:
         assert fd.read() == "foo-text"
@@ -85,7 +86,7 @@ def test_open_external(remote_url, erepo_dir):
     erepo_dir.dvc.push(all_branches=True)
 
     # Remove cache to force download
-    shutil.rmtree(erepo_dir.dvc.cache.local.cache_dir)
+    remove(erepo_dir.dvc.cache.local.cache_dir)
 
     # Using file url to force clone to tmp repo
     repo_url = "file://{}".format(erepo_dir)
@@ -101,7 +102,7 @@ def test_missing(remote_url, tmp_dir, dvc):
     run_dvc("remote", "add", "-d", "upstream", remote_url)
 
     # Remove cache to make foo missing
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     with pytest.raises(FileMissingError):
         api.read("foo")
diff --git a/tests/func/test_cache.py b/tests/func/test_cache.py
index 147840ee4..8d692938d 100644
--- a/tests/func/test_cache.py
+++ b/tests/func/test_cache.py
@@ -184,12 +184,12 @@ def test_default_cache_type(dvc):
 
 @pytest.mark.skipif(os.name == "nt", reason="Not supported for Windows.")
 @pytest.mark.parametrize(
-    "protected,dir_mode,file_mode",
-    [(False, 0o775, 0o664), (True, 0o775, 0o444)],
+    "group, dir_mode", [(False, 0o755), (True, 0o775)],
 )
-def test_shared_cache(tmp_dir, dvc, protected, dir_mode, file_mode):
-    with dvc.config.edit() as conf:
-        conf["cache"].update({"shared": "group", "protected": str(protected)})
+def test_shared_cache(tmp_dir, dvc, group, dir_mode):
+    if group:
+        with dvc.config.edit() as conf:
+            conf["cache"].update({"shared": "group"})
     dvc.cache = Cache(dvc)
 
     tmp_dir.dvc_gen(
@@ -203,4 +203,4 @@ def test_shared_cache(tmp_dir, dvc, protected, dir_mode, file_mode):
 
         for fname in fnames:
             path = os.path.join(root, fname)
-            assert stat.S_IMODE(os.stat(path).st_mode) == file_mode
+            assert stat.S_IMODE(os.stat(path).st_mode) == 0o444
diff --git a/tests/func/test_checkout.py b/tests/func/test_checkout.py
index a75367975..e6b92111a 100644
--- a/tests/func/test_checkout.py
+++ b/tests/func/test_checkout.py
@@ -72,6 +72,7 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
     def test(self):
         cache = self.foo_stage.outs[0].cache_path
 
+        os.chmod(cache, 0o644)
         with open(cache, "a") as fd:
             fd.write("1")
 
@@ -100,6 +101,7 @@ class TestCheckoutCorruptedCacheDir(TestDvc):
         checksum = entry[self.dvc.cache.local.PARAM_CHECKSUM]
         cache = self.dvc.cache.local.get(checksum)
 
+        os.chmod(cache, 0o644)
         with open(cache, "w+") as fobj:
             fobj.write("1")
 
@@ -502,7 +504,6 @@ def test_checkout_relink(tmp_dir, dvc, link, link_test_func):
 @pytest.mark.parametrize("link", ["hardlink", "symlink", "copy"])
 def test_checkout_relink_protected(tmp_dir, dvc, link):
     dvc.cache.local.cache_types = [link]
-    dvc.cache.local.protected = True
 
     tmp_dir.dvc_gen("foo", "foo")
     dvc.unprotect("foo")
@@ -510,7 +511,12 @@ def test_checkout_relink_protected(tmp_dir, dvc, link):
 
     stats = dvc.checkout(["foo.dvc"], relink=True)
     assert stats == empty_checkout
-    assert not os.access("foo", os.W_OK)
+
+    # NOTE: Windows symlink perms don't propagate to the target
+    if link == "copy" or (link == "symlink" and os.name == "nt"):
+        assert os.access("foo", os.W_OK)
+    else:
+        assert not os.access("foo", os.W_OK)
 
 
 @pytest.mark.parametrize(
@@ -582,6 +588,7 @@ def test_checkout_stats_on_failure(tmp_dir, dvc, scm):
 
     # corrupt cache
     cache = stage.outs[0].cache_path
+    os.chmod(cache, 0o644)
     with open(cache, "a") as fd:
         fd.write("destroy cache")
 
diff --git a/tests/func/test_data_cloud.py b/tests/func/test_data_cloud.py
index 1e875f79c..590094d8a 100644
--- a/tests/func/test_data_cloud.py
+++ b/tests/func/test_data_cloud.py
@@ -22,6 +22,7 @@ from dvc.remote import RemoteS3
 from dvc.remote import RemoteSSH
 from dvc.remote.base import STATUS_DELETED, STATUS_NEW, STATUS_OK
 from dvc.utils import file_md5
+from dvc.utils.fs import remove
 from dvc.utils.stage import dump_stage_file, load_stage_file
 from tests.basic_env import TestDvc
 
@@ -149,7 +150,7 @@ class TestDataCloudBase(TestDvc):
             self.assertEqual(status_dir, expected)
 
             # Remove and check status
-            shutil.rmtree(self.dvc.cache.local.cache_dir)
+            remove(self.dvc.cache.local.cache_dir)
 
             status = self.cloud.status(info, show_checksums=True)
             expected = {md5: {"name": md5, "status": STATUS_DELETED}}
@@ -348,7 +349,7 @@ class TestDataCloudCLIBase(TestDvc):
         self.assertTrue(os.path.isfile(cache))
         self.assertTrue(os.path.isfile(cache_dir))
 
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
         self.main(["fetch"] + args)
         self.assertTrue(os.path.exists(cache))
@@ -572,7 +573,7 @@ class TestRecursiveSyncOperations(Local, TestDataCloudBase):
         self.assertTrue(os.path.exists(self.DATA_SUB))
 
     def _clear_local_cache(self):
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
     def _test_recursive_fetch(self, data_md5, data_sub_md5):
         self._clear_local_cache()
@@ -644,7 +645,7 @@ class TestShouldWarnOnNoChecksumInLocalAndRemoteCache(TestDvc):
         self.assertEqual(0, ret)
 
         # purge cache
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
         ret = main(["remote", "add", "remote_name", "-d", cache_dir])
         self.assertEqual(0, ret)
@@ -693,9 +694,9 @@ def test_verify_checksums(tmp_dir, scm, dvc, mocker, tmp_path_factory):
     dvc.push()
 
     # remove artifacts and cache to trigger fetching
-    os.remove("file")
-    shutil.rmtree("dir")
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove("file")
+    remove("dir")
+    remove(dvc.cache.local.cache_dir)
 
     checksum_spy = mocker.spy(dvc.cache.local, "get_file_checksum")
 
@@ -703,7 +704,7 @@ def test_verify_checksums(tmp_dir, scm, dvc, mocker, tmp_path_factory):
     assert checksum_spy.call_count == 0
 
     # Removing cache will invalidate existing state entries
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     dvc.config["remote"]["local_remote"]["verify"] = True
 
diff --git a/tests/func/test_diff.py b/tests/func/test_diff.py
index 668fa5ebf..bb474cd24 100644
--- a/tests/func/test_diff.py
+++ b/tests/func/test_diff.py
@@ -1,11 +1,11 @@
 import hashlib
 import os
 import pytest
-import shutil
 
 from funcy import first
 
 from dvc.compat import fspath
+from dvc.utils.fs import remove
 from dvc.exceptions import DvcException
 
 
@@ -36,7 +36,7 @@ def test_no_cache_entry(tmp_dir, scm, dvc):
     tmp_dir.dvc_gen({"dir": {"1": "1", "2": "2"}})
     tmp_dir.dvc_gen("file", "second")
 
-    shutil.rmtree(fspath(tmp_dir / ".dvc" / "cache"))
+    remove(fspath(tmp_dir / ".dvc" / "cache"))
     (tmp_dir / ".dvc" / "state").unlink()
 
     dir_checksum = "5fb6b29836c388e093ca0715c872fe2a.dir"
@@ -167,8 +167,8 @@ def test_diff_no_cache(tmp_dir, scm, dvc):
         {"dir": {"file": "modified file content"}}, commit="second"
     )
 
-    os.remove(first(stage.outs).cache_path)
-    shutil.rmtree("dir")
+    remove(first(stage.outs).cache_path)
+    remove("dir")
 
     # invalidate_dir_info to force cache loading
     dvc.cache.local._dir_info = {}
diff --git a/tests/func/test_external_repo.py b/tests/func/test_external_repo.py
index fbbaf37fd..43b8394ad 100644
--- a/tests/func/test_external_repo.py
+++ b/tests/func/test_external_repo.py
@@ -1,5 +1,4 @@
 import os
-import shutil
 
 from mock import patch
 from dvc.compat import fspath
@@ -8,6 +7,7 @@ from dvc.external_repo import external_repo
 from dvc.scm.git import Git
 from dvc.remote import RemoteLOCAL
 from dvc.utils import relpath
+from dvc.utils.fs import remove
 
 
 def test_external_repo(erepo_dir):
@@ -110,7 +110,7 @@ def test_relative_remote(erepo_dir, tmp_dir):
     erepo_dir.dvc.push()
 
     (erepo_dir / "file").unlink()
-    shutil.rmtree(erepo_dir.dvc.cache.local.cache_dir)
+    remove(erepo_dir.dvc.cache.local.cache_dir)
 
     url = fspath(erepo_dir)
 
diff --git a/tests/func/test_gc.py b/tests/func/test_gc.py
index 85790ebeb..c6dba9d73 100644
--- a/tests/func/test_gc.py
+++ b/tests/func/test_gc.py
@@ -10,6 +10,7 @@ from dvc.exceptions import CollectCacheError
 from dvc.main import main
 from dvc.repo import Repo as DvcRepo
 from dvc.remote.local import RemoteLOCAL
+from dvc.utils.fs import remove
 from tests.basic_env import TestDir, TestDvcGit
 
 
@@ -195,7 +196,7 @@ def test_gc_no_dir_cache(tmp_dir, dvc):
     tmp_dir.dvc_gen({"foo": "foo", "bar": "bar"})
     (dir_stage,) = tmp_dir.dvc_gen({"dir": {"x": "x", "subdir": {"y": "y"}}})
 
-    os.unlink(dir_stage.outs[0].cache_path)
+    remove(dir_stage.outs[0].cache_path)
 
     with pytest.raises(CollectCacheError):
         dvc.gc(workspace=True)
diff --git a/tests/func/test_import.py b/tests/func/test_import.py
index 23ede6236..c4fa1dc17 100644
--- a/tests/func/test_import.py
+++ b/tests/func/test_import.py
@@ -1,6 +1,5 @@
 import filecmp
 import os
-import shutil
 from dvc.compat import fspath
 
 import pytest
@@ -11,7 +10,7 @@ from dvc.exceptions import DownloadError, PathMissingError
 from dvc.config import NoRemoteError
 from dvc.stage import Stage
 from dvc.system import System
-from dvc.utils.fs import makedirs
+from dvc.utils.fs import makedirs, remove
 import dvc.data_cloud as cloud
 from tests.utils import trees_equal
 
@@ -187,8 +186,8 @@ def test_pull_imported_stage(tmp_dir, dvc, erepo_dir):
     dst_stage = Stage.load(dvc, "foo_imported.dvc")
     dst_cache = dst_stage.outs[0].cache_path
 
-    os.remove("foo_imported")
-    os.remove(dst_cache)
+    remove("foo_imported")
+    remove(dst_cache)
     dvc.pull(["foo_imported.dvc"])
 
     assert os.path.isfile("foo_imported")
@@ -220,8 +219,8 @@ def test_pull_imported_directory_stage(tmp_dir, dvc, erepo_dir):
 
     dvc.imp(fspath(erepo_dir), "dir", "dir_imported")
 
-    shutil.rmtree("dir_imported")
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove("dir_imported")
+    remove(dvc.cache.local.cache_dir)
 
     dvc.pull(["dir_imported.dvc"])
 
@@ -237,8 +236,8 @@ def test_download_error_pulling_imported_stage(tmp_dir, dvc, erepo_dir):
     dst_stage = Stage.load(dvc, "foo_imported.dvc")
     dst_cache = dst_stage.outs[0].cache_path
 
-    os.remove("foo_imported")
-    os.remove(dst_cache)
+    remove("foo_imported")
+    remove(dst_cache)
 
     with patch(
         "dvc.remote.RemoteLOCAL._download", side_effect=Exception
@@ -276,7 +275,7 @@ def test_pull_non_workspace(tmp_dir, scm, dvc, erepo_dir):
     # Overwrite via import
     dvc.imp(fspath(erepo_dir), "foo", "foo_imported", rev="master")
 
-    os.remove(stage.outs[0].cache_path)
+    remove(stage.outs[0].cache_path)
     dvc.fetch(all_tags=True)
     assert os.path.exists(stage.outs[0].cache_path)
 
@@ -299,7 +298,7 @@ def test_pull_no_rev_lock(erepo_dir, tmp_dir, dvc):
     stage.deps[0].def_repo.pop("rev_lock")
     stage.dump()
 
-    os.remove(stage.outs[0].cache_path)
+    remove(stage.outs[0].cache_path)
     (tmp_dir / "foo_imported").unlink()
 
     dvc.pull([stage.path])
diff --git a/tests/func/test_remote.py b/tests/func/test_remote.py
index 7ee4546bc..f1ebd3506 100644
--- a/tests/func/test_remote.py
+++ b/tests/func/test_remote.py
@@ -1,6 +1,5 @@
 import errno
 import os
-import shutil
 
 import configobj
 import pytest
@@ -13,6 +12,7 @@ from dvc.path_info import PathInfo
 from dvc.remote import RemoteLOCAL
 from dvc.remote.base import RemoteBASE, RemoteCacheRequiredError
 from dvc.compat import fspath
+from dvc.utils.fs import remove
 from tests.basic_env import TestDvc
 from tests.remotes import Local
 
@@ -197,7 +197,7 @@ def test_partial_push_n_pull(tmp_dir, dvc, tmp_path_factory):
 
     # Push everything and delete local cache
     dvc.push()
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     with patch.object(RemoteLOCAL, "_download", side_effect=Exception):
         with pytest.raises(DownloadError) as download_error_info:
diff --git a/tests/func/test_remove.py b/tests/func/test_remove.py
index b55b837d4..d2a622af3 100644
--- a/tests/func/test_remove.py
+++ b/tests/func/test_remove.py
@@ -1,5 +1,4 @@
 import os
-import shutil
 
 from mock import patch
 
@@ -8,6 +7,7 @@ from dvc.main import main
 from dvc.stage import Stage
 from dvc.stage import StageFileDoesNotExistError
 from dvc.system import System
+from dvc.utils.fs import remove
 from tests.basic_env import TestDvc
 
 
@@ -44,7 +44,7 @@ class TestRemoveBrokenSymlink(TestDvc):
         ret = main(["add", self.FOO])
         self.assertEqual(ret, 0)
 
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
         self.assertTrue(System.is_symlink(self.FOO))
 
diff --git a/tests/func/test_repro.py b/tests/func/test_repro.py
index d084de26b..17acb2433 100644
--- a/tests/func/test_repro.py
+++ b/tests/func/test_repro.py
@@ -34,6 +34,7 @@ from dvc.utils import file_md5
 from dvc.utils import relpath
 from dvc.utils.stage import dump_stage_file
 from dvc.utils.stage import load_stage_file
+from dvc.utils.fs import remove
 from tests.basic_env import TestDvc
 from tests.remotes import (
     GCP,
@@ -1274,7 +1275,7 @@ class TestReproAllPipelines(TestDvc):
 
 class TestReproNoCommit(TestRepro):
     def test(self):
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
         ret = main(["repro", self.file1_stage, "--no-commit"])
         self.assertEqual(ret, 0)
         self.assertFalse(os.path.exists(self.dvc.cache.local.cache_dir))
diff --git a/tests/func/test_run.py b/tests/func/test_run.py
index 1b49bc540..0f97672a6 100644
--- a/tests/func/test_run.py
+++ b/tests/func/test_run.py
@@ -304,9 +304,6 @@ class TestRunUnprotectOutsCopy(TestDvc):
             fobj.write("with open(sys.argv[1], 'a+') as fobj:\n")
             fobj.write("    fobj.write('foo')\n")
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["config", "cache.type", "copy"])
         self.assertEqual(ret, 0)
 
@@ -323,7 +320,7 @@ class TestRunUnprotectOutsCopy(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertTrue(os.access(self.FOO, os.W_OK))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
 
@@ -342,7 +339,7 @@ class TestRunUnprotectOutsCopy(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertTrue(os.access(self.FOO, os.W_OK))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
 
@@ -355,9 +352,6 @@ class TestRunUnprotectOutsSymlink(TestDvc):
             fobj.write("with open(sys.argv[1], 'a+') as fobj:\n")
             fobj.write("    fobj.write('foo')\n")
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["config", "cache.type", "symlink"])
         self.assertEqual(ret, 0)
 
@@ -375,7 +369,13 @@ class TestRunUnprotectOutsSymlink(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+
+        if os.name == "nt":
+            # NOTE: Windows symlink perms don't propagate to the target
+            self.assertTrue(os.access(self.FOO, os.W_OK))
+        else:
+            self.assertFalse(os.access(self.FOO, os.W_OK))
+
         self.assertTrue(System.is_symlink(self.FOO))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
@@ -395,7 +395,13 @@ class TestRunUnprotectOutsSymlink(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+
+        if os.name == "nt":
+            # NOTE: Windows symlink perms don't propagate to the target
+            self.assertTrue(os.access(self.FOO, os.W_OK))
+        else:
+            self.assertFalse(os.access(self.FOO, os.W_OK))
+
         self.assertTrue(System.is_symlink(self.FOO))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
@@ -409,9 +415,6 @@ class TestRunUnprotectOutsHardlink(TestDvc):
             fobj.write("with open(sys.argv[1], 'a+') as fobj:\n")
             fobj.write("    fobj.write('foo')\n")
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["config", "cache.type", "hardlink"])
         self.assertEqual(ret, 0)
 
@@ -848,6 +851,7 @@ class TestShouldNotCheckoutUponCorruptedLocalHardlinkCache(TestDvc):
         cmd = "python {} {} {}".format(self.CODE, self.FOO, self.BAR)
         stage = self.dvc.run(deps=[self.FOO], outs=[self.BAR], cmd=cmd)
 
+        os.chmod(self.BAR, 0o644)
         with open(self.BAR, "w") as fd:
             fd.write("corrupting the output cache")
 
diff --git a/tests/func/test_unprotect.py b/tests/func/test_unprotect.py
index 5e504b214..782ee7c5c 100644
--- a/tests/func/test_unprotect.py
+++ b/tests/func/test_unprotect.py
@@ -1,5 +1,4 @@
 import os
-import stat
 
 from dvc.main import main
 from tests.basic_env import TestDvc
@@ -7,28 +6,29 @@ from tests.basic_env import TestDvc
 
 class TestUnprotect(TestDvc):
     def test(self):
-        ret = main(["config", "cache.protected", "true"])
+        ret = main(["config", "cache.type", "hardlink"])
         self.assertEqual(ret, 0)
 
         ret = main(["add", self.FOO])
         self.assertEqual(ret, 0)
 
-        self.assertNotEqual(
-            os.stat(self.FOO).st_mode
-            & (stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH),
-            0,
-        )
-        self.assertEqual(
-            os.stat(self.FOO).st_mode
-            & (stat.S_IWRITE | stat.S_IWGRP | stat.S_IWOTH),
-            0,
+        cache = os.path.join(
+            ".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"
         )
 
+        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertFalse(os.access(cache, os.W_OK))
+
         ret = main(["unprotect", self.FOO])
         self.assertEqual(ret, 0)
 
-        self.assertNotEqual(
-            os.stat(self.FOO).st_mode
-            & (stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH | stat.S_IWRITE),
-            0,
-        )
+        self.assertTrue(os.access(self.FOO, os.W_OK))
+
+        # NOTE: cache is now unprotected, bceause we try to set write perms
+        # on files that we try to delete, as some filesystems require that
+        # (e.g. NTFS). But it should be restored after the next cache check,
+        # hence why we call `dvc status` here.
+        self.assertTrue(os.access(cache, os.W_OK))
+        ret = main(["status"])
+        self.assertEqual(ret, 0)
+        self.assertFalse(os.access(cache, os.W_OK))
diff --git a/tests/unit/remote/test_local.py b/tests/unit/remote/test_local.py
index 461e37b79..d18e3b4fb 100644
--- a/tests/unit/remote/test_local.py
+++ b/tests/unit/remote/test_local.py
@@ -1,4 +1,9 @@
+import os
+
+import pytest
+
 from dvc.cache import NamedCache
+from dvc.path_info import PathInfo
 from dvc.remote.local import RemoteLOCAL
 
 
@@ -23,3 +28,33 @@ def test_status_download_optimization(mocker):
     remote.status(infos, other_remote, download=True)
 
     assert other_remote.cache_exists.call_count == 0
+
+
+@pytest.mark.parametrize("link_name", ["hardlink", "symlink"])
+def test_is_protected(tmp_dir, link_name):
+    remote = RemoteLOCAL(None, {})
+    link_method = getattr(remote, link_name)
+
+    (tmp_dir / "foo").write_text("foo")
+
+    foo = PathInfo(tmp_dir / "foo")
+    link = PathInfo(tmp_dir / "link")
+
+    link_method(foo, link)
+
+    assert not remote.is_protected(foo)
+    assert not remote.is_protected(link)
+
+    remote.protect(foo)
+
+    assert remote.is_protected(foo)
+    assert remote.is_protected(link)
+
+    remote.unprotect(link)
+
+    assert not remote.is_protected(link)
+    if link_name == "symlink" and os.name == "nt":
+        # NOTE: Windows symlink perms don't propagate to the target
+        assert remote.is_protected(foo)
+    else:
+        assert not remote.is_protected(foo)

pip install -e .[all]
parse_log_pytest
pytest
pytest pytest-timeout pytest-cov pytest-xdist pytest-mock flaky mock xmltodict awscli google-compute-engine Pygments collective.checkdocs flake8 psutil flake8-docstrings pydocstyle jaraco.windows mock-ssh-server moto rangehttpserver
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aliyun-python-sdk-core==2.16.0
aliyun-python-sdk-core-v3==2.13.33
aliyun-python-sdk-kms==2.16.5
appdirs==1.4.4
atpublic==3.1.2
attrs @ file:///croot/attrs_1668696182826/work
autocommand==2.2.2
awscli==1.31.13
azure-common==1.1.28
azure-storage-blob==2.1.0
azure-storage-common==2.1.0
bcrypt==4.2.1
boto==2.49.0
boto3==1.33.13
botocore==1.33.13
cachetools==4.2.4
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
collective.checkdocs==0.2
colorama==0.4.4
configobj==5.0.9
coverage==7.2.7
crcmod==1.7
cryptography==44.0.2
decorator==5.1.1
distro==1.9.0
docutils==0.16
-e git+https://github.com/iterative/dvc.git@4b5d4d759ac4f90f618cc4317fd635dd93be1ad2#egg=dvc
execnet==2.0.2
flake8==5.0.4
flake8-docstrings==1.7.0
flaky==3.8.1
flatten-json==0.1.14
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
flufl.lock==7.1.1
funcy==2.0
future==1.0.0
gitdb==4.0.12
GitPython==3.1.44
google-api-core==2.10.2
google-api-python-client==2.166.0
google-auth==1.35.0
google-auth-httplib2==0.2.0
google-cloud-core==1.7.3
google-cloud-storage==1.19.0
google-compute-engine==2.8.13
google-crc32c==1.5.0
google-resumable-media==2.7.2
googleapis-common-protos==1.69.2
grandalf==0.6
httplib2==0.22.0
humanize==4.6.0
idna==3.10
importlib-metadata==4.2.0
importlib-resources==5.12.0
inflect==3.0.2
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
jaraco.classes==3.2.3
jaraco.collections==4.2.0
jaraco.context==4.3.0
jaraco.functools==3.7.0
jaraco.structures==2.1.0
jaraco.text==3.11.1
jaraco.ui==2.3.0
jaraco.windows==5.7.0
Jinja2==3.1.6
jmespath==0.10.0
jsonpath-ng==1.7.0
MarkupSafe==2.1.5
mccabe==0.7.0
mock==5.2.0
mock-ssh-server==0.9.1
more-itertools==9.1.0
moto==4.2.14
nanotime==0.5.2
networkx==2.3
numpy==1.21.6
oauth2client==4.1.3
oss2==2.6.1
packaging @ file:///croot/packaging_1671697413597/work
paramiko==3.5.1
path==16.6.0
pathspec==0.11.2
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
ply==3.11
protobuf==4.24.4
psutil==7.0.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyarrow==0.15.1
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycodestyle==2.9.1
pycparser==2.21
pycryptodome==3.22.0
pydantic==1.10.21
pydocstyle==6.3.0
pydot==2.0.0
PyDrive2==1.15.4
pyflakes==2.5.0
Pygments==2.17.2
pygtrie==2.3.2
PyNaCl==1.5.0
pyOpenSSL==25.0.0
pyparsing==3.1.4
pytest==7.1.2
pytest-cov==4.1.0
pytest-mock==3.11.1
pytest-timeout==2.3.1
pytest-xdist==3.5.0
python-dateutil==2.8.0
PyYAML==5.1.2
rangehttpserver==1.4.0
requests==2.31.0
responses==0.23.3
rsa==4.7.2
ruamel.yaml==0.18.10
ruamel.yaml.clib==0.2.8
s3transfer==0.8.2
shortuuid==1.0.13
six==1.17.0
smmap==5.0.2
snowballstemmer==2.2.0
texttable==1.7.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tqdm==4.67.1
treelib==1.7.1
types-PyYAML==6.0.12.12
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
uritemplate==4.1.1
urllib3==1.26.20
voluptuous==0.14.1
Werkzeug==2.2.3
xmltodict==0.14.2
zc.lockfile==3.0.post1
zipp @ file:///croot/zipp_1672387121353/work

name: dvc
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aliyun-python-sdk-core==2.16.0
      - aliyun-python-sdk-core-v3==2.13.33
      - aliyun-python-sdk-kms==2.16.5
      - appdirs==1.4.4
      - atpublic==3.1.2
      - autocommand==2.2.2
      - awscli==1.31.13
      - azure-common==1.1.28
      - azure-storage-blob==2.1.0
      - azure-storage-common==2.1.0
      - bcrypt==4.2.1
      - boto==2.49.0
      - boto3==1.33.13
      - botocore==1.33.13
      - cachetools==4.2.4
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - collective-checkdocs==0.2
      - colorama==0.4.4
      - configobj==5.0.9
      - coverage==7.2.7
      - crcmod==1.7
      - cryptography==44.0.2
      - decorator==5.1.1
      - distro==1.9.0
      - docutils==0.16
      - dvc==0.89.0+4b5d4d
      - execnet==2.0.2
      - flake8==5.0.4
      - flake8-docstrings==1.7.0
      - flaky==3.8.1
      - flatten-json==0.1.14
      - flufl-lock==7.1.1
      - funcy==2.0
      - future==1.0.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - google-api-core==2.10.2
      - google-api-python-client==2.166.0
      - google-auth==1.35.0
      - google-auth-httplib2==0.2.0
      - google-cloud-core==1.7.3
      - google-cloud-storage==1.19.0
      - google-compute-engine==2.8.13
      - google-crc32c==1.5.0
      - google-resumable-media==2.7.2
      - googleapis-common-protos==1.69.2
      - grandalf==0.6
      - httplib2==0.22.0
      - humanize==4.6.0
      - idna==3.10
      - importlib-metadata==4.2.0
      - importlib-resources==5.12.0
      - inflect==3.0.2
      - jaraco-classes==3.2.3
      - jaraco-collections==4.2.0
      - jaraco-context==4.3.0
      - jaraco-functools==3.7.0
      - jaraco-structures==2.1.0
      - jaraco-text==3.11.1
      - jaraco-ui==2.3.0
      - jaraco-windows==5.7.0
      - jinja2==3.1.6
      - jmespath==0.10.0
      - jsonpath-ng==1.7.0
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - mock==5.2.0
      - mock-ssh-server==0.9.1
      - more-itertools==9.1.0
      - moto==4.2.14
      - nanotime==0.5.2
      - networkx==2.3
      - numpy==1.21.6
      - oauth2client==4.1.3
      - oss2==2.6.1
      - paramiko==3.5.1
      - path==16.6.0
      - pathspec==0.11.2
      - ply==3.11
      - protobuf==4.24.4
      - psutil==7.0.0
      - pyarrow==0.15.1
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pycryptodome==3.22.0
      - pydantic==1.10.21
      - pydocstyle==6.3.0
      - pydot==2.0.0
      - pydrive2==1.15.4
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pygtrie==2.3.2
      - pynacl==1.5.0
      - pyopenssl==25.0.0
      - pyparsing==3.1.4
      - pytest-cov==4.1.0
      - pytest-mock==3.11.1
      - pytest-timeout==2.3.1
      - pytest-xdist==3.5.0
      - python-dateutil==2.8.0
      - pyyaml==5.1.2
      - rangehttpserver==1.4.0
      - requests==2.31.0
      - responses==0.23.3
      - rsa==4.7.2
      - ruamel-yaml==0.18.10
      - ruamel-yaml-clib==0.2.8
      - s3transfer==0.8.2
      - shortuuid==1.0.13
      - six==1.17.0
      - smmap==5.0.2
      - snowballstemmer==2.2.0
      - texttable==1.7.0
      - tqdm==4.67.1
      - treelib==1.7.1
      - types-pyyaml==6.0.12.12
      - uritemplate==4.1.1
      - urllib3==1.26.20
      - voluptuous==0.14.1
      - werkzeug==2.2.3
      - xmltodict==0.14.2
      - zc-lockfile==3.0.post1
prefix: /opt/conda/envs/dvc

tests/func/test_cache.py::test_shared_cache[False-493] tests/func/test_cache.py::test_shared_cache[True-509] tests/unit/remote/test_local.py::test_is_protected[hardlink] tests/unit/remote/test_local.py::test_is_protected[symlink]
tests/func/test_add.py::TestAddUnprotected::test tests/func/test_add.py::test_should_protect_on_repeated_add[hardlink] tests/func/test_add.py::test_should_protect_on_repeated_add[symlink] tests/func/test_add.py::test_should_protect_on_repeated_add[copy] tests/func/test_checkout.py::test_checkout_relink_protected[hardlink] tests/func/test_checkout.py::test_checkout_relink_protected[symlink] tests/func/test_repro.py::TestReproShell::test tests/func/test_run.py::TestRunUnprotectOutsSymlink::test tests/func/test_run.py::TestRunUnprotectOutsHardlink::test tests/func/test_unprotect.py::TestUnprotect::test
tests/func/remote/test_local.py::test_dont_fail_on_unpacked_create_fail tests/func/remote/test_local.py::test_remove_unpacked_on_create_fail tests/func/remote/test_local.py::test_create_unpacked_on_status tests/func/remote/test_local.py::test_dir_cache_changed_on_single_cache_file_modification tests/func/test_add.py::test_add tests/func/test_add.py::test_add_unicode tests/func/test_add.py::test_add_unsupported_file tests/func/test_add.py::test_add_directory tests/func/test_add.py::TestAddDirectoryRecursive::test tests/func/test_add.py::TestAddCmdDirectoryRecursive::test tests/func/test_add.py::TestAddCmdDirectoryRecursive::test_warn_about_large_directories tests/func/test_add.py::TestAddDirectoryWithForwardSlash::test tests/func/test_add.py::test_add_tracked_file tests/func/test_add.py::TestAddDirWithExistingCache::test tests/func/test_add.py::TestAddModifiedDir::test tests/func/test_add.py::test_add_file_in_dir tests/func/test_add.py::TestAddExternalLocalFile::test tests/func/test_add.py::TestAddLocalRemoteFile::test tests/func/test_add.py::TestCmdAdd::test tests/func/test_add.py::TestDoubleAddUnchanged::test_dir tests/func/test_add.py::TestDoubleAddUnchanged::test_file tests/func/test_add.py::test_should_update_state_entry_for_file_after_add tests/func/test_add.py::test_should_update_state_entry_for_directory_after_add tests/func/test_add.py::TestAddCommit::test tests/func/test_add.py::test_should_collect_dir_cache_only_once tests/func/test_add.py::TestShouldAddDataFromExternalSymlink::test tests/func/test_add.py::TestShouldAddDataFromInternalSymlink::test tests/func/test_add.py::TestShouldPlaceStageInDataDirIfRepositoryBelowSymlink::test tests/func/test_add.py::TestShouldThrowProperExceptionOnCorruptedStageFile::test tests/func/test_add.py::TestAddFilename::test tests/func/test_add.py::test_failed_add_cleanup tests/func/test_add.py::test_should_not_track_git_internal_files tests/func/test_add.py::test_readding_dir_should_not_unprotect_all tests/func/test_add.py::test_should_not_checkout_when_adding_cached_copy tests/func/test_add.py::test_should_relink_on_repeated_add[hardlink-copy-<lambda>] tests/func/test_add.py::test_should_relink_on_repeated_add[symlink-copy-<lambda>] tests/func/test_add.py::test_should_relink_on_repeated_add[copy-hardlink-is_hardlink] tests/func/test_add.py::test_should_relink_on_repeated_add[copy-symlink-is_symlink] tests/func/test_add.py::test_escape_gitignore_entries tests/func/test_add.py::test_add_from_data_dir tests/func/test_add.py::test_not_raises_on_re_add tests/func/test_add.py::test_add_empty_files[hardlink] tests/func/test_add.py::test_add_empty_files[symlink] tests/func/test_add.py::test_add_empty_files[copy] tests/func/test_add.py::test_add_optimization_for_hardlink_on_empty_files tests/func/test_api.py::test_get_url_requires_dvc tests/func/test_api.py::test_open[Local] tests/func/test_api.py::test_open_external[Local] tests/func/test_api.py::test_missing[Local] tests/func/test_api.py::test_open_scm_controlled tests/func/test_api.py::test_open_not_cached tests/func/test_cache.py::TestCache::test_all tests/func/test_cache.py::TestCache::test_get tests/func/test_cache.py::TestCacheLoadBadDirCache::test tests/func/test_cache.py::TestExternalCacheDir::test tests/func/test_cache.py::TestExternalCacheDir::test_remote_references tests/func/test_cache.py::TestSharedCacheDir::test tests/func/test_cache.py::TestCacheLinkType::test tests/func/test_cache.py::TestCmdCacheDir::test tests/func/test_cache.py::TestCmdCacheDir::test_abs_path tests/func/test_cache.py::TestCmdCacheDir::test_relative_path tests/func/test_cache.py::test_default_cache_type tests/func/test_checkout.py::TestCheckout::test tests/func/test_checkout.py::TestCheckoutSingleStage::test tests/func/test_checkout.py::TestCheckoutCorruptedCacheFile::test tests/func/test_checkout.py::TestCheckoutCorruptedCacheDir::test tests/func/test_checkout.py::TestCmdCheckout::test tests/func/test_checkout.py::TestRemoveFilesWhenCheckout::test tests/func/test_checkout.py::TestCheckoutCleanWorkingDir::test tests/func/test_checkout.py::TestCheckoutCleanWorkingDir::test_force tests/func/test_checkout.py::TestCheckoutSelectiveRemove::test tests/func/test_checkout.py::TestGitIgnoreBasic::test tests/func/test_checkout.py::TestGitIgnoreWhenCheckout::test tests/func/test_checkout.py::TestCheckoutMissingMd5InStageFile::test tests/func/test_checkout.py::TestCheckoutEmptyDir::test tests/func/test_checkout.py::TestCheckoutNotCachedFile::test tests/func/test_checkout.py::TestCheckoutWithDeps::test tests/func/test_checkout.py::TestCheckoutDirectory::test tests/func/test_checkout.py::TestCheckoutHook::test tests/func/test_checkout.py::TestCheckoutSuggestGit::test tests/func/test_checkout.py::TestCheckoutTargetRecursiveShouldNotRemoveOtherUsedFiles::test tests/func/test_checkout.py::TestCheckoutRecursiveNotDirectory::test tests/func/test_checkout.py::TestCheckoutMovedCacheDirWithSymlinks::test tests/func/test_checkout.py::test_checkout_no_checksum tests/func/test_checkout.py::test_checkout_relink[hardlink-is_hardlink] tests/func/test_checkout.py::test_checkout_relink[symlink-is_symlink] tests/func/test_checkout.py::test_checkout_relink_protected[copy] tests/func/test_checkout.py::test_partial_checkout[dir/subdir] tests/func/test_checkout.py::test_partial_checkout[dir/subdir/file] tests/func/test_checkout.py::test_stats_on_empty_checkout tests/func/test_checkout.py::test_stats_on_checkout tests/func/test_checkout.py::test_checkout_stats_on_failure tests/func/test_checkout.py::test_stats_on_added_file_from_tracked_dir tests/func/test_checkout.py::test_stats_on_updated_file_from_tracked_dir tests/func/test_checkout.py::test_stats_on_removed_file_from_tracked_dir tests/func/test_checkout.py::test_stats_on_show_changes_does_not_show_summary tests/func/test_checkout.py::test_stats_does_not_show_changes_by_default tests/func/test_checkout.py::test_checkout_with_relink_existing[hardlink] tests/func/test_checkout.py::test_checkout_with_relink_existing[symlink] tests/func/test_checkout.py::test_checkout_with_relink_existing[copy] tests/func/test_checkout.py::test_checkout_with_deps tests/func/test_checkout.py::test_checkout_recursive tests/func/test_data_cloud.py::TestDataCloud::test tests/func/test_data_cloud.py::TestRemoteLOCAL::test tests/func/test_data_cloud.py::TestRemoteSSHMocked::test tests/func/test_data_cloud.py::TestRemoteHTTP::test tests/func/test_data_cloud.py::TestRemoteLOCALCLI::test tests/func/test_data_cloud.py::TestDataCloudErrorCLI::test_error tests/func/test_data_cloud.py::TestWarnOnOutdatedStage::test tests/func/test_data_cloud.py::TestRecursiveSyncOperations::test tests/func/test_data_cloud.py::test_checksum_recalculation tests/func/test_data_cloud.py::TestShouldWarnOnNoChecksumInLocalAndRemoteCache::test tests/func/test_data_cloud.py::test_verify_checksums tests/func/test_diff.py::test_no_scm tests/func/test_diff.py::test_added tests/func/test_diff.py::test_no_cache_entry tests/func/test_diff.py::test_deleted tests/func/test_diff.py::test_modified tests/func/test_diff.py::test_refs tests/func/test_diff.py::test_directories tests/func/test_diff.py::test_diff_no_cache tests/func/test_external_repo.py::test_external_repo tests/func/test_external_repo.py::test_source_change tests/func/test_external_repo.py::test_cache_reused tests/func/test_external_repo.py::test_known_sha tests/func/test_external_repo.py::test_pull_subdir_file tests/func/test_external_repo.py::test_relative_remote tests/func/test_gc.py::TestGC::test_api tests/func/test_gc.py::TestGC::test_cli tests/func/test_gc.py::TestGCBranchesTags::test tests/func/test_gc.py::TestGCMultipleDvcRepos::test tests/func/test_gc.py::test_all_commits tests/func/test_gc.py::test_gc_no_dir_cache tests/func/test_gc.py::test_gc_no_unpacked_dir tests/func/test_gc.py::test_gc_without_workspace_raises_error tests/func/test_gc.py::test_gc_cloud_with_or_without_specifier tests/func/test_gc.py::test_gc_without_workspace_on_tags_branches_commits tests/func/test_gc.py::test_gc_without_workspace tests/func/test_gc.py::test_gc_cloud_without_any_specifier tests/func/test_gc.py::test_gc_with_possible_args_positive tests/func/test_gc.py::test_gc_cloud_positive tests/func/test_import.py::test_import tests/func/test_import.py::test_import_git_file[True] tests/func/test_import.py::test_import_git_file[False] tests/func/test_import.py::test_import_cached_file tests/func/test_import.py::test_import_git_dir[True] tests/func/test_import.py::test_import_git_dir[False] tests/func/test_import.py::test_import_dir tests/func/test_import.py::test_import_file_from_dir tests/func/test_import.py::test_import_non_cached tests/func/test_import.py::test_import_rev tests/func/test_import.py::test_pull_imported_stage tests/func/test_import.py::test_cache_type_is_properly_overridden tests/func/test_import.py::test_pull_imported_directory_stage tests/func/test_import.py::test_download_error_pulling_imported_stage tests/func/test_import.py::test_import_to_dir[.] tests/func/test_import.py::test_import_to_dir[dir] tests/func/test_import.py::test_import_to_dir[dir/subdir] tests/func/test_import.py::test_pull_non_workspace tests/func/test_import.py::test_import_non_existing tests/func/test_import.py::test_pull_no_rev_lock tests/func/test_remote.py::TestRemote::test tests/func/test_remote.py::TestRemote::test_overwrite tests/func/test_remote.py::TestRemote::test_referencing_other_remotes tests/func/test_remote.py::TestRemote::test_relative_path tests/func/test_remote.py::test_remove_default tests/func/test_remote.py::TestRemoteRemove::test tests/func/test_remote.py::TestRemoteDefault::test tests/func/test_remote.py::test_show_default tests/func/test_remote.py::TestRemoteShouldHandleUppercaseRemoteName::test tests/func/test_remote.py::test_dir_checksum_should_be_key_order_agnostic tests/func/test_remote.py::test_partial_push_n_pull tests/func/test_remote.py::test_raise_on_too_many_open_files tests/func/test_remote.py::test_modify_missing_remote tests/func/test_remote.py::test_external_dir_resource_on_no_cache tests/func/test_remove.py::TestRemove::test tests/func/test_remove.py::TestRemoveNonExistentFile::test tests/func/test_remove.py::TestRemoveBrokenSymlink::test tests/func/test_remove.py::TestRemoveDirectory::test tests/func/test_remove.py::TestCmdRemove::test tests/func/test_remove.py::TestRemovePurge::test tests/func/test_remove.py::TestRemovePurge::test_force tests/func/test_repro.py::TestReproFail::test tests/func/test_repro.py::TestReproCyclicGraph::test tests/func/test_repro.py::TestReproWorkingDirectoryAsOutput::test tests/func/test_repro.py::TestReproWorkingDirectoryAsOutput::test_nested tests/func/test_repro.py::TestReproWorkingDirectoryAsOutput::test_similar_paths tests/func/test_repro.py::TestReproDepUnderDir::test tests/func/test_repro.py::TestReproDepDirWithOutputsUnderIt::test tests/func/test_repro.py::TestReproNoDeps::test tests/func/test_repro.py::TestReproForce::test tests/func/test_repro.py::TestReproChangedCode::test tests/func/test_repro.py::TestReproChangedData::test tests/func/test_repro.py::TestReproDry::test tests/func/test_repro.py::TestReproUpToDate::test tests/func/test_repro.py::TestReproDryNoExec::test tests/func/test_repro.py::TestReproChangedDeepData::test tests/func/test_repro.py::TestReproIgnoreBuildCache::test tests/func/test_repro.py::TestReproPipeline::test tests/func/test_repro.py::TestReproPipeline::test_cli tests/func/test_repro.py::TestReproPipelines::test tests/func/test_repro.py::TestReproPipelines::test_cli tests/func/test_repro.py::TestReproLocked::test tests/func/test_repro.py::TestReproLocked::test_non_existing tests/func/test_repro.py::TestReproLockedCallback::test tests/func/test_repro.py::TestReproLockedUnchanged::test tests/func/test_repro.py::TestReproMetricsAddUnchanged::test tests/func/test_repro.py::TestReproPhony::test tests/func/test_repro.py::TestNonExistingOutput::test tests/func/test_repro.py::TestReproDataSource::test tests/func/test_repro.py::TestReproChangedDir::test tests/func/test_repro.py::TestReproChangedDirData::test tests/func/test_repro.py::TestReproMissingMd5InStageFile::test tests/func/test_repro.py::TestCmdRepro::test tests/func/test_repro.py::TestCmdReproChdirCwdBackwardCompatible::test tests/func/test_repro.py::TestCmdReproChdir::test tests/func/test_repro.py::TestReproExternalLOCAL::test tests/func/test_repro.py::TestReproExternalHTTP::test tests/func/test_repro.py::TestReproAllPipelines::test tests/func/test_repro.py::TestReproNoCommit::test tests/func/test_repro.py::TestReproAlreadyCached::test tests/func/test_repro.py::TestReproAlreadyCached::test_force_import tests/func/test_repro.py::TestReproAlreadyCached::test_force_with_dependencies tests/func/test_repro.py::TestShouldDisplayMetricsOnReproWithMetricsOption::test tests/func/test_repro.py::test_recursive_repro_default tests/func/test_repro.py::test_recursive_repro_single tests/func/test_repro.py::test_recursive_repro_single_force tests/func/test_repro.py::test_recursive_repro_empty_dir tests/func/test_repro.py::test_recursive_repro_recursive_missing_file tests/func/test_repro.py::test_recursive_repro_on_stage_file tests/func/test_repro.py::test_dvc_formatting_retained tests/func/test_repro.py::TestReproDownstream::test tests/func/test_repro.py::test_ssh_dir_out tests/func/test_run.py::TestRun::test tests/func/test_run.py::TestRunEmpty::test tests/func/test_run.py::TestRunMissingDep::test tests/func/test_run.py::TestRunBadStageFilename::test tests/func/test_run.py::TestRunNoExec::test tests/func/test_run.py::TestRunCircularDependency::test tests/func/test_run.py::TestRunCircularDependency::test_graph tests/func/test_run.py::TestRunCircularDependency::test_non_normalized_paths tests/func/test_run.py::TestRunCircularDependency::test_outs_no_cache tests/func/test_run.py::TestRunDuplicatedArguments::test tests/func/test_run.py::TestRunDuplicatedArguments::test_non_normalized_paths tests/func/test_run.py::TestRunDuplicatedArguments::test_outs_no_cache tests/func/test_run.py::TestRunStageInsideOutput::test_cwd tests/func/test_run.py::TestRunStageInsideOutput::test_file_name tests/func/test_run.py::TestRunBadCwd::test tests/func/test_run.py::TestRunBadCwd::test_same_prefix tests/func/test_run.py::TestRunBadWdir::test tests/func/test_run.py::TestRunBadWdir::test_not_dir tests/func/test_run.py::TestRunBadWdir::test_not_found tests/func/test_run.py::TestRunBadWdir::test_same_prefix tests/func/test_run.py::TestRunBadName::test tests/func/test_run.py::TestRunBadName::test_not_found tests/func/test_run.py::TestRunBadName::test_same_prefix tests/func/test_run.py::TestRunRemoveOuts::test tests/func/test_run.py::TestRunUnprotectOutsCopy::test tests/func/test_run.py::TestCmdRunOverwrite::test tests/func/test_run.py::TestCmdRunCliMetrics::test_cached tests/func/test_run.py::TestCmdRunCliMetrics::test_not_cached tests/func/test_run.py::TestCmdRunWorkingDirectory::test_cwd_is_ignored tests/func/test_run.py::TestCmdRunWorkingDirectory::test_default_wdir_is_not_written tests/func/test_run.py::TestCmdRunWorkingDirectory::test_fname_changes_path_and_wdir tests/func/test_run.py::test_rerun_deterministic tests/func/test_run.py::test_rerun_deterministic_ignore_cache tests/func/test_run.py::test_rerun_callback tests/func/test_run.py::test_rerun_changed_dep tests/func/test_run.py::test_rerun_changed_stage tests/func/test_run.py::test_rerun_changed_out tests/func/test_run.py::TestRunCommit::test tests/func/test_run.py::TestRunPersistOuts::test tests/func/test_run.py::TestRunPersistOutsNoCache::test tests/func/test_run.py::TestShouldRaiseOnOverlappingOutputPaths::test tests/func/test_run.py::TestNewRunShouldRemoveOutsOnNoPersist::test tests/func/test_run.py::TestNewRunShouldNotRemoveOutsOnPersist::test tests/func/test_run.py::TestShouldNotCheckoutUponCorruptedLocalHardlinkCache::test tests/func/test_run.py::TestPersistentOutput::test_ignore_build_cache tests/func/test_run.py::test_bad_stage_fname tests/func/test_run.py::test_should_raise_on_stage_dependency tests/func/test_run.py::test_should_raise_on_stage_output tests/unit/remote/test_local.py::test_status_download_optimization
dvc/config.py dvc/command/unprotect.py dvc/remote/ssh/__init__.py dvc/remote/base.py dvc/remote/local.py
dvc/config.py dvc/command/unprotect.py dvc/remote/ssh/__init__.py dvc/remote/base.py dvc/remote/local.py