Skip to content

Commit c328075

Browse files
authored
Merge pull request #2868 from Suor/better-fixtures
test: refactor tmp dir helper fixtures
2 parents baee120 + c27988a commit c328075

File tree

7 files changed

+335
-172
lines changed

7 files changed

+335
-172
lines changed

tests/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from git import Repo
88
from git.exc import GitCommandNotFound
99

10-
from .basic_env import TestDirFixture
11-
from .basic_env import TestDvcGitFixture
12-
from .basic_env import TestGitFixture
1310
from dvc.remote.config import RemoteConfig
1411
from dvc.remote.ssh.connection import SSHConnection
1512
from dvc.repo import Repo as DvcRepo
1613
from dvc.utils.compat import cast_bytes_py2
14+
from .basic_env import TestDirFixture, TestDvcGitFixture, TestGitFixture
15+
from .dir_helpers import * # noqa
16+
1717

1818
# Prevent updater and analytics from running their processes
1919
os.environ[cast_bytes_py2("DVC_TEST")] = cast_bytes_py2("true")

tests/dir_helpers.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
from __future__ import unicode_literals
2+
3+
import os
4+
import pytest
5+
6+
from funcy.py3 import lmap, retry
7+
8+
from dvc.utils import makedirs
9+
from dvc.utils.compat import basestring, is_py2, pathlib, fspath, fspath_py35
10+
11+
12+
__all__ = ["tmp_dir", "scm", "dvc", "repo_template", "run_copy", "erepo_dir"]
13+
REPO_TEMPLATE = {
14+
"foo": "foo",
15+
"bar": "bar",
16+
"dir": {
17+
"data": "dir/data text",
18+
"subdir": {"subdata": "dir/subdir/subdata text"},
19+
},
20+
}
21+
22+
23+
class TmpDir(pathlib.Path):
24+
def __new__(cls, *args, **kwargs):
25+
if cls is TmpDir:
26+
cls = WindowsTmpDir if os.name == "nt" else PosixTmpDir
27+
self = cls._from_parts(args, init=False)
28+
if not self._flavour.is_supported:
29+
raise NotImplementedError(
30+
"cannot instantiate %r on your system" % (cls.__name__,)
31+
)
32+
self._init()
33+
return self
34+
35+
# Not needed in Python 3.6+
36+
def __fspath__(self):
37+
return str(self)
38+
39+
def _require(self, name):
40+
if not hasattr(self, name):
41+
raise TypeError(
42+
"Can't use {name} for this temporary dir. "
43+
'Did you forget to use "{name}" fixture?'.format(name=name)
44+
)
45+
46+
def gen(self, struct, text=""):
47+
if isinstance(struct, basestring):
48+
struct = {struct: text}
49+
50+
self._gen(struct)
51+
return struct.keys()
52+
53+
def _gen(self, struct, prefix=None):
54+
for name, contents in struct.items():
55+
path = (prefix or self) / name
56+
57+
if isinstance(contents, dict):
58+
self._gen(contents, prefix=path)
59+
else:
60+
makedirs(path.parent, exist_ok=True)
61+
if is_py2 and isinstance(contents, str):
62+
path.write_bytes(contents)
63+
else:
64+
path.write_text(contents)
65+
66+
def dvc_gen(self, struct, text="", commit=None):
67+
paths = self.gen(struct, text)
68+
return self.dvc_add(paths, commit=commit)
69+
70+
def scm_gen(self, struct, text="", commit=None):
71+
paths = self.gen(struct, text)
72+
return self.scm_add(paths, commit=commit)
73+
74+
def dvc_add(self, filenames, commit=None):
75+
self._require("dvc")
76+
filenames = _coerce_filenames(filenames)
77+
78+
stages = self.dvc.add(filenames)
79+
if commit:
80+
stage_paths = [s.path for s in stages]
81+
self.scm_add(stage_paths, commit=commit)
82+
83+
return stages
84+
85+
def scm_add(self, filenames, commit=None):
86+
self._require("scm")
87+
filenames = _coerce_filenames(filenames)
88+
89+
self.scm.add(filenames)
90+
if commit:
91+
self.scm.commit(commit)
92+
93+
# Introspection methods
94+
def list(self):
95+
return [p.name for p in self.iterdir()]
96+
97+
98+
def _coerce_filenames(filenames):
99+
if isinstance(filenames, (basestring, pathlib.PurePath)):
100+
filenames = [filenames]
101+
return lmap(fspath, filenames)
102+
103+
104+
class WindowsTmpDir(TmpDir, pathlib.PureWindowsPath):
105+
pass
106+
107+
108+
class PosixTmpDir(TmpDir, pathlib.PurePosixPath):
109+
pass
110+
111+
112+
@pytest.fixture
113+
def tmp_dir(tmp_path, monkeypatch):
114+
monkeypatch.chdir(tmp_path)
115+
return TmpDir(fspath_py35(tmp_path))
116+
117+
118+
@pytest.fixture
119+
def scm(tmp_dir, request):
120+
# Use dvc.scm if available
121+
if "dvc" in request.fixturenames:
122+
dvc = request.getfixturevalue("dvc")
123+
tmp_dir.scm = dvc.scm
124+
yield dvc.scm
125+
126+
else:
127+
from dvc.scm.git import Git
128+
129+
_git_init()
130+
try:
131+
scm = tmp_dir.scm = Git(fspath(tmp_dir))
132+
yield scm
133+
finally:
134+
scm.close()
135+
136+
137+
def _git_init():
138+
from git import Repo
139+
from git.exc import GitCommandNotFound
140+
141+
# NOTE: handles EAGAIN error on BSD systems (osx in our case).
142+
# Otherwise when running tests you might get this exception:
143+
#
144+
# GitCommandNotFound: Cmd('git') not found due to:
145+
# OSError('[Errno 35] Resource temporarily unavailable')
146+
git = retry(5, GitCommandNotFound)(Repo.init)()
147+
git.close()
148+
149+
150+
@pytest.fixture
151+
def dvc(tmp_dir, request):
152+
from dvc.repo import Repo
153+
154+
if "scm" in request.fixturenames:
155+
if not hasattr(tmp_dir, "scm"):
156+
_git_init()
157+
158+
dvc = Repo.init(fspath(tmp_dir))
159+
dvc.scm.commit("init dvc")
160+
else:
161+
dvc = Repo.init(fspath(tmp_dir), no_scm=True)
162+
163+
try:
164+
tmp_dir.dvc = dvc
165+
yield dvc
166+
finally:
167+
dvc.close()
168+
169+
170+
@pytest.fixture
171+
def repo_template(tmp_dir):
172+
tmp_dir.gen(REPO_TEMPLATE)
173+
174+
175+
@pytest.fixture
176+
def run_copy(tmp_dir, dvc, request):
177+
tmp_dir.gen(
178+
"copy.py",
179+
"import sys, shutil\nshutil.copyfile(sys.argv[1], sys.argv[2])",
180+
)
181+
182+
# Do we need this?
183+
if "scm" in request.fixturenames:
184+
request.getfixturevalue("scm")
185+
tmp_dir.git_add("copy.py", commit="add copy.py")
186+
187+
def run_copy(src, dst, **run_kwargs):
188+
return dvc.run(
189+
cmd="python copy.py {} {}".format(src, dst),
190+
outs=[dst],
191+
deps=[src, "copy.py"],
192+
**run_kwargs
193+
)
194+
195+
return run_copy
196+
197+
198+
@pytest.fixture
199+
def erepo_dir(tmp_path_factory, monkeypatch):
200+
from dvc.repo import Repo
201+
from dvc.remote.config import RemoteConfig
202+
203+
path = TmpDir(fspath_py35(tmp_path_factory.mktemp("erepo")))
204+
205+
# Chdir for git and dvc to work locally
206+
monkeypatch.chdir(fspath_py35(path))
207+
208+
_git_init()
209+
path.dvc = Repo.init()
210+
path.scm = path.dvc.scm
211+
path.dvc_gen(REPO_TEMPLATE, commit="init repo")
212+
213+
rconfig = RemoteConfig(path.dvc.config)
214+
rconfig.add("upstream", path.dvc.cache.local.cache_dir, default=True)
215+
path.scm_add([path.dvc.config.config_file], commit="add remote")
216+
217+
path.dvc_gen("version", "master")
218+
path.scm_add([".gitignore", "version.dvc"], commit="master")
219+
220+
path.scm.checkout("branch", create_new=True)
221+
(path / "version").unlink() # For mac ???
222+
path.dvc_gen("version", "branch")
223+
path.scm_add([".gitignore", "version.dvc"], commit="branch")
224+
225+
path.scm.checkout("master")
226+
path.dvc.close()
227+
monkeypatch.undo() # Undo chdir
228+
229+
return path

