Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / tests / compat / test_client.py @ 959

History | View | Annotate | Download (19 KB)

1
# test_client.py -- Compatibilty tests for git client.
2
# Copyright (C) 2010 Google, Inc.
3
#
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.
8
#
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.
14
#
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.
19
#
20

    
21
"""Compatibilty tests between the Dulwich client and the cgit server."""
22

    
23
import copy
24
from io import BytesIO
25
import os
26
import select
27
import signal
28
import subprocess
29
import sys
30
import tarfile
31
import tempfile
32
import threading
33

    
34
try:
35
    from urlparse import unquote
36
except ImportError:
37
    from urllib.parse import unquote
38

    
39

    
40
try:
41
    import BaseHTTPServer
42
    import SimpleHTTPServer
43
except ImportError:
44
    import http.server
45
    BaseHTTPServer = http.server
46
    SimpleHTTPServer = http.server
47

    
48
if sys.platform == 'win32':
49
    import ctypes
50

    
51
from dulwich import (
52
    client,
53
    errors,
54
    file,
55
    index,
56
    protocol,
57
    objects,
58
    repo,
59
    )
60
from dulwich.tests import (
61
    SkipTest,
62
    expectedFailure,
63
    )
64
from dulwich.tests.compat.utils import (
65
    CompatTestCase,
66
    check_for_daemon,
67
    import_repo_to_dir,
68
    rmtree_ro,
69
    run_git_or_fail,
70
    _DEFAULT_GIT,
71
    )
72

    
73

    
74
class DulwichClientTestBase(object):
75
    """Tests for client/server compatibility."""
76

    
77
    def setUp(self):
78
        self.gitroot = os.path.dirname(import_repo_to_dir('server_new.export').rstrip(os.sep))
79
        self.dest = os.path.join(self.gitroot, 'dest')
80
        file.ensure_dir_exists(self.dest)
81
        run_git_or_fail(['init', '--quiet', '--bare'], cwd=self.dest)
82

    
83
    def tearDown(self):
84
        rmtree_ro(self.gitroot)
85

    
86
    def assertDestEqualsSrc(self):
87
        repo_dir = os.path.join(self.gitroot, 'server_new.export')
88
        dest_repo_dir = os.path.join(self.gitroot, 'dest')
89
        with repo.Repo(repo_dir) as src:
90
            with repo.Repo(dest_repo_dir) as dest:
91
                self.assertReposEqual(src, dest)
92

    
93
    def _client(self):
94
        raise NotImplementedError()
95

    
96
    def _build_path(self):
97
        raise NotImplementedError()
98

    
99
    def _do_send_pack(self):
100
        c = self._client()
101
        srcpath = os.path.join(self.gitroot, 'server_new.export')
102
        with repo.Repo(srcpath) as src:
103
            sendrefs = dict(src.get_refs())
104
            del sendrefs[b'HEAD']
105
            c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs,
106
                        src.object_store.generate_pack_contents)
107

    
108
    def test_send_pack(self):
109
        self._do_send_pack()
110
        self.assertDestEqualsSrc()
111

    
112
    def test_send_pack_nothing_to_send(self):
113
        self._do_send_pack()
114
        self.assertDestEqualsSrc()
115
        # nothing to send, but shouldn't raise either.
116
        self._do_send_pack()
117

    
118
    def test_send_without_report_status(self):
119
        c = self._client()
120
        c._send_capabilities.remove(b'report-status')
121
        srcpath = os.path.join(self.gitroot, 'server_new.export')
122
        with repo.Repo(srcpath) as src:
123
            sendrefs = dict(src.get_refs())
124
            del sendrefs[b'HEAD']
125
            c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs,
126
                        src.object_store.generate_pack_contents)
127
            self.assertDestEqualsSrc()
128

    
129
    def make_dummy_commit(self, dest):
130
        b = objects.Blob.from_string(b'hi')
131
        dest.object_store.add_object(b)
132
        t = index.commit_tree(dest.object_store, [(b'hi', b.id, 0o100644)])
133
        c = objects.Commit()
134
        c.author = c.committer = b'Foo Bar <foo@example.com>'
