60
57
"""A socket-like object that can be given a predefined content."""
62
59
def __init__(self, data):
63
self.readfile = BytesIO(data)
60
self.readfile = StringIO(data)
65
62
def makefile(self, mode='r', bufsize=None):
66
63
return self.readfile
69
class FakeHTTPConnection(HTTPConnection):
66
class FakeHTTPConnection(_urllib2_wrappers.HTTPConnection):
71
68
def __init__(self, sock):
72
HTTPConnection.__init__(self, 'localhost')
69
_urllib2_wrappers.HTTPConnection.__init__(self, 'localhost')
73
70
# Set the socket to bypass the connection
81
78
class TestResponseFileIter(tests.TestCase):
83
80
def test_iter_empty(self):
84
f = response.ResponseFile('empty', BytesIO())
81
f = response.ResponseFile('empty', StringIO())
85
82
self.assertEqual([], list(f))
87
84
def test_iter_many(self):
88
f = response.ResponseFile('many', BytesIO(b'0\n1\nboo!\n'))
89
self.assertEqual([b'0\n', b'1\n', b'boo!\n'], list(f))
85
f = response.ResponseFile('many', StringIO('0\n1\nboo!\n'))
86
self.assertEqual(['0\n', '1\n', 'boo!\n'], list(f))
92
89
class TestHTTPConnection(tests.TestCase):
94
91
def test_cleanup_pipe(self):
95
sock = ReadSocket(b"""HTTP/1.1 200 OK\r
92
sock = ReadSocket("""HTTP/1.1 200 OK\r
96
93
Content-Type: text/plain; charset=UTF-8\r
106
103
# Now, get the response
107
104
resp = conn.getresponse()
108
105
# Read part of the response
109
self.assertEqual(b'0123456789\n', resp.read(11))
106
self.assertEqual('0123456789\n', resp.read(11))
110
107
# Override the thresold to force the warning emission
111
conn._range_warning_thresold = 6 # There are 7 bytes pending
108
conn._range_warning_thresold = 6 # There are 7 bytes pending
112
109
conn.cleanup_pipe()
113
110
self.assertContainsRe(self.get_log(), 'Got a 200 response when asking')
120
117
# which offsets are easy to calculate for test writers. It's used as a
121
118
# building block with slight variations but basically 'a' is the first char
122
119
# of the range and 'z' is the last.
123
alpha = b'abcdefghijklmnopqrstuvwxyz'
120
alpha = 'abcdefghijklmnopqrstuvwxyz'
125
122
def test_can_read_at_first_access(self):
126
123
"""Test that the just created file can be read."""
132
129
start = self.first_range_start
133
130
# Before any use, tell() should be at the range start
134
131
self.assertEqual(start, f.tell())
135
cur = start # For an overall offset assertion
132
cur = start # For an overall offset assertion
136
133
f.seek(start + 3)
138
self.assertEqual(b'def', f.read(3))
135
self.assertEqual('def', f.read(3))
139
136
cur += len('def')
142
self.assertEqual(b'klmn', f.read(4))
139
self.assertEqual('klmn', f.read(4))
143
140
cur += len('klmn')
144
141
# read(0) in the middle of a range
145
self.assertEqual(b'', f.read(0))
142
self.assertEqual('', f.read(0))
152
149
def test_read_zero(self):
154
self.assertEqual(b'', f.read(0))
151
self.assertEqual('', f.read(0))
156
self.assertEqual(b'', f.read(0))
153
self.assertEqual('', f.read(0))
158
155
def test_seek_at_range_end(self):
163
160
"""Test read behaviour at range end."""
165
162
self.assertEqual(self.alpha, f.read())
166
self.assertEqual(b'', f.read(0))
163
self.assertEqual('', f.read(0))
167
164
self.assertRaises(errors.InvalidRange, f.read, 1)
169
166
def test_unbounded_read_after_seek(self):
172
169
# Should not cross ranges
173
self.assertEqual(b'yz', f.read())
170
self.assertEqual('yz', f.read())
175
172
def test_seek_backwards(self):
196
193
self.assertRaises(errors.InvalidRange, f.read, 10)
198
195
def test_seek_from_end(self):
199
"""Test seeking from the end of the file.
201
The semantic is unclear in case of multiple ranges. Seeking from end
202
exists only for the http transports, cannot be used if the file size is
203
unknown and is not used in breezy itself. This test must be (and is)
204
overridden by daughter classes.
206
Reading from end makes sense only when a range has been requested from
207
the end of the file (see HttpTransportBase._get() when using the
208
'tail_amount' parameter). The HTTP response can only be a whole file or
213
self.assertEqual(b'yz', f.read())
196
"""Test seeking from the end of the file.
198
The semantic is unclear in case of multiple ranges. Seeking from end
199
exists only for the http transports, cannot be used if the file size is
200
unknown and is not used in brzlib itself. This test must be (and is)
201
overridden by daughter classes.
203
Reading from end makes sense only when a range has been requested from
204
the end of the file (see HttpTransportBase._get() when using the
205
'tail_amount' parameter). The HTTP response can only be a whole file or
210
self.assertEqual('yz', f.read())
216
213
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
220
217
super(TestRangeFileSizeUnknown, self).setUp()
221
218
self._file = response.RangeFile('Whole_file_size_known',
219
StringIO(self.alpha))
223
220
# We define no range, relying on RangeFile to provide default values
224
self.first_range_start = 0 # It's the whole file
221
self.first_range_start = 0 # It's the whole file
226
223
def test_seek_from_end(self):
227
224
"""See TestRangeFileMixin.test_seek_from_end.
234
231
"""Test read behaviour at range end."""
236
233
self.assertEqual(self.alpha, f.read())
237
self.assertEqual(b'', f.read(0))
238
self.assertEqual(b'', f.read(1))
234
self.assertEqual('', f.read(0))
235
self.assertEqual('', f.read(1))
241
238
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
245
242
super(TestRangeFileSizeKnown, self).setUp()
246
243
self._file = response.RangeFile('Whole_file_size_known',
244
StringIO(self.alpha))
248
245
self._file.set_range(0, len(self.alpha))
249
self.first_range_start = 0 # It's the whole file
246
self.first_range_start = 0 # It's the whole file
252
249
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
256
253
super(TestRangeFileSingleRange, self).setUp()
257
254
self._file = response.RangeFile('Single_range_file',
255
StringIO(self.alpha))
259
256
self.first_range_start = 15
260
257
self._file.set_range(self.first_range_start, len(self.alpha))
262
260
def test_read_before_range(self):
263
261
# This can't occur under normal circumstances, we have to force it
265
f._pos = 0 # Force an invalid pos
263
f._pos = 0 # Force an invalid pos
266
264
self.assertRaises(errors.InvalidRange, f.read, 2)
283
281
# in HTTP response headers and the boundary lines that separate
284
282
# multipart content.
286
boundary = b"separation"
284
boundary = "separation"
289
287
super(TestRangeFileMultipleRanges, self).setUp()
291
289
boundary = self.boundary
294
292
self.first_range_start = 25
295
file_size = 200 # big enough to encompass all ranges
293
file_size = 200 # big enough to encompass all ranges
296
294
for (start, part) in [(self.first_range_start, self.alpha),
297
295
# Two contiguous ranges
298
296
(100, self.alpha),
303
301
content += self._boundary_line()
305
303
self._file = response.RangeFile('Multiple_ranges_file',
307
305
self.set_file_boundary()
309
307
def _boundary_line(self):
310
308
"""Helper to build the formatted boundary line."""
311
return b'--' + self.boundary + b'\r\n'
309
return '--' + self.boundary + '\r\n'
313
311
def set_file_boundary(self):
314
312
# Ranges are set by decoding the range headers, the RangeFile user is
317
315
# which is part of the Content-Type header).
318
316
self._file.set_boundary(self.boundary)
320
def _multipart_byterange(self, data, offset, boundary, file_size=b'*'):
318
def _multipart_byterange(self, data, offset, boundary, file_size='*'):
321
319
"""Encode a part of a file as a multipart/byterange MIME type.
323
321
When a range request is issued, the HTTP response body can be
339
337
# A range is described by a set of headers, but only 'Content-Range' is
340
338
# required for our implementation (TestHandleResponse below will
341
339
# exercise ranges with multiple or missing headers')
342
if isinstance(file_size, int):
343
file_size = b'%d' % file_size
344
range += b'Content-Range: bytes %d-%d/%s\r\n' % (offset,
340
range += 'Content-Range: bytes %d-%d/%d\r\n' % (offset,
349
344
# Finally the raw bytes
353
348
def test_read_all_ranges(self):
355
self.assertEqual(self.alpha, f.read()) # Read first range
356
f.seek(100) # Trigger the second range recognition
357
self.assertEqual(self.alpha, f.read()) # Read second range
350
self.assertEqual(self.alpha, f.read()) # Read first range
351
f.seek(100) # Trigger the second range recognition
352
self.assertEqual(self.alpha, f.read()) # Read second range
358
353
self.assertEqual(126, f.tell())
359
f.seek(126) # Start of third range which is also the current pos !
360
self.assertEqual(b'A', f.read(1))
354
f.seek(126) # Start of third range which is also the current pos !
355
self.assertEqual('A', f.read(1))
362
self.assertEqual(b'LMN', f.read(3))
357
self.assertEqual('LMN', f.read(3))
364
359
def test_seek_from_end(self):
365
360
"""See TestRangeFileMixin.test_seek_from_end."""
387
382
def test_seek_across_ranges(self):
389
f.seek(126) # skip the two first ranges
390
self.assertEqual(b'AB', f.read(2))
384
f.seek(126) # skip the two first ranges
385
self.assertEqual('AB', f.read(2))
392
387
def test_checked_read_dont_overflow_buffers(self):
394
389
# We force a very low value to exercise all code paths in _checked_read
395
390
f._discarded_buf_size = 8
396
f.seek(126) # skip the two first ranges
397
self.assertEqual(b'AB', f.read(2))
391
f.seek(126) # skip the two first ranges
392
self.assertEqual('AB', f.read(2))
399
394
def test_seek_twice_between_ranges(self):
401
396
start = self.first_range_start
402
f.seek(start + 40) # Past the first range but before the second
397
f.seek(start + 40) # Past the first range but before the second
403
398
# Now the file is positioned at the second range start (100)
404
399
self.assertRaises(errors.InvalidRange, f.seek, start + 41)
436
431
# The boundary as it appears in boundary lines
437
432
# IIS 6 and 7 use this value
438
_boundary_trimmed = b"q1w2e3r4t5y6u7i8o9p0zaxscdvfbgnhmjklkl"
439
boundary = b'<' + _boundary_trimmed + b'>'
433
_boundary_trimmed = "q1w2e3r4t5y6u7i8o9p0zaxscdvfbgnhmjklkl"
434
boundary = '<' + _boundary_trimmed + '>'
441
436
def set_file_boundary(self):
442
437
# Emulate broken rfc822.unquote() here by removing angles
458
453
def test_range_syntax(self):
459
454
"""Test the Content-Range scanning."""
461
f = response.RangeFile('foo', BytesIO())
456
f = response.RangeFile('foo', StringIO())
463
458
def ok(expected, header_value):
464
459
f.set_range_from_header(header_value)
470
465
ok((12, 2), '\tbytes 12-13/*')
471
466
ok((28, 1), ' bytes 28-28/*')
472
467
ok((2123, 2120), 'bytes 2123-4242/12310')
473
ok((1, 10), 'bytes 1-10/ttt') # We don't check total (ttt)
468
ok((1, 10), 'bytes 1-10/ttt') # We don't check total (ttt)
475
470
def nok(header_value):
476
471
self.assertRaises(errors.InvalidHttpRange,
487
482
# Taken from real request responses
488
_full_text_response = (200, b"""HTTP/1.1 200 OK\r
483
_full_text_response = (200, """HTTP/1.1 200 OK\r
489
484
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
490
485
Server: Apache/2.0.54 (Fedora)\r
491
486
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
495
490
Connection: close\r
496
491
Content-Type: text/plain; charset=UTF-8\r
498
""", b"""Bazaar-NG meta directory, format 1
493
""", """Bazaar-NG meta directory, format 1
502
_single_range_response = (206, b"""HTTP/1.1 206 Partial Content\r
497
_single_range_response = (206, """HTTP/1.1 206 Partial Content\r
503
498
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
504
499
Server: Apache/2.0.54 (Fedora)\r
505
500
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
510
505
Connection: close\r
511
506
Content-Type: text/plain; charset=UTF-8\r
513
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
508
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
514
509
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
517
_single_range_no_content_type = (206, b"""HTTP/1.1 206 Partial Content\r
512
_single_range_no_content_type = (206, """HTTP/1.1 206 Partial Content\r
518
513
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
519
514
Server: Apache/2.0.54 (Fedora)\r
520
515
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
524
519
Content-Range: bytes 100-199/93890\r
525
520
Connection: close\r
527
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
522
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
528
523
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
531
_multipart_range_response = (206, b"""HTTP/1.1 206 Partial Content\r
526
_multipart_range_response = (206, """HTTP/1.1 206 Partial Content\r
532
527
Date: Tue, 11 Jul 2006 04:49:48 GMT\r
533
528
Server: Apache/2.0.54 (Fedora)\r
534
529
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
620
615
Content-Length: 35\r
621
616
Connection: close\r
623
""", b"""Bazaar-NG meta directory, format 1
618
""", """Bazaar-NG meta directory, format 1
627
_full_text_response_no_content_length = (200, b"""HTTP/1.1 200 OK\r
622
_full_text_response_no_content_length = (200, """HTTP/1.1 200 OK\r
628
623
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
629
624
Server: Apache/2.0.54 (Fedora)\r
630
625
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
633
628
Connection: close\r
634
629
Content-Type: text/plain; charset=UTF-8\r
636
""", b"""Bazaar-NG meta directory, format 1
631
""", """Bazaar-NG meta directory, format 1
640
_single_range_no_content_range = (206, b"""HTTP/1.1 206 Partial Content\r
635
_single_range_no_content_range = (206, """HTTP/1.1 206 Partial Content\r
641
636
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
642
637
Server: Apache/2.0.54 (Fedora)\r
643
638
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
646
641
Content-Length: 100\r
647
642
Connection: close\r
649
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
644
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
650
645
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
653
_single_range_response_truncated = (206, b"""HTTP/1.1 206 Partial Content\r
648
_single_range_response_truncated = (206, """HTTP/1.1 206 Partial Content\r
654
649
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
655
650
Server: Apache/2.0.54 (Fedora)\r
656
651
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
661
656
Connection: close\r
662
657
Content-Type: text/plain; charset=UTF-8\r
664
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
667
_invalid_response = (444, b"""HTTP/1.1 444 Bad Response\r
659
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
662
_invalid_response = (444, """HTTP/1.1 444 Bad Response\r
668
663
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
669
664
Connection: close\r
670
665
Content-Type: text/html; charset=iso-8859-1\r
672
""", b"""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
667
""", """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
674
669
<title>404 Not Found</title>
714
709
class TestHandleResponse(tests.TestCase):
716
711
def _build_HTTPMessage(self, raw_headers):
717
status_and_headers = BytesIO(raw_headers)
712
status_and_headers = StringIO(raw_headers)
718
713
# Get rid of the status line
719
714
status_and_headers.readline()
720
msg = parse_headers(status_and_headers)
715
msg = httplib.HTTPMessage(status_and_headers)
723
718
def get_response(self, a_response):
724
719
"""Process a supplied response, and return the result."""
725
720
code, raw_headers, body = a_response
726
getheader = self._build_HTTPMessage(raw_headers)
727
return response.handle_response(
728
'http://foo', code, getheader, BytesIO(a_response[2]))
721
msg = self._build_HTTPMessage(raw_headers)
722
return response.handle_response('http://foo', code, msg,
723
StringIO(a_response[2]))
730
725
def test_full_text(self):
731
726
out = self.get_response(_full_text_response)
732
# It is a BytesIO from the original data
727
# It is a StringIO from the original data
733
728
self.assertEqual(_full_text_response[2], out.read())
735
730
def test_single_range(self):
776
771
def test_full_text_no_content_type(self):
777
772
# We should not require Content-Type for a full response
778
773
code, raw_headers, body = _full_text_response_no_content_type
779
getheader = self._build_HTTPMessage(raw_headers)
780
out = response.handle_response(
781
'http://foo', code, getheader, BytesIO(body))
774
msg = self._build_HTTPMessage(raw_headers)
775
out = response.handle_response('http://foo', code, msg, StringIO(body))
782
776
self.assertEqual(body, out.read())
784
778
def test_full_text_no_content_length(self):
785
779
code, raw_headers, body = _full_text_response_no_content_length
786
getheader = self._build_HTTPMessage(raw_headers)
787
out = response.handle_response(
788
'http://foo', code, getheader, BytesIO(body))
780
msg = self._build_HTTPMessage(raw_headers)
781
out = response.handle_response('http://foo', code, msg, StringIO(body))
789
782
self.assertEqual(body, out.read())
791
784
def test_missing_content_range(self):
792
785
code, raw_headers, body = _single_range_no_content_range
793
getheader = self._build_HTTPMessage(raw_headers)
786
msg = self._build_HTTPMessage(raw_headers)
794
787
self.assertRaises(errors.InvalidHttpResponse,
795
788
response.handle_response,
796
'http://bogus', code, getheader, BytesIO(body))
789
'http://bogus', code, msg, StringIO(body))
798
791
def test_multipart_no_content_range(self):
799
792
code, raw_headers, body = _multipart_no_content_range
800
getheader = self._build_HTTPMessage(raw_headers)
793
msg = self._build_HTTPMessage(raw_headers)
801
794
self.assertRaises(errors.InvalidHttpResponse,
802
795
response.handle_response,
803
'http://bogus', code, getheader, BytesIO(body))
796
'http://bogus', code, msg, StringIO(body))
805
798
def test_multipart_no_boundary(self):
806
799
out = self.get_response(_multipart_no_boundary)
818
811
super(TestRangeFileSizeReadLimited, self).setUp()
819
812
# create a test datablock larger than _max_read_size.
820
813
chunk_size = response.RangeFile._max_read_size
821
test_pattern = b'0123456789ABCDEF'
822
self.test_data = test_pattern * (3 * chunk_size // len(test_pattern))
814
test_pattern = '0123456789ABCDEF'
815
self.test_data = test_pattern * (3 * chunk_size / len(test_pattern))
823
816
self.test_data_len = len(self.test_data)
825
818
def test_max_read_size(self):