1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
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.
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.
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
17
"""Tests from HTTP response parsing.
19
The handle_response method read the response body of a GET request an returns
20
the corresponding RangeFile.
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
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,
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
40
from cStringIO import StringIO
47
from bzrlib.transport.http import (
53
class ReadSocket(object):
54
"""A socket-like object that can be given a predefined content."""
56
def __init__(self, data):
57
self.readfile = StringIO(data)
59
def makefile(self, mode='r', bufsize=None):
62
class FakeHTTPConnection(_urllib2_wrappers.HTTPConnection):
64
def __init__(self, sock):
65
_urllib2_wrappers.HTTPConnection.__init__(self, 'localhost')
66
# Set the socket to bypass the connection
70
"""Ignores the writes on the socket."""
74
class TestHTTPConnection(tests.TestCase):
76
def test_cleanup_pipe(self):
77
sock = ReadSocket("""HTTP/1.1 200 OK\r
78
Content-Type: text/plain; charset=UTF-8\r
83
conn = FakeHTTPConnection(sock)
84
# Simulate the request sending so that the connection will be able to
86
conn.putrequest('GET', 'http://localhost/fictious')
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
95
self.assertContainsRe(self._get_log(keep_log_file=True),
96
'Got a 200 response when asking')
99
class TestRangeFileMixin(object):
100
"""Tests for accessing the first range in a RangeFile."""
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'
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())
112
def test_seek_read(self):
113
"""Test seek/read inside the range."""
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
121
self.assertEquals('def', f.read(3))
125
self.assertEquals('klmn', f.read(4))
127
# read(0) in the middle of a range
128
self.assertEquals('', f.read(0))
132
self.assertEquals(here, f.tell())
133
self.assertEquals(cur, f.tell())
135
def test_read_zero(self):
137
start = self.first_range_start
138
self.assertEquals('', f.read(0))
140
self.assertEquals('', f.read(0))
142
def test_seek_at_range_end(self):
146
def test_read_at_range_end(self):
147
"""Test read behaviour at range end."""
149
self.assertEquals(self.alpha, f.read())
150
self.assertEquals('', f.read(0))
151
self.assertRaises(errors.InvalidRange, f.read, 1)
153
def test_unbounded_read_after_seek(self):
156
# Should not cross ranges
157
self.assertEquals('yz', f.read())
159
def test_seek_backwards(self):
161
start = self.first_range_start
164
self.assertRaises(errors.InvalidRange, f.seek, start + 5)
166
def test_seek_outside_single_range(self):
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)
174
def test_read_past_end_of_range(self):
177
raise tests.TestNotApplicable("Can't check an unknown size")
178
start = self.first_range_start
180
self.assertRaises(errors.InvalidRange, f.read, 10)
182
def test_seek_from_end(self):
183
"""Test seeking from the end of the file.
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.
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
197
self.assertEquals('yz', f.read())
200
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
201
"""Test a RangeFile for a whole file whose size is not known."""
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
210
def test_seek_from_end(self):
211
"""See TestRangeFileMixin.test_seek_from_end.
213
The end of the file can't be determined since the size is unknown.
215
self.assertRaises(errors.InvalidRange, self._file.seek, -1, 2)
217
def test_read_at_range_end(self):
218
"""Test read behaviour at range end."""
220
self.assertEquals(self.alpha, f.read())
221
self.assertEquals('', f.read(0))
222
self.assertEquals('', f.read(1))
224
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
225
"""Test a RangeFile for a whole file whose size is known."""
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
235
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
236
"""Test a RangeFile for a single range."""
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))
246
def test_read_before_range(self):
247
# This can't occur under normal circumstances, we have to force it
249
f._pos = 0 # Force an invalid pos
250
self.assertRaises(errors.InvalidRange, f.read, 2)
252
class TestRangeFilMultipleRanges(tests.TestCase, TestRangeFileMixin):
253
"""Test a RangeFile for multiple ranges.
255
The RangeFile used for the tests contains three ranges:
257
- at offset 25: alpha
258
- at offset 100: alpha
259
- at offset 126: alpha.upper()
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.
266
super(TestRangeFilMultipleRanges, self).setUp()
268
boundary = 'separation'
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
276
(126, self.alpha.upper())]:
277
content += self._multipart_byterange(part, start, boundary,
280
content += self._boundary_line(boundary)
282
self._file = response.RangeFile('Multiple_ranges_file',
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)
290
def _boundary_line(self, boundary):
291
"""Helper to build the formatted boundary line."""
292
return '--' + boundary + '\r\n'
294
def _multipart_byterange(self, data, offset, boundary, file_size='*'):
295
"""Encode a part of a file as a multipart/byterange MIME type.
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
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
307
:return: a string containing the data encoded as it will appear in the
310
bline = self._boundary_line(boundary)
311
# Each range begins with a boundary line
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,
320
# Finally the raw bytes
324
def test_read_all_ranges(self):
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))
333
self.assertEquals('LMN', f.read(3))
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
343
self.assertEquals('yz', f.read())
344
self.assertRaises(errors.InvalidRange, f.seek, -2, 2)
346
def test_seek_into_void(self):
348
start = self.first_range_start
350
# Seeking to a point between two ranges is possible (only once) but
351
# reading there is forbidden
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)
358
def test_seek_across_ranges(self):
360
start = self.first_range_start
361
f.seek(126) # skip the two first ranges
362
self.assertEquals('AB', f.read(2))
364
def test_seek_twice_between_ranges(self):
366
start = self.first_range_start
367
f.seek(start + 40) # Past the first range but before the second
368
# Now the file is positioned at the second range start (100)
369
self.assertRaises(errors.InvalidRange, f.seek, start + 41)
371
def test_seek_at_range_end(self):
372
"""Test seek behavior at range end."""
378
def test_read_at_range_end(self):
380
self.assertEquals(self.alpha, f.read())
381
self.assertEquals(self.alpha, f.read())
382
self.assertEquals(self.alpha.upper(), f.read())
383
self.assertRaises(errors.InvalidHttpResponse, f.read, 1)
386
class TestRangeFileVarious(tests.TestCase):
387
"""Tests RangeFile aspects not covered elsewhere."""
389
def test_seek_whence(self):
390
"""Test the seek whence parameter values."""
391
f = response.RangeFile('foo', StringIO('abc'))
396
self.assertRaises(ValueError, f.seek, 0, 14)
398
def test_range_syntax(self):
399
"""Test the Content-Range scanning."""
401
f = response.RangeFile('foo', StringIO())
403
def ok(expected, header_value):
404
f.set_range_from_header(header_value)
405
# Slightly peek under the covers to get the size
406
self.assertEquals(expected, (f.tell(), f._size))
408
ok((1, 10), 'bytes 1-10/11')
409
ok((1, 10), 'bytes 1-10/*')
410
ok((12, 2), '\tbytes 12-13/*')
411
ok((28, 1), ' bytes 28-28/*')
412
ok((2123, 2120), 'bytes 2123-4242/12310')
413
ok((1, 10), 'bytes 1-10/ttt') # We don't check total (ttt)
415
def nok(header_value):
416
self.assertRaises(errors.InvalidHttpRange,
417
f.set_range_from_header, header_value)
421
nok('bytes xx-yyy/zzz')
422
nok('bytes xx-12/zzz')
423
nok('bytes 11-yy/zzz')
427
# Taken from real request responses
428
_full_text_response = (200, """HTTP/1.1 200 OK\r
429
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
430
Server: Apache/2.0.54 (Fedora)\r
431
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
432
ETag: "56691-23-38e9ae00"\r
433
Accept-Ranges: bytes\r
436
Content-Type: text/plain; charset=UTF-8\r
438
""", """Bazaar-NG meta directory, format 1
442
_single_range_response = (206, """HTTP/1.1 206 Partial Content\r
443
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
444
Server: Apache/2.0.54 (Fedora)\r
445
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
446
ETag: "238a3c-16ec2-805c5540"\r
447
Accept-Ranges: bytes\r
448
Content-Length: 100\r
449
Content-Range: bytes 100-199/93890\r
451
Content-Type: text/plain; charset=UTF-8\r
453
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
454
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
457
_single_range_no_content_type = (206, """HTTP/1.1 206 Partial Content\r
458
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
459
Server: Apache/2.0.54 (Fedora)\r
460
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
461
ETag: "238a3c-16ec2-805c5540"\r
462
Accept-Ranges: bytes\r
463
Content-Length: 100\r
464
Content-Range: bytes 100-199/93890\r
467
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
468
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
471
_multipart_range_response = (206, """HTTP/1.1 206 Partial Content\r
472
Date: Tue, 11 Jul 2006 04:49:48 GMT\r
473
Server: Apache/2.0.54 (Fedora)\r
474
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
475
ETag: "238a3c-16ec2-805c5540"\r
476
Accept-Ranges: bytes\r
477
Content-Length: 1534\r
479
Content-Type: multipart/byteranges; boundary=418470f848b63279b\r
481
\r""", """--418470f848b63279b\r
482
Content-type: text/plain; charset=UTF-8\r
483
Content-range: bytes 0-254/93890\r
485
mbp@sourcefrog.net-20050309040815-13242001617e4a06
486
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e7627
487
mbp@sourcefrog.net-20050309040957-6cad07f466bb0bb8
488
mbp@sourcefrog.net-20050309041501-c840e09071de3b67
489
mbp@sourcefrog.net-20050309044615-c24a3250be83220a
491
--418470f848b63279b\r
492
Content-type: text/plain; charset=UTF-8\r
493
Content-range: bytes 1000-2049/93890\r
496
mbp@sourcefrog.net-20050311063625-07858525021f270b
497
mbp@sourcefrog.net-20050311231934-aa3776aff5200bb9
498
mbp@sourcefrog.net-20050311231953-73aeb3a131c3699a
499
mbp@sourcefrog.net-20050311232353-f5e33da490872c6a
500
mbp@sourcefrog.net-20050312071639-0a8f59a34a024ff0
501
mbp@sourcefrog.net-20050312073432-b2c16a55e0d6e9fb
502
mbp@sourcefrog.net-20050312073831-a47c3335ece1920f
503
mbp@sourcefrog.net-20050312085412-13373aa129ccbad3
504
mbp@sourcefrog.net-20050313052251-2bf004cb96b39933
505
mbp@sourcefrog.net-20050313052856-3edd84094687cb11
506
mbp@sourcefrog.net-20050313053233-e30a4f28aef48f9d
507
mbp@sourcefrog.net-20050313053853-7c64085594ff3072
508
mbp@sourcefrog.net-20050313054757-a86c3f5871069e22
509
mbp@sourcefrog.net-20050313061422-418f1f73b94879b9
510
mbp@sourcefrog.net-20050313120651-497bd231b19df600
511
mbp@sourcefrog.net-20050314024931-eae0170ef25a5d1a
512
mbp@sourcefrog.net-20050314025438-d52099f915fe65fc
513
mbp@sourcefrog.net-20050314025539-637a636692c055cf
514
mbp@sourcefrog.net-20050314025737-55eb441f430ab4ba
515
mbp@sourcefrog.net-20050314025901-d74aa93bb7ee8f62
517
--418470f848b63279b--\r
521
_multipart_squid_range_response = (206, """HTTP/1.0 206 Partial Content\r
522
Date: Thu, 31 Aug 2006 21:16:22 GMT\r
523
Server: Apache/2.2.2 (Unix) DAV/2\r
524
Last-Modified: Thu, 31 Aug 2006 17:57:06 GMT\r
525
Accept-Ranges: bytes\r
526
Content-Type: multipart/byteranges; boundary="squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196"\r
527
Content-Length: 598\r
528
X-Cache: MISS from localhost.localdomain\r
529
X-Cache-Lookup: HIT from localhost.localdomain:3128\r
530
Proxy-Connection: keep-alive\r
534
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196\r
535
Content-Type: text/plain\r
536
Content-Range: bytes 0-99/18672\r
540
scott@netsplit.com-20050708230047-47c7868f276b939f fulltext 0 863 :
542
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196\r
543
Content-Type: text/plain\r
544
Content-Range: bytes 300-499/18672\r
546
com-20050708231537-2b124b835395399a :
547
scott@netsplit.com-20050820234126-551311dbb7435b51 line-delta 1803 479 .scott@netsplit.com-20050820232911-dc4322a084eadf7e :
548
scott@netsplit.com-20050821213706-c86\r
549
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196--\r
554
_full_text_response_no_content_type = (200, """HTTP/1.1 200 OK\r
555
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
556
Server: Apache/2.0.54 (Fedora)\r
557
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
558
ETag: "56691-23-38e9ae00"\r
559
Accept-Ranges: bytes\r
563
""", """Bazaar-NG meta directory, format 1
567
_full_text_response_no_content_length = (200, """HTTP/1.1 200 OK\r
568
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
569
Server: Apache/2.0.54 (Fedora)\r
570
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
571
ETag: "56691-23-38e9ae00"\r
572
Accept-Ranges: bytes\r
574
Content-Type: text/plain; charset=UTF-8\r
576
""", """Bazaar-NG meta directory, format 1
580
_single_range_no_content_range = (206, """HTTP/1.1 206 Partial Content\r
581
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
582
Server: Apache/2.0.54 (Fedora)\r
583
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
584
ETag: "238a3c-16ec2-805c5540"\r
585
Accept-Ranges: bytes\r
586
Content-Length: 100\r
589
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
590
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
593
_single_range_response_truncated = (206, """HTTP/1.1 206 Partial Content\r
594
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
595
Server: Apache/2.0.54 (Fedora)\r
596
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
597
ETag: "238a3c-16ec2-805c5540"\r
598
Accept-Ranges: bytes\r
599
Content-Length: 100\r
600
Content-Range: bytes 100-199/93890\r
602
Content-Type: text/plain; charset=UTF-8\r
604
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
607
_invalid_response = (444, """HTTP/1.1 444 Bad Response\r
608
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
610
Content-Type: text/html; charset=iso-8859-1\r
612
""", """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
614
<title>404 Not Found</title>
617
<p>I don't know what I'm doing</p>
623
_multipart_no_content_range = (206, """HTTP/1.0 206 Partial Content\r
624
Content-Type: multipart/byteranges; boundary=THIS_SEPARATES\r
625
Content-Length: 598\r
630
Content-Type: text/plain\r
637
_multipart_no_boundary = (206, """HTTP/1.0 206 Partial Content\r
638
Content-Type: multipart/byteranges; boundary=THIS_SEPARATES\r
639
Content-Length: 598\r
644
Content-Type: text/plain\r
645
Content-Range: bytes 0-18/18672\r
649
The range ended at the line above, this text is garbage instead of a boundary
654
class TestHandleResponse(tests.TestCase):
656
def _build_HTTPMessage(self, raw_headers):
657
status_and_headers = StringIO(raw_headers)
658
# Get rid of the status line
659
status_and_headers.readline()
660
msg = httplib.HTTPMessage(status_and_headers)
663
def get_response(self, a_response):
664
"""Process a supplied response, and return the result."""
665
code, raw_headers, body = a_response
666
msg = self._build_HTTPMessage(raw_headers)
667
return response.handle_response('http://foo', code, msg,
668
StringIO(a_response[2]))
670
def test_full_text(self):
671
out = self.get_response(_full_text_response)
672
# It is a StringIO from the original data
673
self.assertEqual(_full_text_response[2], out.read())
675
def test_single_range(self):
676
out = self.get_response(_single_range_response)
679
self.assertEqual(_single_range_response[2], out.read(100))
681
def test_single_range_no_content(self):
682
out = self.get_response(_single_range_no_content_type)
685
self.assertEqual(_single_range_no_content_type[2], out.read(100))
687
def test_single_range_truncated(self):
688
out = self.get_response(_single_range_response_truncated)
689
# Content-Range declares 100 but only 51 present
690
self.assertRaises(errors.ShortReadvError, out.seek, out.tell() + 51)
692
def test_multi_range(self):
693
out = self.get_response(_multipart_range_response)
695
# Just make sure we can read the right contents
702
def test_multi_squid_range(self):
703
out = self.get_response(_multipart_squid_range_response)
705
# Just make sure we can read the right contents
712
def test_invalid_response(self):
713
self.assertRaises(errors.InvalidHttpResponse,
714
self.get_response, _invalid_response)
716
def test_full_text_no_content_type(self):
717
# We should not require Content-Type for a full response
718
code, raw_headers, body = _full_text_response_no_content_type
719
msg = self._build_HTTPMessage(raw_headers)
720
out = response.handle_response('http://foo', code, msg, StringIO(body))
721
self.assertEqual(body, out.read())
723
def test_full_text_no_content_length(self):
724
code, raw_headers, body = _full_text_response_no_content_length
725
msg = self._build_HTTPMessage(raw_headers)
726
out = response.handle_response('http://foo', code, msg, StringIO(body))
727
self.assertEqual(body, out.read())
729
def test_missing_content_range(self):
730
code, raw_headers, body = _single_range_no_content_range
731
msg = self._build_HTTPMessage(raw_headers)
732
self.assertRaises(errors.InvalidHttpResponse,
733
response.handle_response,
734
'http://bogus', code, msg, StringIO(body))
736
def test_multipart_no_content_range(self):
737
code, raw_headers, body = _multipart_no_content_range
738
msg = self._build_HTTPMessage(raw_headers)
739
self.assertRaises(errors.InvalidHttpResponse,
740
response.handle_response,
741
'http://bogus', code, msg, StringIO(body))
743
def test_multipart_no_boundary(self):
744
out = self.get_response(_multipart_no_boundary)
745
out.read() # Read the whole range
746
# Fail to find the boundary line
747
self.assertRaises(errors.InvalidHttpResponse, out.seek, 1, 1)