Skip to content

Commit 47efb41

Browse files
committed
test: rework git-daemon helper wrapper
+ Extensive use of contextlib `ExitStack`. - DO NOT LEAVE DIRS ON DISK on failures (impossible now that using tempfile standard files). + cmd: Use subprocess.DEVNULL instead of opening file. + PY2: Add `contextlib2` and `backports.tempfile` deps.
1 parent 49a8cdd commit 47efb41

9 files changed

+260
-235
lines changed

Diff for: .appveyor.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ install:
5252
conda info -a &
5353
conda install --yes --quiet pip
5454
)
55-
- pip install nose ddt wheel codecov
55+
- pip install nose ddt wheel codecov
5656
- IF "%PYTHON_VERSION%"=="2.7" (
57-
pip install mock
57+
pip install mock contextlib2 backports.tempfile
5858
)
5959

6060
## Copied from `init-tests-after-clone.sh`.

Diff for: .travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ install:
1818
- git submodule update --init --recursive
1919
- git fetch --tags
2020
- pip install codecov flake8 ddt sphinx
21+
- if [ "$TRAVIS_PYTHON_VERSION" == '2.7' ]; then pip install mock contextlib2 backports.tempfile; fi
2122

2223
# generate some reflog as git-python tests need it (in master)
2324
- ./init-tests-after-clone.sh

Diff for: git/test/lib/helper.py

+142-133
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,38 @@
55
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
66
from __future__ import print_function
77

8+
import contextlib
89
from functools import wraps
910
import io
1011
import logging
1112
import os
12-
import tempfile
13+
import tempfile # @UnusedImport
1314
import textwrap
1415
import time
1516
from unittest import TestCase
1617

1718
from git.compat import string_types, is_win
18-
from git.util import rmtree
19-
19+
from git.util import rmtree, cwd
2020
import os.path as osp
2121

2222

23+
try:
24+
from unittest import mock
25+
from contextlib import ExitStack
26+
from tempfile import TemporaryDirectory
27+
except ImportError: # PY2
28+
from contextlib2 import ExitStack # @UnusedImport
29+
from backports.tempfile import TemporaryDirectory # @UnusedImport
30+
31+
2332
ospd = osp.dirname
2433

2534
GIT_REPO = os.environ.get("GIT_PYTHON_TEST_GIT_REPO_BASE", ospd(ospd(ospd(ospd(__file__)))))
2635
GIT_DAEMON_PORT = os.environ.get("GIT_PYTHON_TEST_GIT_DAEMON_PORT", "19418")
2736

2837
__all__ = (
2938
'fixture_path', 'fixture', 'StringProcessAdapter',
30-
'with_rw_directory', 'with_rw_repo', 'with_rw_and_rw_remote_repo', 'TestBase', 'TestCase',
39+
'with_rw_directory', 'with_rw_repo', 'rw_and_rw_remote_repos', 'TestBase', 'TestCase',
3140
'GIT_REPO', 'GIT_DAEMON_PORT'
3241
)
3342

@@ -162,39 +171,91 @@ def repo_creator(self):
162171
return argument_passer
163172

164173