135
        c.author_time = c.commit_time = 0
136
        c.author_timezone = c.commit_timezone = 0
137
        c.message = b'hi'
138
        c.tree = t
139
        dest.object_store.add_object(c)
140
        return c.id
141

    
142
    def disable_ff_and_make_dummy_commit(self):
143
        # disable non-fast-forward pushes to the server
144
        dest = repo.Repo(os.path.join(self.gitroot, 'dest'))
145
        run_git_or_fail(['config', 'receive.denyNonFastForwards', 'true'],
146
                        cwd=dest.path)
147
        commit_id = self.make_dummy_commit(dest)
148
        return dest, commit_id
149

    
150
    def compute_send(self, src):
151
        sendrefs = dict(src.get_refs())
152
        del sendrefs[b'HEAD']
153
        return sendrefs, src.object_store.generate_pack_contents
154

    
155
    def test_send_pack_one_error(self):
156
        dest, dummy_commit = self.disable_ff_and_make_dummy_commit()
157
        dest.refs[b'refs/heads/master'] = dummy_commit
158
        repo_dir = os.path.join(self.gitroot, 'server_new.export')
159
        with repo.Repo(repo_dir) as src:
160
            sendrefs, gen_pack = self.compute_send(src)
161
            c = self._client()
162
            try:
163
                c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs, gen_pack)
164
            except errors.UpdateRefsError as e:
165
                self.assertEqual('refs/heads/master failed to update',
166
                                 e.args[0])
167
                self.assertEqual({b'refs/heads/branch': b'ok',
168
                                  b'refs/heads/master': b'non-fast-forward'},
169
                                 e.ref_status)
170

    
171
    def test_send_pack_multiple_errors(self):
172
        dest, dummy = self.disable_ff_and_make_dummy_commit()
173
        # set up for two non-ff errors
174
        branch, master = b'refs/heads/branch', b'refs/heads/master'
175
        dest.refs[branch] = dest.refs[master] = dummy
176
        repo_dir = os.path.join(self.gitroot, 'server_new.export')
177
        with repo.Repo(repo_dir) as src:
178
            sendrefs, gen_pack = self.compute_send(src)
179
            c = self._client()
180
            try:
181
                c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs, gen_pack)
182
            except errors.UpdateRefsError as e:
183
                self.assertIn(str(e),
184
                              ['{0}, {1} failed to update'.format(
185
                                  branch.decode('ascii'), master.decode('ascii')),
186
                               '{1}, {0} failed to update'.format(
187
                                   branch.decode('ascii'), master.decode('ascii'))])
188
                self.assertEqual({branch: b'non-fast-forward',
189
                                  master: b'non-fast-forward'},
190
                                 e.ref_status)
191

    
192
    def test_archive(self):
193
        c = self._client()
194
        f = BytesIO()
195
        c.archive(self._build_path(b'/server_new.export'), b'HEAD', f.write)
196
        f.seek(0)
197
        tf = tarfile.open(fileobj=f)
198
        self.assertEqual(['baz', 'foo'], tf.getnames())
199

    
200
    def test_fetch_pack(self):
201
        c = self._client()
202
        with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
203
            refs = c.fetch(self._build_path(b'/server_new.export'), dest)
204
            for r in refs.items():
205
                dest.refs.set_if_equals(r[0], None, r[1])
206
            self.assertDestEqualsSrc()
207

    
208
    def test_incremental_fetch_pack(self):
209
        self.test_fetch_pack()
210
        dest, dummy = self.disable_ff_and_make_dummy_commit()
211
        dest.refs[b'refs/heads/master'] = dummy
212
        c = self._client()
213
        repo_dir = os.path.join(self.gitroot, 'server_new.export')
214
        with repo.Repo(repo_dir) as dest:
215
            refs = c.fetch(self._build_path(b'/dest'), dest)
216
            for r in refs.items():
217
                dest.refs.set_if_equals(r[0], None, r[1])
218
            self.assertDestEqualsSrc()
219

    
220
    def test_fetch_pack_no_side_band_64k(self):
221
        c = self._client()
222
        c._fetch_capabilities.remove(b'side-band-64k')
