/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_http_response.py

  • Committer: Vincent Ladeuil
  • Date: 2007-12-07 22:46:31 UTC
  • mto: (2929.3.16 https) (3097.2.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 3099.
  • Revision ID: v.ladeuil+lp@free.fr-20071207224631-eauq1t40u3jqh9rw
Spiv review feedback.

* bzrlib/tests/test_http_response.py:
Redesigned following spiv advices (with some liberties so all
errors are still mine ;).

* bzrlib/tests/test_errors.py:
Add tests for InvalidRange and InvalidHttpRange.

* bzrlib/tests/HttpServer.py:
(TestingHTTPRequestHandler.get_multiple_ranges): One boundary line
before each range and one final boundary line.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
16
16
 
17
 
"""Tests from HTTP response parsing."""
 
17
"""Tests from HTTP response parsing.
 
18
 
 
19
 
 
20
We test two main things in this module the RangeFile class and the
 
21
handle_response method.
 
22
 
 
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
 
28
  and size.
 
29
 
 
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
 
36
 
 
37
The handle_response method read the response body of a GET request an returns
 
38
the corresponding RangeFile.
 
39
 
 
40
"""
18
41
 
19
42
from cStringIO import StringIO
20
43
import httplib
21
44
 
22
 
from bzrlib import errors
23
 
from bzrlib.transport import http
 
45
from bzrlib import (
 
46
    errors,
 
47
    tests,
 
48
    )
24
49
from bzrlib.transport.http import response
25
 
from bzrlib.tests import TestCase
26
 
 
27
 
 
28
 
class TestRangeFileAccess(TestCase):
29
 
    """Test RangeFile."""
30
 
 
31
 
    def setUp(self):
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
35
 
        # unique) range.
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),]
40
 
 
41
 
 
42
 
    def _file_size_unknown(self):
43
 
        return response.RangeFile('Whole_file_size_unknown',
44
 
                                  StringIO(self.alpha))
45
 
 
46
 
    def _file_size_known(self):
47
 
        alpha = self.alpha
48
 
        f = response.RangeFile('Whole_file_size_known', StringIO(alpha))
49
 
        f.set_range(0, len(alpha))
50
 
        return f
51
 
 
52
 
    def _file_single_range(self):
53
 
        alpha = self.alpha
54
 
        f = response.RangeFile('Single_range_file', StringIO(alpha))
55
 
        f.set_range(10, len(alpha))
56
 
        return f
57
 
 
58
 
    def _file_multi_ranges(self):
59
 
        alpha = self.alpha
60
 
 
61
 
        boundary = 'separation'
62
 
        bline = '--' + boundary + '\r\n'
63
 
        content = []
64
 
        content += bline
65
 
        file_size = 200
66
 
        for (start, part) in [(10, alpha), (100, alpha), (126, alpha.upper())]:
67
 
            plen = len(part)
68
 
            content += 'Content-Range: bytes %d-%d/%d\r\n' % (start,
69
 
                                                              start+plen-1,
70
 
                                                              file_size)
71
 
            content += '\r\n'
72
 
            content += part
73
 
            content += bline
74
 
 
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)
79
 
        return f
80
 
 
81
 
    def _check_accesses_inside_range(self, f, start=0):
 
50
 
 
51
 
 
52
class TestRangeFileMixin(object):
 
53
    """Tests for accessing the first range in a RangeFile."""
 
54
 
 
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'
 
60
 
 
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())
 
64
 
 
65
    def test_seek_read(self):
 
66
        """Test seek/read inside the range."""
 
67
        f = self._file
 
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
 
72
        f.seek(start + 3)
 
73
        cur += 3
84
74
        self.assertEquals('def', f.read(3))
85
 
        self.assertEquals(start + 6, f.tell())
86
 
        f.seek(start + 10)
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())
92
 
 
93
 
    def test_valid_accesses(self):
94
 
        """Test valid accesses: inside one or more ranges"""
95
 
        alpha = 'abcdefghijklmnopqrstuvwxyz'
96
 
 
97
 
        for builder, start in self.files[:3]:
98
 
            self._check_accesses_inside_range(builder(), start)
99
 
 
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)
104
 
 
105
 
        f =  self._file_multi_ranges()
106
 
        f.seek(10)
 
75
        cur += len('def')
 
76
        f.seek(4, 1)
 
77
        cur += 4
 
78
        self.assertEquals('klmn', f.read(4))
 
79
        cur += len('klmn')
 
80
        self.assertEquals(cur, f.tell())
 
81
 
 
82
    def test_unbounded_read_after_seek(self):
 
83
        f = self._file
 
84
        f.seek(24, 1)
 
85
        # Should not cross ranges
 
86
        self.assertEquals('yz', f.read())
 
87
 
 
88
    def test_seek_backwards(self):
 
89
        f = self._file
 
90
        start = self.first_range_start
 
91
        f.seek(start)
 
92
        f.read(12)
 
93
        self.assertRaises(errors.InvalidRange, f.seek, start + 5)
 
94
 
 
95
    def test_seek_outside_single_range(self):
 
96
        f = self._file
 
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)
 
102
 
 
103
    def test_read_past_end_of_range(self):
 
104
        f = self._file
 
105
        if f._size == -1:
 
106
            raise tests.TestNotApplicable("Can't check an unknown size")
 
107
        start = self.first_range_start
 
108
        f.seek(start + 20)
 
109
        self.assertRaises(errors.InvalidRange, f.read, 10)
 
110
 
 
111
 
 
112
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
 
113
    """Test a RangeFile for a whole file whose size is not known."""
 
114
 
 
115
    def setUp(self):
 
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
 
121
 
 
122
    def test_seek_from_end(self):
 
