1 # test_client.py -- Compatibilty tests for git client.
2 # Copyright (C) 2010 Google, Inc.
4 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
5 # General Public License as public by the Free Software Foundation; version 2.0
6 # or (at your option) any later version. You can redistribute it and/or
7 # modify it under the terms of either of these two licenses.
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # You should have received a copy of the licenses; if not, see
16 # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
17 # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
18 # License, Version 2.0.
21 """Compatibilty tests between the Dulwich client and the cgit server."""
24 from io import BytesIO
35 from urlparse import unquote
37 from urllib.parse import unquote
42 import SimpleHTTPServer
45 BaseHTTPServer = http.server
46 SimpleHTTPServer = http.server
57 from dulwich.tests import (
61 from dulwich.tests.compat.utils import (
71 if sys.platform == 'win32':
75 class DulwichClientTestBase(object):
76 """Tests for client/server compatibility."""
79 self.gitroot = os.path.dirname(
80 import_repo_to_dir('server_new.export').rstrip(os.sep))
81 self.dest = os.path.join(self.gitroot, 'dest')
82 file.ensure_dir_exists(self.dest)
83 run_git_or_fail(['init', '--quiet', '--bare'], cwd=self.dest)
86 rmtree_ro(self.gitroot)
88 def assertDestEqualsSrc(self):
89 repo_dir = os.path.join(self.gitroot, 'server_new.export')
90 dest_repo_dir = os.path.join(self.gitroot, 'dest')
91 with repo.Repo(repo_dir) as src:
92 with repo.Repo(dest_repo_dir) as dest:
93 self.assertReposEqual(src, dest)
96 raise NotImplementedError()
98 def _build_path(self):
99 raise NotImplementedError()
101 def _do_send_pack(self):
103 srcpath = os.path.join(self.gitroot, 'server_new.export')
104 with repo.Repo(srcpath) as src:
105 sendrefs = dict(src.get_refs())
106 del sendrefs[b'HEAD']
107 c.send_pack(self._build_path('/dest'), lambda _: sendrefs,
108 src.object_store.generate_pack_data)
110 def test_send_pack(self):
112 self.assertDestEqualsSrc()
114 def test_send_pack_nothing_to_send(self):
116 self.assertDestEqualsSrc()
117 # nothing to send, but shouldn't raise either.
120 def test_send_without_report_status(self):
122 c._send_capabilities.remove(b'report-status')
123 srcpath = os.path.join(self.gitroot, 'server_new.export')
124 with repo.Repo(srcpath) as src:
125 sendrefs = dict(src.get_refs())
126 del sendrefs[b'HEAD']
127 c.send_pack(self._build_path('/dest'), lambda _: sendrefs,
128 src.object_store.generate_pack_data)
129 self.assertDestEqualsSrc()
131 def make_dummy_commit(self, dest):
132 b = objects.Blob.from_string(b'hi')
133 dest.object_store.add_object(b)
134 t = index.commit_tree(dest.object_store, [(b'hi', b.id, 0o100644)])
136 c.author = c.committer = b'Foo Bar <foo@example.com>'
137 c.author_time = c.commit_time = 0
138 c.author_timezone = c.commit_timezone = 0
141 dest.object_store.add_object(c)
144 def disable_ff_and_make_dummy_commit(self):
145 # disable non-fast-forward pushes to the server
146 dest = repo.Repo(os.path.join(self.gitroot, 'dest'))
147 run_git_or_fail(['config', 'receive.denyNonFastForwards', 'true'],
149 commit_id = self.make_dummy_commit(dest)
150 return dest, commit_id
152 def compute_send(self, src):
153 sendrefs = dict(src.get_refs())
154 del sendrefs[b'HEAD']
155 return sendrefs, src.object_store.generate_pack_data
157 def test_send_pack_one_error(self):
158 dest, dummy_commit = self.disable_ff_and_make_dummy_commit()
159 dest.refs[b'refs/heads/master'] = dummy_commit
160 repo_dir = os.path.join(self.gitroot, 'server_new.export')
161 with repo.Repo(repo_dir) as src:
162 sendrefs, gen_pack = self.compute_send(src)
165 c.send_pack(self._build_path('/dest'),
166 lambda _: sendrefs, gen_pack)
167 except errors.UpdateRefsError as e:
168 self.assertEqual('refs/heads/master failed to update',
170 self.assertEqual({b'refs/heads/branch': b'ok',
171 b'refs/heads/master': b'non-fast-forward'},
174 def test_send_pack_multiple_errors(self):
175 dest, dummy = self.disable_ff_and_make_dummy_commit()
176 # set up for two non-ff errors
177 branch, master = b'refs/heads/branch', b'refs/heads/master'
178 dest.refs[branch] = dest.refs[master] = dummy
179 repo_dir = os.path.join(self.gitroot, 'server_new.export')
180 with repo.Repo(repo_dir) as src:
181 sendrefs, gen_pack = self.compute_send(src)
184 c.send_pack(self._build_path('/dest'), lambda _: sendrefs,
186 except errors.UpdateRefsError as e:
189 ['{0}, {1} failed to update'.format(
190 branch.decode('ascii'), master.decode('ascii')),
191 '{1}, {0} failed to update'.format(
192 branch.decode('ascii'), master.decode('ascii'))])
193 self.assertEqual({branch: b'non-fast-forward',
194 master: b'non-fast-forward'},
197 def test_archive(self):
200 c.archive(self._build_path('/server_new.export'), b'HEAD', f.write)
202 tf = tarfile.open(fileobj=f)
203 self.assertEqual(['baz', 'foo'], tf.getnames())
205 def test_fetch_pack(self):
207 with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
208 result = c.fetch(self._build_path('/server_new.export'), dest)
209 for r in result.refs.items():
210 dest.refs.set_if_equals(r[0], None, r[1])
211 self.assertDestEqualsSrc()
213 def test_repeat(self):
215 with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
216 result = c.fetch(self._build_path('/server_new.export'), dest)
217 for r in result.refs.items():
218 dest.refs.set_if_equals(r[0], None, r[1])
219 self.assertDestEqualsSrc()
220 result = c.fetch(self._build_path('/server_new.export'), dest)
221 for r in result.refs.items():
222 dest.refs.set_if_equals(r[0], None, r[1])
223 self.assertDestEqualsSrc()
225 def test_incremental_fetch_pack(self):
226 self.test_fetch_pack()
227 dest, dummy = self.disable_ff_and_make_dummy_commit()
228 dest.refs[b'refs/heads/master'] = dummy
230 repo_dir = os.path.join(self.gitroot, 'server_new.export')
231 with repo.Repo(repo_dir) as dest:
232 result = c.fetch(self._build_path('/dest'), dest)
233 for r in result.refs.items():
234 dest.refs.set_if_equals(r[0], None, r[1])
235 self.assertDestEqualsSrc()
237 def test_fetch_pack_no_side_band_64k(self):
239 c._fetch_capabilities.remove(b'side-band-64k')
240 with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
241 result = c.fetch(self._build_path('/server_new.export'), dest)
242 for r in result.refs.items():
243 dest.refs.set_if_equals(r[0], None, r[1])
244 self.assertDestEqualsSrc()
246 def test_fetch_pack_zero_sha(self):
247 # zero sha1s are already present on the client, and should
250 with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
252 self._build_path('/server_new.export'), dest,
253 lambda refs: [protocol.ZERO_SHA])
254 for r in result.refs.items():
255 dest.refs.set_if_equals(r[0], None, r[1])
257 def test_send_remove_branch(self):
258 with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
259 dummy_commit = self.make_dummy_commit(dest)
260 dest.refs[b'refs/heads/master'] = dummy_commit
261 dest.refs[b'refs/heads/abranch'] = dummy_commit
262 sendrefs = dict(dest.refs)
263 sendrefs[b'refs/heads/abranch'] = b"00" * 20
264 del sendrefs[b'HEAD']
266 def gen_pack(have, want, ofs_delta=False):
269 self.assertEqual(dest.refs[b"refs/heads/abranch"], dummy_commit)
271 self._build_path('/dest'), lambda _: sendrefs, gen_pack)
272 self.assertFalse(b"refs/heads/abranch" in dest.refs)
274 def test_get_refs(self):
276 refs = c.get_refs(self._build_path('/server_new.export'))
278 repo_dir = os.path.join(self.gitroot, 'server_new.export')
279 with repo.Repo(repo_dir) as dest:
280 self.assertDictEqual(dest.refs.as_dict(), refs)
283 class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
286 CompatTestCase.setUp(self)
287 DulwichClientTestBase.setUp(self)
288 if check_for_daemon(limit=1):
289 raise SkipTest('git-daemon was already running on port %s' %
290 protocol.TCP_GIT_PORT)
291 fd, self.pidfile = tempfile.mkstemp(prefix='dulwich-test-git-client',
293 os.fdopen(fd).close()
294 args = [_DEFAULT_GIT, 'daemon', '--verbose', '--export-all',
295 '--pid-file=%s' % self.pidfile,
296 '--base-path=%s' % self.gitroot,
297 '--enable=receive-pack', '--enable=upload-archive',
298 '--listen=localhost', '--reuseaddr',
300 self.process = subprocess.Popen(
301 args, cwd=self.gitroot,
302 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
303 if not check_for_daemon():
304 raise SkipTest('git-daemon failed to start')
307 with open(self.pidfile) as f:
308 pid = int(f.read().strip())
309 if sys.platform == 'win32':
310 PROCESS_TERMINATE = 1
311 handle = ctypes.windll.kernel32.OpenProcess(
312 PROCESS_TERMINATE, False, pid)
313 ctypes.windll.kernel32.TerminateProcess(handle, -1)
314 ctypes.windll.kernel32.CloseHandle(handle)
317 os.kill(pid, signal.SIGKILL)
318 os.unlink(self.pidfile)
319 except (OSError, IOError):
322 self.process.stdout.close()
323 self.process.stderr.close()
324 DulwichClientTestBase.tearDown(self)
325 CompatTestCase.tearDown(self)
328 return client.TCPGitClient('localhost')
330 def _build_path(self, path):
333 if sys.platform == 'win32':
335 def test_fetch_pack_no_side_band_64k(self):
336 DulwichClientTestBase.test_fetch_pack_no_side_band_64k(self)
339 class TestSSHVendor(object):
342 def run_command(host, command, username=None, port=None,
343 password=None, key_filename=None):
344 cmd, path = command.split(' ')
345 cmd = cmd.split('-', 1)
346 path = path.replace("'", "")
347 p = subprocess.Popen(cmd + [path], bufsize=0, stdin=subprocess.PIPE,
348 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
349 return client.SubprocessWrapper(p)
352 class DulwichMockSSHClientTest(CompatTestCase, DulwichClientTestBase):
355 CompatTestCase.setUp(self)
356 DulwichClientTestBase.setUp(self)
357 self.real_vendor = client.get_ssh_vendor
358 client.get_ssh_vendor = TestSSHVendor
361 DulwichClientTestBase.tearDown(self)
362 CompatTestCase.tearDown(self)
363 client.get_ssh_vendor = self.real_vendor
366 return client.SSHGitClient('localhost')
368 def _build_path(self, path):
369 return self.gitroot + path
372 class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
375 CompatTestCase.setUp(self)
376 DulwichClientTestBase.setUp(self)
379 DulwichClientTestBase.tearDown(self)
380 CompatTestCase.tearDown(self)
383 return client.SubprocessGitClient(stderr=subprocess.PIPE)
385 def _build_path(self, path):
386 return self.gitroot + path
389 class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
390 """HTTP Request handler that calls out to 'git http-backend'."""
392 # Make rfile unbuffered -- we need to read one line and then pass
393 # the rest to a subprocess, so we can't use buffered input.
403 return self.run_backend()
405 def log_request(self, code='-', size='-'):
406 # Let's be quiet, the test suite is noisy enough already
409 def run_backend(self):
410 """Call out to git http-backend."""
411 # Based on CGIHTTPServer.CGIHTTPRequestHandler.run_cgi:
412 # Copyright (c) 2001-2010 Python Software Foundation;
413 # All Rights Reserved
414 # Licensed under the Python Software Foundation License.
416 # find an explicit query string, if present.
419 rest, query = rest[:i], rest[i+1:]
423 env = copy.deepcopy(os.environ)
424 env['SERVER_SOFTWARE'] = self.version_string()
425 env['SERVER_NAME'] = self.server.server_name
426 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
427 env['SERVER_PROTOCOL'] = self.protocol_version
428 env['SERVER_PORT'] = str(self.server.server_port)
429 env['GIT_PROJECT_ROOT'] = self.server.root_path
430 env["GIT_HTTP_EXPORT_ALL"] = "1"
431 env['REQUEST_METHOD'] = self.command
432 uqrest = unquote(rest)
433 env['PATH_INFO'] = uqrest
434 env['SCRIPT_NAME'] = "/"
436 env['QUERY_STRING'] = query
437 host = self.address_string()
438 if host != self.client_address[0]:
439 env['REMOTE_HOST'] = host
440 env['REMOTE_ADDR'] = self.client_address[0]
441 authorization = self.headers.get("authorization")
443 authorization = authorization.split()
444 if len(authorization) == 2:
447 env['AUTH_TYPE'] = authorization[0]
448 if authorization[0].lower() == "basic":
450 authorization = base64.decodestring(authorization[1])
451 except binascii.Error:
454 authorization = authorization.split(':')
455 if len(authorization) == 2:
456 env['REMOTE_USER'] = authorization[0]
458 content_type = self.headers.get('content-type')
460 env['CONTENT_TYPE'] = content_type
461 length = self.headers.get('content-length')
463 env['CONTENT_LENGTH'] = length
464 referer = self.headers.get('referer')
466 env['HTTP_REFERER'] = referer
468 for line in self.headers.getallmatchingheaders('accept'):
469 if line[:1] in "\t\n\r ":
470 accept.append(line.strip())
472 accept = accept + line[7:].split(',')
473 env['HTTP_ACCEPT'] = ','.join(accept)
474 ua = self.headers.get('user-agent')
476 env['HTTP_USER_AGENT'] = ua
477 co = self.headers.get('cookie')
479 env['HTTP_COOKIE'] = co
480 # XXX Other HTTP_* headers
481 # Since we're setting the env in the parent, provide empty
482 # values to override previously set values
483 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
484 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
485 env.setdefault(k, "")
487 self.wfile.write(b"HTTP/1.1 200 Script output follows\r\n")
489 ("Server: %s\r\n" % self.server.server_name).encode('ascii'))
491 ("Date: %s\r\n" % self.date_time_string()).encode('ascii'))
493 decoded_query = query.replace('+', ' ')
497 except (TypeError, ValueError):
499 if self.command.lower() == "post" and nbytes > 0:
500 data = self.rfile.read(nbytes)
503 # throw away additional data [see bug #427345]
504 while select.select([self.rfile._sock], [], [], 0)[0]:
505 if not self.rfile._sock.recv(1):
507 args = ['http-backend']
508 if '=' not in decoded_query:
509 args.append(decoded_query)
510 stdout = run_git_or_fail(
511 args, input=data, env=env, stderr=subprocess.PIPE)
512 self.wfile.write(stdout)
515 class HTTPGitServer(BaseHTTPServer.HTTPServer):
517 allow_reuse_address = True
519 def __init__(self, server_address, root_path):
520 BaseHTTPServer.HTTPServer.__init__(
521 self, server_address, GitHTTPRequestHandler)
522 self.root_path = root_path
523 self.server_name = "localhost"
526 return 'http://%s:%s/' % (self.server_name, self.server_port)
529 class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):
531 min_git_version = (1, 7, 0, 2)
534 CompatTestCase.setUp(self)
535 DulwichClientTestBase.setUp(self)
536 self._httpd = HTTPGitServer(("localhost", 0), self.gitroot)
537 self.addCleanup(self._httpd.shutdown)
538 threading.Thread(target=self._httpd.serve_forever).start()
539 run_git_or_fail(['config', 'http.uploadpack', 'true'],
541 run_git_or_fail(['config', 'http.receivepack', 'true'],
545 DulwichClientTestBase.tearDown(self)
546 CompatTestCase.tearDown(self)
547 self._httpd.shutdown()
548 self._httpd.socket.close()
551 return client.HttpGitClient(self._httpd.get_url())
553 def _build_path(self, path):
556 def test_archive(self):
557 raise SkipTest("exporting archives not supported over http")