223
        with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
224
            refs = c.fetch(self._build_path(b'/server_new.export'), dest)
225
            for r in refs.items():
226
                dest.refs.set_if_equals(r[0], None, r[1])
227
            self.assertDestEqualsSrc()
228

    
229
    def test_fetch_pack_zero_sha(self):
230
        # zero sha1s are already present on the client, and should
231
        # be ignored
232
        c = self._client()
233
        with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
234
            refs = c.fetch(self._build_path(b'/server_new.export'), dest,
235
                lambda refs: [protocol.ZERO_SHA])
236
            for r in refs.items():
237
                dest.refs.set_if_equals(r[0], None, r[1])
238

    
239
    def test_send_remove_branch(self):
240
        with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
241
            dummy_commit = self.make_dummy_commit(dest)
242
            dest.refs[b'refs/heads/master'] = dummy_commit
243
            dest.refs[b'refs/heads/abranch'] = dummy_commit
244
            sendrefs = dict(dest.refs)
245
            sendrefs[b'refs/heads/abranch'] = b"00" * 20
246
            del sendrefs[b'HEAD']
247
            gen_pack = lambda have, want: []
248
            c = self._client()
249
            self.assertEqual(dest.refs[b"refs/heads/abranch"], dummy_commit)
250
            c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs, gen_pack)
251
            self.assertFalse(b"refs/heads/abranch" in dest.refs)
252

    
253
    def test_get_refs(self):
254
        c = self._client()
255
        refs = c.get_refs(self._build_path(b'/server_new.export'))
256

    
257
        repo_dir = os.path.join(self.gitroot, 'server_new.export')
258
        with repo.Repo(repo_dir) as dest:
259
            self.assertDictEqual(dest.refs.as_dict(), refs)
260

    
261

    
262
class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
263

    
264
    def setUp(self):
265
        CompatTestCase.setUp(self)
266
        DulwichClientTestBase.setUp(self)
267
        if check_for_daemon(limit=1):
268
            raise SkipTest('git-daemon was already running on port %s' %
269
                              protocol.TCP_GIT_PORT)
270
        fd, self.pidfile = tempfile.mkstemp(prefix='dulwich-test-git-client',
271
                                            suffix=".pid")
272
        os.fdopen(fd).close()
273
        args = [_DEFAULT_GIT, 'daemon', '--verbose', '--export-all',
274
                '--pid-file=%s' % self.pidfile,
275
                '--base-path=%s' % self.gitroot,
276
                '--enable=receive-pack', '--enable=upload-archive',
277
                '--listen=localhost', '--reuseaddr',
278
                self.gitroot]