tests/func/test_add.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_unicode(self):
5959
self.assertTrue(os.path.isfile(stage.path))
6060

6161

62-
class TestAddUnSupportedFile(TestDvc):
62+
class TestAddUnsupportedFile(TestDvc):
6363
def test(self):
6464
with self.assertRaises(DvcException):
6565
self.dvc.add("unsupported://unsupported")
@@ -132,15 +132,11 @@ def test(self):
132132
self.assertEqual(os.path.abspath("directory.dvc"), stage.path)
133133

134134

135-
def test_add_tracked_file(git, dvc_repo, repo_dir):
136-
fname = "tracked_file"
137-
repo_dir.create(fname, "tracked file contents")
138-
139-
dvc_repo.scm.add([fname])
140-
dvc_repo.scm.commit("add {}".format(fname))
135+
def test_add_tracked_file(tmp_dir, scm, dvc):
136+
tmp_dir.scm_gen("tracked_file", "...", commit="add tracked file")
141137

142138
with pytest.raises(OutputAlreadyTrackedError):
143-
dvc_repo.add(fname)
139+
dvc.add("tracked_file")
144140

145141

146142
class TestAddDirWithExistingCache(TestDvc):
@@ -474,23 +470,18 @@ def test(self):
474470
self.assertFalse(os.path.exists("foo.dvc"))
475471

