/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/__init__.py

  • Committer: v.ladeuil+lp at free
  • Date: 2006-10-12 14:29:32 UTC
  • mto: (2145.1.1 keepalive)
  • mto: This revision was merged to the branch mainline in revision 2146.
  • Revision ID: v.ladeuil+lp@free.fr-20061012142932-7221fe16d2b48fa3
Shuffle http related test code. Hopefully it ends up at the right place :)

* bzrlib/tests/HttpServer.py: 
New file. bzrlib.tests.ChrootedTestCase use HttpServer. So the
class can't be defined in bzrlib.tests.HTTPUtils because it
creates a circular dependency (bzrlib.tests.HTTPUtils needs to
import bzrlib.tests).

* bzrlib/transport/http/_urllib.py: 
Transfer test server definition to bzrlib.tests.HttpServer. Clean
up imports.

* bzrlib/transport/http/_pycurl.py: 
Transfer test server definition to bzrlib.tests.HttpServer. Clean
up imports.

* bzrlib/transport/http/__init__.py: 
Transfer all test related code to either bzrlib.tests.HttpServer
and bzrlib.tests.HTTPUtils.
Fix all use of TransportNotPossible and InvalidURL by prefixing it
by 'errors.' (this seems to be the preferred way in the rest of
bzr).
Get rid of unused imports.

* bzrlib/tests/test_transport.py:
(ReadonlyDecoratorTransportTest.test_local_parameters,
FakeNFSDecoratorTests.test_http_parameters): Use HttpServer from
bzrlib.tests.HttpServer instead of bzrlib.transport.http.

* bzrlib/tests/test_sftp_transport.py:
(set_test_transport_to_sftp): Use HttpServer from
bzrlib.tests.HttpServer instead of bzrlib.transport.http.

* bzrlib/tests/test_selftest.py:
(TestTestCaseWithTransport.test_get_readonly_url_http): Use
HttpServer from bzrlib.tests.HttpServer instead of
bzrlib.transport.http.

* bzrlib/tests/test_repository.py: 
Does *not* use HttpServer.

* bzrlib/tests/test_http.py: 
Build on top of bzrlib.tests.HttpServer and bzrlib.tests.HTTPUtils
instead of bzrlib.transport.http.

* bzrlib/tests/test_bzrdir.py:
(ChrootedTests.setUp): Use HttpServer from bzrlib.tests.HttpServer
instead of bzrlib.transport.http.

* bzrlib/tests/branch_implementations/test_http.py:
(HTTPBranchTests.setUp): Use HttpServer from bzrlib.tests.HttpServer
instead of bzrlib.transport.http.

* bzrlib/tests/branch_implementations/test_branch.py:
(ChrootedTests.setUp): Use HttpServer from bzrlib.tests.HttpServer
instead of bzrlib.transport.http.

* bzrlib/tests/__init__.py:
(ChrootedTestCase.setUp): Use HttpServer from
bzrlib.tests.HttpServer instead of bzrlib.transport.http.

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
"""
21
21
 
22
22
from cStringIO import StringIO
23
 
import errno
24
23
import mimetools
25
 
import os
26
 
import posixpath
27
 
import random
28
24
import re
29
 
import sys
30
25
import urlparse
31
26
import urllib
32
 
from warnings import warn
33
 
 
34
 
# TODO: load these only when running http tests
35
 
import BaseHTTPServer
36
 
from SimpleHTTPServer import SimpleHTTPRequestHandler
37
 
import socket
38
 
import threading
39
 
import time
40
27
 
41
28
from bzrlib import errors
42
 
from bzrlib.errors import (TransportNotPossible, NoSuchFile,
43
 
                           TransportError, ConnectionError, InvalidURL)
44
 
from bzrlib.branch import Branch
45
29
from bzrlib.trace import mutter
46
 
from bzrlib.transport import Transport, register_transport, Server
47
 
from bzrlib.transport.http.response import (HttpMultipartRangeResponse,
48
 
                                            HttpRangeResponse)
 
30
from bzrlib.transport import Transport
49
31
from bzrlib.ui import ui_factory
50
32
 
51
33
 
169
151
        """
170
152
        assert isinstance(relpath, basestring)
171
153
        if isinstance(relpath, unicode):
172
 
            raise InvalidURL(relpath, 'paths must not be unicode.')
 
154
            raise errors.InvalidURL(relpath, 'paths must not be unicode.')
173
155
        if isinstance(relpath, basestring):
