~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_http_response.py

  • Committer: Martin Pool
  • Date: 2005-07-11 02:55:35 UTC
  • Revision ID: mbp@sourcefrog.net-20050711025535-0990d4c48dce9542
- update testweave

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Tests from HTTP response parsing.
18
 
 
19
 
The handle_response method read the response body of a GET request an returns
20
 
the corresponding RangeFile.
21
 
 
22
 
There are four different kinds of RangeFile:
23
 
- a whole file whose size is unknown, seen as a simple byte stream,
24
 
- a whole file whose size is known, we can't read past its end,
25
 
- a single range file, a part of a file with a start and a size,
26
 
- a multiple range file, several consecutive parts with known start offset
27
 
  and size.
28
 
 
29
 
Some properties are common to all kinds:
30
 
- seek can only be forward (its really a socket underneath),
31
 
- read can't cross ranges,
32
 
- successive ranges are taken into account transparently,
33
 
 
34
 
- the expected pattern of use is either seek(offset)+read(size) or a single
35
 
  read with no size specified. For multiple range files, multiple read() will
36
 
  return the corresponding ranges, trying to read further will raise
37
 
  InvalidHttpResponse.
38
 
"""
39
 
 
40
 
from cStringIO import StringIO
41
 
import httplib
42
 
 
43
 
from bzrlib import (
44
 
    errors,
45
 
    tests,
46
 
    )
47
 
from bzrlib.transport.http import (
48
 
    response,
49
 
    _urllib2_wrappers,
50
 
    )
51
 
 
52
 
 
53
 
class ReadSocket(object):
54
 
    """A socket-like object that can be given a predefined content."""
55
 
 
56
 
    def __init__(self, data):
57
 
        self.readfile = StringIO(data)
58
 
 
59
 
    def makefile(self, mode='r', bufsize=None):
60
 
        return self.readfile
61
 
 
62
 
class FakeHTTPConnection(_urllib2_wrappers.HTTPConnection):
63
 
 
64
 
    def __init__(self, sock):
65
 
        _urllib2_wrappers.HTTPConnection.__init__(self, 'localhost')
66
 
        # Set the socket to bypass the connection
67
 
        self.sock = sock
68
 
 
69
 
    def send(self, str):
70
 
        """Ignores the writes on the socket."""
71
 
        pass
72
 
 
73
 
 
74
 
class TestHTTPConnection(tests.TestCase):
75
 
 
76
 
    def test_cleanup_pipe(self):
77
 
        sock = ReadSocket("""HTTP/1.1 200 OK\r
78
 
Content-Type: text/plain; charset=UTF-8\r
79
 
Content-Length: 18
80
 
\r
81
 
0123456789
82
 
