Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 # pylint: disable=W0212 | 5 # pylint: disable=W0212 |
| 6 | 6 |
| 7 """Unit tests for download_from_google_storage.py.""" | 7 """Unit tests for download_from_google_storage.py.""" |
| 8 | 8 |
| 9 import optparse | 9 import optparse |
| 10 import os | 10 import os |
| 11 import Queue | 11 import Queue |
| 12 import shutil | 12 import shutil |
| 13 import sys | 13 import sys |
| 14 import tarfile | |
| 14 import tempfile | 15 import tempfile |
| 15 import threading | 16 import threading |
| 16 import unittest | 17 import unittest |
| 17 | 18 |
| 18 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | 19 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
ricow1
2015/08/14 06:31:13
This is the reason why PRESUBMIT is failing. pylin
| |
| 19 | 20 |
| 20 import upload_to_google_storage | 21 import upload_to_google_storage |
| 21 import download_from_google_storage | 22 import download_from_google_storage |
| 22 | 23 |
| 23 # ../third_party/gsutil/gsutil | 24 # ../third_party/gsutil/gsutil |
| 24 GSUTIL_DEFAULT_PATH = os.path.join( | 25 GSUTIL_DEFAULT_PATH = os.path.join( |
| 25 os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | 26 os.path.dirname(os.path.dirname(os.path.abspath(__file__))), |
| 26 'gsutil.py') | 27 'gsutil.py') |
| 27 TEST_DIR = os.path.dirname(os.path.abspath(__file__)) | 28 TEST_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 28 | 29 |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 58 self.append_history('check_call', args) | 59 self.append_history('check_call', args) |
| 59 if self.expected: | 60 if self.expected: |
| 60 code, out, err, fn = self.expected.pop(0) | 61 code, out, err, fn = self.expected.pop(0) |
| 61 if fn: | 62 if fn: |
| 62 fn() | 63 fn() |
| 63 return code, out, err | 64 return code, out, err |
| 64 else: | 65 else: |
| 65 return (0, '', '') | 66 return (0, '', '') |
| 66 | 67 |
| 67 | 68 |
| 69 class ChangedWorkingDirectory(object): | |
|
Ryan Tseng
2015/08/13 16:12:11
I'm pretty sure would be racy if this test was run
| |
| 70 def __init__(self, working_directory): | |
| 71 self._old_cwd = '' | |
| 72 self._working_directory = working_directory | |
| 73 | |
| 74 def __enter__(self): | |
| 75 self._old_cwd = os.getcwd() | |
| 76 print "Enter directory = ", self._working_directory | |
| 77 os.chdir(self._working_directory) | |
| 78 | |
| 79 def __exit__(self, *_): | |
| 80 print "Enter directory = ", self._old_cwd | |
| 81 os.chdir(self._old_cwd) | |
| 82 | |
| 83 | |
| 68 class GstoolsUnitTests(unittest.TestCase): | 84 class GstoolsUnitTests(unittest.TestCase): |
| 69 def setUp(self): | 85 def setUp(self): |
| 70 self.temp_dir = tempfile.mkdtemp(prefix='gstools_test') | 86 self.temp_dir = tempfile.mkdtemp(prefix='gstools_test') |
| 71 self.base_path = os.path.join(self.temp_dir, 'test_files') | 87 self.base_path = os.path.join(self.temp_dir, 'test_files') |
| 72 shutil.copytree(os.path.join(TEST_DIR, 'gstools'), self.base_path) | 88 shutil.copytree(os.path.join(TEST_DIR, 'gstools'), self.base_path) |
| 73 | 89 |
| 74 def cleanUp(self): | 90 def cleanUp(self): |
| 75 shutil.rmtree(self.temp_dir) | 91 shutil.rmtree(self.temp_dir) |
| 76 | 92 |
| 93 def test_validate_tar_file(self): | |
| 94 lorem_ipsum = os.path.join(self.base_path, 'lorem_ipsum.txt') | |
| 95 with ChangedWorkingDirectory(self.base_path): | |
| 96 # Sanity ok check. | |
| 97 tar_dir = 'ok_dir' | |
| 98 os.makedirs(os.path.join(self.base_path, tar_dir)) | |
| 99 tar = 'good.tar.gz' | |
| 100 lorem_ipsum_copy = os.path.join(tar_dir, 'lorem_ipsum.txt') | |
| 101 shutil.copyfile(lorem_ipsum, lorem_ipsum_copy) | |
| 102 with tarfile.open(tar, 'w:gz') as tar: | |
| 103 tar.add(lorem_ipsum_copy) | |
| 104 self.assertTrue( | |
| 105 download_from_google_storage._validate_tar_file(tar, tar_dir)) | |
| 106 | |
| 107 # Test no links. | |
| 108 tar_dir_link = 'for_tar_link' | |
| 109 os.makedirs(tar_dir_link) | |
| 110 link = os.path.join(tar_dir_link, 'link') | |
| 111 os.symlink(lorem_ipsum, link) | |
| 112 tar_with_links = 'with_links.tar.gz' | |
| 113 with tarfile.open(tar_with_links, 'w:gz') as tar: | |
| 114 tar.add(link) | |
| 115 self.assertFalse( | |
| 116 download_from_google_storage._validate_tar_file(tar, tar_dir_link)) | |
| 117 | |
| 118 # Test not outside. | |
| 119 tar_dir_outside = 'outside_tar' | |
| 120 os.makedirs(tar_dir_outside) | |
| 121 tar_with_outside = 'with_outside.tar.gz' | |
| 122 with tarfile.open(tar_with_outside, 'w:gz') as tar: | |
| 123 tar.add(lorem_ipsum) | |
| 124 self.assertFalse( | |
| 125 download_from_google_storage._validate_tar_file(tar, | |
| 126 tar_dir_outside)) | |
| 127 # Test no .. | |
| 128 tar_with_dotdot = 'with_dotdot.tar.gz' | |
| 129 dotdot_file = os.path.join(tar_dir, '..', tar_dir, 'lorem_ipsum.txt') | |
| 130 with tarfile.open(tar_with_dotdot, 'w:gz') as tar: | |
| 131 tar.add(dotdot_file) | |
| 132 self.assertFalse( | |
| 133 download_from_google_storage._validate_tar_file(tar, | |
| 134 tar_dir)) | |
| 135 | |
| 77 def test_gsutil(self): | 136 def test_gsutil(self): |
| 78 gsutil = download_from_google_storage.Gsutil(GSUTIL_DEFAULT_PATH, None) | 137 gsutil = download_from_google_storage.Gsutil(GSUTIL_DEFAULT_PATH, None) |
| 79 self.assertEqual(gsutil.path, GSUTIL_DEFAULT_PATH) | 138 self.assertEqual(gsutil.path, GSUTIL_DEFAULT_PATH) |
| 80 code, _, err = gsutil.check_call() | 139 code, _, err = gsutil.check_call() |
| 81 self.assertEqual(code, 0) | 140 self.assertEqual(code, 0) |
| 82 self.assertEqual(err, '') | 141 self.assertEqual(err, '') |
| 83 | 142 |
| 84 def test_get_sha1(self): | 143 def test_get_sha1(self): |
| 85 lorem_ipsum = os.path.join(self.base_path, 'lorem_ipsum.txt') | 144 lorem_ipsum = os.path.join(self.base_path, 'lorem_ipsum.txt') |
| 86 self.assertEqual( | 145 self.assertEqual( |
| (...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 163 | 222 |
| 164 def test_download_worker_single_file(self): | 223 def test_download_worker_single_file(self): |
| 165 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' | 224 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' |
| 166 input_filename = '%s/%s' % (self.base_url, sha1_hash) | 225 input_filename = '%s/%s' % (self.base_url, sha1_hash) |
| 167 output_filename = os.path.join(self.base_path, 'uploaded_lorem_ipsum.txt') | 226 output_filename = os.path.join(self.base_path, 'uploaded_lorem_ipsum.txt') |
| 168 self.queue.put((sha1_hash, output_filename)) | 227 self.queue.put((sha1_hash, output_filename)) |
| 169 self.queue.put((None, None)) | 228 self.queue.put((None, None)) |
| 170 stdout_queue = Queue.Queue() | 229 stdout_queue = Queue.Queue() |
| 171 download_from_google_storage._downloader_worker_thread( | 230 download_from_google_storage._downloader_worker_thread( |
| 172 0, self.queue, False, self.base_url, self.gsutil, | 231 0, self.queue, False, self.base_url, self.gsutil, |
| 173 stdout_queue, self.ret_codes, True) | 232 stdout_queue, self.ret_codes, True, False) |
| 174 expected_calls = [ | 233 expected_calls = [ |
| 175 ('check_call', | 234 ('check_call', |
| 176 ('ls', input_filename)), | 235 ('ls', input_filename)), |
| 177 ('check_call', | 236 ('check_call', |
| 178 ('cp', input_filename, output_filename))] | 237 ('cp', input_filename, output_filename))] |
| 179 if sys.platform != 'win32': | 238 if sys.platform != 'win32': |
| 180 expected_calls.append( | 239 expected_calls.append( |
| 181 ('check_call', | 240 ('check_call', |
| 182 ('stat', | 241 ('stat', |
| 183 'gs://sometesturl/7871c8e24da15bad8b0be2c36edc9dc77e37727f'))) | 242 'gs://sometesturl/7871c8e24da15bad8b0be2c36edc9dc77e37727f'))) |
| 184 expected_output = [ | 243 expected_output = [ |
| 185 '0> Downloading %s...' % output_filename] | 244 '0> Downloading %s...' % output_filename] |
| 186 expected_ret_codes = [] | 245 expected_ret_codes = [] |
| 187 self.assertEqual(list(stdout_queue.queue), expected_output) | 246 self.assertEqual(list(stdout_queue.queue), expected_output) |
| 188 self.assertEqual(self.gsutil.history, expected_calls) | 247 self.assertEqual(self.gsutil.history, expected_calls) |
| 189 self.assertEqual(list(self.ret_codes.queue), expected_ret_codes) | 248 self.assertEqual(list(self.ret_codes.queue), expected_ret_codes) |
| 190 | 249 |
| 191 def test_download_worker_skips_file(self): | 250 def test_download_worker_skips_file(self): |
| 192 sha1_hash = 'e6c4fbd4fe7607f3e6ebf68b2ea4ef694da7b4fe' | 251 sha1_hash = 'e6c4fbd4fe7607f3e6ebf68b2ea4ef694da7b4fe' |
| 193 output_filename = os.path.join(self.base_path, 'rootfolder_text.txt') | 252 output_filename = os.path.join(self.base_path, 'rootfolder_text.txt') |
| 194 self.queue.put((sha1_hash, output_filename)) | 253 self.queue.put((sha1_hash, output_filename)) |
| 195 self.queue.put((None, None)) | 254 self.queue.put((None, None)) |
| 196 stdout_queue = Queue.Queue() | 255 stdout_queue = Queue.Queue() |
| 197 download_from_google_storage._downloader_worker_thread( | 256 download_from_google_storage._downloader_worker_thread( |
| 198 0, self.queue, False, self.base_url, self.gsutil, | 257 0, self.queue, False, self.base_url, self.gsutil, |
| 199 stdout_queue, self.ret_codes, True) | 258 stdout_queue, self.ret_codes, True, False) |
| 200 expected_output = [ | 259 expected_output = [ |
| 201 '0> File %s exists and SHA1 matches. Skipping.' % output_filename | 260 '0> File %s exists and SHA1 matches. Skipping.' % output_filename |
| 202 ] | 261 ] |
| 203 self.assertEqual(list(stdout_queue.queue), expected_output) | 262 self.assertEqual(list(stdout_queue.queue), expected_output) |
| 204 self.assertEqual(self.gsutil.history, []) | 263 self.assertEqual(self.gsutil.history, []) |
| 205 | 264 |
| 265 def test_download_extract_archive(self): | |
| 266 # Generate a gzipped tarfile | |
| 267 output_filename = os.path.join(self.base_path, 'subfolder.tar.gz') | |
| 268 output_dirname = os.path.join(self.base_path, 'subfolder') | |
| 269 extracted_filename = os.path.join(output_dirname, 'subfolder_text.txt') | |
| 270 with tarfile.open(output_filename, 'w:gz') as tar: | |
| 271 tar.add(output_dirname, arcname='subfolder') | |
| 272 shutil.rmtree(output_dirname) | |
| 273 sha1_hash = download_from_google_storage.get_sha1(output_filename) | |
| 274 input_filename = '%s/%s' % (self.base_url, sha1_hash) | |
| 275 self.queue.put((sha1_hash, output_filename)) | |
| 276 self.queue.put((None, None)) | |
| 277 stdout_queue = Queue.Queue() | |
| 278 download_from_google_storage._downloader_worker_thread( | |
| 279 0, self.queue, True, self.base_url, self.gsutil, | |
| 280 stdout_queue, self.ret_codes, True, True, delete=False) | |
| 281 expected_calls = [ | |
| 282 ('check_call', | |
| 283 ('ls', input_filename)), | |
| 284 ('check_call', | |
| 285 ('cp', input_filename, output_filename))] | |
| 286 if sys.platform != 'win32': | |
| 287 expected_calls.append( | |
| 288 ('check_call', | |
| 289 ('stat', | |
| 290 'gs://sometesturl/%s' % sha1_hash))) | |
| 291 expected_output = [ | |
| 292 '0> Downloading %s...' % output_filename] | |
| 293 expected_output.extend([ | |
| 294 '0> Extracting 3 entries from %s to %s' % (output_filename, | |
| 295 output_dirname)]) | |
| 296 expected_ret_codes = [] | |
| 297 self.assertEqual(list(stdout_queue.queue), expected_output) | |
| 298 self.assertEqual(self.gsutil.history, expected_calls) | |
| 299 self.assertEqual(list(self.ret_codes.queue), expected_ret_codes) | |
| 300 self.assertTrue(os.path.exists(output_dirname)) | |
| 301 self.assertTrue(os.path.exists(extracted_filename)) | |
| 302 | |
| 206 def test_download_worker_skips_not_found_file(self): | 303 def test_download_worker_skips_not_found_file(self): |
| 207 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' | 304 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' |
| 208 input_filename = '%s/%s' % (self.base_url, sha1_hash) | 305 input_filename = '%s/%s' % (self.base_url, sha1_hash) |
| 209 output_filename = os.path.join(self.base_path, 'uploaded_lorem_ipsum.txt') | 306 output_filename = os.path.join(self.base_path, 'uploaded_lorem_ipsum.txt') |
| 210 self.queue.put((sha1_hash, output_filename)) | 307 self.queue.put((sha1_hash, output_filename)) |
| 211 self.queue.put((None, None)) | 308 self.queue.put((None, None)) |
| 212 stdout_queue = Queue.Queue() | 309 stdout_queue = Queue.Queue() |
| 213 self.gsutil.add_expected(1, '', '') # Return error when 'ls' is called. | 310 self.gsutil.add_expected(1, '', '') # Return error when 'ls' is called. |
| 214 download_from_google_storage._downloader_worker_thread( | 311 download_from_google_storage._downloader_worker_thread( |
| 215 0, self.queue, False, self.base_url, self.gsutil, | 312 0, self.queue, False, self.base_url, self.gsutil, |
| 216 stdout_queue, self.ret_codes, True) | 313 stdout_queue, self.ret_codes, True, False) |
| 217 expected_output = [ | 314 expected_output = [ |
| 218 '0> Failed to fetch file %s for %s, skipping. [Err: ]' % ( | 315 '0> Failed to fetch file %s for %s, skipping. [Err: ]' % ( |
| 219 input_filename, output_filename), | 316 input_filename, output_filename), |
| 220 ] | 317 ] |
| 221 expected_calls = [ | 318 expected_calls = [ |
| 222 ('check_call', | 319 ('check_call', |
| 223 ('ls', input_filename)) | 320 ('ls', input_filename)) |
| 224 ] | 321 ] |
| 225 expected_ret_codes = [ | 322 expected_ret_codes = [ |
| 226 (1, 'Failed to fetch file %s for %s. [Err: ]' % ( | 323 (1, 'Failed to fetch file %s for %s. [Err: ]' % ( |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 241 base_url=self.base_url, | 338 base_url=self.base_url, |
| 242 gsutil=self.gsutil, | 339 gsutil=self.gsutil, |
| 243 num_threads=1, | 340 num_threads=1, |
| 244 directory=False, | 341 directory=False, |
| 245 recursive=False, | 342 recursive=False, |
| 246 force=True, | 343 force=True, |
| 247 output=output_filename, | 344 output=output_filename, |
| 248 ignore_errors=False, | 345 ignore_errors=False, |
| 249 sha1_file=False, | 346 sha1_file=False, |
| 250 verbose=True, | 347 verbose=True, |
| 251 auto_platform=False) | 348 auto_platform=False, |
| 349 extract=False) | |
| 252 expected_calls = [ | 350 expected_calls = [ |
| 253 ('check_call', | 351 ('check_call', |
| 254 ('ls', input_filename)), | 352 ('ls', input_filename)), |
| 255 ('check_call', | 353 ('check_call', |
| 256 ('cp', input_filename, output_filename)) | 354 ('cp', input_filename, output_filename)) |
| 257 ] | 355 ] |
| 258 if sys.platform != 'win32': | 356 if sys.platform != 'win32': |
| 259 expected_calls.append( | 357 expected_calls.append( |
| 260 ('check_call', | 358 ('check_call', |
| 261 ('stat', | 359 ('stat', |
| 262 'gs://sometesturl/7871c8e24da15bad8b0be2c36edc9dc77e37727f'))) | 360 'gs://sometesturl/7871c8e24da15bad8b0be2c36edc9dc77e37727f'))) |
| 263 self.assertEqual(self.gsutil.history, expected_calls) | 361 self.assertEqual(self.gsutil.history, expected_calls) |
| 264 self.assertEqual(code, 101) | 362 self.assertEqual(code, 101) |
| 265 | 363 |
| 266 def test_corrupt_download(self): | 364 def test_corrupt_download(self): |
| 267 q = Queue.Queue() | 365 q = Queue.Queue() |
| 268 out_q = Queue.Queue() | 366 out_q = Queue.Queue() |
| 269 ret_codes = Queue.Queue() | 367 ret_codes = Queue.Queue() |
| 270 tmp_dir = tempfile.mkdtemp() | 368 tmp_dir = tempfile.mkdtemp() |
| 271 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' | 369 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' |
| 272 output_filename = os.path.join(tmp_dir, 'lorem_ipsum.txt') | 370 output_filename = os.path.join(tmp_dir, 'lorem_ipsum.txt') |
| 273 q.put(('7871c8e24da15bad8b0be2c36edc9dc77e37727f', output_filename)) | 371 q.put(('7871c8e24da15bad8b0be2c36edc9dc77e37727f', output_filename)) |
| 274 q.put((None, None)) | 372 q.put((None, None)) |
| 275 def _write_bad_file(): | 373 def _write_bad_file(): |
| 276 with open(output_filename, 'w') as f: | 374 with open(output_filename, 'w') as f: |
| 277 f.write('foobar') | 375 f.write('foobar') |
| 278 self.gsutil.add_expected(0, '', '') | 376 self.gsutil.add_expected(0, '', '') |
| 279 self.gsutil.add_expected(0, '', '', _write_bad_file) | 377 self.gsutil.add_expected(0, '', '', _write_bad_file) |
| 280 download_from_google_storage._downloader_worker_thread( | 378 download_from_google_storage._downloader_worker_thread( |
| 281 1, q, True, self.base_url, self.gsutil, out_q, ret_codes, True) | 379 1, q, True, self.base_url, self.gsutil, out_q, ret_codes, True, False) |
| 282 self.assertTrue(q.empty()) | 380 self.assertTrue(q.empty()) |
| 283 msg = ('1> ERROR remote sha1 (%s) does not match expected sha1 (%s).' % | 381 msg = ('1> ERROR remote sha1 (%s) does not match expected sha1 (%s).' % |
| 284 ('8843d7f92416211de9ebb963ff4ce28125932878', sha1_hash)) | 382 ('8843d7f92416211de9ebb963ff4ce28125932878', sha1_hash)) |
| 285 self.assertEquals(out_q.get(), '1> Downloading %s...' % output_filename) | 383 self.assertEquals(out_q.get(), '1> Downloading %s...' % output_filename) |
| 286 self.assertEquals(out_q.get(), msg) | 384 self.assertEquals(out_q.get(), msg) |
| 287 self.assertEquals(ret_codes.get(), (20, msg)) | 385 self.assertEquals(ret_codes.get(), (20, msg)) |
| 288 self.assertTrue(out_q.empty()) | 386 self.assertTrue(out_q.empty()) |
| 289 self.assertTrue(ret_codes.empty()) | 387 self.assertTrue(ret_codes.empty()) |
| 290 | 388 |
| 291 | 389 |
| 292 def test_download_directory_no_recursive_non_force(self): | 390 def test_download_directory_no_recursive_non_force(self): |
| 293 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' | 391 sha1_hash = '7871c8e24da15bad8b0be2c36edc9dc77e37727f' |
| 294 input_filename = '%s/%s' % (self.base_url, sha1_hash) | 392 input_filename = '%s/%s' % (self.base_url, sha1_hash) |
| 295 output_filename = os.path.join(self.base_path, 'uploaded_lorem_ipsum.txt') | 393 output_filename = os.path.join(self.base_path, 'uploaded_lorem_ipsum.txt') |
| 296 code = download_from_google_storage.download_from_google_storage( | 394 code = download_from_google_storage.download_from_google_storage( |
| 297 input_filename=self.base_path, | 395 input_filename=self.base_path, |
| 298 base_url=self.base_url, | 396 base_url=self.base_url, |
| 299 gsutil=self.gsutil, | 397 gsutil=self.gsutil, |
| 300 num_threads=1, | 398 num_threads=1, |
| 301 directory=True, | 399 directory=True, |
| 302 recursive=False, | 400 recursive=False, |
| 303 force=False, | 401 force=False, |
| 304 output=None, | 402 output=None, |
| 305 ignore_errors=False, | 403 ignore_errors=False, |
| 306 sha1_file=False, | 404 sha1_file=False, |
| 307 verbose=True, | 405 verbose=True, |
| 308 auto_platform=False) | 406 auto_platform=False, |
| 407 extract=False) | |
| 309 expected_calls = [ | 408 expected_calls = [ |
| 310 ('check_call', | 409 ('check_call', |
| 311 ('ls', input_filename)), | 410 ('ls', input_filename)), |
| 312 ('check_call', | 411 ('check_call', |
| 313 ('cp', input_filename, output_filename))] | 412 ('cp', input_filename, output_filename))] |
| 314 if sys.platform != 'win32': | 413 if sys.platform != 'win32': |
| 315 expected_calls.append( | 414 expected_calls.append( |
| 316 ('check_call', | 415 ('check_call', |
| 317 ('stat', | 416 ('stat', |
| 318 'gs://sometesturl/7871c8e24da15bad8b0be2c36edc9dc77e37727f'))) | 417 'gs://sometesturl/7871c8e24da15bad8b0be2c36edc9dc77e37727f'))) |
| 319 self.assertEqual(self.gsutil.history, expected_calls) | 418 self.assertEqual(self.gsutil.history, expected_calls) |
| 320 self.assertEqual(code, 0) | 419 self.assertEqual(code, 0) |
| 321 | 420 |
| 322 | 421 |
| 323 if __name__ == '__main__': | 422 if __name__ == '__main__': |
| 324 unittest.main() | 423 unittest.main() |
| OLD | NEW |