174
156
            relpath_parts = relpath.split('/')
175
157
        else:
296
278
        :param relpath: Location to put the contents, relative to base.
297
279
        :param f:       File-like object.
298
280
        """
299
 
        raise TransportNotPossible('http PUT not supported')
 
281
        raise errors.TransportNotPossible('http PUT not supported')
300
282
 
301
283
    def mkdir(self, relpath, mode=None):
302
284
        """Create a directory at the given path."""
303
 
        raise TransportNotPossible('http does not support mkdir()')
 
285
        raise errors.TransportNotPossible('http does not support mkdir()')
304
286
 
305
287
    def rmdir(self, relpath):
306
288
        """See Transport.rmdir."""
307
 
        raise TransportNotPossible('http does not support rmdir()')
 
289
        raise errors.TransportNotPossible('http does not support rmdir()')
308
290
 
309
291
    def append_file(self, relpath, f, mode=None):
310
292
        """Append the text in the file-like object into the final
311
293
        location.
312
294
        """
313
 
        raise TransportNotPossible('http does not support append()')
 
295
        raise errors.TransportNotPossible('http does not support append()')
314
296
 
315
297
    def copy(self, rel_from, rel_to):
316
298
        """Copy the item at rel_from to the location at rel_to"""
317
 
        raise TransportNotPossible('http does not support copy()')
 
299
        raise errors.TransportNotPossible('http does not support copy()')
318
300
 
319
301
    def copy_to(self, relpaths, other, mode=None, pb=None):
320
302
        """Copy a set of entries from self into another Transport.
328
310
        # the remote location is the same, and rather than download, and
329
311
        # then upload, it could just issue a remote copy_this command.
330
312
        if isinstance(other, HttpTransportBase):
331
 
            raise TransportNotPossible('http cannot be the target of copy_to()')
 
313
            raise errors.TransportNotPossible(
 
314
                'http cannot be the target of copy_to()')
332
315
        else:
333
316
            return super(HttpTransportBase, self).\
334
317
                    copy_to(relpaths, other, mode=mode, pb=pb)
335
318
 
336
319
    def move(self, rel_from, rel_to):
337
320
        """Move the item at rel_from to the location at rel_to"""
338
 
        raise TransportNotPossible('http does not support move()')
 
321
        raise errors.TransportNotPossible('http does not support move()')
339
322
 
340
323
    def delete(self, relpath):
341
324
        """Delete the item at relpath"""
342
 
        raise TransportNotPossible('http does not support delete()')
 
325
        raise errors.TransportNotPossible('http does not support delete()')
343
326
 
344
327
    def is_readonly(self):
345
328
        """See Transport.is_readonly."""
352
335
    def stat(self, relpath):
353
336
        """Return the stat information for a file.
354
337
        """
355
 
        raise TransportNotPossible('http does not support stat()')
 
338
        raise errors.TransportNotPossible('http does not support stat()')
356
339
 
357
340
    def lock_read(self, relpath):
358
341
        """Lock the given file for shared (read) access.
373
356
 
374
357
        :return: A lock object, which should be passed to Transport.unlock()
375
358
        """
376
 
        raise TransportNotPossible('http does not support lock_write()')
 
359
        raise errors.TransportNotPossible('http does not support lock_write()')
377
360
 
378
361
    def clone(self, offset=None):
379
362
        """Return a new HttpTransportBase with root at self.base + offset
403
386
            strings.append('-%d' % tail_amount)
404
387
 
405
388
        return ','.join(strings)
406
 
 
407
 
 
408
 
#---------------- test server facilities ----------------
409
 
# TODO: load these only when running tests
410
 
# FIXME: By moving them to HTTPTestUtil.py ?
411
 
 
412
 
 
413
 
class WebserverNotAvailable(Exception):
414
 
    pass
415
 
 
416
 
 
417
 
class BadWebserverPath(ValueError):
418
 
    def __str__(self):
419
 
        return 'path %s is not in %s' % self.args
420
 
 
421
 
 
422
 
class TestingHTTPRequestHandler(SimpleHTTPRequestHandler):
423
 
 
424
 
    def log_message(self, format, *args):
425
 
        self.server.test_case.log('webserver - %s - - [%s] %s "%s" "%s"',
426
 
                                  self.address_string(),
427
 
                                  self.log_date_time_string(),
428
 
                                  format % args,
429
 
                                  self.headers.get('referer', '-'),
430
 
                                  self.headers.get('user-agent', '-'))
431
 
 
432
 
    def handle_one_request(self):
433
 
        """Handle a single HTTP request.
