Skip to content

Commit adc35f2

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 6310480 commit adc35f2

9 files changed

+261
-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

+143-133
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,39 @@
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+
import mock
29+
from contextlib2 import ExitStack # @UnusedImport
30+
from backports.tempfile import TemporaryDirectory # @UnusedImport
31+
32+
2333
ospd = osp.dirname
2434

2535
GIT_REPO = os.environ.get("GIT_PYTHON_TEST_GIT_REPO_BASE", ospd(ospd(ospd(ospd(__file__)))))
2636
GIT_DAEMON_PORT = os.environ.get("GIT_PYTHON_TEST_GIT_DAEMON_PORT", "19418")
2737

2838
__all__ = (
2939
'fixture_path', 'fixture', 'StringProcessAdapter',
30-
'with_rw_directory', 'with_rw_repo', 'with_rw_and_rw_remote_repo', 'TestBase', 'TestCase',
40+
'with_rw_directory', 'with_rw_repo', 'rw_and_rw_remote_repos', 'TestBase', 'TestCase',
3141
'GIT_REPO', 'GIT_DAEMON_PORT'
3242
)
3343

@@ -162,39 +172,91 @@ def repo_creator(self):
162172
return argument_passer
163173

164174

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

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

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-
285+
with ExitStack() as stack:
286+
rw_remote_repo = stack.enter_context(tmp_clone(repo,
287+
clone_prefix="remote_bare_repo_%s",
288+
shared=True,
289+
bare=True))
290+
rw_repo = stack.enter_context(tmp_clone(rw_remote_repo,
291+
clone_prefix="remote_clone_non_bare_repo_",
292+
shared=True,
293+
bare=False,
294+
n=True))
295+
remote_repo_dir = rw_remote_repo.working_dir
296+
297+
# recursive alternates info ?
298+
rw_repo.head.commit = working_tree_ref
299+
rw_repo.head.reference.checkout()
300+
301+
# Allow git-daemon in bare-repo (https://git-scm.com/book/en/v2/Git-on-the-Server-Git-Daemon).
302+
rw_remote_repo.daemon_export = True
303+
304+
section = "daemon"
305+
with rw_remote_repo.config_writer() as crw:
257306
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)
307+
crw.add_section(section) # TODO: Add section if not exists.
308+
except Exception:
309+
pass
310+
crw.set(section, "receivepack", True)
292311

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
312+
# Initialize the non-bare repo - first do it as local remote and pull, then
313+
# we change the URL to point to the "relative" against "daemon's `--base-path`.
314+
#
315+
d_remote = Remote.create(rw_repo, "daemon_origin", remote_repo_dir)
316+
d_remote.fetch()
317+
base_path, rel_repo_dir = osp.split(remote_repo_dir)
318+
remote_repo_url = Git.polish_url("git://localhost:%s/%s" % (GIT_DAEMON_PORT, rel_repo_dir))
319+
with d_remote.config_writer as cw:
320+
cw.set('url', remote_repo_url)
300321

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)
322+
stack.enter_context(launch_git_daemon(Git.polish_url(base_path), '127.0.0.1', GIT_DAEMON_PORT))
310323

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
324+
# Try listing remotes, to diagnose whether the daemon is up.
325+
rw_repo.git.ls_remote(d_remote)
318326

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

321331
#} END decorators
322332

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)