476472

477-
def test_should_cleanup_after_failed_add(git, dvc_repo, repo_dir):
478-
stages = dvc_repo.add(repo_dir.FOO)
479-
assert len(stages) == 1
480-
481-
foo_stage_file = repo_dir.FOO + Stage.STAGE_FILE_SUFFIX
482-
483-
# corrupt stage file
484-
repo_dir.create(foo_stage_file, "this will break yaml structure")
473+
def test_should_cleanup_after_failed_add(tmp_dir, scm, dvc, repo_template):
474+
# Add and corrupt a stage file
475+
dvc.add("foo")
476+
tmp_dir.gen("foo.dvc", "- broken\nyaml")
485477

486478
with pytest.raises(StageFileCorruptedError):
487-
dvc_repo.add(repo_dir.BAR)
479+
dvc.add("bar")
488480

489-
bar_stage_file = repo_dir.BAR + Stage.STAGE_FILE_SUFFIX
490-
assert not os.path.exists(bar_stage_file)
481+
assert not os.path.exists("bar.dvc")
491482

492483
gitignore_content = get_gitignore_content()
493-
assert "/" + repo_dir.BAR not in gitignore_content
484+
assert "/bar" not in gitignore_content
494485

495486

496487
class TestShouldNotTrackGitInternalFiles(TestDvc):
@@ -634,7 +625,7 @@ def test_should_protect_on_repeated_add(link, dvc_repo, repo_dir):
634625
assert not os.access(repo_dir.FOO, os.W_OK)
635626

636627

637-
def test_escape_gitignore_entries(git, dvc_repo, repo_dir):
628+
def test_escape_gitignore_entries(tmp_dir, scm, dvc):
638629
fname = "file!with*weird#naming_[1].t?t"
639630
ignored_fname = r"/file\!with\*weird\#naming_\[1\].t\?t"
640631

@@ -644,8 +635,5 @@ def test_escape_gitignore_entries(git, dvc_repo, repo_dir):
644635
fname = "file!with_weird#naming_[1].txt"
645636
ignored_fname = r"/file\!with_weird\#naming_\[1\].txt"
646637

647-
os.rename(repo_dir.FOO, fname)
648-
649-
dvc_repo.add(fname)
650-
638+
tmp_dir.dvc_gen(fname, "...")
651639
assert ignored_fname in get_gitignore_content()

0 commit comments

Comments
 (0)