434
 
 
435
 
        You normally don't need to override this method; see the class
436
 
        __doc__ string for information on how to handle specific HTTP
437
 
        commands such as GET and POST.
438
 
 
439
 
        """
440
 
        for i in xrange(1,11): # Don't try more than 10 times
441
 
            try:
442
 
                self.raw_requestline = self.rfile.readline()
443
 
            except socket.error, e:
444
 
                if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
445
 
                    # omitted for now because some tests look at the log of
446
 
                    # the server and expect to see no errors.  see recent
447
 
                    # email thread. -- mbp 20051021. 
448
 
                    ## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
449
 
                    time.sleep(0.01)
450
 
                    continue
451
 
                raise
452
 
            else:
453
 
                break
454
 
        if not self.raw_requestline:
455
 
            self.close_connection = 1
456
 
            return
457
 
        if not self.parse_request(): # An error code has been sent, just exit
458
 
            return
459
 
        mname = 'do_' + self.command
460
 
        if getattr(self, mname, None) is None:
461
 
            self.send_error(501, "Unsupported method (%r)" % self.command)
462
 
            return
463
 
        method = getattr(self, mname)
464
 
        method()
465
 
 
466
 
    _range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)$')
467
 
    _tail_regexp = re.compile(r'^-(?P<tail>\d+)$')
468
 
 
469
 
    def parse_ranges(self, ranges_header):
470
 
        """Parse the range header value and returns ranges and tail"""
471
 
        tail = 0
472
 
        ranges = []
473
 
        assert ranges_header.startswith('bytes=')
474
 
        ranges_header = ranges_header[len('bytes='):]
475
 
        for range_str in ranges_header.split(','):
476
 
            range_match = self._range_regexp.match(range_str)
477
 
            if range_match is not None:
478
 
                ranges.append((int(range_match.group('start')),
479
 
                               int(range_match.group('end'))))
480
 
            else:
481
 
                tail_match = self._tail_regexp.match(range_str)
482
 
                if tail_match is not None:
483
 
                    tail = int(tail_match.group('tail'))
484
 
        return tail, ranges
485
 
 
486
 
    def send_range_content(self, file, start, length):
487
 
        file.seek(start)
488
 
        self.wfile.write(file.read(length))
489
 
 
490
 
    def get_single_range(self, file, file_size, start, end):
491
 
        self.send_response(206)
492
 
        length = end - start + 1
493
 
        self.send_header('Accept-Ranges', 'bytes')
494
 
        self.send_header("Content-Length", "%d" % length)
495
 
 
496
 
        self.send_header("Content-Type", 'application/octet-stream')
497
 
        self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
498
 
                                                              end,
499
 
                                                              file_size))
500
 
        self.end_headers()
501
 
        self.send_range_content(file, start, length)
502
 
 
503
 
    def get_multiple_ranges(self, file, file_size, ranges):
504
 
        self.send_response(206)
505
 
        self.send_header('Accept-Ranges', 'bytes')
506
 
        boundary = "%d" % random.randint(0,0x7FFFFFFF)
507
 
        self.send_header("Content-Type",
508
 
                         "multipart/byteranges; boundary=%s" % boundary)
509
 
        self.end_headers()
510
 
        for (start, end) in ranges:
511
 
            self.wfile.write("--%s\r\n" % boundary)
512
 
            self.send_header("Content-type", 'application/octet-stream')
513
 
            self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
514
 
                                                                  end,
515
 
                                                                  file_size))
516
 
            self.end_headers()
517
 
            self.send_range_content(file, start, end - start + 1)
518
 
            self.wfile.write("--%s\r\n" % boundary)
519
 
            pass
520
 
 
521
 
    def do_GET(self):
522
 
        """Serve a GET request.
523
 
 
524
 
        Handles the Range header.