garbage""")
83
 
        conn = FakeHTTPConnection(sock)
84
 
        # Simulate the request sending so that the connection will be able to
85
 
        # read the response.
86
 
        conn.putrequest('GET', 'http://localhost/fictious')
87
 
        conn.endheaders()
88
 
        # Now, get the response
89
 
        resp = conn.getresponse()
90
 
        # Read part of the response
91
 
        self.assertEquals('0123456789\n', resp.read(11))
92
 
        # Override the thresold to force the warning emission
93
 
        conn._range_warning_thresold = 6 # There are 7 bytes pending
94
 
        conn.cleanup_pipe()
95
 
        self.assertContainsRe(self._get_log(keep_log_file=True),
96
 
                              'Got a 200 response when asking')
97
 
 
98
 
 
99
 
class TestRangeFileMixin(object):
100
 
    """Tests for accessing the first range in a RangeFile."""
101
 
 
102
 
    # A simple string used to represent a file part (also called a range), in
103
 
    # which offsets are easy to calculate for test writers. It's used as a
104
 
    # building block with slight variations but basically 'a' is the first char
105
 
    # of the range and 'z' is the last.
106
 
    alpha = 'abcdefghijklmnopqrstuvwxyz'
107
 
 
108
 
    def test_can_read_at_first_access(self):
109
 
        """Test that the just created file can be read."""
110
 
        self.assertEquals(self.alpha, self._file.read())
111
 
 
112
 
    def test_seek_read(self):
113
 
        """Test seek/read inside the range."""
114
 
        f = self._file
115
 
        start = self.first_range_start
116
 
        # Before any use, tell() should be at the range start
117
 
        self.assertEquals(start, f.tell())
118
 
        cur = start # For an overall offset assertion
119
 
        f.seek(start + 3)
120
 
        cur += 3
121
 
        self.assertEquals('def', f.read(3))
122
 
        cur += len('def')
123
 
        f.seek(4, 1)
124
 
        cur += 4
125
 
        self.assertEquals('klmn', f.read(4))
126
 
        cur += len('klmn')
127
 
        # read(0) in the middle of a range
128
 
        self.assertEquals('', f.read(0))
129
 
        # seek in place
130
 
        here = f.tell()
131
 
        f.seek(0, 1)
132
 
        self.assertEquals(here, f.tell())
133
 
        self.assertEquals(cur, f.tell())
134
 
 
135
 
    def test_read_zero(self):
136
 
        f = self._file
137
 
        start = self.first_range_start
138
 
        self.assertEquals('', f.read(0))
139
 
        f.seek(10, 1)
140
 
        self.assertEquals('', f.read(0))
141
 
 
142
 
    def test_seek_at_range_end(self):
143
 
        f = self._file
144
 
        f.seek(26, 1)
145
 
 
146
 
    def test_read_at_range_end(self):
147
 
        """Test read behaviour at range end."""
148
 
        f = self._file
149
 
        self.assertEquals(self.alpha, f.read())
150
 
        self.assertEquals('', f.read(0))
151
 
        self.assertRaises(errors.InvalidRange, f.read, 1)
152
 
 
153
 
    def test_unbounded_read_after_seek(self):
154
 
        f = self._file
155
 
        f.seek(24, 1)
156
 
        # Should not cross ranges
157
 
        self.assertEquals('yz', f.read())
158
 
 
159
 
    def test_seek_backwards(self):
160
 
        f = self._file
161
 
        start = self.first_range_start
162
 
        f.seek(start)
163
 
        f.read(12)
164
 
        self.assertRaises(errors.InvalidRange, f.seek, start + 5)
165
 
 
166
 
    def test_seek_outside_single_range(self):
167
 
        f = self._file
168
 
        if f._size == -1 or f._boundary is not None:
169
 
            raise tests.TestNotApplicable('Needs a fully defined range')
170
 
        # Will seek past the range and then errors out
171
 
        self.assertRaises(errors.InvalidRange,
172
 
                          f.seek, self.first_range_start + 27)
173
 
 
174
 
    def test_read_past_end_of_range(self):
175
 
        f = self._file
176
 
        if f._size == -1:
177
 
            raise tests.TestNotApplicable("Can't check an unknown size")
178
 
        start = self.first_range_start
179
 
        f.seek(start + 20)
180
 
        self.assertRaises(errors.InvalidRange, f.read, 10)
181
 
 
182
 
    def test_seek_from_end(self):
183
 
       """Test seeking from the end of the file.
184
 
 
185
 
       The semantic is unclear in case of multiple ranges. Seeking from end
186
 
       exists only for the http transports, cannot be used if the file size is
187
 
       unknown and is not used in bzrlib itself. This test must be (and is)
188
 
       overridden by daughter classes.
189
 
 
190
 
       Reading from end makes sense only when a range has been requested from
191
 
       the end of the file (see HttpTransportBase._get() when using the
192
 
       'tail_amount' parameter). The HTTP response can only be a whole file or
193
 
       a single range.
194
 
       """
195
 
       f = self._file
196
 
       f.seek(-2, 2)
197
 
       self.assertEquals('yz', f.read())
198
 
 
199
 
 
200
 
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
201
 
    """Test a RangeFile for a whole file whose size is not known."""
202
 
 
203
 
    def setUp(self):
204
 
        super(TestRangeFileSizeUnknown, self).setUp()
205
 
        self._file = response.RangeFile('Whole_file_size_known',
206
 
                                        StringIO(self.alpha))
207
 
        # We define no range, relying on RangeFile to provide default values
208
 
        self.first_range_start = 0 # It's the whole file
209
 
 
210
 
    def test_seek_from_end(self):
211
 
        """See TestRangeFileMixin.test_seek_from_end.