123
        self.assertRaises(errors.InvalidRange, self._file.seek, -1, 2)
 
124
 
 
125
 
 
126
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
 
127
    """Test a RangeFile for a whole file whose size is known."""
 
128
 
 
129
    def setUp(self):
 
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
 
135
 
 
136
 
 
137
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
 
138
    """Test a RangeFile for a single range."""
 
139
 
 
140
    def setUp(self):
 
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))
 
146
 
 
147
 
 
148
class TestRangeFilMultipleRanges(tests.TestCase, TestRangeFileMixin):
 
149
    """Test a RangeFile for multiple ranges."""
 
150
 
 
151
    def setUp(self):
 
152
        super(TestRangeFilMultipleRanges, self).setUp()
 
153
 
 
154
        boundary = 'separation'
 
155
 
 
156
        content = ''
 
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
 
161
                              (100, self.alpha),
 
162
                              (126, self.alpha.upper())]:
 
163
            content += self._multipart_byterange(part, start, boundary,
 
164
                                                 file_size)
 
165
        # Final boundary
 
166
        content += self._boundary_line(boundary)
 
167
 
 
168
        self._file = response.RangeFile('Multiple_ranges_file',
 
169
                                        StringIO(content))
 
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)
 
175
 
 
176
    def _boundary_line(self, boundary):
 
177
        """Helper to build the formatted boundary line."""
 
178
        return '--' + boundary + '\r\n'
 
179
 
 
180
    def _multipart_byterange(self, data, offset, boundary, file_size='*'):
 
181
        """Encode a part of a file as a multipart/byterange MIME type.
 
182
 
 
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
 
185
        file.
 
186
 
 
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
 
191
            '*' meaning unknown)
 
192
 
 
193
        :return: a string containing the data encoded as it will appear in the
 
194
            HTTP response body.
 
195
        """
 
196
        bline = self._boundary_line(boundary)
 
197
        # Each range begins with a boundary line
 
198
        range = bline
 
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,
 
203
                                                        offset+len(data)-1,
 
204
                                                        file_size)
 
205
        range += '\r\n'
 
206
        # Finally the raw bytes
 
207
        range += data
 
208
        return range
 
209
 
 
210
    def test_read_all_ranges(self):
 
211
        f = self._file
 
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))
 
218
        f.seek(10, 1)
 
219
        self.assertEquals('LMN', f.read(3))
 
220
 
 
221
    def test_seek_into_void(self):
 
222
        f = self._file
 
223
        start = self.first_range_start
 
224
        f.seek(start)
107
225
        # Seeking to a point between two ranges is possible (only once) but
108
226
        # reading there is forbidden
109
 
        f.seek(40)
 
227
        f.seek(start + 40)
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)
112
230
        f.seek(100)
113
231
        f.seek(125)
114
232
 
115
 
        f =  self._file_multi_ranges()
116
 
        self.assertEquals(self.alpha, f.read()) # Read first range
117
 
        f.seek(100)
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))
122
 
 
123
 
    def _check_file_boundaries(self, f, start=0):
124
 
        f.seek(start)
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)
128
 
 
129
 
    def _check_beyond_range(self, builder, start):
130
 
        f = builder()
131
 
        f.seek(start + 20)
132
 
        # Will try to read past the end of the range
133
 
        self.assertRaises(errors.InvalidRange, f.read, 10)
134
 
 
135
 
    def _check_seek_backwards(self, f, start=0):
136
 
        f.read(start + 12)
137
 
        # Can't seek backwards
138
 
        self.assertRaises(errors.InvalidRange, f.seek, start + 5)
139
 
 
140
 
    def test_invalid_accesses(self):
141
 
        """Test errors triggered by invalid accesses."""
142
 
 
143
 
        f = self._file_size_unknown()
144
 
        self.assertRaises(errors.InvalidRange, f.seek, -1, 2)
145
 
 
146
 
        for builder, start in self.files:
147
 
            self._check_seek_backwards(builder(), start)
148
 
 
149
 
        for builder, start in self.files[1:3]:
150
 
            self._check_file_boundaries(builder(), start)
151
 
 
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)
157
 
 
158
 
 
159
 
        self._check_beyond_range(self._file_single_range, start=10)
160
 
        self._check_beyond_range(self._file_multi_ranges, start=10)
161
 
 
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):
 
234
        f = self._file
 
235
        start = self.first_range_start
 
236
        f.seek(126) # skip the two first ranges
 
237
        self.assertEquals('AB', f.read(2))
 
238
 
 
239
    def test_seek_twice_between_ranges(self):
 
240
        f = self._file
 
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)
166
 
 
167
 
        f = self._file_multi_ranges()
168
 
        # We can seek across ranges but not beyond
169
 
        self.assertRaises(errors.InvalidRange, f.read, 127)
170
 
 
171
 
 
172
 
class TestRanges(TestCase):
 
244
        self.assertRaises(errors.InvalidRange, f.seek, start + 41)
 
245
 
 
246
 
 
247
class TestRanges(tests.TestCase):
173
248
 
174
249
    def test_range_syntax(self):
175
250
 
196
271
        nok('bytes xx-12/zzz')
197
272
        nok('bytes 11-yy/zzz')
198
273
 
 
274
 
199
275
# Taken from real request responses
200
276
_full_text_response = (200, """HTTP/1.1 200 OK\r
201
277
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
364
440
""")
365
441
 
366
442
 
367
 
class TestHandleResponse(TestCase):
 
443
class TestHandleResponse(tests.TestCase):
368
444
 
369
445
    def _build_HTTPMessage(self, raw_headers):
370
446
        status_and_headers = StringIO(raw_headers)