525
 
        """
526
 
 
527
 
        path = self.translate_path(self.path)
528
 
        ranges_header_value = self.headers.get('Range')
529
 
        if ranges_header_value is None or os.path.isdir(path):
530
 
            # Let the mother class handle most cases
531
 
            return SimpleHTTPRequestHandler.do_GET(self)
532
 
 
533
 
        try:
534
 
            # Always read in binary mode. Opening files in text
535
 
            # mode may cause newline translations, making the
536
 
            # actual size of the content transmitted *less* than
537
 
            # the content-length!
538
 
            file = open(path, 'rb')
539
 
        except IOError:
540
 
            self.send_error(404, "File not found")
541
 
            return None
542
 
 
543
 
        file_size = os.fstat(file.fileno())[6]
544
 
        tail, ranges = self.parse_ranges(ranges_header_value)
545
 
        # Normalize tail into ranges
546
 
        if tail != 0:
547
 
            ranges.append((file_size - tail, file_size))
548
 
 
549
 
        ranges_valid = True
550
 
        if len(ranges) == 0:
551
 
            ranges_valid = False
552
 
        else:
553
 
            for (start, end) in ranges:
554
 
                if start >= file_size or end >= file_size:
555
 
                    ranges_valid = False
556
 
                    break
557
 
        if not ranges_valid:
558
 
            # RFC2616 14-16 says that invalid Range headers
559
 
            # should be ignored and in that case, the whole file
560
 
            # should be returned as if no Range header was
561
 
            # present
562
 
            file.close() # Will be reopened by the following call
563
 
            return SimpleHTTPRequestHandler.do_GET(self)
564
 
 
565
 
        if len(ranges) == 1:
566
 
            (start, end) = ranges[0]
567
 
            self.get_single_range(file, file_size, start, end)
568
 
        else:
569
 
            self.get_multiple_ranges(file, file_size, ranges)
570
 
        file.close()
571
 
 
572
 
    if sys.platform == 'win32':
573
 
        # On win32 you cannot access non-ascii filenames without
574
 
        # decoding them into unicode first.
575
 
        # However, under Linux, you can access bytestream paths
576
 
        # without any problems. If this function was always active
577
 
        # it would probably break tests when LANG=C was set
578
 
        def translate_path(self, path):
579
 
            """Translate a /-separated PATH to the local filename syntax.
580
 
 
581
 
            For bzr, all url paths are considered to be utf8 paths.
582
 
            On Linux, you can access these paths directly over the bytestream
583
 
            request, but on win32, you must decode them, and access them
584
 
            as Unicode files.
585
 
            """
586
 
            # abandon query parameters
587
 
            path = urlparse.urlparse(path)[2]
588
 
            path = posixpath.normpath(urllib.unquote(path))
589
 
            path = path.decode('utf-8')
590
 
            words = path.split('/')
591
 
            words = filter(None, words)
592
 
            path = os.getcwdu()
593
 
            for word in words:
594
 
                drive, word = os.path.splitdrive(word)
595
 
                head, word = os.path.split(word)
596
 
                if word in (os.curdir, os.pardir): continue
597
 
                path = os.path.join(path, word)
598
 
            return path
599
 
 
600
 
 
601
 
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
602
 
    def __init__(self, server_address, RequestHandlerClass, test_case):
603
 
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
604
 
                                                RequestHandlerClass)
605
 
        self.test_case = test_case
606
 
 
607
 
 
608
 
class HttpServer(Server):
609
 
    """A test server for http transports.
610
 
 
611
 
    Subclasses can provide a specific request handler.