212
 
 
213
 
        The end of the file can't be determined since the size is unknown.
214
 
        """
215
 
        self.assertRaises(errors.InvalidRange, self._file.seek, -1, 2)
216
 
 
217
 
    def test_read_at_range_end(self):
218
 
        """Test read behaviour at range end."""
219
 
        f = self._file
220
 
        self.assertEquals(self.alpha, f.read())
221
 
        self.assertEquals('', f.read(0))
222
 
        self.assertEquals('', f.read(1))
223
 
 
224
 
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
225
 
    """Test a RangeFile for a whole file whose size is known."""
226
 
 
227
 
    def setUp(self):
228
 
        super(TestRangeFileSizeKnown, self).setUp()
229
 
        self._file = response.RangeFile('Whole_file_size_known',
230
 
                                        StringIO(self.alpha))
231
 
        self._file.set_range(0, len(self.alpha))
232
 
        self.first_range_start = 0 # It's the whole file
233
 
 
234
 
 
235
 
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
236
 
    """Test a RangeFile for a single range."""
237
 
 
238
 
    def setUp(self):
239
 
        super(TestRangeFileSingleRange, self).setUp()
240
 
        self._file = response.RangeFile('Single_range_file',
241
 
                                        StringIO(self.alpha))
242
 
        self.first_range_start = 15
243
 
        self._file.set_range(self.first_range_start, len(self.alpha))
244
 
 
245
 
 
246
 
    def test_read_before_range(self):
247
 
        # This can't occur under normal circumstances, we have to force it
248
 
        f = self._file
249
 
        f._pos = 0 # Force an invalid pos
250
 
        self.assertRaises(errors.InvalidRange, f.read, 2)
251
 
 
252
 
class TestRangeFileMultipleRanges(tests.TestCase, TestRangeFileMixin):
253
 
    """Test a RangeFile for multiple ranges.
254
 
 
255
 
    The RangeFile used for the tests contains three ranges:
256
 
 
257
 
    - at offset 25: alpha
258
 
    - at offset 100: alpha
259
 
    - at offset 126: alpha.upper()
260
 
 
261
 
    The two last ranges are contiguous. This only rarely occurs (should not in
262
 
    fact) in real uses but may lead to hard to track bugs.
263
 
    """
264
 
 
265
 
    def setUp(self):
266
 
        super(TestRangeFileMultipleRanges, self).setUp()
267
 
 
268
 
        boundary = 'separation'
269
 
 
270
 
        content = ''
271
 
        self.first_range_start = 25
272
 
        file_size = 200 # big enough to encompass all ranges
273
 
        for (start, part) in [(self.first_range_start, self.alpha),
274
 
                              # Two contiguous ranges
275
 
                              (100, self.alpha),
276
 
                              (126, self.alpha.upper())]:
277
 
            content += self._multipart_byterange(part, start, boundary,
278
 
                                                 file_size)
279
 
        # Final boundary
280
 
        content += self._boundary_line(boundary)
281
 
 
282
 
        self._file = response.RangeFile('Multiple_ranges_file',
283
 
                                        StringIO(content))
284
 
        # Ranges are set by decoding the range headers, the RangeFile user is
285
 
        # supposed to call the following before using seek or read since it
286
 
        # requires knowing the *response* headers (in that case the boundary
287
 
        # which is part of the Content-Type header).
288
 
        self._file.set_boundary(boundary)
289
 
 
290
 
    def _boundary_line(self, boundary):
291
 
        """Helper to build the formatted boundary line."""
292
 
        return '--' + boundary + '\r\n'
293
 
 
294
 
    def _multipart_byterange(self, data, offset, boundary, file_size='*'):
295
 
        """Encode a part of a file as a multipart/byterange MIME type.
296
 
 
297
 
        When a range request is issued, the HTTP response body can be
298
 
        decomposed in parts, each one representing a range (start, size) in a
299
 
        file.
300
 
 
301
 
        :param data: The payload.