174+
@contextlib.contextmanager
165175
def launch_git_daemon(base_path, ip, port):
166-
from git import Git
167-
if is_win:
168-
## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\,
169-
# but if invoked as 'git daemon', it detaches from parent `git` cmd,
170-
# and then CANNOT DIE!
171-
# So, invoke it as a single command.
172-
## Cygwin-git has no daemon. But it can use MINGW's.
173-
#
174-
daemon_cmd = ['git-daemon',
175-
'--enable=receive-pack',
176-
'--listen=%s' % ip,
177-
'--port=%s' % port,
178-
'--base-path=%s' % base_path,
179-
base_path]
180-
gd = Git().execute(daemon_cmd, as_process=True)
181-
else:
182-
gd = Git().daemon(base_path,
183-
enable='receive-pack',
184-
listen=ip,
185-
port=port,
186-
base_path=base_path,
187-
as_process=True)
188-
# yes, I know ... fortunately, this is always going to work if sleep time is just large enough
189-
time.sleep(0.5)
190-
return gd
191-
192-
193-
def with_rw_and_rw_remote_repo(working_tree_ref):
176+
from git import Git # Avoid circular deps.
177+
178+
gd_launched = False
179+
try:
180+
if is_win:
181+
## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\,
182+
# but if invoked as 'git daemon', it detaches from parent `git` cmd,
183+
# and then CANNOT DIE!
184+
# So, invoke it as a single command.
185+
## Cygwin-git has no daemon. But it can use MINGW's.
186+
#
187+
daemon_cmd = ['git-daemon',
188+
'--enable=receive-pack',
189+
'--listen=%s' % ip,
190+
'--port=%s' % port,
191+
'--base-path=%s' % base_path,
192+
base_path]
193+
gd = Git().execute(daemon_cmd, as_process=True)
194+
else:
195+
gd = Git().daemon(base_path,
196+
enable='receive-pack',
197+
listen=ip,
198+
port=port,
199+
base_path=base_path,
200+
as_process=True)
201+
gd_launched = True
202+
# yes, I know ... fortunately, this is always going to work if sleep time is just large enough
203+
time.sleep(0.5 * (1 + is_win))
204+
205+
yield gd
206+
207+
except Exception as ex:
208+
msg = textwrap.dedent("""
209+
Launching git-daemon failed due to: %s
210+
Probably test will fail subsequently.
211+
212+
BUT you may start *git-daemon* manually with this command:"
213+
git daemon --enable=receive-pack --listen=%s --port=%s --base-path=%s %s
214+
You may also run the daemon on a different port by passing --port=<port>"
215+
and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>
216+
""")
217+
if is_win:
218+
msg += textwrap.dedent("""
219+
220+
On Windows,
221+
the `git-daemon.exe` must be in PATH.
222+
For MINGW, look into .\Git\mingw64\libexec\git-core\), but problems with paths might appear.
223+
CYGWIN has no daemon, but if one exists, it gets along fine (but has also paths problems).""")
224+
log.warning(msg, ex, ip, port, base_path, base_path, exc_info=1)
225+
226+
yield mock.MagicMock() # @UndefinedVariable
227+
228+
finally:
229+
if gd_launched:
230+
try:
231+
log.debug("Killing git-daemon...")
232+
gd.proc.kill()
233+
except Exception as ex:
234+
## Either it has died (and we're here), or it won't die, again here...
235+
log.debug("Hidden error while Killing git-daemon: %s", ex, exc_info=1)
236+
237+
238+
@contextlib.contextmanager
239+
def tmp_clone(repo, clone_prefix, **clone_kwargs):
240+
def cleanup_clone(repo):
241+
repo.git.clear_cache()
242+
import gc
243+
gc.collect()
244+
245+
with ExitStack() as stack:
246+
clone_dir = stack.enter_context(TemporaryDirectory(prefix=clone_prefix))
247+
clone = repo.clone(clone_dir, **clone_kwargs)
248+
stack.callback(cleanup_clone, clone)
249+
250+
yield clone
251+
252+
253+
@contextlib.contextmanager
254+
def rw_and_rw_remote_repos(repo, working_tree_ref):
194255
"""
195-
Same as with_rw_repo, but also provides a writable remote repository from which the
196-
rw_repo has been forked as well as a handle for a git-daemon that may be started to
197-
run the remote_repo.
256+
A context-manager creating the same temporary-repo as `with_rw_repo` and in addition
257+
a writable remote non-bare repository from which the rw_repo has been forked as well as a handle
258+
for a git-daemon that may be started to run the remote_repo.
198259
The remote repository was cloned as bare repository from the rorepo, wheras
199260
the rw repo has a working tree and was cloned from the remote repository.
200261
@@ -203,11 +264,13 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
203264
and should be an inetd service that serves tempdir.gettempdir() and all
204265
directories in it.
205266
206-
The following scetch demonstrates this::
207-
rorepo ---<bare clone>---> rw_remote_repo ---<clone>---> rw_repo
267+
The following sketch demonstrates this::
268+
rorepo ---<bare clone>---> remote_repo ---<clone>---> rw_repo
269+
270+
It is used like that::
208271
209-
The test case needs to support the following signature::
210-
def case(self, rw_repo, rw_remote_repo)
272+
with rw_and_rw_remote_repos(origin_repo) as (rw_repo, remote_repo):
273+
...
211274
212275
This setup allows you to test push and pull scenarios and hooks nicely.
213276
@@ -218,105 +281,51 @@ def case(self, rw_repo, rw_remote_repo)
218281

219282
assert isinstance(working_tree_ref, string_types), "Decorator requires ref name for working tree checkout"
220283

221-
def argument_passer(func):
222-
223-
@wraps(func)
224-
def remote_repo_creator(self):
225-
remote_repo_dir = _mktemp("remote_repo_%s" % func.__name__)
226-
repo_dir = _mktemp("remote_clone_non_bare_repo")
227-
228-
rw_remote_repo = self.rorepo.clone(remote_repo_dir, shared=True, bare=True)
229-
# recursive alternates info ?
230-
rw_repo = rw_remote_repo.clone(repo_dir, shared=True, bare=False, n=True)
231-
rw_repo.head.commit = working_tree_ref
232-
rw_repo.head.reference.checkout()
233-
234-
# prepare for git-daemon
235-
rw_remote_repo.daemon_export = True
236-
237-
# this thing is just annoying !
238-
with rw_remote_repo.config_writer() as crw:
239-
section = "daemon"
240-
try:
241-
crw.add_section(section)
242-
except Exception:
243-
pass
244-
crw.set(section, "receivepack", True)
245-
246-
# Initialize the remote - first do it as local remote and pull, then
247-
# we change the url to point to the daemon.
248-
d_remote = Remote.create(rw_repo, "daemon_origin", remote_repo_dir)
249-
d_remote.fetch()
250-
251-
base_path, rel_repo_dir = osp.split(remote_repo_dir)
252-
253-
remote_repo_url = Git.polish_url("git://localhost:%s/%s" % (GIT_DAEMON_PORT, rel_repo_dir))
254-
with d_remote.config_writer as cw:
255-
cw.set('url', remote_repo_url)
256-
284+
with ExitStack() as stack:
285+
rw_remote_repo = stack.enter_context(tmp_clone(repo,
286+
clone_prefix="remote_bare_repo_%s",
287+
shared=True,
288+
bare=True))
289+
rw_repo = stack.enter_context(tmp_clone(rw_remote_repo,
290+
clone_prefix="remote_clone_non_bare_repo_",
291+
shared=True,
292+
bare=False,
293+
n=True))
294+
remote_repo_dir = rw_remote_repo.working_dir
295+
296+
# recursive alternates info ?
297+
rw_repo.head.commit = working_tree_ref
298+
rw_repo.head.reference.checkout()
299+
300+
# Allow git-daemon in bare-repo (https://git-scm.com/book/en/v2/Git-on-the-Server-Git-Daemon).
301+
rw_remote_repo.daemon_export = True
302+
303+
section = "daemon"
304+
with rw_remote_repo.config_writer() as crw:
257305
try:
258-
gd = launch_git_daemon(Git.polish_url(base_path), '127.0.0.1', GIT_DAEMON_PORT)
259-
except Exception as ex:
260-
if is_win:
261-
msg = textwrap.dedent("""
262-
The `git-daemon.exe` must be in PATH.
263-
For MINGW, look into .\Git\mingw64\libexec\git-core\), but problems with paths might appear.
264-
CYGWIN has no daemon, but if one exists, it gets along fine (has also paths problems)
265-
Anyhow, alternatively try starting `git-daemon` manually:""")
266-
else:
267-
msg = "Please try starting `git-daemon` manually:"
268-
msg += textwrap.dedent("""
269-
git daemon --enable=receive-pack --base-path=%s %s
270-
You can also run the daemon on a different port by passing --port=<port>"
271-
and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>
272-
""" % (base_path, base_path))
273-
raise AssertionError(ex, msg)
274-
# END make assertion
275-
else:
276-
# Try listing remotes, to diagnose whether the daemon is up.
277-
rw_repo.git.ls_remote(d_remote)
278-
279-
# adjust working dir
280-
prev_cwd = os.getcwd()
281-
os.chdir(rw_repo.working_dir)
282-
283-
try:
284-
return func(self, rw_repo, rw_remote_repo)
285-
except:
286-
log.info("Keeping repos after failure: repo_dir = %s, remote_repo_dir = %s",
287-
repo_dir, remote_repo_dir)
288-
repo_dir = remote_repo_dir = None
289-
raise
290-
finally:
291-
os.chdir(prev_cwd)
306+
crw.add_section(section) # TODO: Add section if not exists.
307+
except Exception:
308+
pass
309+
crw.set(section, "receivepack", True)
292310

293-
finally:
294-
try:
295-
log.debug("Killing git-daemon...")
296-
gd.proc.kill()
297-
except:
298-
## Either it has died (and we're here), or it won't die, again here...
299-
pass
311+
# Initialize the non-bare repo - first do it as local remote and pull, then
312+
# we change the URL to point to the "relative" against "daemon's `--base-path`.
313+
#
314+
d_remote = Remote.create(rw_repo, "daemon_origin", remote_repo_dir)
315+
d_remote.fetch()
316+
base_path, rel_repo_dir = osp.split(remote_repo_dir)
317+
remote_repo_url = Git.polish_url("git://localhost:%s/%s" % (GIT_DAEMON_PORT, rel_repo_dir))
318+
with d_remote.config_writer as cw:
319+
cw.set('url', remote_repo_url)
300320

301-
rw_repo.git.clear_cache()
302-
rw_remote_repo.git.clear_cache()
303-
rw_repo = rw_remote_repo = None
304-
import gc
305-
gc.collect()
306-
if repo_dir:
307-
rmtree(repo_dir)
308-
if remote_repo_dir:
309-
rmtree(remote_repo_dir)
321+
stack.enter_context(launch_git_daemon(Git.polish_url(base_path), '127.0.0.1', GIT_DAEMON_PORT))
310322

311-
if gd is not None:
312-
gd.proc.wait()
313-
# END cleanup
314-
# END bare repo creator
315-
return remote_repo_creator
316-
# END remote repo creator
317-
# END argument parser
323+
# Try listing remotes, to diagnose whether the daemon is up.
324+
rw_repo.git.ls_remote(d_remote)
318325

319-
return argument_passer
326+
# adjust working dir
327+
stack.enter_context(cwd(rw_repo.working_dir))
328+
yield rw_repo, rw_remote_repo
320329

321330
#} END decorators
322331

Diff for: git/test/test_base.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
TestBase,
1515
assert_raises,
1616
with_rw_repo,
17-
with_rw_and_rw_remote_repo
17+
rw_and_rw_remote_repos
1818
)
1919
from git import (
2020
Blob,
@@ -25,6 +25,7 @@
2525
from git.objects.util import get_object_type_by_name
2626
from gitdb.util import hex_to_bin
2727
from git.compat import is_win
28+
from git.util import HIDE_WINDOWS_KNOWN_ERRORS
2829

2930

3031
class TestBase(TestBase):
@@ -110,11 +111,12 @@ def test_with_rw_repo(self, rw_repo):
110111
assert not rw_repo.config_reader("repository").getboolean("core", "bare")
111112
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
112113

113-
@with_rw_and_rw_remote_repo('0.1.6')
114-
def test_with_rw_remote_and_rw_repo(self, rw_repo, rw_remote_repo):
115-
assert not rw_repo.config_reader("repository").getboolean("core", "bare")
116-
assert rw_remote_repo.config_reader("repository").getboolean("core", "bare")
117-
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
114+
@skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "FIXME: Freezes!")
115+
def test_with_rw_remote_and_rw_repo(self):
116+
with rw_and_rw_remote_repos(self.rorepo, '0.1.6') as (rw_repo, rw_remote_repo):
117+
assert not rw_repo.config_reader("repository").getboolean("core", "bare")
118+
assert rw_remote_repo.config_reader("repository").getboolean("core", "bare")
119+
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
118120

119121
@skipIf(sys.version_info < (3,) and is_win,
120122
"Unicode woes, see https://github.com/gitpython-developers/GitPython/pull/519")

0 commit comments

Comments
 (0)