Skip to content

Commit 26fc602

Browse files
author
Benjamin Moody
committed
New module tests.test_url.
This module provides test cases to check that the behavior of wfdb.io._url.NetFile conforms to the standard file object API.
1 parent 3826460 commit 26fc602

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed

tests/test_url.py

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
import gzip
2+
import http.server
3+
import threading
4+
import unittest
5+
6+
import wfdb.io._url
7+
8+
9+
class TestNetFiles(unittest.TestCase):
10+
"""
11+
Test accessing remote files.
12+
"""
13+
def test_requests(self):
14+
"""
15+
Test reading a remote file using various APIs.
16+
17+
This tests that we can create a file object using
18+
wfdb.io._url.openurl(), and tests that the object implements
19+
the standard Python API functions for a file of the
20+
appropriate type.
21+
22+
Parameters
23+
----------
24+
N/A
25+
26+
Returns
27+
-------
28+
N/A
29+
30+
"""
31+
32+
text_data = """
33+
BERNARDO: Who's there?
34+
FRANCISCO: Nay, answer me: stand, and unfold yourself.
35+
BERNARDO: Long live the king!
36+
FRANCISCO: Bernardo?
37+
BERNARDO: He.
38+
FRANCISCO: You come most carefully upon your hour.
39+
BERNARDO: 'Tis now struck twelve; get thee to bed, Francisco.
40+
"""
41+
binary_data = text_data.encode()
42+
file_content = {'/foo.txt': binary_data}
43+
44+
# Test all possible combinations of:
45+
# - whether or not the server supports compression
46+
# - whether or not the server supports random access
47+
# - chosen buffering policy
48+
for allow_gzip in (False, True):
49+
for allow_range in (False, True):
50+
with DummyHTTPServer(file_content=file_content,
51+
allow_gzip=allow_gzip,
52+
allow_range=allow_range) as server:
53+
url = server.url('/foo.txt')
54+
for buffering in (-2, -1, 0, 20):
55+
self._test_text(url, text_data, buffering)
56+
self._test_binary(url, binary_data, buffering)
57+
58+
def _test_text(self, url, content, buffering):
59+
"""
60+
Test reading a URL using text-mode file APIs.
61+
62+
Parameters
63+
----------
64+
url : str
65+
URL of the remote resource.
66+
content : str
67+
Expected content of the resource.
68+
buffering : int
69+
Buffering policy for openurl().
70+
71+
Returns
72+
-------
73+
N/A
74+
75+
"""
76+
# read(-1), readable(), seekable()
77+
with wfdb.io._url.openurl(url, 'r', buffering=buffering) as tf:
78+
self.assertTrue(tf.readable())
79+
self.assertTrue(tf.seekable())
80+
self.assertEqual(tf.read(), content)
81+
self.assertEqual(tf.read(), '')
82+
83+
# read(10)
84+
with wfdb.io._url.openurl(url, 'r', buffering=buffering) as tf:
85+
result = ''
86+
while True:
87+
chunk = tf.read(10)
88+
result += chunk
89+
if len(chunk) < 10:
90+
break
91+
self.assertEqual(result, content)
92+
93+
# readline(), seek(), tell()
94+
with wfdb.io._url.openurl(url, 'r', buffering=buffering) as tf:
95+
result = ''
96+
while True:
97+
rpos = tf.tell()
98+
tf.seek(0)
99+
tf.seek(rpos)
100+
chunk = tf.readline()
101+
result += chunk
102+
if len(chunk) == 0:
103+
break
104+
self.assertEqual(result, content)
105+
106+
def _test_binary(self, url, content, buffering):
107+
"""
108+
Test reading a URL using binary-mode file APIs.
109+
110+
Parameters
111+
----------
112+
url : str
113+
URL of the remote resource.
114+
content : bytes
115+
Expected content of the resource.
116+
buffering : int
117+
Buffering policy for openurl().
118+
119+
Returns
120+
-------
121+
N/A
122+
123+
"""
124+
# read(-1), readable(), seekable()
125+
with wfdb.io._url.openurl(url, 'rb', buffering=buffering) as bf:
126+
self.assertTrue(bf.readable())
127+
self.assertTrue(bf.seekable())
128+
self.assertEqual(bf.read(), content)
129+
self.assertEqual(bf.read(), b'')
130+
self.assertEqual(bf.tell(), len(content))
131+
132+
# read(10)
133+
with wfdb.io._url.openurl(url, 'rb', buffering=buffering) as bf:
134+
result = b''
135+
while True:
136+
chunk = bf.read(10)
137+
result += chunk
138+
if len(chunk) < 10:
139+
break
140+
self.assertEqual(result, content)
141+
self.assertEqual(bf.tell(), len(content))
142+
143+
# readline()
144+
with wfdb.io._url.openurl(url, 'rb', buffering=buffering) as bf:
145+
result = b''
146+
while True:
147+
chunk = bf.readline()
148+
result += chunk
149+
if len(chunk) == 0:
150+
break
151+
self.assertEqual(result, content)
152+
self.assertEqual(bf.tell(), len(content))
153+
154+
# read1(10), seek(), tell()
155+
with wfdb.io._url.openurl(url, 'rb', buffering=buffering) as bf:
156+
bf.seek(0, 2)
157+
self.assertEqual(bf.tell(), len(content))
158+
bf.seek(0)
159+
result = b''
160+
while True:
161+
rpos = bf.tell()
162+
bf.seek(0)
163+
bf.seek(rpos)
164+
chunk = bf.read1(10)
165+
result += chunk
166+
if len(chunk) == 0:
167+
break
168+
self.assertEqual(result, content)
169+
self.assertEqual(bf.tell(), len(content))
170+
171+
# readinto(bytearray(10))
172+
with wfdb.io._url.openurl(url, 'rb', buffering=buffering) as bf:
173+
result = b''
174+
chunk = bytearray(10)
175+
while True:
176+
count = bf.readinto(chunk)
177+
result += chunk[:count]
178+
if count < 10:
179+
break
180+
self.assertEqual(result, content)
181+
self.assertEqual(bf.tell(), len(content))
182+
183+
# readinto1(bytearray(10))
184+
with wfdb.io._url.openurl(url, 'rb', buffering=buffering) as bf:
185+
result = b''
186+
chunk = bytearray(10)
187+
while True:
188+
count = bf.readinto1(chunk)
189+
result += chunk[:count]
190+
if count == 0:
191+
break
192+
self.assertEqual(result, content)
193+
self.assertEqual(bf.tell(), len(content))
194+
195+
196+
class DummyHTTPServer(http.server.HTTPServer):
197+
"""
198+
HTTPServer used to simulate a web server for testing.
199+
200+
The server may be used as a context manager (using "with"); during
201+
execution of the "with" block, a background thread runs that
202+
listens for and handles client requests.
203+
204+
Attributes
205+
----------
206+
file_content : dict
207+
Dictionary containing the content of each file on the server.
208+
The keys are absolute paths (such as "/foo.txt"); the values
209+
are the corresponding content (bytes).
210+
allow_gzip : bool, optional
211+
True if the server should return compressed responses (using
212+
"Content-Encoding: gzip") when the client requests them (using
213+
"Accept-Encoding: gzip").
214+
allow_range : bool, optional
215+
True if the server should return partial responses (using 206
216+
Partial Content and "Content-Range") when the client requests
217+
them (using "Range").
218+
server_address : tuple (str, int), optional
219+
A tuple specifying the address and port number where the
220+
server should listen for connections. If the port is 0, an
221+
arbitrary unused port is selected. The default address is
222+
"127.0.0.1" and the default port is 0.
223+
224+
"""
225+
def __init__(self, file_content, allow_gzip=True, allow_range=True,
226+
server_address=('127.0.0.1', 0)):
227+
super().__init__(server_address, DummyHTTPRequestHandler)
228+
self.file_content = file_content
229+
self.allow_gzip = allow_gzip
230+
self.allow_range = allow_range
231+
232+
def url(self, path='/'):
233+
"""
234+
Generate a URL that points to a file on this server.
235+
236+
Parameters
237+
----------
238+
path : str, optional
239+
Path of the file on the server.
240+
241+
Returns
242+
-------
243+
url : str
244+
Absolute URL for the specified file.
245+
246+
"""
247+
return 'http://127.0.0.1:%d/%s' % (self.server_address[1],
248+
path.lstrip('/'))
249+
250+
def __enter__(self):
251+
super().__enter__()
252+
self.thread = threading.Thread(target=self.serve_forever)
253+
self.thread.start()
254+
return self
255+
256+
def __exit__(self, exc_type, exc_val, exc_tb):
257+
self.shutdown()
258+
self.thread.join()
259+
self.thread = None
260+
return super().__exit__(exc_type, exc_val, exc_tb)
261+
262+
263+
class DummyHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
264+
"""
265+
HTTPRequestHandler used to simulate a web server for testing.
266+
"""
267+
def do_HEAD(self):
268+
self.send_head()
269+
270+
def do_GET(self):
271+
body = self.send_head()
272+
self.wfile.write(body)
273+
274+
def log_message(self, message, *args):
275+
pass
276+
277+
def send_head(self):
278+
content = self.server.file_content.get(self.path)
279+
if content is None:
280+
self.send_error(404)
281+
return b''
282+
283+
headers = {'Content-Type': 'text/plain'}
284+
status = 200
285+
286+
if self.server.allow_gzip:
287+
headers['Vary'] = 'Accept-Encoding'
288+
if 'gzip' in self.headers.get('Accept-Encoding', ''):
289+
content = gzip.compress(content)
290+
headers['Content-Encoding'] = 'gzip'
291+
292+
if self.server.allow_range:
293+
headers['Accept-Ranges'] = 'bytes'
294+
req_range = self.headers.get('Range', '')
295+
if req_range.startswith('bytes='):
296+
start, end = req_range.split('=')[1].split('-')
297+
start = int(start)
298+
if end == '':
299+
end = len(content)
300+
else:
301+
end = min(len(content), int(end) + 1)
302+
if start < end:
303+
status = 206
304+
resp_range = 'bytes %d-%d/%d' % (
305+
start, end - 1, len(content))
306+
content = content[start:end]
307+
else:
308+
status = 416
309+
resp_range = 'bytes */%d' % len(content)
310+
content = b''
311+
headers['Content-Range'] = resp_range
312+
313+
headers['Content-Length'] = len(content)
314+
self.send_response(status)
315+
for h, v in sorted(headers.items()):
316+
self.send_header(h, v)
317+
self.end_headers()
318+
return content
319+
320+
321+
if __name__ == "__main__":
322+
unittest.main()

0 commit comments

Comments
 (0)