efiop: @skshetry Please check the tests, they've failed.
codecov[bot]: # [Codecov](https://codecov.io/gh/iterative/dvc/pull/3222?src=pr&el=h1) Report
> Merging [#3222](https://codecov.io/gh/iterative/dvc/pull/3222?src=pr&el=desc) into [master](https://codecov.io/gh/iterative/dvc/commit/e9caa44b554daf7f0f21dc2bb9bb0df028369fb1?src=pr&el=desc) will **decrease** coverage by `0.36%`.
> The diff coverage is `80%`.

[![Impacted file tree graph](https://codecov.io/gh/iterative/dvc/pull/3222/graphs/tree.svg?width=650&token=VOfa1EJWC4&height=150&src=pr)](https://codecov.io/gh/iterative/dvc/pull/3222?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #3222      +/-   ##
==========================================
- Coverage   91.68%   91.31%   -0.37%     
==========================================
  Files         139      139              
  Lines        8681     8681              
==========================================
- Hits         7959     7927      -32     
- Misses        722      754      +32
```


| [Impacted Files](https://codecov.io/gh/iterative/dvc/pull/3222?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [dvc/cache.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL2NhY2hlLnB5) | `98.48% <ø> (ø)` | :arrow_up: |
| [dvc/remote/config.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL3JlbW90ZS9jb25maWcucHk=) | `98.3% <ø> (ø)` | :arrow_up: |
| [dvc/command/daemon.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL2NvbW1hbmQvZGFlbW9uLnB5) | `66.66% <ø> (ø)` | :arrow_up: |
| [dvc/scm/git/\_\_init\_\_.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL3NjbS9naXQvX19pbml0X18ucHk=) | `88.39% <ø> (ø)` | :arrow_up: |
| [dvc/command/pipeline.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL2NvbW1hbmQvcGlwZWxpbmUucHk=) | `95.45% <ø> (ø)` | :arrow_up: |
| [dvc/progress.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL3Byb2dyZXNzLnB5) | `98.11% <ø> (ø)` | :arrow_up: |
| [dvc/scm/\_\_init\_\_.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL3NjbS9fX2luaXRfXy5weQ==) | `100% <ø> (ø)` | :arrow_up: |
| [dvc/config.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL2NvbmZpZy5weQ==) | `95.1% <ø> (ø)` | :arrow_up: |
| [dvc/lock.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL2xvY2sucHk=) | `68.05% <ø> (ø)` | :arrow_up: |
| [dvc/command/cache.py](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree#diff-ZHZjL2NvbW1hbmQvY2FjaGUucHk=) | `100% <ø> (ø)` | :arrow_up: |
| ... and [15 more](https://codecov.io/gh/iterative/dvc/pull/3222/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/iterative/dvc/pull/3222?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/iterative/dvc/pull/3222?src=pr&el=footer). Last update [e9caa44...908f5c6](https://codecov.io/gh/iterative/dvc/pull/3222?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/dvc/api.py b/dvc/api.py
index 2ae39bae7..7efaf524a 100644
--- a/dvc/api.py
+++ b/dvc/api.py
@@ -10,7 +10,7 @@ import ruamel.yaml
 from voluptuous import Schema, Required, Invalid
 
 from dvc.repo import Repo
-from dvc.exceptions import DvcException
+from dvc.exceptions import DvcException, NotDvcRepoError
 from dvc.external_repo import external_repo
 
 
@@ -108,12 +108,16 @@ def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
 
 
 @contextmanager
-def _make_repo(repo_url, rev=None):
-    if rev is None and (not repo_url or os.path.exists(repo_url)):
-        yield Repo(repo_url)
-    else:
-        with external_repo(url=repo_url, rev=rev) as repo:
-            yield repo
+def _make_repo(repo_url=None, rev=None):
+    repo_url = repo_url or os.getcwd()
+    if rev is None and os.path.exists(repo_url):
+        try:
+            yield Repo(repo_url)
+            return
+        except NotDvcRepoError:
+            pass  # fallthrough to external_repo
+    with external_repo(url=repo_url, rev=rev) as repo:
+        yield repo
 
 
 def summon(name, repo=None, rev=None, summon_file="dvcsummon.yaml", args=None):
diff --git a/dvc/external_repo.py b/dvc/external_repo.py
index 470646c6e..306e7d7c6 100644
--- a/dvc/external_repo.py
+++ b/dvc/external_repo.py
@@ -128,10 +128,10 @@ class ExternalGitRepo:
             raise PathMissingError(path, self.url)
 
     @contextmanager
-    def open_by_relpath(self, path, mode="r", encoding=None):
+    def open_by_relpath(self, path, mode="r", encoding=None, **kwargs):
         try:
             abs_path = os.path.join(self.root_dir, path)
-            with open(abs_path, mode, encoding) as fd:
+            with open(abs_path, mode, encoding=encoding) as fd:
                 yield fd
         except FileNotFoundError:
             raise PathMissingError(path, self.url)
diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py
index 2d334a649..3872c8e72 100644
--- a/dvc/scm/git/__init__.py
+++ b/dvc/scm/git/__init__.py
@@ -250,7 +250,7 @@ class Git(Base):
     def _install_hook(self, name, cmd):
         command = (
             '[ "$3" = "0" ]'
-            ' || [ -z "$(git ls-files --full-name .dvc)" ]'
+            ' || [ -z "$(git ls-files .dvc)" ]'
             " || exec dvc {}".format(cmd)
         )
 

api: does not work with git only local repository
```sh
➜ cd "$(mktemp -d)" 
➜ git init
Initialized empty Git repository in /tmp/tmp.TGV6SVXYsz/.git/

➜ echo "foo" > foo
➜ git add foo
➜ git -c commit.gpgsign=false commit -m "first"
[master (root-commit) 136fd94] first
 1 file changed, 1 insertion(+)
 create mode 100644 foo

➜ python -c '
➜ import dvc.api
➜ with dvc.api.open(path="foo", repo=".") as fd:
➜     print(fd.read())
➜ '
Traceback (most recent call last):
  File "<string>", line 3, in <module>
  File "/home/saugat/.pyenv/versions/3.6.6/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/saugat/repos/iterative/dvc/dvc/api.py", line 95, in _open
    with _make_repo(repo, rev=rev) as _repo:
  File "/home/saugat/.pyenv/versions/3.6.6/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/saugat/repos/iterative/dvc/dvc/api.py", line 113, in _make_repo
    yield Repo(repo_url)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/__init__.py", line 76, in __init__
    root_dir = self.find_root(root_dir)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/__init__.py", line 144, in find_root
    raise NotDvcRepoError(root)
dvc.exceptions.NotDvcRepoError: you are not inside of a dvc repository (checked up to mount point '/tmp')
```

This is because of the recent changes in `_make_repo` that instead of falling back to external repo on error, it assumes the local path to be a DVC repository.
https://github.com/iterative/dvc/blob/e9caa44b554daf7f0f21dc2bb9bb0df028369fb1/dvc/api.py#L112-L113
diff --git a/tests/func/test_add.py b/tests/func/test_add.py
index 1ce44a0b0..18d77d39e 100644
--- a/tests/func/test_add.py
+++ b/tests/func/test_add.py
@@ -9,7 +9,7 @@ import colorama
 import pytest
 from mock import patch
 
-import dvc as dvc_module
+import dvc
 from dvc.cache import Cache
 from dvc.exceptions import DvcException
 from dvc.exceptions import RecursiveAddingWhileUsingFilename
@@ -28,6 +28,7 @@ from dvc.utils.fs import path_isin
 from dvc.utils.stage import load_stage_file
 from tests.basic_env import TestDvc
 from tests.utils import get_gitignore_content
+from tests.utils import spy
 
 
 def test_add(tmp_dir, dvc):
@@ -244,66 +245,66 @@ class TestDoubleAddUnchanged(TestDvc):
         self.assertEqual(ret, 0)
 
 
-def test_should_update_state_entry_for_file_after_add(mocker, dvc, tmp_dir):
-    file_md5_counter = mocker.spy(dvc_module.remote.local, "file_md5")
-    tmp_dir.gen("foo", "foo")
-
-    ret = main(["config", "cache.type", "copy"])
-    assert ret == 0
-
-    ret = main(["add", "foo"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 1
+class TestShouldUpdateStateEntryForFileAfterAdd(TestDvc):
+    def test(self):
+        file_md5_counter = spy(dvc.remote.local.file_md5)
+        with patch.object(dvc.remote.local, "file_md5", file_md5_counter):
+            ret = main(["config", "cache.type", "copy"])
+            self.assertEqual(ret, 0)
 
-    ret = main(["status"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 1
+            ret = main(["add", self.FOO])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 1)
 
-    ret = main(["run", "-d", "foo", "echo foo"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 1
+            ret = main(["status"])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 1)
 
-    os.rename("foo", "foo.back")
-    ret = main(["checkout"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 1
+            ret = main(["run", "-d", self.FOO, "echo foo"])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 1)
 
-    ret = main(["status"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 1
+            os.rename(self.FOO, self.FOO + ".back")
+            ret = main(["checkout"])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 1)
 
+            ret = main(["status"])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 1)
 
-def test_should_update_state_entry_for_directory_after_add(
-    mocker, dvc, tmp_dir
-):
-    file_md5_counter = mocker.spy(dvc_module.remote.local, "file_md5")
 
-    tmp_dir.gen({"data/data": "foo", "data/data_sub/sub_data": "foo"})
+class TestShouldUpdateStateEntryForDirectoryAfterAdd(TestDvc):
+    def test(self):
+        file_md5_counter = spy(dvc.remote.local.file_md5)
+        with patch.object(dvc.remote.local, "file_md5", file_md5_counter):
 
-    ret = main(["config", "cache.type", "copy"])
-    assert ret == 0
+            ret = main(["config", "cache.type", "copy"])
+            self.assertEqual(ret, 0)
 
-    ret = main(["add", "data"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 3
+            ret = main(["add", self.DATA_DIR])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 3)
 
-    ret = main(["status"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 3
+            ret = main(["status"])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 3)
 
-    ls = "dir" if os.name == "nt" else "ls"
-    ret = main(["run", "-d", "data", "{} {}".format(ls, "data")])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 3
+            ls = "dir" if os.name == "nt" else "ls"
+            ret = main(
+                ["run", "-d", self.DATA_DIR, "{} {}".format(ls, self.DATA_DIR)]
+            )
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 3)
 
-    os.rename("data", "data" + ".back")
-    ret = main(["checkout"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 3
+            os.rename(self.DATA_DIR, self.DATA_DIR + ".back")
+            ret = main(["checkout"])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 3)
 
-    ret = main(["status"])
-    assert ret == 0
-    assert file_md5_counter.mock.call_count == 3
+            ret = main(["status"])
+            self.assertEqual(ret, 0)
+            self.assertEqual(file_md5_counter.mock.call_count, 3)
 
 
 class TestAddCommit(TestDvc):
@@ -319,18 +320,23 @@ class TestAddCommit(TestDvc):
         self.assertEqual(len(os.listdir(self.dvc.cache.local.cache_dir)), 1)
 
 
-def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
-    tmp_dir.gen({"data/data": "foo"})
-    get_dir_checksum_counter = mocker.spy(RemoteLOCAL, "get_dir_checksum")
-    ret = main(["add", "data"])
-    assert ret == 0
+class TestShouldCollectDirCacheOnlyOnce(TestDvc):
+    def test(self):
+        from dvc.remote.local import RemoteLOCAL
 
-    ret = main(["status"])
-    assert ret == 0
+        get_dir_checksum_counter = spy(RemoteLOCAL.get_dir_checksum)
+        with patch.object(
+            RemoteLOCAL, "get_dir_checksum", get_dir_checksum_counter
+        ):
+            ret = main(["add", self.DATA_DIR])
+            self.assertEqual(0, ret)
 
-    ret = main(["status"])
-    assert ret == 0
-    assert get_dir_checksum_counter.mock.call_count == 1
+            ret = main(["status"])
+            self.assertEqual(0, ret)
+
+            ret = main(["status"])
+            self.assertEqual(0, ret)
+        self.assertEqual(1, get_dir_checksum_counter.mock.call_count)
 
 
 class SymlinkAddTestBase(TestDvc):
@@ -473,15 +479,17 @@ def test_failed_add_cleanup(tmp_dir, scm, dvc):
     assert "/bar" not in gitignore_content
 
 
-def test_should_not_track_git_internal_files(mocker, dvc, tmp_dir):
-    stage_creator_spy = mocker.spy(dvc_module.repo.add, "_create_stages")
+class TestShouldNotTrackGitInternalFiles(TestDvc):
+    def test(self):
+        stage_creator_spy = spy(dvc.repo.add._create_stages)
 
-    ret = main(["add", "-R", dvc.root_dir])
-    assert ret == 0
+        with patch.object(dvc.repo.add, "_create_stages", stage_creator_spy):
+            ret = main(["add", "-R", self.dvc.root_dir])
+            self.assertEqual(0, ret)
 
-    created_stages_filenames = stage_creator_spy.mock.call_args[0][1]
-    for fname in created_stages_filenames:
-        assert ".git" not in fname
+        created_stages_filenames = stage_creator_spy.mock.call_args[0][0]
+        for fname in created_stages_filenames:
+            self.assertNotIn(".git", fname)
 
 
 class TestAddUnprotected(TestDvc):
@@ -566,7 +574,8 @@ def test_readding_dir_should_not_unprotect_all(tmp_dir, dvc, mocker):
     dvc.add("dir")
     tmp_dir.gen("dir/new_file", "new_file_content")
 
-    unprotect_spy = mocker.spy(RemoteLOCAL, "unprotect")
+    unprotect_spy = spy(RemoteLOCAL.unprotect)
+    mocker.patch.object(RemoteLOCAL, "unprotect", unprotect_spy)
     dvc.add("dir")
 
     assert not unprotect_spy.mock.called
@@ -580,7 +589,8 @@ def test_should_not_checkout_when_adding_cached_copy(tmp_dir, dvc, mocker):
 
     shutil.copy("bar", "foo")
 
-    copy_spy = mocker.spy(dvc.cache.local, "copy")
+    copy_spy = spy(dvc.cache.local.copy)
+    mocker.patch.object(dvc.cache.local, "copy", copy_spy)
 
     dvc.add("foo")
 
diff --git a/tests/func/test_api.py b/tests/func/test_api.py
index 687476d61..04f707826 100644
--- a/tests/func/test_api.py
+++ b/tests/func/test_api.py
@@ -8,7 +8,7 @@ import pytest
 from dvc import api
 from dvc.api import SummonError, UrlNotDvcRepoError
 from dvc.compat import fspath
-from dvc.exceptions import FileMissingError, NotDvcRepoError
+from dvc.exceptions import FileMissingError
 from dvc.main import main
 from dvc.path_info import URLInfo
 from dvc.remote.config import RemoteConfig
@@ -54,7 +54,7 @@ def test_get_url_external(erepo_dir, remote_url):
 def test_get_url_requires_dvc(tmp_dir, scm):
     tmp_dir.scm_gen({"foo": "foo"}, commit="initial")
 
-    with pytest.raises(NotDvcRepoError, match="not inside of a DVC repo"):
+    with pytest.raises(UrlNotDvcRepoError, match="not a DVC repository"):
         api.get_url("foo", repo=fspath(tmp_dir))
 
     with pytest.raises(UrlNotDvcRepoError):
diff --git a/tests/func/test_data_cloud.py b/tests/func/test_data_cloud.py
index fe3aa432f..137fd674c 100644
--- a/tests/func/test_data_cloud.py
+++ b/tests/func/test_data_cloud.py
@@ -6,6 +6,7 @@ import uuid
 from unittest import SkipTest
 
 import pytest
+from mock import patch
 
 from dvc.cache import NamedCache
 from dvc.compat import fspath
@@ -28,6 +29,7 @@ from dvc.utils import file_md5
 from dvc.utils.stage import dump_stage_file
 from dvc.utils.stage import load_stage_file
 from tests.basic_env import TestDvc
+from tests.utils import spy
 
 from tests.remotes import (
     Azure,
@@ -584,21 +586,24 @@ class TestRecursiveSyncOperations(Local, TestDataCloudBase):
         self._test_recursive_pull()
 
 
-def test_checksum_recalculation(mocker, dvc, tmp_dir):
-    tmp_dir.gen({"foo": "foo"})
-    test_get_file_checksum = mocker.spy(RemoteLOCAL, "get_file_checksum")
-    url = Local.get_url()
-    ret = main(["remote", "add", "-d", TEST_REMOTE, url])
-    assert ret == 0
-    ret = main(["config", "cache.type", "hardlink"])
-    assert ret == 0
-    ret = main(["add", "foo"])
-    assert ret == 0
-    ret = main(["push"])
-    assert ret == 0
-    ret = main(["run", "-d", "foo", "echo foo"])
-    assert ret == 0
-    assert test_get_file_checksum.mock.call_count == 1
+class TestCheckSumRecalculation(TestDvc):
+    def test(self):
+        test_get_file_checksum = spy(RemoteLOCAL.get_file_checksum)
+        with patch.object(
+            RemoteLOCAL, "get_file_checksum", test_get_file_checksum
+        ):
+            url = Local.get_url()
+            ret = main(["remote", "add", "-d", TEST_REMOTE, url])
+            self.assertEqual(ret, 0)
+            ret = main(["config", "cache.type", "hardlink"])
+            self.assertEqual(ret, 0)
+            ret = main(["add", self.FOO])
+            self.assertEqual(ret, 0)
+            ret = main(["push"])
+            self.assertEqual(ret, 0)
+            ret = main(["run", "-d", self.FOO, "echo foo"])
+            self.assertEqual(ret, 0)
+        self.assertEqual(test_get_file_checksum.mock.call_count, 1)
 
 
 class TestShouldWarnOnNoChecksumInLocalAndRemoteCache(TestDvc):
diff --git a/tests/func/test_import_url.py b/tests/func/test_import_url.py
index bf042e32b..ae59e3aa5 100644
--- a/tests/func/test_import_url.py
+++ b/tests/func/test_import_url.py
@@ -2,12 +2,13 @@ import os
 from uuid import uuid4
 
 import pytest
+from mock import patch
 
-from dvc.stage import Stage
+import dvc
 from dvc.main import main
 from dvc.utils.fs import makedirs
-from dvc.compat import fspath
 from tests.basic_env import TestDvc
+from tests.utils import spy
 
 
 class TestCmdImport(TestDvc):
@@ -40,14 +41,23 @@ class TestDefaultOutput(TestDvc):
             self.assertEqual(fd.read(), "content")
 
 
-def test_should_remove_outs_before_import(mocker, erepo_dir):
-    erepo_dir.gen({"foo": "foo"})
-
-    remove_outs_call_counter = mocker.spy(Stage, "remove_outs")
-    ret = main(["import-url", fspath(erepo_dir / "foo")])
+class TestShouldRemoveOutsBeforeImport(TestDvc):
+    def setUp(self):
+        super().setUp()
+        tmp_dir = self.mkdtemp()
+        self.external_source = os.path.join(tmp_dir, "file")
+        with open(self.external_source, "w") as fobj:
+            fobj.write("content")
 
-    assert ret == 0
-    assert remove_outs_call_counter.mock.call_count == 1
+    def test(self):
+        remove_outs_call_counter = spy(dvc.stage.Stage.remove_outs)
+        with patch.object(
+            dvc.stage.Stage, "remove_outs", remove_outs_call_counter
+        ):
+            ret = main(["import-url", self.external_source])
+            self.assertEqual(0, ret)
+
+        self.assertEqual(1, remove_outs_call_counter.mock.call_count)
 
 
 class TestImportFilename(TestDvc):
diff --git a/tests/unit/utils/test_fs.py b/tests/unit/utils/test_fs.py
index 07c62e0a2..0c0e19a0b 100644
--- a/tests/unit/utils/test_fs.py
+++ b/tests/unit/utils/test_fs.py
@@ -22,6 +22,7 @@ from dvc.utils.fs import path_isin, remove
 from dvc.utils.fs import makedirs
 from dvc.utils.fs import walk_files
 from tests.basic_env import TestDir
+from tests.utils import spy
 
 
 class TestMtimeAndSize(TestDir):
@@ -69,6 +70,21 @@ class TestContainsLink(TestCase):
         )
         dirname_patch.assert_called_once()
 
+    @patch.object(System, "is_symlink", return_value=False)
+    def test_should_call_recursive_on_no_condition_matched(self, _):
+        contains_symlink_spy = spy(contains_symlink_up_to)
+        with patch.object(
+            dvc.utils.fs, "contains_symlink_up_to", contains_symlink_spy
+        ):
+
+            # call from full path to match contains_symlink_spy patch path
+            self.assertFalse(
+                dvc.utils.fs.contains_symlink_up_to(
+                    os.path.join("foo", "path"), "foo"
+                )
+            )
+            self.assertEqual(2, contains_symlink_spy.mock.call_count)
+
     @patch.object(System, "is_symlink", return_value=True)
     def test_should_return_false_when_base_path_is_symlink(self, _):
         base_path = "foo"
@@ -93,18 +109,6 @@ class TestContainsLink(TestCase):
         )
 
 
-def test_should_call_recursive_on_no_condition_matched(mocker):
-    mocker.patch.object(System, "is_symlink", return_value=False)
-
-    contains_symlink_spy = mocker.spy(dvc.utils.fs, "contains_symlink_up_to")
-
-    # call from full path to match contains_symlink_spy patch path
-    assert not dvc.utils.fs.contains_symlink_up_to(
-        os.path.join("foo", "path"), "foo"
-    )
-    assert contains_symlink_spy.mock.call_count == 2
-
-
 @pytest.mark.skipif(os.name != "nt", reason="Windows specific")
 def test_relpath_windows_different_drives():
     path1 = os.path.join("A:", os.sep, "some", "path")
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
index 9e5448abb..a40a91f2c 100644
--- a/tests/utils/__init__.py
+++ b/tests/utils/__init__.py
@@ -2,9 +2,22 @@ import os
 from contextlib import contextmanager
 from filecmp import dircmp
 
+from mock import MagicMock
+
 from dvc.scm import Git
 
 
+def spy(method_to_decorate):
+    mock = MagicMock()
+
+    def wrapper(self, *args, **kwargs):
+        mock(*args, **kwargs)
+        return method_to_decorate(self, *args, **kwargs)
+
+    wrapper.mock = mock
+    return wrapper
+
+
 def get_gitignore_content():
     with open(Git.GITIGNORE, "r") as gitignore:
         return gitignore.read().splitlines()

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==5.5.2
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@49039968f74282bdab3aa05b4582e5d392fc0ccc#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
gitdb2==4.0.2
GitPython==3.1.44
google-api-core==1.34.1
google-api-python-client==2.166.0
google-auth==2.38.0
google-auth-httplib2==0.2.0
google-cloud-core==1.5.0
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==3.20.3
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
PyDrive2==1.15.4
pyfastcopy==1.0.3
pyflakes==2.5.0
Pygments==2.17.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
speedcopy==2.1.5
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==5.5.2
      - 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.82.1+490399
      - 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
      - gitdb2==4.0.2
      - gitpython==3.1.44
      - google-api-core==1.34.1
      - google-api-python-client==2.166.0
      - google-auth==2.38.0
      - google-auth-httplib2==0.2.0
      - google-cloud-core==1.5.0
      - 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==3.20.3
      - 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
      - pydrive2==1.15.4
      - pyfastcopy==1.0.3
      - pyflakes==2.5.0
      - pygments==2.17.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
      - speedcopy==2.1.5
      - 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_api.py::test_get_url_requires_dvc
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_api.py::test_summon tests/func/test_data_cloud.py::TestRemoteLOCALCLI::test
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::TestShouldUpdateStateEntryForFileAfterAdd::test tests/func/test_add.py::TestShouldUpdateStateEntryForDirectoryAfterAdd::test tests/func/test_add.py::TestAddCommit::test tests/func/test_add.py::TestShouldCollectDirCacheOnlyOnce::test 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::TestShouldNotTrackGitInternalFiles::test 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_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_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::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::TestCheckSumRecalculation::test tests/func/test_data_cloud.py::TestShouldWarnOnNoChecksumInLocalAndRemoteCache::test tests/func/test_data_cloud.py::test_verify_checksums tests/func/test_import_url.py::TestCmdImport::test tests/func/test_import_url.py::TestCmdImport::test_unsupported tests/func/test_import_url.py::TestDefaultOutput::test tests/func/test_import_url.py::TestShouldRemoveOutsBeforeImport::test tests/func/test_import_url.py::TestImportFilename::test tests/func/test_import_url.py::test_import_url_to_dir[.] tests/func/test_import_url.py::test_import_url_to_dir[dir] tests/func/test_import_url.py::test_import_url_to_dir[dir/subdir] tests/unit/utils/test_fs.py::TestMtimeAndSize::test tests/unit/utils/test_fs.py::TestContainsLink::test_path_object_and_str_are_valid_arg_types tests/unit/utils/test_fs.py::TestContainsLink::test_should_call_recursive_on_no_condition_matched tests/unit/utils/test_fs.py::TestContainsLink::test_should_raise_exception_on_base_path_not_in_path tests/unit/utils/test_fs.py::TestContainsLink::test_should_return_false_on_no_more_dirs_below_path tests/unit/utils/test_fs.py::TestContainsLink::test_should_return_false_on_path_eq_to_base_path tests/unit/utils/test_fs.py::TestContainsLink::test_should_return_false_when_base_path_is_symlink tests/unit/utils/test_fs.py::TestContainsLink::test_should_return_true_on_symlink_in_path tests/unit/utils/test_fs.py::test_get_inode tests/unit/utils/test_fs.py::test_path_object_and_str_are_valid_types_get_mtime_and_size tests/unit/utils/test_fs.py::test_move tests/unit/utils/test_fs.py::test_remove tests/unit/utils/test_fs.py::test_path_isin_positive tests/unit/utils/test_fs.py::test_path_isin_on_same_path tests/unit/utils/test_fs.py::test_path_isin_on_common_substring_path tests/unit/utils/test_fs.py::test_path_isin_accepts_pathinfo tests/unit/utils/test_fs.py::test_path_isin_with_absolute_path tests/unit/utils/test_fs.py::test_makedirs tests/unit/utils/test_fs.py::test_copyfile[file] tests/unit/utils/test_fs.py::test_copyfile[dir] tests/unit/utils/test_fs.py::test_walk_files
dvc/external_repo.py dvc/api.py dvc/scm/git/__init__.py
dvc/external_repo.py dvc/api.py dvc/scm/git/__init__.py