302
 
        :param offset: where data starts in the file
303
 
        :param boundary: used to separate the parts
304
 
        :param file_size: the size of the file containing the range (default to
305
 
            '*' meaning unknown)
306
 
 
307
 
        :return: a string containing the data encoded as it will appear in the
308
 
            HTTP response body.
309
 
        """
310
 
        bline = self._boundary_line(boundary)
311
 
        # Each range begins with a boundary line
312
 
        range = bline
313
 
        # A range is described by a set of headers, but only 'Content-Range' is
314
 
        # required for our implementation (TestHandleResponse below will
315
 
        # exercise ranges with multiple or missing headers')
316
 
        range += 'Content-Range: bytes %d-%d/%d\r\n' % (offset,
317
 
                                                        offset+len(data)-1,
318
 
                                                        file_size)
319
 
        range += '\r\n'
320
 
        # Finally the raw bytes
321
 
        range += data
322
 
        return range
323
 
 
324
 
    def test_read_all_ranges(self):
325
 
        f = self._file
326
 
        self.assertEquals(self.alpha, f.read()) # Read first range
327
 
        f.seek(100) # Trigger the second range recognition
328
 
        self.assertEquals(self.alpha, f.read()) # Read second range
329
 
        self.assertEquals(126, f.tell())
330
 
        f.seek(126) # Start of third range which is also the current pos !
331
 
        self.assertEquals('A', f.read(1))
332
 
        f.seek(10, 1)
333
 
        self.assertEquals('LMN', f.read(3))
334
 
 
335
 
    def test_seek_from_end(self):
336
 
        """See TestRangeFileMixin.test_seek_from_end."""
337
 
        # The actual implementation will seek from end for the first range only
338
 
        # and then fail. Since seeking from end is intended to be used for a
339
 
        # single range only anyway, this test just document the actual
340
 
        # behaviour.
341
 
        f = self._file
342
 
        f.seek(-2, 2)
343
 
        self.assertEquals('yz', f.read())
344
 
        self.assertRaises(errors.InvalidRange, f.seek, -2, 2)
345
 
 
346
 
    def test_seek_into_void(self):
347
 
        f = self._file
348
 
        start = self.first_range_start
349
 
        f.seek(start)
350
 
        # Seeking to a point between two ranges is possible (only once) but
351
 
        # reading there is forbidden
352
 
        f.seek(start + 40)
353
 
        # We crossed a range boundary, so now the file is positioned at the
354
 
        # start of the new range (i.e. trying to seek below 100 will error out)
355
 
        f.seek(100)
356
 
        f.seek(125)
357
 
 
358
 
    def test_seek_across_ranges(self):
359
 
        f = self._file
360
 
        start = self.first_range_start
361
 
        f.seek(126) # skip the two first ranges
362
 
        self.assertEquals('AB', f.read(2))
363
 
 
364
 
    def test_checked_read_dont_overflow_buffers(self):
365
 
        f = self._file
366
 
        start = self.first_range_start
367
 
        # We force a very low value to exercise all code paths in _checked_read
368
 
        f._discarded_buf_size = 8
369
 
        f.seek(126) # skip the two first ranges
370
 
        self.assertEquals('AB', f.read(2))
371
 
 
372
 
    def test_seek_twice_between_ranges(self):
373
 
        f = self._file
374
 
        start = self.first_range_start
375
 
        f.seek(start + 40) # Past the first range but before the second
376
 
        # Now the file is positioned at the second range start (100)
377
 
        self.assertRaises(errors.InvalidRange, f.seek, start + 41)
378
 
 
379
 
    def test_seek_at_range_end(self):
380
 
        """Test seek behavior at range end."""
381
 
        f = self._file
382
 
        f.seek(25 + 25)
383
 
        f.seek(100 + 25)
384
 
        f.seek(126 + 25)
385
 
 
386
 
    def test_read_at_range_end(self):
387
 
        f = self._file
388
 
        self.assertEquals(self.alpha, f.read())
389
 
        self.assertEquals(self.alpha, f.read())
390
 
        self.assertEquals(self.alpha.upper(), f.read())
391
 
        self.assertRaises(errors.InvalidHttpResponse, f.read, 1)
392
 
 
393
 
 
394
 
class TestRangeFileVarious(tests.TestCase):
395
 
    """Tests RangeFile aspects not covered elsewhere."""
396
 
 
397
 
    def test_seek_whence(self):
398
 
        """Test the seek whence parameter values."""
399
 
        f = response.RangeFile('foo', StringIO('abc'))
400
 
        f.set_range(0, 3)
401
 
        f.seek(0)
402
 
        f.seek(1, 1)
403
 
        f.seek(-1, 2)
404
 
        self.assertRaises(ValueError, f.seek, 0, 14)
405
 
 
406
 
    def test_range_syntax(self):
407
 
        """Test the Content-Range scanning."""
408
 
 
409
 
        f = response.RangeFile('foo', StringIO())
410
 
 
411
 
        def ok(expected, header_value):
412
 
            f.set_range_from_header(header_value)
413
 
            # Slightly peek under the covers to get the size
414
 
            self.assertEquals(expected, (f.tell(), f._size))
415
 
 
416
 
        ok((1, 10), 'bytes 1-10/11')
417
 
        ok((1, 10), 'bytes 1-10/*')
418
 
        ok((12, 2), '\tbytes 12-13/*')
419
 
        ok((28, 1), '  bytes 28-28/*')
420
 
        ok((2123, 2120), 'bytes  2123-4242/12310')
421
 
        ok((1, 10), 'bytes 1-10/ttt') # We don't check total (ttt)
422
 
 
423
 
        def nok(header_value):
424
 
            self.assertRaises(errors.InvalidHttpRange,
425
 
                              f.set_range_from_header, header_value)
426
 
 
427
 
        nok('bytes 10-2/3')
428
 
        nok('chars 1-2/3')
429
 
        nok('bytes xx-yyy/zzz')
430
 
        nok('bytes xx-12/zzz')
431
 
        nok('bytes 11-yy/zzz')
432
 
        nok('bytes10-2/3')
433
 
 
434
 
 
435
 
# Taken from real request responses
436
 
_full_text_response = (200, """HTTP/1.1 200 OK\r
437
 
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
438
 
