| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2013 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 | 5 |
| 6 import StringIO |
| 6 import json | 7 import json |
| 7 import logging | 8 import logging |
| 8 import os | 9 import os |
| 9 import StringIO | |
| 10 import sys | 10 import sys |
| 11 import threading | 11 import threading |
| 12 import unittest | 12 import unittest |
| 13 import urllib2 | 13 import urllib2 |
| 14 | 14 |
| 15 import auto_stub | 15 import auto_stub |
| 16 | 16 |
| 17 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 17 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 18 sys.path.insert(0, ROOT_DIR) | 18 sys.path.insert(0, ROOT_DIR) |
| 19 | 19 |
| 20 import run_isolated | 20 import run_isolated |
| 21 import swarm_get_results | 21 import swarming |
| 22 |
| 23 |
| 24 FILE_NAME = u'test.isolated' |
| 25 FILE_HASH = u'brodoyouevenhash' |
| 26 TEST_NAME = u'unit_tests' |
| 27 STDOUT_FOR_TRIGGER_LEN = 188 |
| 22 | 28 |
| 23 | 29 |
| 24 TEST_CASE_SUCCESS = ( | 30 TEST_CASE_SUCCESS = ( |
| 25 '[----------] 2 tests from StaticCookiePolicyTest\n' | 31 '[----------] 2 tests from StaticCookiePolicyTest\n' |
| 26 '[ RUN ] StaticCookiePolicyTest.AllowAllCookiesTest\n' | 32 '[ RUN ] StaticCookiePolicyTest.AllowAllCookiesTest\n' |
| 27 '[ OK ] StaticCookiePolicyTest.AllowAllCookiesTest (0 ms)\n' | 33 '[ OK ] StaticCookiePolicyTest.AllowAllCookiesTest (0 ms)\n' |
| 28 '[ RUN ] StaticCookiePolicyTest.BlockAllCookiesTest\n' | 34 '[ RUN ] StaticCookiePolicyTest.BlockAllCookiesTest\n' |
| 29 '[ OK ] StaticCookiePolicyTest.BlockAllCookiesTest (0 ms)\n' | 35 '[ OK ] StaticCookiePolicyTest.BlockAllCookiesTest (0 ms)\n' |
| 30 '[----------] 2 tests from StaticCookiePolicyTest (0 ms total)\n' | 36 '[----------] 2 tests from StaticCookiePolicyTest (0 ms total)\n' |
| 31 '\n' | 37 '\n' |
| (...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 127 url_response.msg = 'OK' | 133 url_response.msg = 'OK' |
| 128 return url_response | 134 return url_response |
| 129 | 135 |
| 130 | 136 |
| 131 def get_swarm_results(keys): | 137 def get_swarm_results(keys): |
| 132 """Simplifies the call to yield_results(). | 138 """Simplifies the call to yield_results(). |
| 133 | 139 |
| 134 The timeout is hard-coded to 10 seconds. | 140 The timeout is hard-coded to 10 seconds. |
| 135 """ | 141 """ |
| 136 return list( | 142 return list( |
| 137 swarm_get_results.yield_results( | 143 swarming.yield_results( |
| 138 'http://host:9001', keys, 10., None)) | 144 'http://host:9001', keys, 10., None)) |
| 139 | 145 |
| 140 | 146 |
| 141 class TestCase(auto_stub.TestCase): | 147 class TestCase(auto_stub.TestCase): |
| 142 """Base class that defines the url_open mock.""" | 148 """Base class that defines the url_open mock.""" |
| 143 def setUp(self): | 149 def setUp(self): |
| 144 super(TestCase, self).setUp() | 150 super(TestCase, self).setUp() |
| 145 self._lock = threading.Lock() | 151 self._lock = threading.Lock() |
| 146 self.requests = [] | 152 self.requests = [] |
| 147 self.mock( | 153 self.mock( |
| 148 swarm_get_results.run_isolated, 'url_open', | 154 swarming.run_isolated, 'url_open', |
| 149 self._url_open) | 155 self._url_open) |
| 150 | 156 |
| 151 def tearDown(self): | 157 def tearDown(self): |
| 152 try: | 158 try: |
| 153 if not self.has_failed(): | 159 if not self.has_failed(): |
| 154 self.assertEqual([], self.requests) | 160 self.assertEqual([], self.requests) |
| 155 finally: | 161 finally: |
| 156 super(TestCase, self).tearDown() | 162 super(TestCase, self).tearDown() |
| 157 | 163 |
| 158 def _url_open(self, url, **kwargs): | 164 def _url_open(self, url, **kwargs): |
| 159 logging.info('url_open(%s)', url) | 165 logging.info('url_open(%s)', url) |
| 160 with self._lock: | 166 with self._lock: |
| 161 # Since the client is multi-threaded, requests can be processed out of | 167 # Since the client is multi-threaded, requests can be processed out of |
| 162 # order. | 168 # order. |
| 163 for index, r in enumerate(self.requests): | 169 for index, r in enumerate(self.requests): |
| 164 if r[0] == url and r[1] == kwargs: | 170 if r[0] == url and r[1] == kwargs: |
| 165 _, _, returned = self.requests.pop(index) | 171 _, _, returned = self.requests.pop(index) |
| 166 break | 172 break |
| 167 else: | 173 else: |
| 168 self.fail('Failed to find url %s' % url) | 174 self.fail('Failed to find url %s' % url) |
| 169 return returned | 175 return returned |
| 170 | 176 |
| 171 | 177 |
| 172 class TestGetTestKeys(TestCase): | 178 class TestGetTestKeys(TestCase): |
| 173 def test_no_keys(self): | 179 def test_no_keys(self): |
| 174 old_sleep = swarm_get_results.time.sleep | 180 self.mock(swarming.time, 'sleep', lambda x: x) |
| 175 swarm_get_results.time.sleep = lambda x: x | 181 self.requests = [ |
| 182 ( |
| 183 'http://host:9001/get_matching_test_cases?name=my_test', |
| 184 {'retry_404': True}, |
| 185 StringIO.StringIO('No matching Test Cases'), |
| 186 ) for _ in range(run_isolated.URL_OPEN_MAX_ATTEMPTS) |
| 187 ] |
| 176 try: | 188 try: |
| 177 self.requests = [ | 189 swarming.get_test_keys('http://host:9001', 'my_test') |
| 178 ( | 190 self.fail() |
| 179 'http://host:9001/get_matching_test_cases?name=my_test', | 191 except swarming.Failure as e: |
| 180 {'retry_404': True}, | 192 msg = ( |
| 181 StringIO.StringIO('No matching Test Cases'), | 193 'Error: Unable to find any tests with the name, my_test, on swarm ' |
| 182 ) for _ in range(run_isolated.URL_OPEN_MAX_ATTEMPTS) | 194 'server') |
| 183 ] | 195 self.assertEqual(msg, e.args[0]) |
| 184 try: | |
| 185 swarm_get_results.get_test_keys('http://host:9001', 'my_test') | |
| 186 self.fail() | |
| 187 except swarm_get_results.Failure as e: | |
| 188 msg = ( | |
| 189 'Error: Unable to find any tests with the name, my_test, on swarm ' | |
| 190 'server') | |
| 191 self.assertEqual(msg, e.args[0]) | |
| 192 finally: | |
| 193 swarm_get_results.time.sleep = old_sleep | |
| 194 | 196 |
| 195 def test_no_keys_on_first_attempt(self): | 197 def test_no_keys_on_first_attempt(self): |
| 196 old_sleep = swarm_get_results.time.sleep | 198 self.mock(swarming.time, 'sleep', lambda x: x) |
| 197 swarm_get_results.time.sleep = lambda x: x | 199 keys = ['key_1', 'key_2'] |
| 198 try: | 200 self.requests = [ |
| 199 keys = ['key_1', 'key_2'] | 201 ( |
| 200 self.requests = [ | 202 'http://host:9001/get_matching_test_cases?name=my_test', |
| 201 ( | 203 {'retry_404': True}, |
| 202 'http://host:9001/get_matching_test_cases?name=my_test', | 204 StringIO.StringIO('No matching Test Cases'), |
| 203 {'retry_404': True}, | 205 ), |
| 204 StringIO.StringIO('No matching Test Cases'), | 206 ( |
| 205 ), | 207 'http://host:9001/get_matching_test_cases?name=my_test', |
| 206 ( | 208 {'retry_404': True}, |
| 207 'http://host:9001/get_matching_test_cases?name=my_test', | 209 StringIO.StringIO(json.dumps(keys)), |
| 208 {'retry_404': True}, | 210 ), |
| 209 StringIO.StringIO(json.dumps(keys)), | 211 ] |
| 210 ), | 212 actual = swarming.get_test_keys('http://host:9001', 'my_test') |
| 211 ] | 213 self.assertEqual(keys, actual) |
| 212 actual = swarm_get_results.get_test_keys('http://host:9001', 'my_test') | |
| 213 self.assertEqual(keys, actual) | |
| 214 finally: | |
| 215 swarm_get_results.time.sleep = old_sleep | |
| 216 | 214 |
| 217 def test_find_keys(self): | 215 def test_find_keys(self): |
| 218 keys = ['key_1', 'key_2'] | 216 keys = ['key_1', 'key_2'] |
| 219 self.requests = [ | 217 self.requests = [ |
| 220 ( | 218 ( |
| 221 'http://host:9001/get_matching_test_cases?name=my_test', | 219 'http://host:9001/get_matching_test_cases?name=my_test', |
| 222 {'retry_404': True}, | 220 {'retry_404': True}, |
| 223 StringIO.StringIO(json.dumps(keys)), | 221 StringIO.StringIO(json.dumps(keys)), |
| 224 ), | 222 ), |
| 225 ] | 223 ] |
| 226 actual = swarm_get_results.get_test_keys('http://host:9001', 'my_test') | 224 actual = swarming.get_test_keys('http://host:9001', 'my_test') |
| 227 self.assertEqual(keys, actual) | 225 self.assertEqual(keys, actual) |
| 228 | 226 |
| 229 | 227 |
| 230 class TestGetSwarmResults(TestCase): | 228 class TestGetSwarmResults(TestCase): |
| 231 def test_success(self): | 229 def test_success(self): |
| 232 self.requests = [ | 230 self.requests = [ |
| 233 ( | 231 ( |
| 234 'http://host:9001/get_result?r=key1', | 232 'http://host:9001/get_result?r=key1', |
| 235 {'retry_404': False, 'retry_50x': False}, | 233 {'retry_404': False, 'retry_50x': False}, |
| 236 generate_url_response(0, SWARM_OUTPUT_SUCCESS, '0, 0'), | 234 generate_url_response(0, SWARM_OUTPUT_SUCCESS, '0, 0'), |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 271 def test_url_errors(self): | 269 def test_url_errors(self): |
| 272 # NOTE: get_swarm_results() hardcodes timeout=10. range(12) is because of an | 270 # NOTE: get_swarm_results() hardcodes timeout=10. range(12) is because of an |
| 273 # additional time.time() call deep in run_isolated.url_open(). | 271 # additional time.time() call deep in run_isolated.url_open(). |
| 274 now = {} | 272 now = {} |
| 275 lock = threading.Lock() | 273 lock = threading.Lock() |
| 276 def get_now(): | 274 def get_now(): |
| 277 t = threading.current_thread() | 275 t = threading.current_thread() |
| 278 with lock: | 276 with lock: |
| 279 return now.setdefault(t, range(12)).pop(0) | 277 return now.setdefault(t, range(12)).pop(0) |
| 280 self.mock( | 278 self.mock( |
| 281 swarm_get_results.run_isolated.HttpService, | 279 swarming.run_isolated.HttpService, |
| 282 'sleep_before_retry', | 280 'sleep_before_retry', |
| 283 staticmethod(lambda _x, _y: None)) | 281 staticmethod(lambda _x, _y: None)) |
| 284 self.mock(swarm_get_results, 'now', get_now) | 282 self.mock(swarming, 'now', get_now) |
| 285 # The actual number of requests here depends on 'now' progressing to 10 | 283 # The actual number of requests here depends on 'now' progressing to 10 |
| 286 # seconds. It's called twice per loop. | 284 # seconds. It's called twice per loop. |
| 287 self.requests = [ | 285 self.requests = [ |
| 288 ( | 286 ( |
| 289 'http://host:9001/get_result?r=key1', | 287 'http://host:9001/get_result?r=key1', |
| 290 {'retry_404': False, 'retry_50x': False}, | 288 {'retry_404': False, 'retry_50x': False}, |
| 291 None, | 289 None, |
| 292 ), | 290 ), |
| 293 ( | 291 ( |
| 294 'http://host:9001/get_result?r=key1', | 292 'http://host:9001/get_result?r=key1', |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 358 ] | 356 ] |
| 359 expected = [ | 357 expected = [ |
| 360 gen_yielded_data(0, TEST_SHARD_OUTPUT_1, '0, 0'), | 358 gen_yielded_data(0, TEST_SHARD_OUTPUT_1, '0, 0'), |
| 361 gen_yielded_data(1, TEST_SHARD_OUTPUT_2, '0, 0'), | 359 gen_yielded_data(1, TEST_SHARD_OUTPUT_2, '0, 0'), |
| 362 gen_yielded_data(2, TEST_SHARD_OUTPUT_3, '0, 0'), | 360 gen_yielded_data(2, TEST_SHARD_OUTPUT_3, '0, 0'), |
| 363 ] | 361 ] |
| 364 actual = get_swarm_results(['key1', 'key1-repeat', 'key2', 'key3']) | 362 actual = get_swarm_results(['key1', 'key1-repeat', 'key2', 'key3']) |
| 365 self.assertEqual(expected, sorted(actual)) | 363 self.assertEqual(expected, sorted(actual)) |
| 366 | 364 |
| 367 | 365 |
| 366 def chromium_tasks(retrieval_url): |
| 367 return [ |
| 368 { |
| 369 u'action': [ |
| 370 u'python', u'run_isolated.py', |
| 371 u'--hash', FILE_HASH, |
| 372 u'--remote', retrieval_url + u'default-gzip/', |
| 373 ], |
| 374 u'decorate_output': False, |
| 375 u'test_name': u'Run Test', |
| 376 u'time_out': 600, |
| 377 }, |
| 378 { |
| 379 u'action' : [ |
| 380 u'python', u'swarm_cleanup.py', |
| 381 ], |
| 382 u'decorate_output': False, |
| 383 u'test_name': u'Clean Up', |
| 384 u'time_out': 600, |
| 385 } |
| 386 ] |
| 387 |
| 388 |
| 389 def generate_expected_json( |
| 390 shards, |
| 391 os_image, |
| 392 working_dir, |
| 393 data_server, |
| 394 profile): |
| 395 retrieval_url = data_server + '/content/retrieve/' |
| 396 os_value = unicode(swarming.PLATFORM_MAPPING[os_image]) |
| 397 expected = { |
| 398 u'cleanup': u'root', |
| 399 u'configurations': [ |
| 400 { |
| 401 u'config_name': os_value, |
| 402 u'dimensions': { |
| 403 u'os': os_value, |
| 404 }, |
| 405 u'min_instances': shards, |
| 406 }, |
| 407 ], |
| 408 u'data': [[retrieval_url + u'default/', u'swarm_data.zip']], |
| 409 u'env_vars': {}, |
| 410 u'restart_on_failure': True, |
| 411 u'test_case_name': TEST_NAME, |
| 412 u'tests': chromium_tasks(retrieval_url), |
| 413 u'working_dir': unicode(working_dir), |
| 414 u'priority': 101, |
| 415 } |
| 416 if shards > 1: |
| 417 expected[u'env_vars'][u'GTEST_SHARD_INDEX'] = u'%(instance_index)s' |
| 418 expected[u'env_vars'][u'GTEST_TOTAL_SHARDS'] = u'%(num_instances)s' |
| 419 if profile: |
| 420 expected[u'tests'][0][u'action'].append(u'--verbose') |
| 421 return expected |
| 422 |
| 423 |
| 424 class MockZipFile(object): |
| 425 def __init__(self, filename, mode): |
| 426 pass |
| 427 |
| 428 def write(self, source, dest=None): |
| 429 pass |
| 430 |
| 431 def close(self): |
| 432 pass |
| 433 |
| 434 |
| 435 def MockUrlOpen(url, _data, has_return_value): |
| 436 if '/content/contains' in url: |
| 437 return StringIO.StringIO(has_return_value) |
| 438 return StringIO.StringIO('{}') |
| 439 |
| 440 |
| 441 def MockUrlOpenHasZip(url, data=None, content_type=None): |
| 442 assert content_type in (None, 'application/json', 'application/octet-stream') |
| 443 return MockUrlOpen(url, data, has_return_value=chr(1)) |
| 444 |
| 445 |
| 446 def MockUrlOpenNoZip(url, data=None, content_type=None): |
| 447 assert content_type in (None, 'application/json', 'application/octet-stream') |
| 448 return MockUrlOpen(url, data, has_return_value=chr(0)) |
| 449 |
| 450 |
| 451 class ManifestTest(auto_stub.TestCase): |
| 452 def setUp(self): |
| 453 self.mock(swarming.time, 'sleep', lambda x: None) |
| 454 self.mock(swarming.zipfile, 'ZipFile', MockZipFile) |
| 455 self.mock(sys, 'stdout', StringIO.StringIO()) |
| 456 self.mock(sys, 'stderr', StringIO.StringIO()) |
| 457 |
| 458 def tearDown(self): |
| 459 if not self.has_failed(): |
| 460 self._check_output('', '') |
| 461 super(ManifestTest, self).tearDown() |
| 462 |
| 463 def _check_output(self, out, err): |
| 464 self.assertEqual(out, sys.stdout.getvalue()) |
| 465 self.assertEqual(err, sys.stderr.getvalue()) |
| 466 |
| 467 # Flush their content by mocking them again. |
| 468 self.mock(sys, 'stdout', StringIO.StringIO()) |
| 469 self.mock(sys, 'stderr', StringIO.StringIO()) |
| 470 |
| 471 def test_basic_manifest(self): |
| 472 manifest = swarming.Manifest( |
| 473 manifest_hash=FILE_HASH, |
| 474 test_name=TEST_NAME, |
| 475 shards=2, |
| 476 test_filter='*', |
| 477 os_image='win32', |
| 478 working_dir='swarm_tests', |
| 479 data_server='http://localhost:8081', |
| 480 verbose=False, |
| 481 profile=False, |
| 482 priority=101) |
| 483 |
| 484 swarming.chromium_setup(manifest) |
| 485 manifest_json = json.loads(manifest.to_json()) |
| 486 |
| 487 expected = generate_expected_json( |
| 488 shards=2, |
| 489 os_image='win32', |
| 490 working_dir='swarm_tests', |
| 491 data_server='http://localhost:8081', |
| 492 profile=False) |
| 493 self.assertEqual(expected, manifest_json) |
| 494 |
| 495 def test_basic_linux(self): |
| 496 """A basic linux manifest test to ensure that windows specific values |
| 497 aren't used. |
| 498 """ |
| 499 manifest = swarming.Manifest( |
| 500 manifest_hash=FILE_HASH, |
| 501 test_name=TEST_NAME, |
| 502 shards=1, |
| 503 test_filter='*', |
| 504 os_image='linux2', |
| 505 working_dir='swarm_tests', |
| 506 data_server='http://localhost:8081', |
| 507 verbose=False, |
| 508 profile=False, |
| 509 priority=101) |
| 510 |
| 511 swarming.chromium_setup(manifest) |
| 512 manifest_json = json.loads(manifest.to_json()) |
| 513 |
| 514 expected = generate_expected_json( |
| 515 shards=1, |
| 516 os_image='linux2', |
| 517 working_dir='swarm_tests', |
| 518 data_server='http://localhost:8081', |
| 519 profile=False) |
| 520 self.assertEqual(expected, manifest_json) |
| 521 |
| 522 def test_basic_linux_profile(self): |
| 523 manifest = swarming.Manifest( |
| 524 manifest_hash=FILE_HASH, |
| 525 test_name=TEST_NAME, |
| 526 shards=1, |
| 527 test_filter='*', |
| 528 os_image='linux2', |
| 529 working_dir='swarm_tests', |
| 530 data_server='http://localhost:8081', |
| 531 verbose=False, |
| 532 profile=True, |
| 533 priority=101) |
| 534 |
| 535 swarming.chromium_setup(manifest) |
| 536 manifest_json = json.loads(manifest.to_json()) |
| 537 |
| 538 expected = generate_expected_json( |
| 539 shards=1, |
| 540 os_image='linux2', |
| 541 working_dir='swarm_tests', |
| 542 data_server='http://localhost:8081', |
| 543 profile=True) |
| 544 self.assertEqual(expected, manifest_json) |
| 545 |
| 546 def test_process_manifest_success(self): |
| 547 self.mock(swarming.run_isolated, 'url_open', MockUrlOpenNoZip) |
| 548 |
| 549 result = swarming.process_manifest( |
| 550 file_sha1=FILE_HASH, |
| 551 test_name=TEST_NAME, |
| 552 shards=1, |
| 553 test_filter='*', |
| 554 os_image='linux2', |
| 555 working_dir='swarm_tests', |
| 556 data_server='http://localhost:8081', |
| 557 swarm_url='http://localhost:8082', |
| 558 verbose=False, |
| 559 profile=False, |
| 560 priority=101) |
| 561 self.assertEqual(0, result) |
| 562 |
| 563 # Just assert it printed enough, since it contains variable output. |
| 564 out = sys.stdout.getvalue() |
| 565 self.assertTrue(len(out) > STDOUT_FOR_TRIGGER_LEN) |
| 566 self.assertTrue('Zip file not on server, starting uploading.' in out) |
| 567 self.mock(sys, 'stdout', StringIO.StringIO()) |
| 568 |
| 569 def test_process_manifest_success_zip_already_uploaded(self): |
| 570 self.mock(swarming.run_isolated, 'url_open', MockUrlOpenHasZip) |
| 571 |
| 572 result = swarming.process_manifest( |
| 573 file_sha1=FILE_HASH, |
| 574 test_name=TEST_NAME, |
| 575 shards=1, |
| 576 test_filter='*', |
| 577 os_image='linux2', |
| 578 working_dir='swarm_tests', |
| 579 data_server='http://localhost:8081', |
| 580 swarm_url='http://localhost:8082', |
| 581 verbose=False, |
| 582 profile=False, |
| 583 priority=101) |
| 584 self.assertEqual(0, result) |
| 585 |
| 586 # Just assert it printed enough, since it contains variable output. |
| 587 out = sys.stdout.getvalue() |
| 588 self.assertTrue(len(out) > STDOUT_FOR_TRIGGER_LEN) |
| 589 self.assertTrue('Zip file already on server, no need to reupload.' in out) |
| 590 self.mock(sys, 'stdout', StringIO.StringIO()) |
| 591 |
| 592 def test_no_dir(self): |
| 593 try: |
| 594 swarming.main(['trigger']) |
| 595 self.fail() |
| 596 except SystemExit as e: |
| 597 self.assertEqual(2, e.code) |
| 598 self._check_output( |
| 599 '', |
| 600 'Usage: swarming.py trigger [options]\n\n' |
| 601 'swarming.py: error: Must specify the data directory\n') |
| 602 |
| 603 def test_no_request(self): |
| 604 try: |
| 605 swarming.main(['trigger', '-d', '.']) |
| 606 self.fail() |
| 607 except SystemExit as e: |
| 608 self.assertEqual(2, e.code) |
| 609 self._check_output( |
| 610 '', |
| 611 'Usage: swarming.py trigger [options]\n\n' |
| 612 'swarming.py: error: At least one --run_from_hash is required.\n') |
| 613 |
| 614 |
| 368 if __name__ == '__main__': | 615 if __name__ == '__main__': |
| 369 logging.basicConfig( | 616 logging.basicConfig( |
| 370 level=logging.DEBUG if '-v' in sys.argv else logging.ERROR) | 617 level=logging.DEBUG if '-v' in sys.argv else logging.ERROR) |
| 371 if '-v' in sys.argv: | 618 if '-v' in sys.argv: |
| 372 unittest.TestCase.maxDiff = None | 619 unittest.TestCase.maxDiff = None |
| 373 unittest.main() | 620 unittest.main() |
| OLD | NEW |