37
37
InvalidHttpResponse.
41
import http.client as http_client
42
except ImportError: # python < 3 without future
43
import httplib as http_client
46
parse_headers = http_client.parse_headers
47
except AttributeError: # python 2
48
parse_headers = http_client.HTTPMessage
40
from cStringIO import StringIO
54
from ..sixish import (
58
from ..transport.http import (
47
from bzrlib.transport.http import (
62
from .file_utils import (
51
from bzrlib.tests.file_utils import (
68
57
"""A socket-like object that can be given a predefined content."""
70
59
def __init__(self, data):
71
self.readfile = BytesIO(data)
60
self.readfile = StringIO(data)
73
62
def makefile(self, mode='r', bufsize=None):
74
63
return self.readfile
77
class FakeHTTPConnection(HTTPConnection):
66
class FakeHTTPConnection(_urllib2_wrappers.HTTPConnection):
79
68
def __init__(self, sock):
80
HTTPConnection.__init__(self, 'localhost')
69
_urllib2_wrappers.HTTPConnection.__init__(self, 'localhost')
81
70
# Set the socket to bypass the connection
89
class TestResponseFileIter(tests.TestCase):
91
def test_iter_empty(self):
92
f = response.ResponseFile('empty', BytesIO())
93
self.assertEqual([], list(f))
95
def test_iter_many(self):
96
f = response.ResponseFile('many', BytesIO(b'0\n1\nboo!\n'))
97
self.assertEqual([b'0\n', b'1\n', b'boo!\n'], list(f))
99
def test_readlines(self):
100
f = response.ResponseFile('many', BytesIO(b'0\n1\nboo!\n'))
101
self.assertEqual([b'0\n', b'1\n', b'boo!\n'], f.readlines())
104
78
class TestHTTPConnection(tests.TestCase):
106
80
def test_cleanup_pipe(self):
107
sock = ReadSocket(b"""HTTP/1.1 200 OK\r
81
sock = ReadSocket("""HTTP/1.1 200 OK\r
108
82
Content-Type: text/plain; charset=UTF-8\r
109
83
Content-Length: 18
118
92
# Now, get the response
119
93
resp = conn.getresponse()
120
94
# Read part of the response
121
self.assertEqual(b'0123456789\n', resp.read(11))
95
self.assertEquals('0123456789\n', resp.read(11))
122
96
# Override the thresold to force the warning emission
123
conn._range_warning_thresold = 6 # There are 7 bytes pending
97
conn._range_warning_thresold = 6 # There are 7 bytes pending
124
98
conn.cleanup_pipe()
125
99
self.assertContainsRe(self.get_log(), 'Got a 200 response when asking')
132
106
# which offsets are easy to calculate for test writers. It's used as a
133
107
# building block with slight variations but basically 'a' is the first char
134
108
# of the range and 'z' is the last.
135
alpha = b'abcdefghijklmnopqrstuvwxyz'
109
alpha = 'abcdefghijklmnopqrstuvwxyz'
137
111
def test_can_read_at_first_access(self):
138
112
"""Test that the just created file can be read."""
139
self.assertEqual(self.alpha, self._file.read())
113
self.assertEquals(self.alpha, self._file.read())
141
115
def test_seek_read(self):
142
116
"""Test seek/read inside the range."""
144
118
start = self.first_range_start
145
119
# Before any use, tell() should be at the range start
146
self.assertEqual(start, f.tell())
147
cur = start # For an overall offset assertion
120
self.assertEquals(start, f.tell())
121
cur = start # For an overall offset assertion
148
122
f.seek(start + 3)
150
self.assertEqual(b'def', f.read(3))
124
self.assertEquals('def', f.read(3))
151
125
cur += len('def')
154
self.assertEqual(b'klmn', f.read(4))
128
self.assertEquals('klmn', f.read(4))
155
129
cur += len('klmn')
156
130
# read(0) in the middle of a range
157
self.assertEqual(b'', f.read(0))
131
self.assertEquals('', f.read(0))
161
self.assertEqual(here, f.tell())
162
self.assertEqual(cur, f.tell())
135
self.assertEquals(here, f.tell())
136
self.assertEquals(cur, f.tell())
164
138
def test_read_zero(self):
166
self.assertEqual(b'', f.read(0))
140
start = self.first_range_start
141
self.assertEquals('', f.read(0))
168
self.assertEqual(b'', f.read(0))
143
self.assertEquals('', f.read(0))
170
145
def test_seek_at_range_end(self):
174
149
def test_read_at_range_end(self):
175
150
"""Test read behaviour at range end."""
177
self.assertEqual(self.alpha, f.read())
178
self.assertEqual(b'', f.read(0))
152
self.assertEquals(self.alpha, f.read())
153
self.assertEquals('', f.read(0))
179
154
self.assertRaises(errors.InvalidRange, f.read, 1)
181
156
def test_unbounded_read_after_seek(self):
184
159
# Should not cross ranges
185
self.assertEqual(b'yz', f.read())
160
self.assertEquals('yz', f.read())
187
162
def test_seek_backwards(self):
208
183
self.assertRaises(errors.InvalidRange, f.read, 10)
210
185
def test_seek_from_end(self):
211
"""Test seeking from the end of the file.
213
The semantic is unclear in case of multiple ranges. Seeking from end
214
exists only for the http transports, cannot be used if the file size is
215
unknown and is not used in breezy itself. This test must be (and is)
216
overridden by daughter classes.
218
Reading from end makes sense only when a range has been requested from
219
the end of the file (see HttpTransportBase._get() when using the
220
'tail_amount' parameter). The HTTP response can only be a whole file or
225
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())
228
203
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
232
207
super(TestRangeFileSizeUnknown, self).setUp()
233
208
self._file = response.RangeFile('Whole_file_size_known',
209
StringIO(self.alpha))
235
210
# We define no range, relying on RangeFile to provide default values
236
self.first_range_start = 0 # It's the whole file
211
self.first_range_start = 0 # It's the whole file
238
213
def test_seek_from_end(self):
239
214
"""See TestRangeFileMixin.test_seek_from_end.
245
220
def test_read_at_range_end(self):
246
221
"""Test read behaviour at range end."""
248
self.assertEqual(self.alpha, f.read())
249
self.assertEqual(b'', f.read(0))
250
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))
253
228
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
257
232
super(TestRangeFileSizeKnown, self).setUp()
258
233
self._file = response.RangeFile('Whole_file_size_known',
234
StringIO(self.alpha))
260
235
self._file.set_range(0, len(self.alpha))
261
self.first_range_start = 0 # It's the whole file
236
self.first_range_start = 0 # It's the whole file
264
239
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
268
243
super(TestRangeFileSingleRange, self).setUp()
269
244
self._file = response.RangeFile('Single_range_file',
245
StringIO(self.alpha))
271
246
self.first_range_start = 15
272
247
self._file.set_range(self.first_range_start, len(self.alpha))
274
250
def test_read_before_range(self):
275
251
# This can't occur under normal circumstances, we have to force it
277
f._pos = 0 # Force an invalid pos
253
f._pos = 0 # Force an invalid pos
278
254
self.assertRaises(errors.InvalidRange, f.read, 2)
295
271
# in HTTP response headers and the boundary lines that separate
296
272
# multipart content.
298
boundary = b"separation"
274
boundary = "separation"
301
277
super(TestRangeFileMultipleRanges, self).setUp()
303
279
boundary = self.boundary
306
282
self.first_range_start = 25
307
file_size = 200 # big enough to encompass all ranges
283
file_size = 200 # big enough to encompass all ranges
308
284
for (start, part) in [(self.first_range_start, self.alpha),
309
285
# Two contiguous ranges
310
286
(100, self.alpha),
315
291
content += self._boundary_line()
317
293
self._file = response.RangeFile('Multiple_ranges_file',
319
295
self.set_file_boundary()
321
297
def _boundary_line(self):
322
298
"""Helper to build the formatted boundary line."""
323
return b'--' + self.boundary + b'\r\n'
299
return '--' + self.boundary + '\r\n'
325
301
def set_file_boundary(self):
326
302
# Ranges are set by decoding the range headers, the RangeFile user is
329
305
# which is part of the Content-Type header).
330
306
self._file.set_boundary(self.boundary)
332
def _multipart_byterange(self, data, offset, boundary, file_size=b'*'):
308
def _multipart_byterange(self, data, offset, boundary, file_size='*'):
333
309
"""Encode a part of a file as a multipart/byterange MIME type.
335
311
When a range request is issued, the HTTP response body can be
351
327
# A range is described by a set of headers, but only 'Content-Range' is
352
328
# required for our implementation (TestHandleResponse below will
353
329
# exercise ranges with multiple or missing headers')
354
if isinstance(file_size, int):
355
file_size = b'%d' % file_size
356
range += b'Content-Range: bytes %d-%d/%s\r\n' % (offset,
330
range += 'Content-Range: bytes %d-%d/%d\r\n' % (offset,
361
334
# Finally the raw bytes
365
338
def test_read_all_ranges(self):
367
self.assertEqual(self.alpha, f.read()) # Read first range
368
f.seek(100) # Trigger the second range recognition
369
self.assertEqual(self.alpha, f.read()) # Read second range
370
self.assertEqual(126, f.tell())
371
f.seek(126) # Start of third range which is also the current pos !
372
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))
374
self.assertEqual(b'LMN', f.read(3))
347
self.assertEquals('LMN', f.read(3))
376
349
def test_seek_from_end(self):
377
350
"""See TestRangeFileMixin.test_seek_from_end."""
399
372
def test_seek_across_ranges(self):
401
f.seek(126) # skip the two first ranges
402
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))
404
378
def test_checked_read_dont_overflow_buffers(self):
380
start = self.first_range_start
406
381
# We force a very low value to exercise all code paths in _checked_read
407
382
f._discarded_buf_size = 8
408
f.seek(126) # skip the two first ranges
409
self.assertEqual(b'AB', f.read(2))
383
f.seek(126) # skip the two first ranges
384
self.assertEquals('AB', f.read(2))
411
386
def test_seek_twice_between_ranges(self):
413
388
start = self.first_range_start
414
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
415
390
# Now the file is positioned at the second range start (100)
416
391
self.assertRaises(errors.InvalidRange, f.seek, start + 41)
425
400
def test_read_at_range_end(self):
427
self.assertEqual(self.alpha, f.read())
428
self.assertEqual(self.alpha, f.read())
429
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())
430
405
self.assertRaises(errors.InvalidHttpResponse, f.read, 1)
448
423
# The boundary as it appears in boundary lines
449
424
# IIS 6 and 7 use this value
450
_boundary_trimmed = b"q1w2e3r4t5y6u7i8o9p0zaxscdvfbgnhmjklkl"
451
boundary = b'<' + _boundary_trimmed + b'>'
425
_boundary_trimmed = "q1w2e3r4t5y6u7i8o9p0zaxscdvfbgnhmjklkl"
426
boundary = '<' + _boundary_trimmed + '>'
453
428
def set_file_boundary(self):
454
429
# Emulate broken rfc822.unquote() here by removing angles
470
445
def test_range_syntax(self):
471
446
"""Test the Content-Range scanning."""
473
f = response.RangeFile('foo', BytesIO())
448
f = response.RangeFile('foo', StringIO())
475
450
def ok(expected, header_value):
476
451
f.set_range_from_header(header_value)
477
452
# Slightly peek under the covers to get the size
478
self.assertEqual(expected, (f.tell(), f._size))
453
self.assertEquals(expected, (f.tell(), f._size))
480
455
ok((1, 10), 'bytes 1-10/11')
481
456
ok((1, 10), 'bytes 1-10/*')
482
457
ok((12, 2), '\tbytes 12-13/*')
483
458
ok((28, 1), ' bytes 28-28/*')
484
459
ok((2123, 2120), 'bytes 2123-4242/12310')
485
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)
487
462
def nok(header_value):
488
463
self.assertRaises(errors.InvalidHttpRange,
499
474
# Taken from real request responses
500
_full_text_response = (200, b"""HTTP/1.1 200 OK\r
475
_full_text_response = (200, """HTTP/1.1 200 OK\r
501
476
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
502
477
Server: Apache/2.0.54 (Fedora)\r
503
478
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
507
482
Connection: close\r
508
483
Content-Type: text/plain; charset=UTF-8\r
510
""", b"""Bazaar-NG meta directory, format 1
485
""", """Bazaar-NG meta directory, format 1
514
_single_range_response = (206, b"""HTTP/1.1 206 Partial Content\r
489
_single_range_response = (206, """HTTP/1.1 206 Partial Content\r
515
490
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
516
491
Server: Apache/2.0.54 (Fedora)\r
517
492
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
522
497
Connection: close\r
523
498
Content-Type: text/plain; charset=UTF-8\r
525
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
500
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
526
501
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
529
_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
530
505
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
531
506
Server: Apache/2.0.54 (Fedora)\r
532
507
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
536
511
Content-Range: bytes 100-199/93890\r
537
512
Connection: close\r
539
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
514
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
540
515
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
543
_multipart_range_response = (206, b"""HTTP/1.1 206 Partial Content\r
518
_multipart_range_response = (206, """HTTP/1.1 206 Partial Content\r
544
519
Date: Tue, 11 Jul 2006 04:49:48 GMT\r
545
520
Server: Apache/2.0.54 (Fedora)\r
546
521
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
632
607
Content-Length: 35\r
633
608
Connection: close\r
635
""", b"""Bazaar-NG meta directory, format 1
610
""", """Bazaar-NG meta directory, format 1
639
_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
640
615
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
641
616
Server: Apache/2.0.54 (Fedora)\r
642
617
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
645
620
Connection: close\r
646
621
Content-Type: text/plain; charset=UTF-8\r
648
""", b"""Bazaar-NG meta directory, format 1
623
""", """Bazaar-NG meta directory, format 1
652
_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
653
628
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
654
629
Server: Apache/2.0.54 (Fedora)\r
655
630
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
658
633
Content-Length: 100\r
659
634
Connection: close\r
661
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06
636
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
662
637
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
665
_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
666
641
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
667
642
Server: Apache/2.0.54 (Fedora)\r
668
643
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
673
648
Connection: close\r
674
649
Content-Type: text/plain; charset=UTF-8\r
676
""", b"""mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
679
_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
680
655
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
681
656
Connection: close\r
682
657
Content-Type: text/html; charset=iso-8859-1\r
684
""", b"""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
659
""", """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
686
661
<title>404 Not Found</title>
726
701
class TestHandleResponse(tests.TestCase):
728
703
def _build_HTTPMessage(self, raw_headers):
729
status_and_headers = BytesIO(raw_headers)
704
status_and_headers = StringIO(raw_headers)
730
705
# Get rid of the status line
731
706
status_and_headers.readline()
732
msg = parse_headers(status_and_headers)
707
msg = httplib.HTTPMessage(status_and_headers)
738
710
def get_response(self, a_response):
739
711
"""Process a supplied response, and return the result."""
740
712
code, raw_headers, body = a_response
741
getheader = self._build_HTTPMessage(raw_headers)
742
return response.handle_response(
743
'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]))
745
717
def test_full_text(self):
746
718
out = self.get_response(_full_text_response)
747
# It is a BytesIO from the original data
719
# It is a StringIO from the original data
748
720
self.assertEqual(_full_text_response[2], out.read())
750
722
def test_single_range(self):
791
763
def test_full_text_no_content_type(self):
792
764
# We should not require Content-Type for a full response
793
765
code, raw_headers, body = _full_text_response_no_content_type
794
getheader = self._build_HTTPMessage(raw_headers)
795
out = response.handle_response(
796
'http://foo', code, getheader, BytesIO(body))
766
msg = self._build_HTTPMessage(raw_headers)
767
out = response.handle_response('http://foo', code, msg, StringIO(body))
797
768
self.assertEqual(body, out.read())
799
770
def test_full_text_no_content_length(self):
800
771
code, raw_headers, body = _full_text_response_no_content_length
801
getheader = self._build_HTTPMessage(raw_headers)
802
out = response.handle_response(
803
'http://foo', code, getheader, BytesIO(body))
772
msg = self._build_HTTPMessage(raw_headers)
773
out = response.handle_response('http://foo', code, msg, StringIO(body))
804
774
self.assertEqual(body, out.read())
806
776
def test_missing_content_range(self):
807
777
code, raw_headers, body = _single_range_no_content_range
808
getheader = self._build_HTTPMessage(raw_headers)
778
msg = self._build_HTTPMessage(raw_headers)
809
779
self.assertRaises(errors.InvalidHttpResponse,
810
780
response.handle_response,
811
'http://bogus', code, getheader, BytesIO(body))
781
'http://bogus', code, msg, StringIO(body))
813
783
def test_multipart_no_content_range(self):
814
784
code, raw_headers, body = _multipart_no_content_range
815
getheader = self._build_HTTPMessage(raw_headers)
785
msg = self._build_HTTPMessage(raw_headers)
816
786
self.assertRaises(errors.InvalidHttpResponse,
817
787
response.handle_response,
818
'http://bogus', code, getheader, BytesIO(body))
788
'http://bogus', code, msg, StringIO(body))
820
790
def test_multipart_no_boundary(self):
821
791
out = self.get_response(_multipart_no_boundary)
833
super(TestRangeFileSizeReadLimited, self).setUp()
803
tests.TestCase.setUp(self)
834
804
# create a test datablock larger than _max_read_size.
835
805
chunk_size = response.RangeFile._max_read_size
836
test_pattern = b'0123456789ABCDEF'
837
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))
838
808
self.test_data_len = len(self.test_data)
840
810
def test_max_read_size(self):