Server: Apache/2.0.54 (Fedora)\r
439
 
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
440
 
ETag: "56691-23-38e9ae00"\r
441
 
Accept-Ranges: bytes\r
442
 
Content-Length: 35\r
443
 
Connection: close\r
444
 
Content-Type: text/plain; charset=UTF-8\r
445
 
\r
446
 
""", """Bazaar-NG meta directory, format 1
447
 
""")
448
 
 
449
 
 
450
 
_single_range_response = (206, """HTTP/1.1 206 Partial Content\r
451
 
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
452
 
Server: Apache/2.0.54 (Fedora)\r
453
 
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
454
 
ETag: "238a3c-16ec2-805c5540"\r
455
 
Accept-Ranges: bytes\r
456
 
Content-Length: 100\r
457
 
Content-Range: bytes 100-199/93890\r
458
 
Connection: close\r
459
 
Content-Type: text/plain; charset=UTF-8\r
460
 
\r
461
 
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
462
 
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
463
 
 
464
 
 
465
 
_single_range_no_content_type = (206, """HTTP/1.1 206 Partial Content\r
466
 
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
467
 
Server: Apache/2.0.54 (Fedora)\r
468
 
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
469
 
ETag: "238a3c-16ec2-805c5540"\r
470
 
Accept-Ranges: bytes\r
471
 
Content-Length: 100\r
472
 
Content-Range: bytes 100-199/93890\r
473
 
Connection: close\r
474
 
\r
475
 
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
476
 
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
477
 
 
478
 
 
479
 
_multipart_range_response = (206, """HTTP/1.1 206 Partial Content\r
480
 
Date: Tue, 11 Jul 2006 04:49:48 GMT\r
481
 
Server: Apache/2.0.54 (Fedora)\r
482
 
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
483
 
ETag: "238a3c-16ec2-805c5540"\r
484
 
Accept-Ranges: bytes\r
485
 
Content-Length: 1534\r
486
 
Connection: close\r
487
 
Content-Type: multipart/byteranges; boundary=418470f848b63279b\r
488
 
\r
489
 
\r""", """--418470f848b63279b\r
490
 
Content-type: text/plain; charset=UTF-8\r
491
 
Content-range: bytes 0-254/93890\r
492
 
\r
493
 
mbp@sourcefrog.net-20050309040815-13242001617e4a06
494
 
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e7627
495
 
mbp@sourcefrog.net-20050309040957-6cad07f466bb0bb8
496
 
mbp@sourcefrog.net-20050309041501-c840e09071de3b67
497
 
mbp@sourcefrog.net-20050309044615-c24a3250be83220a
498
 
\r
499
 
--418470f848b63279b\r
500
 
Content-type: text/plain; charset=UTF-8\r
501
 
Content-range: bytes 1000-2049/93890\r
502
 
\r
503
 
40-fd4ec249b6b139ab
504
 
mbp@sourcefrog.net-20050311063625-07858525021f270b
505
 
mbp@sourcefrog.net-20050311231934-aa3776aff5200bb9
506
 
mbp@sourcefrog.net-20050311231953-73aeb3a131c3699a
507
 
mbp@sourcefrog.net-20050311232353-f5e33da490872c6a
508
 
mbp@sourcefrog.net-20050312071639-0a8f59a34a024ff0
509
 
mbp@sourcefrog.net-20050312073432-b2c16a55e0d6e9fb
510
 
mbp@sourcefrog.net-20050312073831-a47c3335ece1920f
511
 
mbp@sourcefrog.net-20050312085412-13373aa129ccbad3
512
 
mbp@sourcefrog.net-20050313052251-2bf004cb96b39933
513
 
mbp@sourcefrog.net-20050313052856-3edd84094687cb11
514
 
mbp@sourcefrog.net-20050313053233-e30a4f28aef48f9d
515
 
mbp@sourcefrog.net-20050313053853-7c64085594ff3072
516
 
mbp@sourcefrog.net-20050313054757-a86c3f5871069e22
517
 
mbp@sourcefrog.net-20050313061422-418f1f73b94879b9
518
 
mbp@sourcefrog.net-20050313120651-497bd231b19df600
519
 
mbp@sourcefrog.net-20050314024931-eae0170ef25a5d1a
520
 
mbp@sourcefrog.net-20050314025438-d52099f915fe65fc
521
 
mbp@sourcefrog.net-20050314025539-637a636692c055cf
522
 
mbp@sourcefrog.net-20050314025737-55eb441f430ab4ba
523
 
mbp@sourcefrog.net-20050314025901-d74aa93bb7ee8f62
524
 
mbp@source\r
525
 
--418470f848b63279b--\r
526
 
""")
527
 
 
528
 
 
529
 
_multipart_squid_range_response = (206, """HTTP/1.0 206 Partial Content\r
530
 
Date: Thu, 31 Aug 2006 21:16:22 GMT\r
531
 
Server: Apache/2.2.2 (Unix) DAV/2\r
532
 
Last-Modified: Thu, 31 Aug 2006 17:57:06 GMT\r
533
 
Accept-Ranges: bytes\r
534
 
Content-Type: multipart/byteranges; boundary="squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196"\r
535
 
Content-Length: 598\r
536
 
X-Cache: MISS from localhost.localdomain\r
537
 
X-Cache-Lookup: HIT from localhost.localdomain:3128\r
538
 
Proxy-Connection: keep-alive\r
539
 
\r
540
 
""",
541
 
"""\r
542
 
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196\r
543
 
Content-Type: text/plain\r
544
 
Content-Range: bytes 0-99/18672\r
545
 
\r
546
 
# bzr knit index 8
547
 
 
548
 
scott@netsplit.com-20050708230047-47c7868f276b939f fulltext 0 863  :
549
 
scott@netsp\r
550
 
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196\r
551
 
Content-Type: text/plain\r
552
 
Content-Range: bytes 300-499/18672\r
553
 
\r
554
 
com-20050708231537-2b124b835395399a :
555
 
scott@netsplit.com-20050820234126-551311dbb7435b51 line-delta 1803 479 .scott@netsplit.com-20050820232911-dc4322a084eadf7e :
556
 
scott@netsplit.com-20050821213706-c86\r
557
 
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196--\r
558
 
""")
559
 
 
560
 
 
561
 
# This is made up
562
 
_full_text_response_no_content_type = (200, """HTTP/1.1 200 OK\r
563
 
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
564
 
Server: Apache/2.0.54 (Fedora)\r
565
 
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
566
 
ETag: "56691-23-38e9ae00"\r
567
 
Accept-Ranges: bytes\r
568
 
Content-Length: 35\r
569
 
Connection: close\r
570
 
\r
571
 
""", """Bazaar-NG meta directory, format 1
572
 
""")
573
 
 
574
 
 
575
 
_full_text_response_no_content_length = (200, """HTTP/1.1 200 OK\r
576
 
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
577
 
Server: Apache/2.0.54 (Fedora)\r
578
 
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
579
 
ETag: "56691-23-38e9ae00"\r
580
 
Accept-Ranges: bytes\r
581
 
Connection: close\r
582
 
Content-Type: text/plain; charset=UTF-8\r
583
 
\r
584
 
""", """Bazaar-NG meta directory, format 1
585
 
""")
586
 
 
587
 
 
588
 
_single_range_no_content_range = (206, """HTTP/1.1 206 Partial Content\r
589
 
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
590
 
Server: Apache/2.0.54 (Fedora)\r
591
 
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
592
 
ETag: "238a3c-16ec2-805c5540"\r
593
 
Accept-Ranges: bytes\r
594
 
Content-Length: 100\r
595
 
Connection: close\r
596
 
\r
597
 
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
598
 
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
599
 
 
600
 
 
601
 
_single_range_response_truncated = (206, """HTTP/1.1 206 Partial Content\r
602
 
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
603
 
Server: Apache/2.0.54 (Fedora)\r
604
 
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
605
 
ETag: "238a3c-16ec2-805c5540"\r
606
 
Accept-Ranges: bytes\r
607
 
Content-Length: 100\r
608
 
Content-Range: bytes 100-199/93890\r
609
 
Connection: close\r
610
 
Content-Type: text/plain; charset=UTF-8\r
611
 
\r
612
 
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
613
 
 
614
 
 
615
 
_invalid_response = (444, """HTTP/1.1 444 Bad Response\r
616
 
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
617
 
Connection: close\r
618
 
Content-Type: text/html; charset=iso-8859-1\r
619
 
\r
620
 
""", """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
621
 
<html><head>
622
 
<title>404 Not Found</title>
623
 
</head><body>
624
 
<h1>Not Found</h1>
625
 
<p>I don't know what I'm doing</p>
626
 
<hr>
627
 
</body></html>
628
 
""")
629
 
 
630
 
 
631
 
_multipart_no_content_range = (206, """HTTP/1.0 206 Partial Content\r
632
 
Content-Type: multipart/byteranges; boundary=THIS_SEPARATES\r
633
 
Content-Length: 598\r
634
 
\r
635
 
""",
636
 
"""\r
637
 
--THIS_SEPARATES\r
638
 
Content-Type: text/plain\r
639
 
\r
640
 
# bzr knit index 8
641
 
--THIS_SEPARATES\r
642
 
""")
643
 
 
644
 
 
645
 
_multipart_no_boundary = (206, """HTTP/1.0 206 Partial Content\r
646
 
Content-Type: multipart/byteranges; boundary=THIS_SEPARATES\r
647
 
Content-Length: 598\r
648
 
\r
649
 
""",
650
 
"""\r
651
 
--THIS_SEPARATES\r
652
 
Content-Type: text/plain\r
653
 
Content-Range: bytes 0-18/18672\r
654
 
\r
655
 
# bzr knit index 8
656
 
 
657
 
The range ended at the line above, this text is garbage instead of a boundary
658
 
line
659
 
""")
660
 
 
661
 
 
662
 
class TestHandleResponse(tests.TestCase):
663
 
 
664
 
    def _build_HTTPMessage(self, raw_headers):
665
 
        status_and_headers = StringIO(raw_headers)
666
 
        # Get rid of the status line
667
 
        status_and_headers.readline()
668
 
        msg = httplib.HTTPMessage(status_and_headers)
669
 
        return msg
670
 
 
671
 
    def get_response(self, a_response):
672
 
        """Process a supplied response, and return the result."""
673
 
        code, raw_headers, body = a_response
674
 
        msg = self._build_HTTPMessage(raw_headers)
675
 
        return response.handle_response('http://foo', code, msg,
676
 
                                        StringIO(a_response[2]))
677
 
 
678
 
    def test_full_text(self):
679
 
        out = self.get_response(_full_text_response)
680
 
        # It is a StringIO from the original data
681
 
        self.assertEqual(_full_text_response[2], out.read())
682
 
 
683
 
    def test_single_range(self):
684
 
        out = self.get_response(_single_range_response)
685
 
 
686
 
        out.seek(100)
687
 
        self.assertEqual(_single_range_response[2], out.read(100))
688
 
 
689
 
    def test_single_range_no_content(self):
690
 
        out = self.get_response(_single_range_no_content_type)
691
 
 
692
 
        out.seek(100)
693
 
        self.assertEqual(_single_range_no_content_type[2], out.read(100))
694
 
 
695
 
    def test_single_range_truncated(self):
696
 
        out = self.get_response(_single_range_response_truncated)
697
 
        # Content-Range declares 100 but only 51 present
698
 
        self.assertRaises(errors.ShortReadvError, out.seek, out.tell() + 51)
699
 
 
700
 
    def test_multi_range(self):
701
 
        out = self.get_response(_multipart_range_response)
702
 
 
703
 
        # Just make sure we can read the right contents
704
 
        out.seek(0)
705
 
        out.read(255)
706
 
 
707
 
        out.seek(1000)
708
 
        out.read(1050)
709
 
 
710
 
    def test_multi_squid_range(self):
711
 
        out = self.get_response(_multipart_squid_range_response)
712
 
 
713
 
        # Just make sure we can read the right contents
714
 
        out.seek(0)
715
 
        out.read(100)
716
 
 
717
 
        out.seek(300)
718
 
        out.read(200)
719
 
 
720
 
    def test_invalid_response(self):
721
 
        self.assertRaises(errors.InvalidHttpResponse,
722
 
                          self.get_response, _invalid_response)
723
 
 
724
 
    def test_full_text_no_content_type(self):
725
 
        # We should not require Content-Type for a full response
726
 
        code, raw_headers, body = _full_text_response_no_content_type
727
 
        msg = self._build_HTTPMessage(raw_headers)
728
 
        out = response.handle_response('http://foo', code, msg, StringIO(body))
729
 
        self.assertEqual(body, out.read())
730
 
 
731
 
    def test_full_text_no_content_length(self):
732
 
        code, raw_headers, body = _full_text_response_no_content_length
733
 
        msg = self._build_HTTPMessage(raw_headers)
734
 
        out = response.handle_response('http://foo', code, msg, StringIO(body))
735
 
        self.assertEqual(body, out.read())
736
 
 
737
 
    def test_missing_content_range(self):
738
 
        code, raw_headers, body = _single_range_no_content_range
739
 
        msg = self._build_HTTPMessage(raw_headers)
740
 
        self.assertRaises(errors.InvalidHttpResponse,
741
 
                          response.handle_response,
742
 
                          'http://bogus', code, msg, StringIO(body))
743
 
 
744
 
    def test_multipart_no_content_range(self):
745
 
        code, raw_headers, body = _multipart_no_content_range
746
 
        msg = self._build_HTTPMessage(raw_headers)
747
 
        self.assertRaises(errors.InvalidHttpResponse,
748
 
                          response.handle_response,
749
 
                          'http://bogus', code, msg, StringIO(body))
750
 
 
751
 
    def test_multipart_no_boundary(self):
752
 
        out = self.get_response(_multipart_no_boundary)
753
 
        out.read()  # Read the whole range
754
 
        # Fail to find the boundary line
755
 
        self.assertRaises(errors.InvalidHttpResponse, out.seek, 1, 1)