612
 
    """
613
 
 
614
 
    # used to form the url that connects to this server
615
 
    _url_protocol = 'http'
616
 
 
617
 
    # Subclasses can provide a specific request handler
618
 
    def __init__(self, request_handler=TestingHTTPRequestHandler):
619
 
        Server.__init__(self)
620
 
        self.request_handler = request_handler
621
 
 
622
 
    def _http_start(self):
623
 
        httpd = None
624
 
        httpd = TestingHTTPServer(('localhost', 0),
625
 
                                  self.request_handler,
626
 
                                  self)
627
 
        host, port = httpd.socket.getsockname()
628
 
        self._http_base_url = '%s://localhost:%s/' % (self._url_protocol, port)
629
 
        self._http_starting.release()
630
 
        httpd.socket.settimeout(0.1)
631
 
 
632
 
        while self._http_running:
633
 
            try:
634
 
                httpd.handle_request()
635
 
            except socket.timeout:
636
 
                pass
637
 
 
638
 
    def _get_remote_url(self, path):
639
 
        path_parts = path.split(os.path.sep)
640
 
        if os.path.isabs(path):
641
 
            if path_parts[:len(self._local_path_parts)] != \
642
 
                   self._local_path_parts:
643
 
                raise BadWebserverPath(path, self.test_dir)
644
 
            remote_path = '/'.join(path_parts[len(self._local_path_parts):])
645
 
        else:
646
 
            remote_path = '/'.join(path_parts)
647
 
 
648
 
        self._http_starting.acquire()
649
 
        self._http_starting.release()
650
 
        return self._http_base_url + remote_path
651
 
 
652
 
    def log(self, format, *args):
653
 
        """Capture Server log output."""
654
 
        self.logs.append(format % args)
655
 
 
656
 
    def setUp(self):
657
 
        """See bzrlib.transport.Server.setUp."""
658
 
        self._home_dir = os.getcwdu()
659
 
        self._local_path_parts = self._home_dir.split(os.path.sep)
660
 
        self._http_starting = threading.Lock()
661
 
        self._http_starting.acquire()
662
 
        self._http_running = True
663
 
        self._http_base_url = None
664
 
        self._http_thread = threading.Thread(target=self._http_start)
665
 
        self._http_thread.setDaemon(True)
666
 
        self._http_thread.start()
667
 
        self._http_proxy = os.environ.get("http_proxy")
668
 
        if self._http_proxy is not None:
669
 
            del os.environ["http_proxy"]
670
 
        self.logs = []
671
 
 
672
 
    def tearDown(self):
673
 
        """See bzrlib.transport.Server.tearDown."""
674
 
        self._http_running = False
675
 
        self._http_thread.join()
676
 
        if self._http_proxy is not None:
677
 
            import os
678
 
            os.environ["http_proxy"] = self._http_proxy
679
 
 
680
 
    def get_url(self):
681
 
        """See bzrlib.transport.Server.get_url."""
682
 
        return self._get_remote_url(self._home_dir)
683
 
 
684
 
    def get_bogus_url(self):
685
 
        """See bzrlib.transport.Server.get_bogus_url."""
686
 
        # this is chosen to try to prevent trouble with proxies, weird dns,
687
 
        # etc
688
 
        return 'http://127.0.0.1:1/'
689
 
 
690
 
 
691
 
class WallRequestHandler(TestingHTTPRequestHandler):
692
 
    """Whatever request comes in, close the connection"""
693
 
 
694
 
    def handle_one_request(self):
695
 
        """Handle a single HTTP request, by abruptly closing the connection"""
696
 
        self.close_connection = 1
697
 
 
698
 
 
699
 
class BadStatusRequestHandler(TestingHTTPRequestHandler):
700
 
    """Whatever request comes in, returns a bad status"""
701
 
 
702
 
    def parse_request(self):
703
 
        """Fakes handling a single HTTP request, returns a bad status"""
704
 
        ignored = TestingHTTPRequestHandler.parse_request(self)
705
 
        import socket
706
 
        try:
707
 
            self.send_response(0, "Bad status")
708
 
            self.end_headers()
709
 
        except socket.error, e:
710
 
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
711
 
                # We don't want to pollute the test reuslts with
712
 
                # spurious server errors while test succeed. In
713
 
                # our case, it may occur that the test have
714
 
                # already read the 'Bad Status' and closed the
715
 
                # socket while we are still trying to send some
716
 
                # headers... So the test is ok but if we raise
717
 
                # the exception the output is dirty. So we don't
718
 
                # raise, but we close the connection, just to be
719
 
                # safe :)
720
 
                self.close_connection = 1
721
 
                pass
722
 
            else:
723
 
                raise
724
 
        return False
725
 
 
726
 
 
727
 
class InvalidStatusRequestHandler(TestingHTTPRequestHandler):
728
 
    """Whatever request comes in, returns am invalid status"""
729
 
 
730
 
    def parse_request(self):
731
 
        """Fakes handling a single HTTP request, returns a bad status"""
732
 
        ignored = TestingHTTPRequestHandler.parse_request(self)
733
 
        self.wfile.write("Invalid status line\r\n")
734
 
        return False
735
 
 
736
 
 
737
 
class BadProtocolRequestHandler(TestingHTTPRequestHandler):
738
 
    """Whatever request comes in, returns a bad protocol version"""
739
 
 
740
 
    def parse_request(self):
741
 
        """Fakes handling a single HTTP request, returns a bad status"""
742
 
        ignored = TestingHTTPRequestHandler.parse_request(self)
743
 
        # Returns an invalid protocol version, but curl just
744
 
        # ignores it and those cannot be tested.
745
 
        self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
746
 
                                           404,
747
 
                                           'Look at my protocol version'))
748
 
        return False