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
class TestResponseFileIter(tests.TestCase):
83
def test_iter_empty(self):
84
f = response.ResponseFile('empty', BytesIO())
85
self.assertEqual([], list(f))
87
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))
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())
96
78
class TestHTTPConnection(tests.TestCase):
98
80
def test_cleanup_pipe(self):
99
sock = ReadSocket(b"""HTTP/1.1 200 OK\r
81
sock = ReadSocket("""HTTP/1.1 200 OK\r
100
82
Content-Type: text/plain; charset=UTF-8\r
101
83
Content-Length: 18
110
92
# Now, get the response
111
93
resp = conn.getresponse()
112
94
# Read part of the response
113
self.assertEqual(b'0123456789\n', resp.read(11))
95
self.assertEquals('0123456789\n', resp.read(11))
114
96
# Override the thresold to force the warning emission
115
conn._range_warning_thresold = 6 # There are 7 bytes pending
97
conn._range_warning_thresold = 6 # There are 7 bytes pending
116
98
conn.cleanup_pipe()
117
99
self.assertContainsRe(self.get_log(), 'Got a 200 response when asking')
124
106
# which offsets are easy to calculate for test writers. It's used as a
125
107
# building block with slight variations but basically 'a' is the first char
126
108
# of the range and 'z' is the last.
127
alpha = b'abcdefghijklmnopqrstuvwxyz'
109
alpha = 'abcdefghijklmnopqrstuvwxyz'
129
111
def test_can_read_at_first_access(self):
130
112
"""Test that the just created file can be read."""
131
self.assertEqual(self.alpha, self._file.read())
113
self.assertEquals(self.alpha, self._file.read())
133
115
def test_seek_read(self):
134
116
"""Test seek/read inside the range."""
136
118
start = self.first_range_start
137
119
# Before any use, tell() should be at the range start
138
self.assertEqual(start, f.tell())
139
cur = start # For an overall offset assertion
120
self.assertEquals(start, f.tell())
121
cur = start # For an overall offset assertion
140
122
f.seek(start + 3)
142
self.assertEqual(b'def', f.read(3))
124
self.assertEquals('def', f.read(3))
143
125
cur += len('def')
146
self.assertEqual(b'klmn', f.read(4))
128
self.assertEquals('klmn', f.read(4))
147
129
cur += len('klmn')
148
130
# read(0) in the middle of a range
149
self.assertEqual(b'', f.read(0))
131
self.assertEquals('', f.read(0))
153
self.assertEqual(here, f.tell())
154
self.assertEqual(cur, f.tell())
135
self.assertEquals(here, f.tell())
136
self.assertEquals(cur, f.tell())
156
138
def test_read_zero(self):
158
self.assertEqual(b'', f.read(0))
140
start = self.first_range_start
141
self.assertEquals('', f.read(0))
160
self.assertEqual(b'', f.read(0))
143
self.assertEquals('', f.read(0))
162
145
def test_seek_at_range_end(self):
166
149
def test_read_at_range_end(self):
167
150
"""Test read behaviour at range end."""
169
self.assertEqual(self.alpha, f.read())
170
self.assertEqual(b'', f.read(0))
152
self.assertEquals(self.alpha, f.read())
153
self.assertEquals('', f.read(0))
171
154
self.assertRaises(errors.InvalidRange, f.read, 1)
173
156
def test_unbounded_read_after_seek(self):
176
159
# Should not cross ranges
177
self.assertEqual(b'yz', f.read())
160
self.assertEquals('yz', f.read())
179
162
def test_seek_backwards(self):
200
183
self.assertRaises(errors.InvalidRange, f.read, 10)
202
185
def test_seek_from_end(self):
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())
186
"""Test seeking from the end of the file.
188
The semantic is unclear in case of multiple ranges. Seeking from end
189
exists only for the http transports, cannot be used if the file size is
190
unknown and is not used in bzrlib itself. This test must be (and is)
191
overridden by daughter classes.
193
Reading from end makes sense only when a range has been requested from
194
the end of the file (see HttpTransportBase._get() when using the
195
'tail_amount' parameter). The HTTP response can only be a whole file or
200
self.assertEquals('yz', f.read())
220
203
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
224
207
super(TestRangeFileSizeUnknown, self).setUp()
225
208
self._file = response.RangeFile('Whole_file_size_known',
209
StringIO(self.alpha))
227
210
# We define no range, relying on RangeFile to provide default values
228
self.first_range_start = 0 # It's the whole file
211
self.first_range_start = 0 # It's the whole file
230
213
def test_seek_from_end(self):
231
214
"""See TestRangeFileMixin.test_seek_from_end.
237
220
def test_read_at_range_end(self):
238
221
"""Test read behaviour at range end."""
240
self.assertEqual(self.alpha, f.read())
241
self.assertEqual(b'', f.read(0))
242
self.assertEqual(b'', f.read(1))
223
self.assertEquals(self.alpha, f.read())
224
self.assertEquals('', f.read(0))
225
self.assertEquals('', f.read(1))
245
228
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
249
232
super(TestRangeFileSizeKnown, self).setUp()
250
233
self._file = response.RangeFile('Whole_file_size_known',
234
StringIO(self.alpha))
252
235
self._file.set_range(0, len(self.alpha))
253
self.first_range_start = 0 # It's the whole file
236
self.first_range_start = 0 # It's the whole file
256
239
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
260
243
super(TestRangeFileSingleRange, self).setUp()
261
244
self._file = response.RangeFile('Single_range_file',
245
StringIO(self.alpha))
263
246
self.first_range_start = 15
264
247
self._file.set_range(self.first_range_start, len(self.alpha))
266
250
def test_read_before_range(self):
267
251
# This can't occur under normal circumstances, we have to force it
269
f._pos = 0 # Force an invalid pos
253
f._pos = 0 # Force an invalid pos
270
254
self.assertRaises(errors.InvalidRange, f.read, 2)
287
271
# in HTTP response headers and the boundary lines that separate
288
272
# multipart content.
290
boundary = b"separation"
274
boundary = "separation"
293
277
super(TestRangeFileMultipleRanges, self).setUp()
295
279
boundary = self.boundary
298
282
self.first_range_start = 25
299
file_size = 200 # big enough to encompass all ranges
283
file_size = 200 # big enough to encompass all ranges
300
284
for (start, part) in [(self.first_range_start, self.alpha),
301
285
# Two contiguous ranges
302
286
(100, self.alpha),
307
291
content += self._boundary_line()
309
293
self._file = response.RangeFile('Multiple_ranges_file',
311
295
self.set_file_boundary()
313
297
def _boundary_line(self):
314
298
"""Helper to build the formatted boundary line."""
315
return b'--' + self.boundary + b'\r\n'
299
return '--' + self.boundary + '\r\n'
317
301
def set_file_boundary(self):
318
302
# Ranges are set by decoding the range headers, the RangeFile user is
321
305
# which is part of the Content-Type header).
322
306
self._file.set_boundary(self.boundary)
324
def _multipart_byterange(self, data, offset, boundary, file_size=b'*'):
308
def _multipart_byterange(self, data, offset, boundary, file_size='*'):
325
309
"""Encode a part of a file as a multipart/byterange MIME type.
327
311
When a range request is issued, the HTTP response body can be
343
327
# A range is described by a set of headers, but only 'Content-Range' is
344
328
# required for our implementation (TestHandleResponse below will
345
329
# exercise ranges with multiple or missing headers')
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,
330
range += 'Content-Range: bytes %d-%d/%d\r\n' % (offset,
353
334
# Finally the raw bytes
357
338
def test_read_all_ranges(self):
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))
340
self.assertEquals(self.alpha, f.read()) # Read first range
341
f.seek(100) # Trigger the second range recognition
342
self.assertEquals(self.alpha, f.read()) # Read second range
343
self.assertEquals(126, f.tell())
344
f.seek(126) # Start of third range which is also the current pos !
345
self.assertEquals('A', f.read(1))
366
self.assertEqual(b'LMN', f.read(3))
347
self.assertEquals('LMN', f.read(3))
368
349
def test_seek_from_end(self):
369
350
"""See TestRangeFileMixin.test_seek_from_end."""
391
372
def test_seek_across_ranges(self):
393
f.seek(126) # skip the two first ranges
394
self.assertEqual(b'AB', f.read(2))
374
start = self.first_range_start
375
f.seek(126) # skip the two first ranges
376
self.assertEquals('AB', f.read(2))
396
378
def test_checked_read_dont_overflow_buffers(self):
380
start = self.first_range_start
398
381
# We force a very low value to exercise all code paths in _checked_read
399
382
f._discarded_buf_size = 8
400
f.seek(126) # skip the two first ranges
401
self.assertEqual(b'AB', f.read(2))
383
f.seek(126) # skip the two first ranges
384
self.assertEquals('AB', f.read(2))
403
386
def test_seek_twice_between_ranges(self):
405
388
start = self.first_range_start
406
f.seek(start + 40) # Past the first range but before the second
389
f.seek(start + 40) # Past the first range but before the second
407
390
# Now the file is positioned at the second range start (100)
408
391
self.assertRaises(errors.InvalidRange, f.seek, start + 41)
417
400
def test_read_at_range_end(self):
419
self.assertEqual(self.alpha, f.read())
420
self.assertEqual(self.alpha, f.read())
421
self.assertEqual(self.alpha.upper(), f.read())
402
self.assertEquals(self.alpha, f.read())
403
self.assertEquals(self.alpha, f.read())
404
self.assertEquals(self.alpha.upper(), f.read())
422
405
self.assertRaises(errors.InvalidHttpResponse, f.read, 1)
440
423
# The boundary as it appears in boundary lines
441
424
# IIS 6 and 7 use this value
442
_boundary_trimmed = b"q1w2e3r4t5y6u7i8o9p0zaxscdvfbgnhmjklkl"
443
boundary = b'<' + _boundary_trimmed + b'>'
425
_boundary_trimmed = "q1w2e3r4t5y6u7i8o9p0zaxscdvfbgnhmjklkl"
426
boundary = '<' + _boundary_trimmed + '>'
445
428
def set_file_boundary(self):
446
429
# Emulate broken rfc822.unquote() here by removing angles
462
445
def test_range_syntax(self):
463
446
"""Test the Content-Range scanning."""
465
f = response.RangeFile('foo', BytesIO())
448
f = response.RangeFile('foo', StringIO())
467
450
def ok(expected, header_value):
468
451
f.set_range_from_header(header_value)
469
452
# Slightly peek under the covers to get the size
470
self.assertEqual(expected, (f.tell(), f._size))
453
self.assertEquals(expected, (f.tell(), f._size))
472
455
ok((1, 10), 'bytes 1-10/11')
473
456
ok((1, 10), 'bytes 1-10/*')
474
457
ok((12, 2), '\tbytes 12-13/*')
475
458
ok((28, 1), ' bytes 28-28/*')
476
459
ok((2123, 2120), 'bytes 2123-4242/12310')
477
ok((1, 10), 'bytes 1-10/ttt') # We don't check total (ttt)
460
ok((1, 10), 'bytes 1-10/ttt') # We don't check total (ttt)
479
462
def nok(header_value):
480
463
self.assertRaises(errors.InvalidHttpRange,
491
474
# Taken from real request responses
492
_full_text_response = (200, b"""HTTP/1.1 200 OK\r
475
_full_text_response = (200, """HTTP/1.1 200 OK\r
493
476
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
494
477
Server: Apache/2.0.54 (Fedora)\r
495
478
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
499
482
Connection: close\r
500
483
Content-Type: text/plain; charset=UTF-8\r
502
""", b"""Bazaar-NG meta directory, format 1
485
""", """Bazaar-NG meta directory, format 1
506
_single_range_response = (206, b"""HTTP/1.1 206 Partial Content\r
489
_single_range_response = (206, """HTTP/1.1 206 Partial Content\r
507
490
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
508
491
Server: Apache/2.0.54 (Fedora)\r
509
492
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
514
497
Connection: close\r
515
498
Content-Type: text/plain; charset=UTF-8\r
517
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
500
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
518
501
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
521
_single_range_no_content_type = (206, b"""HTTP/1.1 206 Partial Content\r
504
_single_range_no_content_type = (206, """HTTP/1.1 206 Partial Content\r
522
505
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
523
506
Server: Apache/2.0.54 (Fedora)\r
524
507
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
528
511
Content-Range: bytes 100-199/93890\r
529
512
Connection: close\r
531
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
514
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
532
515
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
535
_multipart_range_response = (206, b"""HTTP/1.1 206 Partial Content\r
518
_multipart_range_response = (206, """HTTP/1.1 206 Partial Content\r
536
519
Date: Tue, 11 Jul 2006 04:49:48 GMT\r
537
520
Server: Apache/2.0.54 (Fedora)\r
538
521
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
624
607
Content-Length: 35\r
625
608
Connection: close\r
627
""", b"""Bazaar-NG meta directory, format 1
610
""", """Bazaar-NG meta directory, format 1
631
_full_text_response_no_content_length = (200, b"""HTTP/1.1 200 OK\r
614
_full_text_response_no_content_length = (200, """HTTP/1.1 200 OK\r
632
615
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
633
616
Server: Apache/2.0.54 (Fedora)\r
634
617
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
637
620
Connection: close\r
638
621
Content-Type: text/plain; charset=UTF-8\r
640
""", b"""Bazaar-NG meta directory, format 1
623
""", """Bazaar-NG meta directory, format 1
644
_single_range_no_content_range = (206, b"""HTTP/1.1 206 Partial Content\r
627
_single_range_no_content_range = (206, """HTTP/1.1 206 Partial Content\r
645
628
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
646
629
Server: Apache/2.0.54 (Fedora)\r
647
630
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
650
633
Content-Length: 100\r
651
634
Connection: close\r
653
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
636
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
654
637
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
657
_single_range_response_truncated = (206, b"""HTTP/1.1 206 Partial Content\r
640
_single_range_response_truncated = (206, """HTTP/1.1 206 Partial Content\r
658
641
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
659
642
Server: Apache/2.0.54 (Fedora)\r
660
643
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
665
648
Connection: close\r
666
649
Content-Type: text/plain; charset=UTF-8\r
668
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
671
_invalid_response = (444, b"""HTTP/1.1 444 Bad Response\r
651
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
654
_invalid_response = (444, """HTTP/1.1 444 Bad Response\r
672
655
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
673
656
Connection: close\r
674
657
Content-Type: text/html; charset=iso-8859-1\r
676
""", b"""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
659
""", """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
678
661
<title>404 Not Found</title>
718
701
class TestHandleResponse(tests.TestCase):
720
703
def _build_HTTPMessage(self, raw_headers):
721
status_and_headers = BytesIO(raw_headers)
704
status_and_headers = StringIO(raw_headers)
722
705
# Get rid of the status line
723
706
status_and_headers.readline()
724
msg = parse_headers(status_and_headers)
707
msg = httplib.HTTPMessage(status_and_headers)
727
710
def get_response(self, a_response):
728
711
"""Process a supplied response, and return the result."""
729
712
code, raw_headers, body = a_response
730
getheader = self._build_HTTPMessage(raw_headers)
731
return response.handle_response(
732
'http://foo', code, getheader, BytesIO(a_response[2]))
713
msg = self._build_HTTPMessage(raw_headers)
714
return response.handle_response('http://foo', code, msg,
715
StringIO(a_response[2]))
734
717
def test_full_text(self):
735
718
out = self.get_response(_full_text_response)
736
# It is a BytesIO from the original data
719
# It is a StringIO from the original data
737
720
self.assertEqual(_full_text_response[2], out.read())
739
722
def test_single_range(self):
780
763
def test_full_text_no_content_type(self):
781
764
# We should not require Content-Type for a full response
782
765
code, raw_headers, body = _full_text_response_no_content_type
783
getheader = self._build_HTTPMessage(raw_headers)
784
out = response.handle_response(
785
'http://foo', code, getheader, BytesIO(body))
766
msg = self._build_HTTPMessage(raw_headers)
767
out = response.handle_response('http://foo', code, msg, StringIO(body))
786
768
self.assertEqual(body, out.read())
788
770
def test_full_text_no_content_length(self):
789
771
code, raw_headers, body = _full_text_response_no_content_length
790
getheader = self._build_HTTPMessage(raw_headers)
791
out = response.handle_response(
792
'http://foo', code, getheader, BytesIO(body))
772
msg = self._build_HTTPMessage(raw_headers)
773
out = response.handle_response('http://foo', code, msg, StringIO(body))
793
774
self.assertEqual(body, out.read())
795
776
def test_missing_content_range(self):
796
777
code, raw_headers, body = _single_range_no_content_range
797
getheader = self._build_HTTPMessage(raw_headers)
778
msg = self._build_HTTPMessage(raw_headers)
798
779
self.assertRaises(errors.InvalidHttpResponse,
799
780
response.handle_response,
800
'http://bogus', code, getheader, BytesIO(body))
781
'http://bogus', code, msg, StringIO(body))
802
783
def test_multipart_no_content_range(self):
803
784
code, raw_headers, body = _multipart_no_content_range
804
getheader = self._build_HTTPMessage(raw_headers)
785
msg = self._build_HTTPMessage(raw_headers)
805
786
self.assertRaises(errors.InvalidHttpResponse,
806
787
response.handle_response,
807
'http://bogus', code, getheader, BytesIO(body))
788
'http://bogus', code, msg, StringIO(body))
809
790
def test_multipart_no_boundary(self):
810
791
out = self.get_response(_multipart_no_boundary)
822
super(TestRangeFileSizeReadLimited, self).setUp()
803
tests.TestCase.setUp(self)
823
804
# create a test datablock larger than _max_read_size.
824
805
chunk_size = response.RangeFile._max_read_size
825
test_pattern = b'0123456789ABCDEF'
826
self.test_data = test_pattern * (3 * chunk_size // len(test_pattern))
806
test_pattern = '0123456789ABCDEF'
807
self.test_data = test_pattern * (3 * chunk_size / len(test_pattern))
827
808
self.test_data_len = len(self.test_data)
829
810
def test_max_read_size(self):