Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | """Regresssion tests for urllib""" |
2 | ||
3 | import urllib | |
4 | import httplib | |
5 | import unittest | |
6 | from test import test_support | |
7 | import os | |
8 | import mimetools | |
9 | import tempfile | |
10 | import StringIO | |
11 | ||
12 | def hexescape(char): | |
13 | """Escape char as RFC 2396 specifies""" | |
14 | hex_repr = hex(ord(char))[2:].upper() | |
15 | if len(hex_repr) == 1: | |
16 | hex_repr = "0%s" % hex_repr | |
17 | return "%" + hex_repr | |
18 | ||
19 | class urlopen_FileTests(unittest.TestCase): | |
20 | """Test urlopen() opening a temporary file. | |
21 | ||
22 | Try to test as much functionality as possible so as to cut down on reliance | |
23 | on connecting to the Net for testing. | |
24 | ||
25 | """ | |
26 | ||
27 | def setUp(self): | |
28 | """Setup of a temp file to use for testing""" | |
29 | self.text = "test_urllib: %s\n" % self.__class__.__name__ | |
30 | FILE = file(test_support.TESTFN, 'wb') | |
31 | try: | |
32 | FILE.write(self.text) | |
33 | finally: | |
34 | FILE.close() | |
35 | self.pathname = test_support.TESTFN | |
36 | self.returned_obj = urllib.urlopen("file:%s" % self.pathname) | |
37 | ||
38 | def tearDown(self): | |
39 | """Shut down the open object""" | |
40 | self.returned_obj.close() | |
41 | os.remove(test_support.TESTFN) | |
42 | ||
43 | def test_interface(self): | |
44 | # Make sure object returned by urlopen() has the specified methods | |
45 | for attr in ("read", "readline", "readlines", "fileno", | |
46 | "close", "info", "geturl", "__iter__"): | |
47 | self.assert_(hasattr(self.returned_obj, attr), | |
48 | "object returned by urlopen() lacks %s attribute" % | |
49 | attr) | |
50 | ||
51 | def test_read(self): | |
52 | self.assertEqual(self.text, self.returned_obj.read()) | |
53 | ||
54 | def test_readline(self): | |
55 | self.assertEqual(self.text, self.returned_obj.readline()) | |
56 | self.assertEqual('', self.returned_obj.readline(), | |
57 | "calling readline() after exhausting the file did not" | |
58 | " return an empty string") | |
59 | ||
60 | def test_readlines(self): | |
61 | lines_list = self.returned_obj.readlines() | |
62 | self.assertEqual(len(lines_list), 1, | |
63 | "readlines() returned the wrong number of lines") | |
64 | self.assertEqual(lines_list[0], self.text, | |
65 | "readlines() returned improper text") | |
66 | ||
67 | def test_fileno(self): | |
68 | file_num = self.returned_obj.fileno() | |
69 | self.assert_(isinstance(file_num, int), | |
70 | "fileno() did not return an int") | |
71 | self.assertEqual(os.read(file_num, len(self.text)), self.text, | |
72 | "Reading on the file descriptor returned by fileno() " | |
73 | "did not return the expected text") | |
74 | ||
75 | def test_close(self): | |
76 | # Test close() by calling it hear and then having it be called again | |
77 | # by the tearDown() method for the test | |
78 | self.returned_obj.close() | |
79 | ||
80 | def test_info(self): | |
81 | self.assert_(isinstance(self.returned_obj.info(), mimetools.Message)) | |
82 | ||
83 | def test_geturl(self): | |
84 | self.assertEqual(self.returned_obj.geturl(), self.pathname) | |
85 | ||
86 | def test_iter(self): | |
87 | # Test iterator | |
88 | # Don't need to count number of iterations since test would fail the | |
89 | # instant it returned anything beyond the first line from the | |
90 | # comparison | |
91 | for line in self.returned_obj.__iter__(): | |
92 | self.assertEqual(line, self.text) | |
93 | ||
94 | class urlopen_HttpTests(unittest.TestCase): | |
95 | """Test urlopen() opening a fake http connection.""" | |
96 | ||
97 | def fakehttp(self, fakedata): | |
98 | class FakeSocket(StringIO.StringIO): | |
99 | def sendall(self, str): pass | |
100 | def makefile(self, mode, name): return self | |
101 | def read(self, amt=None): | |
102 | if self.closed: return '' | |
103 | return StringIO.StringIO.read(self, amt) | |
104 | def readline(self, length=None): | |
105 | if self.closed: return '' | |
106 | return StringIO.StringIO.readline(self, length) | |
107 | class FakeHTTPConnection(httplib.HTTPConnection): | |
108 | def connect(self): | |
109 | self.sock = FakeSocket(fakedata) | |
110 | assert httplib.HTTP._connection_class == httplib.HTTPConnection | |
111 | httplib.HTTP._connection_class = FakeHTTPConnection | |
112 | ||
113 | def unfakehttp(self): | |
114 | httplib.HTTP._connection_class = httplib.HTTPConnection | |
115 | ||
116 | def test_read(self): | |
117 | self.fakehttp('Hello!') | |
118 | try: | |
119 | fp = urllib.urlopen("http://python.org/") | |
120 | self.assertEqual(fp.readline(), 'Hello!') | |
121 | self.assertEqual(fp.readline(), '') | |
122 | finally: | |
123 | self.unfakehttp() | |
124 | ||
125 | class urlretrieve_FileTests(unittest.TestCase): | |
126 | """Test urllib.urlretrieve() on local files""" | |
127 | ||
128 | def setUp(self): | |
129 | # Create a list of temporary files. Each item in the list is a file | |
130 | # name (absolute path or relative to the current working directory). | |
131 | # All files in this list will be deleted in the tearDown method. Note, | |
132 | # this only helps to makes sure temporary files get deleted, but it | |
133 | # does nothing about trying to close files that may still be open. It | |
134 | # is the responsibility of the developer to properly close files even | |
135 | # when exceptional conditions occur. | |
136 | self.tempFiles = [] | |
137 | ||
138 | # Create a temporary file. | |
139 | self.registerFileForCleanUp(test_support.TESTFN) | |
140 | self.text = 'testing urllib.urlretrieve' | |
141 | try: | |
142 | FILE = file(test_support.TESTFN, 'wb') | |
143 | FILE.write(self.text) | |
144 | FILE.close() | |
145 | finally: | |
146 | try: FILE.close() | |
147 | except: pass | |
148 | ||
149 | def tearDown(self): | |
150 | # Delete the temporary files. | |
151 | for each in self.tempFiles: | |
152 | try: os.remove(each) | |
153 | except: pass | |
154 | ||
155 | def constructLocalFileUrl(self, filePath): | |
156 | return "file://%s" % urllib.pathname2url(os.path.abspath(filePath)) | |
157 | ||
158 | def createNewTempFile(self, data=""): | |
159 | """Creates a new temporary file containing the specified data, | |
160 | registers the file for deletion during the test fixture tear down, and | |
161 | returns the absolute path of the file.""" | |
162 | ||
163 | newFd, newFilePath = tempfile.mkstemp() | |
164 | try: | |
165 | self.registerFileForCleanUp(newFilePath) | |
166 | newFile = os.fdopen(newFd, "wb") | |
167 | newFile.write(data) | |
168 | newFile.close() | |
169 | finally: | |
170 | try: newFile.close() | |
171 | except: pass | |
172 | return newFilePath | |
173 | ||
174 | def registerFileForCleanUp(self, fileName): | |
175 | self.tempFiles.append(fileName) | |
176 | ||
177 | def test_basic(self): | |
178 | # Make sure that a local file just gets its own location returned and | |
179 | # a headers value is returned. | |
180 | result = urllib.urlretrieve("file:%s" % test_support.TESTFN) | |
181 | self.assertEqual(result[0], test_support.TESTFN) | |
182 | self.assert_(isinstance(result[1], mimetools.Message), | |
183 | "did not get a mimetools.Message instance as second " | |
184 | "returned value") | |
185 | ||
186 | def test_copy(self): | |
187 | # Test that setting the filename argument works. | |
188 | second_temp = "%s.2" % test_support.TESTFN | |
189 | self.registerFileForCleanUp(second_temp) | |
190 | result = urllib.urlretrieve(self.constructLocalFileUrl( | |
191 | test_support.TESTFN), second_temp) | |
192 | self.assertEqual(second_temp, result[0]) | |
193 | self.assert_(os.path.exists(second_temp), "copy of the file was not " | |
194 | "made") | |
195 | FILE = file(second_temp, 'rb') | |
196 | try: | |
197 | text = FILE.read() | |
198 | FILE.close() | |
199 | finally: | |
200 | try: FILE.close() | |
201 | except: pass | |
202 | self.assertEqual(self.text, text) | |
203 | ||
204 | def test_reporthook(self): | |
205 | # Make sure that the reporthook works. | |
206 | def hooktester(count, block_size, total_size, count_holder=[0]): | |
207 | self.assert_(isinstance(count, int)) | |
208 | self.assert_(isinstance(block_size, int)) | |
209 | self.assert_(isinstance(total_size, int)) | |
210 | self.assertEqual(count, count_holder[0]) | |
211 | count_holder[0] = count_holder[0] + 1 | |
212 | second_temp = "%s.2" % test_support.TESTFN | |
213 | self.registerFileForCleanUp(second_temp) | |
214 | urllib.urlretrieve(self.constructLocalFileUrl(test_support.TESTFN), | |
215 | second_temp, hooktester) | |
216 | ||
217 | def test_reporthook_0_bytes(self): | |
218 | # Test on zero length file. Should call reporthook only 1 time. | |
219 | report = [] | |
220 | def hooktester(count, block_size, total_size, _report=report): | |
221 | _report.append((count, block_size, total_size)) | |
222 | srcFileName = self.createNewTempFile() | |
223 | urllib.urlretrieve(self.constructLocalFileUrl(srcFileName), | |
224 | test_support.TESTFN, hooktester) | |
225 | self.assertEqual(len(report), 1) | |
226 | self.assertEqual(report[0][2], 0) | |
227 | ||
228 | def test_reporthook_5_bytes(self): | |
229 | # Test on 5 byte file. Should call reporthook only 2 times (once when | |
230 | # the "network connection" is established and once when the block is | |
231 | # read). Since the block size is 8192 bytes, only one block read is | |
232 | # required to read the entire file. | |
233 | report = [] | |
234 | def hooktester(count, block_size, total_size, _report=report): | |
235 | _report.append((count, block_size, total_size)) | |
236 | srcFileName = self.createNewTempFile("x" * 5) | |
237 | urllib.urlretrieve(self.constructLocalFileUrl(srcFileName), | |
238 | test_support.TESTFN, hooktester) | |
239 | self.assertEqual(len(report), 2) | |
240 | self.assertEqual(report[0][1], 8192) | |
241 | self.assertEqual(report[0][2], 5) | |
242 | ||
243 | def test_reporthook_8193_bytes(self): | |
244 | # Test on 8193 byte file. Should call reporthook only 3 times (once | |
245 | # when the "network connection" is established, once for the next 8192 | |
246 | # bytes, and once for the last byte). | |
247 | report = [] | |
248 | def hooktester(count, block_size, total_size, _report=report): | |
249 | _report.append((count, block_size, total_size)) | |
250 | srcFileName = self.createNewTempFile("x" * 8193) | |
251 | urllib.urlretrieve(self.constructLocalFileUrl(srcFileName), | |
252 | test_support.TESTFN, hooktester) | |
253 | self.assertEqual(len(report), 3) | |
254 | self.assertEqual(report[0][1], 8192) | |
255 | self.assertEqual(report[0][2], 8193) | |
256 | ||
257 | class QuotingTests(unittest.TestCase): | |
258 | """Tests for urllib.quote() and urllib.quote_plus() | |
259 | ||
260 | According to RFC 2396 ("Uniform Resource Identifiers), to escape a | |
261 | character you write it as '%' + <2 character US-ASCII hex value>. The Python | |
262 | code of ``'%' + hex(ord(<character>))[2:]`` escapes a character properly. | |
263 | Case does not matter on the hex letters. | |
264 | ||
265 | The various character sets specified are: | |
266 | ||
267 | Reserved characters : ";/?:@&=+$," | |
268 | Have special meaning in URIs and must be escaped if not being used for | |
269 | their special meaning | |
270 | Data characters : letters, digits, and "-_.!~*'()" | |
271 | Unreserved and do not need to be escaped; can be, though, if desired | |
272 | Control characters : 0x00 - 0x1F, 0x7F | |
273 | Have no use in URIs so must be escaped | |
274 | space : 0x20 | |
275 | Must be escaped | |
276 | Delimiters : '<>#%"' | |
277 | Must be escaped | |
278 | Unwise : "{}|\^[]`" | |
279 | Must be escaped | |
280 | ||
281 | """ | |
282 | ||
283 | def test_never_quote(self): | |
284 | # Make sure quote() does not quote letters, digits, and "_,.-" | |
285 | do_not_quote = '' .join(["ABCDEFGHIJKLMNOPQRSTUVWXYZ", | |
286 | "abcdefghijklmnopqrstuvwxyz", | |
287 | "0123456789", | |
288 | "_.-"]) | |
289 | result = urllib.quote(do_not_quote) | |
290 | self.assertEqual(do_not_quote, result, | |
291 | "using quote(): %s != %s" % (do_not_quote, result)) | |
292 | result = urllib.quote_plus(do_not_quote) | |
293 | self.assertEqual(do_not_quote, result, | |
294 | "using quote_plus(): %s != %s" % (do_not_quote, result)) | |
295 | ||
296 | def test_default_safe(self): | |
297 | # Test '/' is default value for 'safe' parameter | |
298 | self.assertEqual(urllib.quote.func_defaults[0], '/') | |
299 | ||
300 | def test_safe(self): | |
301 | # Test setting 'safe' parameter does what it should do | |
302 | quote_by_default = "<>" | |
303 | result = urllib.quote(quote_by_default, safe=quote_by_default) | |
304 | self.assertEqual(quote_by_default, result, | |
305 | "using quote(): %s != %s" % (quote_by_default, result)) | |
306 | result = urllib.quote_plus(quote_by_default, safe=quote_by_default) | |
307 | self.assertEqual(quote_by_default, result, | |
308 | "using quote_plus(): %s != %s" % | |
309 | (quote_by_default, result)) | |
310 | ||
311 | def test_default_quoting(self): | |
312 | # Make sure all characters that should be quoted are by default sans | |
313 | # space (separate test for that). | |
314 | should_quote = [chr(num) for num in range(32)] # For 0x00 - 0x1F | |
315 | should_quote.append('<>#%"{}|\^[]`') | |
316 | should_quote.append(chr(127)) # For 0x7F | |
317 | should_quote = ''.join(should_quote) | |
318 | for char in should_quote: | |
319 | result = urllib.quote(char) | |
320 | self.assertEqual(hexescape(char), result, | |
321 | "using quote(): %s should be escaped to %s, not %s" % | |
322 | (char, hexescape(char), result)) | |
323 | result = urllib.quote_plus(char) | |
324 | self.assertEqual(hexescape(char), result, | |
325 | "using quote_plus(): " | |
326 | "%s should be escapes to %s, not %s" % | |
327 | (char, hexescape(char), result)) | |
328 | del should_quote | |
329 | partial_quote = "ab[]cd" | |
330 | expected = "ab%5B%5Dcd" | |
331 | result = urllib.quote(partial_quote) | |
332 | self.assertEqual(expected, result, | |
333 | "using quote(): %s != %s" % (expected, result)) | |
334 | self.assertEqual(expected, result, | |
335 | "using quote_plus(): %s != %s" % (expected, result)) | |
336 | ||
337 | def test_quoting_space(self): | |
338 | # Make sure quote() and quote_plus() handle spaces as specified in | |
339 | # their unique way | |
340 | result = urllib.quote(' ') | |
341 | self.assertEqual(result, hexescape(' '), | |
342 | "using quote(): %s != %s" % (result, hexescape(' '))) | |
343 | result = urllib.quote_plus(' ') | |
344 | self.assertEqual(result, '+', | |
345 | "using quote_plus(): %s != +" % result) | |
346 | given = "a b cd e f" | |
347 | expect = given.replace(' ', hexescape(' ')) | |
348 | result = urllib.quote(given) | |
349 | self.assertEqual(expect, result, | |
350 | "using quote(): %s != %s" % (expect, result)) | |
351 | expect = given.replace(' ', '+') | |
352 | result = urllib.quote_plus(given) | |
353 | self.assertEqual(expect, result, | |
354 | "using quote_plus(): %s != %s" % (expect, result)) | |
355 | ||
356 | class UnquotingTests(unittest.TestCase): | |
357 | """Tests for unquote() and unquote_plus() | |
358 | ||
359 | See the doc string for quoting_Tests for details on quoting and such. | |
360 | ||
361 | """ | |
362 | ||
363 | def test_unquoting(self): | |
364 | # Make sure unquoting of all ASCII values works | |
365 | escape_list = [] | |
366 | for num in range(128): | |
367 | given = hexescape(chr(num)) | |
368 | expect = chr(num) | |
369 | result = urllib.unquote(given) | |
370 | self.assertEqual(expect, result, | |
371 | "using unquote(): %s != %s" % (expect, result)) | |
372 | result = urllib.unquote_plus(given) | |
373 | self.assertEqual(expect, result, | |
374 | "using unquote_plus(): %s != %s" % | |
375 | (expect, result)) | |
376 | escape_list.append(given) | |
377 | escape_string = ''.join(escape_list) | |
378 | del escape_list | |
379 | result = urllib.unquote(escape_string) | |
380 | self.assertEqual(result.count('%'), 1, | |
381 | "using quote(): not all characters escaped; %s" % | |
382 | result) | |
383 | result = urllib.unquote(escape_string) | |
384 | self.assertEqual(result.count('%'), 1, | |
385 | "using unquote(): not all characters escaped: " | |
386 | "%s" % result) | |
387 | ||
388 | def test_unquoting_parts(self): | |
389 | # Make sure unquoting works when have non-quoted characters | |
390 | # interspersed | |
391 | given = 'ab%sd' % hexescape('c') | |
392 | expect = "abcd" | |
393 | result = urllib.unquote(given) | |
394 | self.assertEqual(expect, result, | |
395 | "using quote(): %s != %s" % (expect, result)) | |
396 | result = urllib.unquote_plus(given) | |
397 | self.assertEqual(expect, result, | |
398 | "using unquote_plus(): %s != %s" % (expect, result)) | |
399 | ||
400 | def test_unquoting_plus(self): | |
401 | # Test difference between unquote() and unquote_plus() | |
402 | given = "are+there+spaces..." | |
403 | expect = given | |
404 | result = urllib.unquote(given) | |
405 | self.assertEqual(expect, result, | |
406 | "using unquote(): %s != %s" % (expect, result)) | |
407 | expect = given.replace('+', ' ') | |
408 | result = urllib.unquote_plus(given) | |
409 | self.assertEqual(expect, result, | |
410 | "using unquote_plus(): %s != %s" % (expect, result)) | |
411 | ||
412 | class urlencode_Tests(unittest.TestCase): | |
413 | """Tests for urlencode()""" | |
414 | ||
415 | def help_inputtype(self, given, test_type): | |
416 | """Helper method for testing different input types. | |
417 | ||
418 | 'given' must lead to only the pairs: | |
419 | * 1st, 1 | |
420 | * 2nd, 2 | |
421 | * 3rd, 3 | |
422 | ||
423 | Test cannot assume anything about order. Docs make no guarantee and | |
424 | have possible dictionary input. | |
425 | ||
426 | """ | |
427 | expect_somewhere = ["1st=1", "2nd=2", "3rd=3"] | |
428 | result = urllib.urlencode(given) | |
429 | for expected in expect_somewhere: | |
430 | self.assert_(expected in result, | |
431 | "testing %s: %s not found in %s" % | |
432 | (test_type, expected, result)) | |
433 | self.assertEqual(result.count('&'), 2, | |
434 | "testing %s: expected 2 '&'s; got %s" % | |
435 | (test_type, result.count('&'))) | |
436 | amp_location = result.index('&') | |
437 | on_amp_left = result[amp_location - 1] | |
438 | on_amp_right = result[amp_location + 1] | |
439 | self.assert_(on_amp_left.isdigit() and on_amp_right.isdigit(), | |
440 | "testing %s: '&' not located in proper place in %s" % | |
441 | (test_type, result)) | |
442 | self.assertEqual(len(result), (5 * 3) + 2, #5 chars per thing and amps | |
443 | "testing %s: " | |
444 | "unexpected number of characters: %s != %s" % | |
445 | (test_type, len(result), (5 * 3) + 2)) | |
446 | ||
447 | def test_using_mapping(self): | |
448 | # Test passing in a mapping object as an argument. | |
449 | self.help_inputtype({"1st":'1', "2nd":'2', "3rd":'3'}, | |
450 | "using dict as input type") | |
451 | ||
452 | def test_using_sequence(self): | |
453 | # Test passing in a sequence of two-item sequences as an argument. | |
454 | self.help_inputtype([('1st', '1'), ('2nd', '2'), ('3rd', '3')], | |
455 | "using sequence of two-item tuples as input") | |
456 | ||
457 | def test_quoting(self): | |
458 | # Make sure keys and values are quoted using quote_plus() | |
459 | given = {"&":"="} | |
460 | expect = "%s=%s" % (hexescape('&'), hexescape('=')) | |
461 | result = urllib.urlencode(given) | |
462 | self.assertEqual(expect, result) | |
463 | given = {"key name":"A bunch of pluses"} | |
464 | expect = "key+name=A+bunch+of+pluses" | |
465 | result = urllib.urlencode(given) | |
466 | self.assertEqual(expect, result) | |
467 | ||
468 | def test_doseq(self): | |
469 | # Test that passing True for 'doseq' parameter works correctly | |
470 | given = {'sequence':['1', '2', '3']} | |
471 | expect = "sequence=%s" % urllib.quote_plus(str(['1', '2', '3'])) | |
472 | result = urllib.urlencode(given) | |
473 | self.assertEqual(expect, result) | |
474 | result = urllib.urlencode(given, True) | |
475 | for value in given["sequence"]: | |
476 | expect = "sequence=%s" % value | |
477 | self.assert_(expect in result, | |
478 | "%s not found in %s" % (expect, result)) | |
479 | self.assertEqual(result.count('&'), 2, | |
480 | "Expected 2 '&'s, got %s" % result.count('&')) | |
481 | ||
482 | class Pathname_Tests(unittest.TestCase): | |
483 | """Test pathname2url() and url2pathname()""" | |
484 | ||
485 | def test_basic(self): | |
486 | # Make sure simple tests pass | |
487 | expected_path = os.path.join("parts", "of", "a", "path") | |
488 | expected_url = "parts/of/a/path" | |
489 | result = urllib.pathname2url(expected_path) | |
490 | self.assertEqual(expected_url, result, | |
491 | "pathname2url() failed; %s != %s" % | |
492 | (result, expected_url)) | |
493 | result = urllib.url2pathname(expected_url) | |
494 | self.assertEqual(expected_path, result, | |
495 | "url2pathame() failed; %s != %s" % | |
496 | (result, expected_path)) | |
497 | ||
498 | def test_quoting(self): | |
499 | # Test automatic quoting and unquoting works for pathnam2url() and | |
500 | # url2pathname() respectively | |
501 | given = os.path.join("needs", "quot=ing", "here") | |
502 | expect = "needs/%s/here" % urllib.quote("quot=ing") | |
503 | result = urllib.pathname2url(given) | |
504 | self.assertEqual(expect, result, | |
505 | "pathname2url() failed; %s != %s" % | |
506 | (expect, result)) | |
507 | expect = given | |
508 | result = urllib.url2pathname(result) | |
509 | self.assertEqual(expect, result, | |
510 | "url2pathname() failed; %s != %s" % | |
511 | (expect, result)) | |
512 | given = os.path.join("make sure", "using_quote") | |
513 | expect = "%s/using_quote" % urllib.quote("make sure") | |
514 | result = urllib.pathname2url(given) | |
515 | self.assertEqual(expect, result, | |
516 | "pathname2url() failed; %s != %s" % | |
517 | (expect, result)) | |
518 | given = "make+sure/using_unquote" | |
519 | expect = os.path.join("make+sure", "using_unquote") | |
520 | result = urllib.url2pathname(given) | |
521 | self.assertEqual(expect, result, | |
522 | "url2pathname() failed; %s != %s" % | |
523 | (expect, result)) | |
524 | ||
525 | ||
526 | ||
527 | def test_main(): | |
528 | test_support.run_unittest( | |
529 | urlopen_FileTests, | |
530 | urlopen_HttpTests, | |
531 | urlretrieve_FileTests, | |
532 | QuotingTests, | |
533 | UnquotingTests, | |
534 | urlencode_Tests, | |
535 | Pathname_Tests | |
536 | ) | |
537 | ||
538 | ||
539 | ||
540 | if __name__ == '__main__': | |
541 | test_main() |