279
        self.process = subprocess.Popen(
280
            args, cwd=self.gitroot,
281
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
282
        if not check_for_daemon():
283
            raise SkipTest('git-daemon failed to start')
284

    
285
    def tearDown(self):
286
        with open(self.pidfile) as f:
287
            pid = int(f.read().strip())
288
        if sys.platform == 'win32':
289
            PROCESS_TERMINATE = 1
290
            handle = ctypes.windll.kernel32.OpenProcess(
291
                PROCESS_TERMINATE, False, pid)
292
            ctypes.windll.kernel32.TerminateProcess(handle, -1)
293
            ctypes.windll.kernel32.CloseHandle(handle)
294
        else:
295
            try:
296
                os.kill(pid, signal.SIGKILL)
297
                os.unlink(self.pidfile)
298
            except (OSError, IOError):
299
                pass
300
        self.process.wait()
301
        self.process.stdout.close()
302
        self.process.stderr.close()
303
        DulwichClientTestBase.tearDown(self)
304
        CompatTestCase.tearDown(self)
305

    
306
    def _client(self):
307
        return client.TCPGitClient('localhost')
308

    
309
    def _build_path(self, path):
310
        return path
311

    
312
    if sys.platform == 'win32':
313
        @expectedFailure
314
        def test_fetch_pack_no_side_band_64k(self):
315
            DulwichClientTestBase.test_fetch_pack_no_side_band_64k(self)
316

    
317

    
318
class TestSSHVendor(object):
319

    
320
    @staticmethod
321
    def run_command(host, command, username=None, port=None):
322
        cmd, path = command.split(b' ')
323
        cmd = cmd.split(b'-', 1)
324
        path = path.replace(b"'", b"")
325
        p = subprocess.Popen(cmd + [path], bufsize=0, stdin=subprocess.PIPE,
326
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
327
        return client.SubprocessWrapper(p)
328

    
329

    
330
class DulwichMockSSHClientTest(CompatTestCase, DulwichClientTestBase):
331

    
332
    def setUp(self):
333
        CompatTestCase.setUp(self)
334
        DulwichClientTestBase.setUp(self)
335
        self.real_vendor = client.get_ssh_vendor
336
        client.get_ssh_vendor = TestSSHVendor
337

    
338
    def tearDown(self):
339
        DulwichClientTestBase.tearDown(self)
340
        CompatTestCase.tearDown(self)
341
        client.get_ssh_vendor = self.real_vendor
342

    
343
    def _client(self):
344
        return client.SSHGitClient('localhost')
345

    
346
    def _build_path(self, path):
347
        return self.gitroot.encode(sys.getfilesystemencoding()) + path
348

    
349

    
350
class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
351

    
352
    def setUp(self):
353
        CompatTestCase.setUp(self)
354
        DulwichClientTestBase.setUp(self)
355

    
356
    def tearDown(self):
357
        DulwichClientTestBase.tearDown(self)
358
        CompatTestCase.tearDown(self)
359

    
360
    def _client(self):
361
        return client.SubprocessGitClient(stderr=subprocess.PIPE)
362

    
363
    def _build_path(self, path):
364
        return self.gitroot.encode(sys.getfilesystemencoding()) + path
365

    
366

    
367
class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
368
    """HTTP Request handler that calls out to 'git http-backend'."""
369

    
370
    # Make rfile unbuffered -- we need to read one line and then pass
371
    # the rest to a subprocess, so we can't use buffered input.
372
    rbufsize = 0
373

    
374
    def do_POST(self):
375
        self.run_backend()
376

    
377
    def do_GET(self):
378
        self.run_backend()
379

    
380
    def send_head(self):
381
        return self.run_backend()
382

    
383
    def log_request(self, code='-', size='-'):
384
        # Let's be quiet, the test suite is noisy enough already
385
        pass
386

    
387
    def run_backend(self):
388
        """Call out to git http-backend."""
389
        # Based on CGIHTTPServer.CGIHTTPRequestHandler.run_cgi:
390
        # Copyright (c) 2001-2010 Python Software Foundation; All Rights Reserved
391
        # Licensed under the Python Software Foundation License.
392
        rest = self.path
393
        # find an explicit query string, if present.
394
        i = rest.rfind('?')
395
        if i >= 0:
396
            rest, query = rest[:i], rest[i+1:]
397
        else:
398
            query = ''
399

    
400
        env = copy.deepcopy(os.environ)
401
        env['SERVER_SOFTWARE'] = self.version_string()
402
        env['SERVER_NAME'] = self.server.server_name
403
        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
404
        env['SERVER_PROTOCOL'] = self.protocol_version
405
        env['SERVER_PORT'] = str(self.server.server_port)
406
        env['GIT_PROJECT_ROOT'] = self.server.root_path
407
        env["GIT_HTTP_EXPORT_ALL"] = "1"
408
        env['REQUEST_METHOD'] = self.command
409
        uqrest = unquote(rest)
410
        env['PATH_INFO'] = uqrest
411
        env['SCRIPT_NAME'] = "/"
412
        if query:
413
            env['QUERY_STRING'] = query
414
        host = self.address_string()
415
        if host != self.client_address[0]:
416
            env['REMOTE_HOST'] = host
417
        env['REMOTE_ADDR'] = self.client_address[0]
418
        authorization = self.headers.get("authorization")
419
        if authorization:
420
            authorization = authorization.split()
421
            if len(authorization) == 2:
422
                import base64, binascii
423
                env['AUTH_TYPE'] = authorization[0]
424
                if authorization[0].lower() == "basic":
425
                    try:
426
                        authorization = base64.decodestring(authorization[1])
427
                    except binascii.Error:
428
                        pass
429
                    else:
430
                        authorization = authorization.split(':')
431
                        if len(authorization) == 2:
432
                            env['REMOTE_USER'] = authorization[0]
433
        # XXX REMOTE_IDENT
434
        env['CONTENT_TYPE'] = self.headers.get('content-type')
435
        length = self.headers.get('content-length')
436
        if length:
437
            env['CONTENT_LENGTH'] = length
438
        referer = self.headers.get('referer')
439
        if referer:
440
            env['HTTP_REFERER'] = referer
441
        accept = []
442
        for line in self.headers.getallmatchingheaders('accept'):
443
            if line[:1] in "\t\n\r ":
444
                accept.append(line.strip())
445
            else:
446
                accept = accept + line[7:].split(',')
447
        env['HTTP_ACCEPT'] = ','.join(accept)
448
        ua = self.headers.get('user-agent')
449
        if ua:
450
            env['HTTP_USER_AGENT'] = ua
451
        co = self.headers.get('cookie')
452
        if co:
453
            env['HTTP_COOKIE'] = co
454
        # XXX Other HTTP_* headers
455
        # Since we're setting the env in the parent, provide empty
456
        # values to override previously set values
457
        for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
458
                  'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
459
            env.setdefault(k, "")
460

    
461
        self.wfile.write(b"HTTP/1.1 200 Script output follows\r\n")
462
        self.wfile.write(
463
            ("Server: %s\r\n" % self.server.server_name).encode('ascii'))
464
        self.wfile.write(
465
            ("Date: %s\r\n" % self.date_time_string()).encode('ascii'))
466

    
467
        decoded_query = query.replace('+', ' ')
468

    
469
        try:
470
            nbytes = int(length)
471
        except (TypeError, ValueError):
472
            nbytes = 0
473
        if self.command.lower() == "post" and nbytes > 0:
474
            data = self.rfile.read(nbytes)
475
        else:
476
            data = None
477
        # throw away additional data [see bug #427345]
478
        while select.select([self.rfile._sock], [], [], 0)[0]:
479
            if not self.rfile._sock.recv(1):
480
                break
481
        args = ['http-backend']
482
        if '=' not in decoded_query:
483
            args.append(decoded_query)
484
        stdout = run_git_or_fail(args, input=data, env=env, stderr=subprocess.PIPE)
485
        self.wfile.write(stdout)
486

    
487

    
488
class HTTPGitServer(BaseHTTPServer.HTTPServer):
489

    
490
    allow_reuse_address = True
491

    
492
    def __init__(self, server_address, root_path):
493
        BaseHTTPServer.HTTPServer.__init__(self, server_address, GitHTTPRequestHandler)
494
        self.root_path = root_path
495
        self.server_name = "localhost"
496

    
497
    def get_url(self):
498
        return 'http://%s:%s/' % (self.server_name, self.server_port)
499

    
500

    
501
class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):
502

    
503
    min_git_version = (1, 7, 0, 2)
504

    
505
    def setUp(self):
506
        CompatTestCase.setUp(self)
507
        DulwichClientTestBase.setUp(self)
508
        self._httpd = HTTPGitServer(("localhost", 0), self.gitroot)
509
        self.addCleanup(self._httpd.shutdown)
510
        threading.Thread(target=self._httpd.serve_forever).start()
511
        run_git_or_fail(['config', 'http.uploadpack', 'true'],
512
                        cwd=self.dest)
513
        run_git_or_fail(['config', 'http.receivepack', 'true'],
514
                        cwd=self.dest)
515

    
516
    def tearDown(self):
517
        DulwichClientTestBase.tearDown(self)
518
        CompatTestCase.tearDown(self)
519
        self._httpd.shutdown()
520
        self._httpd.socket.close()
521

    
522
    def _client(self):
523
        return client.HttpGitClient(self._httpd.get_url())
524

    
525
    def _build_path(self, path):
526
        if sys.version_info[0] == 3:
527
            return path.decode('ascii')
528
        else:
529
            return path
530

    
531
    def test_archive(self):
532
        raise SkipTest("exporting archives not supported over http")