14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests from HTTP response parsing."""
17
"""Tests from HTTP response parsing.
20
We test two main things in this module the RangeFile class and the
21
handle_response method.
23
There are four different kinds of RangeFile:
24
- a whole file whose size is unknown, seen as a simple byte stream,
25
- a whole file whose size is known, we can't read past its end,
26
- a single range file, a part of a file with a start and a size,
27
- a multiple range file, several consecutive parts with known start offset
30
Some properties are common to all kinds:
31
- seek can only be forward (its really a socket underneath),
32
- read can't cross ranges,
33
- 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
37
The handle_response method read the response body of a GET request an returns
38
the corresponding RangeFile.
19
42
from cStringIO import StringIO
22
from bzrlib import errors
23
from bzrlib.transport import http
24
49
from bzrlib.transport.http import response
25
from bzrlib.tests import TestCase
28
class TestRangeFileAccess(TestCase):
32
self.alpha = 'abcdefghijklmnopqrstuvwxyz'
33
# Each file is defined as a tuple (builder, start), 'builder' is a
34
# callable returning a RangeFile and 'start' the start of the first (or
36
self.files = [(self._file_size_unknown, 0),
37
(self._file_size_known, 0),
38
(self._file_single_range, 10),
39
(self._file_multi_ranges, 10),]
42
def _file_size_unknown(self):
43
return response.RangeFile('Whole_file_size_unknown',
46
def _file_size_known(self):
48
f = response.RangeFile('Whole_file_size_known', StringIO(alpha))
49
f.set_range(0, len(alpha))
52
def _file_single_range(self):
54
f = response.RangeFile('Single_range_file', StringIO(alpha))
55
f.set_range(10, len(alpha))
58
def _file_multi_ranges(self):
61
boundary = 'separation'
62
bline = '--' + boundary + '\r\n'
66
for (start, part) in [(10, alpha), (100, alpha), (126, alpha.upper())]:
68
content += 'Content-Range: bytes %d-%d/%d\r\n' % (start,
75
data = ''.join(content)
76
f = response.RangeFile('Multiple_ranges_file', StringIO(data))
77
# Ranges are set by decoding the headers
78
f.set_boundary(boundary)
81
def _check_accesses_inside_range(self, f, start=0):
52
class TestRangeFileMixin(object):
53
"""Tests for accessing the first range in a RangeFile."""
55
# A simple string used to represent a file part (also called a range), in
56
# which offsets are easy to calculate for test writers. It's used as a
57
# building block with slight variations but basically 'a' if the first char
58
# of the range and 'z' is the last.
59
alpha = 'abcdefghijklmnopqrstuvwxyz'
61
def test_can_read_at_first_access(self):
62
"""Test that the just created file can be read."""
63
self.assertEquals(self.alpha, self._file.read())
65
def test_seek_read(self):
66
"""Test seek/read inside the range."""
68
start = self.first_range_start
69
# Before any use, tell() should be at the range start
82
70
self.assertEquals(start, f.tell())
83
self.assertEquals('abc', f.read(3))
71
cur = start # For an overall offset assertion
84
74
self.assertEquals('def', f.read(3))
85
self.assertEquals(start + 6, f.tell())
87
self.assertEquals('klm', f.read(3))
88
self.assertEquals('no', f.read(2))
89
self.assertEquals(start + 15, f.tell())
90
# Unbounded read, should not cross range
91
self.assertEquals('pqrstuvwxyz', f.read())
93
def test_valid_accesses(self):
94
"""Test valid accesses: inside one or more ranges"""
95
alpha = 'abcdefghijklmnopqrstuvwxyz'
97
for builder, start in self.files[:3]:
98
self._check_accesses_inside_range(builder(), start)
100
f = self._file_multi_ranges()
101
self._check_accesses_inside_range(f, start=10)
102
f.seek(100) # Will trigger the decoding and setting of the second range
103
self._check_accesses_inside_range(f, 100)
105
f = self._file_multi_ranges()
78
self.assertEquals('klmn', f.read(4))
80
self.assertEquals(cur, f.tell())
82
def test_unbounded_read_after_seek(self):
85
# Should not cross ranges
86
self.assertEquals('yz', f.read())
88
def test_seek_backwards(self):
90
start = self.first_range_start
93
self.assertRaises(errors.InvalidRange, f.seek, start + 5)
95
def test_seek_outside_single_range(self):
97
if f._size == -1 or f._boundary is not None:
98
raise tests.TestNotApplicable('Needs a fully defined range')
99
# Will seek past the range and then errors out
100
self.assertRaises(errors.InvalidRange,
101
f.seek, self.first_range_start + 27)
103
def test_read_past_end_of_range(self):
106
raise tests.TestNotApplicable("Can't check an unknown size")
107
start = self.first_range_start
109
self.assertRaises(errors.InvalidRange, f.read, 10)
112
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
113
"""Test a RangeFile for a whole file whose size is not known."""
116
super(TestRangeFileSizeUnknown, self).setUp()
117
self._file = response.RangeFile('Whole_file_size_known',
118
StringIO(self.alpha))
119
# We define no range, relying on RangeFile to provide default values
120
self.first_range_start = 0 # It's the whole file
122
def test_seek_from_end(self):
123
self.assertRaises(errors.InvalidRange, self._file.seek, -1, 2)
126
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
127
"""Test a RangeFile for a whole file whose size is known."""
130
super(TestRangeFileSizeKnown, self).setUp()
131
self._file = response.RangeFile('Whole_file_size_known',
132
StringIO(self.alpha))
133
self._file.set_range(0, len(self.alpha))
134
self.first_range_start = 0 # It's the whole file
137
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
138
"""Test a RangeFile for a single range."""
141
super(TestRangeFileSingleRange, self).setUp()
142
self._file = response.RangeFile('Single_range_file',
143
StringIO(self.alpha))
144
self.first_range_start = 15
145
self._file.set_range(self.first_range_start, len(self.alpha))
148
class TestRangeFilMultipleRanges(tests.TestCase, TestRangeFileMixin):
149
"""Test a RangeFile for multiple ranges."""
152
super(TestRangeFilMultipleRanges, self).setUp()
154
boundary = 'separation'
157
self.first_range_start = 25
158
file_size = 200 # big enough to encompass all ranges
159
for (start, part) in [(self.first_range_start, self.alpha),
160
# Two contiguous ranges
162
(126, self.alpha.upper())]:
163
content += self._multipart_byterange(part, start, boundary,
166
content += self._boundary_line(boundary)
168
self._file = response.RangeFile('Multiple_ranges_file',
170
# Ranges are set by decoding the range headers, the RangeFile user is
171
# supposed to call the following before using seek or read since it
172
# requires knowing the *response* headers (in that case the boundary
173
# which is part of the Content-Type header).
174
self._file.set_boundary(boundary)
176
def _boundary_line(self, boundary):
177
"""Helper to build the formatted boundary line."""
178
return '--' + boundary + '\r\n'
180
def _multipart_byterange(self, data, offset, boundary, file_size='*'):
181
"""Encode a part of a file as a multipart/byterange MIME type.
183
When a range request is issued, the HTTP response body can be
184
decomposed in parts, each one representing a range (start, size) in a
187
:param data: The payload.
188
:param offset: where data starts in the file
189
:param boundary: used to separate the parts
190
:param file_size: the size of the file containing the range (default to
193
:return: a string containing the data encoded as it will appear in the
196
bline = self._boundary_line(boundary)
197
# Each range begins with a boundary line
199
# A range is described by a set of headers, but only 'Content-Range' is
200
# required for our implementation (TestHandleResponse below will
201
# exercise ranges with multiple or missing headers')
202
range += 'Content-Range: bytes %d-%d/%d\r\n' % (offset,
206
# Finally the raw bytes
210
def test_read_all_ranges(self):
212
self.assertEquals(self.alpha, f.read()) # Read first range
213
f.seek(100) # Trigger the second range recognition
214
self.assertEquals(self.alpha, f.read()) # Read second range
215
self.assertEquals(126, f.tell())
216
f.seek(126) # Start of third range which is also the current pos !
217
self.assertEquals('A', f.read(1))
219
self.assertEquals('LMN', f.read(3))
221
def test_seek_into_void(self):
223
start = self.first_range_start
107
225
# Seeking to a point between two ranges is possible (only once) but
108
226
# reading there is forbidden
110
228
# We crossed a range boundary, so now the file is positioned at the
111
229
# start of the new range (i.e. trying to seek below 100 will error out)
115
f = self._file_multi_ranges()
116
self.assertEquals(self.alpha, f.read()) # Read first range
118
self.assertEquals(self.alpha, f.read()) # Read second range
119
self.assertEquals(126, f.tell())
120
f.seek(126) # Start of third range which is also the current pos !
121
self.assertEquals('A', f.read(1))
123
def _check_file_boundaries(self, f, start=0):
125
self.assertRaises(errors.InvalidRange, f.read, 27)
126
# Will seek past the range and then errors out
127
self.assertRaises(errors.InvalidRange, f.seek, start + 27)
129
def _check_beyond_range(self, builder, start):
132
# Will try to read past the end of the range
133
self.assertRaises(errors.InvalidRange, f.read, 10)
135
def _check_seek_backwards(self, f, start=0):
137
# Can't seek backwards
138
self.assertRaises(errors.InvalidRange, f.seek, start + 5)
140
def test_invalid_accesses(self):
141
"""Test errors triggered by invalid accesses."""
143
f = self._file_size_unknown()
144
self.assertRaises(errors.InvalidRange, f.seek, -1, 2)
146
for builder, start in self.files:
147
self._check_seek_backwards(builder(), start)
149
for builder, start in self.files[1:3]:
150
self._check_file_boundaries(builder(), start)
152
f = self._file_multi_ranges()
153
self._check_accesses_inside_range(f, start=10)
154
f.seek(40) # Will trigger the decoding and setting of the second range
155
self.assertEquals(100, f.tell())
156
self._check_accesses_inside_range(f, 100)
159
self._check_beyond_range(self._file_single_range, start=10)
160
self._check_beyond_range(self._file_multi_ranges, start=10)
162
f = self._file_multi_ranges()
163
f.seek(40) # Past the first range but before the second
233
def test_seek_above_ranges(self):
235
start = self.first_range_start
236
f.seek(126) # skip the two first ranges
237
self.assertEquals('AB', f.read(2))
239
def test_seek_twice_between_ranges(self):
241
start = self.first_range_start
242
f.seek(start + 40) # Past the first range but before the second
164
243
# Now the file is positioned at the second range start (100)
165
self.assertRaises(errors.InvalidRange, f.seek, 41)
167
f = self._file_multi_ranges()
168
# We can seek across ranges but not beyond
169
self.assertRaises(errors.InvalidRange, f.read, 127)
172
class TestRanges(TestCase):
244
self.assertRaises(errors.InvalidRange, f.seek, start + 41)
247
class TestRanges(tests.TestCase):
174
249
def test_range_syntax(self):