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