Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # | 2 # |
| 3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 4 # Use of this source code is governed by a BSD-style license that can be | 4 # Use of this source code is governed by a BSD-style license that can be |
| 5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
| 6 | 6 |
| 7 """Runs all the native unit tests. | 7 """Runs all the native unit tests. |
| 8 | 8 |
| 9 1. Copy over test binary to /data/local on device. | 9 1. Copy over test binary to /data/local on device. |
| 10 2. Resources: chrome/unit_tests requires resources (chrome.pak and en-US.pak) | 10 2. Resources: chrome/unit_tests requires resources (chrome.pak and en-US.pak) |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 73 'content_unittests', | 73 'content_unittests', |
| 74 'gpu_unittests', | 74 'gpu_unittests', |
| 75 'ipc_tests', | 75 'ipc_tests', |
| 76 'net_unittests', | 76 'net_unittests', |
| 77 'sql_unittests', | 77 'sql_unittests', |
| 78 'sync_unit_tests', | 78 'sync_unit_tests', |
| 79 'ui_unittests', | 79 'ui_unittests', |
| 80 ] | 80 ] |
| 81 | 81 |
| 82 | 82 |
| 83 def FullyQualifiedTestSuites(exe, test_suites): | 83 def FullyQualifiedTestSuites(exe, option_test_suite): |
| 84 """Return a fully qualified list that represents all known suites. | 84 """Return a fully qualified list |
| 85 | 85 |
| 86 Args: | 86 Args: |
| 87 exe: if True, use the executable-based test runner. | 87 exe: if True, use the executable-based test runner. |
| 88 test_suites: the source test suites to process. | 88 option_test_suite: the test_suite specified as an option. |
| 89 """ | 89 """ |
| 90 # Assume the test suites are in out/Release. | 90 # Assume the test suites are in out/Release. |
| 91 test_suite_dir = os.path.abspath(os.path.join(constants.CHROME_DIR, | 91 test_suite_dir = os.path.abspath(os.path.join(constants.CHROME_DIR, |
| 92 'out', 'Release')) | 92 'out', 'Release')) |
| 93 if option_test_suite: | |
| 94 all_test_suites = [option_test_suite] | |
| 95 else: | |
| 96 all_test_suites = _TEST_SUITES | |
| 97 | |
| 93 if exe: | 98 if exe: |
| 94 suites = [os.path.join(test_suite_dir, t) for t in test_suites] | 99 qualified_test_suites = [os.path.join(test_suite_dir, t) |
| 100 for t in all_test_suites] | |
| 95 else: | 101 else: |
| 96 # out/Release/$SUITE_apk/$SUITE-debug.apk | 102 # out/Release/$SUITE_apk/$SUITE-debug.apk |
| 97 suites = [os.path.join(test_suite_dir, | 103 qualified_test_suites = [os.path.join(test_suite_dir, |
| 98 t + '_apk', | 104 t + '_apk', |
| 99 t + '-debug.apk') | 105 t + '-debug.apk') |
| 100 for t in test_suites] | 106 for t in all_test_suites] |
| 101 return suites | 107 for t, q in zip(all_test_suites, qualified_test_suites): |
| 108 if not os.path.exists(q): | |
| 109 logging.critical('Test suite %s not found in %s.\n' | |
| 110 'Supported test suites:\n %s\n' | |
| 111 'Ensure it has been built.\n', | |
| 112 t, q, _TEST_SUITES) | |
| 113 return [] | |
| 114 return qualified_test_suites | |
| 102 | 115 |
| 103 | 116 |
| 104 class TimeProfile(object): | 117 class TimeProfile(object): |
| 105 """Class for simple profiling of action, with logging of cost.""" | 118 """Class for simple profiling of action, with logging of cost.""" |
| 106 | 119 |
| 107 def __init__(self, description): | 120 def __init__(self, description): |
| 108 self._description = description | 121 self._description = description |
| 109 self.Start() | 122 self.Start() |
| 110 | 123 |
| 111 def Start(self): | 124 def Start(self): |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 168 | 181 |
| 169 def PrintAnnotationForTestResults(test_results): | 182 def PrintAnnotationForTestResults(test_results): |
| 170 if test_results.timed_out: | 183 if test_results.timed_out: |
| 171 buildbot_report.PrintWarning() | 184 buildbot_report.PrintWarning() |
| 172 elif test_results.failed or test_results.crashed or test_results.overall_fail: | 185 elif test_results.failed or test_results.crashed or test_results.overall_fail: |
| 173 buildbot_report.PrintError() | 186 buildbot_report.PrintError() |
| 174 else: | 187 else: |
| 175 print 'Step success!' # No annotation needed | 188 print 'Step success!' # No annotation needed |
| 176 | 189 |
| 177 | 190 |
| 178 def RunTests(exe, device, test_suite, gtest_filter, test_arguments, rebaseline, | |
| 179 timeout, performance_test, cleanup_test_files, tool, | |
| 180 log_dump_name, fast_and_loose): | |
| 181 """Runs the tests. | |
| 182 | |
| 183 Args: | |
| 184 exe: boolean to state if we are using the exe based test runner | |
| 185 device: Device to run the tests. | |
| 186 test_suite: A specific test suite to run, empty to run all. | |
| 187 gtest_filter: A gtest_filter flag. | |
| 188 test_arguments: Additional arguments to pass to the test binary. | |
| 189 rebaseline: Whether or not to run tests in isolation and update the filter. | |
| 190 timeout: Timeout for each test. | |
| 191 performance_test: Whether or not performance test(s). | |
| 192 cleanup_test_files: Whether or not to cleanup test files on device. | |
| 193 tool: Name of the Valgrind tool. | |
| 194 log_dump_name: Name of log dump file. | |
| 195 fast_and_loose: if set, skip copying data files. | |
| 196 | |
| 197 Returns: | |
| 198 A TestResults object. | |
| 199 """ | |
| 200 results = [] | |
| 201 | |
| 202 if test_suite: | |
| 203 if not os.path.exists(test_suite): | |
| 204 logging.critical('Unrecognized test suite %s, supported: %s', | |
| 205 test_suite, _TEST_SUITES) | |
| 206 if test_suite in _TEST_SUITES: | |
| 207 logging.critical('(Remember to include the path: out/Release/%s)', | |
| 208 test_suite) | |
| 209 test_suite_basename = os.path.basename(test_suite) | |
| 210 if test_suite_basename in _TEST_SUITES: | |
| 211 logging.critical('Try "make -j15 %s"', test_suite_basename) | |
| 212 else: | |
| 213 logging.critical('Unrecognized test suite, supported: %s', | |
| 214 _TEST_SUITES) | |
| 215 return TestResults.FromRun([], [BaseTestResult(test_suite, '')], | |
| 216 False, False) | |
| 217 fully_qualified_test_suites = [test_suite] | |
| 218 else: | |
| 219 fully_qualified_test_suites = FullyQualifiedTestSuites(exe, _TEST_SUITES) | |
| 220 debug_info_list = [] | |
| 221 print 'Known suites: ' + str(_TEST_SUITES) | |
| 222 print 'Running these: ' + str(fully_qualified_test_suites) | |
| 223 for t in fully_qualified_test_suites: | |
| 224 buildbot_report.PrintNamedStep('Test suite %s' % os.path.basename(t)) | |
| 225 test = SingleTestRunner(device, t, gtest_filter, test_arguments, | |
| 226 timeout, rebaseline, performance_test, | |
| 227 cleanup_test_files, tool, 0, not not log_dump_name, | |
| 228 fast_and_loose) | |
| 229 test.Run() | |
| 230 | |
| 231 results += [test.test_results] | |
| 232 # Collect debug info. | |
| 233 debug_info_list += [test.dump_debug_info] | |
| 234 if rebaseline: | |
| 235 test.UpdateFilter(test.test_results.failed) | |
| 236 test.test_results.LogFull('Unit test', os.path.basename(t)) | |
| 237 # Zip all debug info outputs into a file named by log_dump_name. | |
| 238 debug_info.GTestDebugInfo.ZipAndCleanResults( | |
| 239 os.path.join(constants.CHROME_DIR, 'out', 'Release', 'debug_info_dumps'), | |
| 240 log_dump_name, [d for d in debug_info_list if d]) | |
| 241 | |
| 242 PrintAnnotationForTestResults(test.test_results) | |
| 243 | |
| 244 return TestResults.FromTestResults(results) | |
| 245 | |
| 246 | |
| 247 class TestSharder(BaseTestSharder): | 191 class TestSharder(BaseTestSharder): |
| 248 """Responsible for sharding the tests on the connected devices.""" | 192 """Responsible for sharding the tests on the connected devices.""" |
| 249 | 193 |
| 250 def __init__(self, attached_devices, test_suite, gtest_filter, | 194 def __init__(self, attached_devices, test_suite, gtest_filter, |
| 251 test_arguments, timeout, rebaseline, performance_test, | 195 test_arguments, timeout, rebaseline, performance_test, |
| 252 cleanup_test_files, tool, log_dump_name, fast_and_loose): | 196 cleanup_test_files, tool, log_dump_name, fast_and_loose): |
| 253 BaseTestSharder.__init__(self, attached_devices) | 197 BaseTestSharder.__init__(self, attached_devices) |
| 254 self.test_suite = test_suite | 198 self.test_suite = test_suite |
| 255 self.test_suite_basename = os.path.basename(test_suite) | 199 self.test_suite_basename = os.path.basename(test_suite) |
| 256 self.gtest_filter = gtest_filter | 200 self.gtest_filter = gtest_filter or '' |
| 257 self.test_arguments = test_arguments | 201 self.test_arguments = test_arguments |
| 258 self.timeout = timeout | 202 self.timeout = timeout |
| 259 self.rebaseline = rebaseline | 203 self.rebaseline = rebaseline |
| 260 self.performance_test = performance_test | 204 self.performance_test = performance_test |
| 261 self.cleanup_test_files = cleanup_test_files | 205 self.cleanup_test_files = cleanup_test_files |
| 262 self.tool = tool | 206 self.tool = tool |
| 263 self.log_dump_name = log_dump_name | 207 self.log_dump_name = log_dump_name |
| 264 self.fast_and_loose = fast_and_loose | 208 self.fast_and_loose = fast_and_loose |
| 265 test = SingleTestRunner(self.attached_devices[0], test_suite, gtest_filter, | 209 test = SingleTestRunner(self.attached_devices[0], test_suite, gtest_filter, |
| 266 test_arguments, timeout, rebaseline, | 210 test_arguments, timeout, rebaseline, |
| 267 performance_test, cleanup_test_files, tool, 0, | 211 performance_test, cleanup_test_files, tool, 0, |
| 268 not not self.log_dump_name, fast_and_loose) | 212 not not self.log_dump_name, fast_and_loose) |
| 269 # The executable/apk needs to be copied before we can call GetAllTests. | 213 self.tests = [] |
| 270 test.test_package.StripAndCopyExecutable() | 214 if not self.gtest_filter: |
| 271 all_tests = test.test_package.GetAllTests() | 215 # No filter has been specified, let's add all tests then. |
| 272 if not rebaseline: | 216 # The executable/apk needs to be copied before we can call GetAllTests. |
| 273 disabled_list = test.GetDisabledTests() | 217 test.test_package.StripAndCopyExecutable() |
| 274 # Only includes tests that do not have any match in the disabled list. | 218 all_tests = test.test_package.GetAllTests() |
| 275 all_tests = filter(lambda t: | 219 if not rebaseline: |
| 276 not any([fnmatch.fnmatch(t, disabled_pattern) | 220 disabled_list = test.GetDisabledTests() |
| 277 for disabled_pattern in disabled_list]), | 221 # Only includes tests that do not have any match in the disabled list. |
| 278 all_tests) | 222 all_tests = filter(lambda t: |
| 279 self.tests = all_tests | 223 not any([fnmatch.fnmatch(t, disabled_pattern) |
| 224 for disabled_pattern in disabled_list]), | |
| 225 all_tests) | |
| 226 self.tests = all_tests | |
| 280 | 227 |
| 281 def CreateShardedTestRunner(self, device, index): | 228 def CreateShardedTestRunner(self, device, index): |
| 282 """Creates a suite-specific test runner. | 229 """Creates a suite-specific test runner. |
| 283 | 230 |
| 284 Args: | 231 Args: |
| 285 device: Device serial where this shard will run. | 232 device: Device serial where this shard will run. |
| 286 index: Index of this device in the pool. | 233 index: Index of this device in the pool. |
| 287 | 234 |
| 288 Returns: | 235 Returns: |
| 289 A SingleTestRunner object. | 236 A SingleTestRunner object. |
| 290 """ | 237 """ |
| 291 device_num = len(self.attached_devices) | 238 device_num = len(self.attached_devices) |
| 292 shard_size = (len(self.tests) + device_num - 1) / device_num | 239 shard_size = (len(self.tests) + device_num - 1) / device_num |
| 293 shard_test_list = self.tests[index * shard_size : (index + 1) * shard_size] | 240 shard_test_list = self.tests[index * shard_size : (index + 1) * shard_size] |
| 294 test_filter = ':'.join(shard_test_list) | 241 test_filter = ':'.join(shard_test_list) + self.gtest_filter |
| 295 return SingleTestRunner(device, self.test_suite, | 242 return SingleTestRunner(device, self.test_suite, |
| 296 test_filter, self.test_arguments, self.timeout, | 243 test_filter, self.test_arguments, self.timeout, |
| 297 self.rebaseline, self.performance_test, | 244 self.rebaseline, self.performance_test, |
| 298 self.cleanup_test_files, self.tool, index, | 245 self.cleanup_test_files, self.tool, index, |
| 299 not not self.log_dump_name, self.fast_and_loose) | 246 not not self.log_dump_name, self.fast_and_loose) |
| 300 | 247 |
| 301 def OnTestsCompleted(self, test_runners, test_results): | 248 def OnTestsCompleted(self, test_runners, test_results): |
| 302 """Notifies that we completed the tests.""" | 249 """Notifies that we completed the tests.""" |
| 303 test_results.LogFull('Unit test', os.path.basename(self.test_suite)) | 250 test_results.LogFull('Unit test', os.path.basename(self.test_suite)) |
| 304 PrintAnnotationForTestResults(test_results) | 251 PrintAnnotationForTestResults(test_results) |
| 305 if test_results.failed and self.rebaseline: | 252 if test_results.failed and self.rebaseline: |
| 306 test_runners[0].UpdateFilter(test_results.failed) | 253 test_runners[0].UpdateFilter(test_results.failed) |
| 254 if self.log_dump_name: | |
| 255 # Zip all debug info outputs into a file named by log_dump_name. | |
| 256 debug_info.GTestDebugInfo.ZipAndCleanResults( | |
| 257 os.path.join(constants.CHROME_DIR, 'out', 'Release', | |
| 258 'debug_info_dumps'), | |
| 259 self.log_dump_name) | |
| 307 | 260 |
| 308 | 261 |
| 309 def _RunATestSuite(options): | 262 def _RunATestSuite(options): |
| 310 """Run a single test suite. | 263 """Run a single test suite. |
| 311 | 264 |
| 312 Helper for Dispatch() to allow stop/restart of the emulator across | 265 Helper for Dispatch() to allow stop/restart of the emulator across |
| 313 test bundles. If using the emulator, we start it on entry and stop | 266 test bundles. If using the emulator, we start it on entry and stop |
| 314 it on exit. | 267 it on exit. |
| 315 | 268 |
| 316 Args: | 269 Args: |
| 317 options: options for running the tests. | 270 options: options for running the tests. |
| 318 | 271 |
| 319 Returns: | 272 Returns: |
| 320 0 if successful, number of failing tests otherwise. | 273 0 if successful, number of failing tests otherwise. |
| 321 """ | 274 """ |
| 275 buildbot_report.PrintNamedStep('Test suite %s' % options.test_suite) | |
| 322 attached_devices = [] | 276 attached_devices = [] |
| 323 buildbot_emulators = [] | 277 buildbot_emulators = [] |
| 324 | 278 |
| 325 if options.use_emulator: | 279 if options.use_emulator: |
| 326 for n in range(options.emulator_count): | 280 for n in range(options.emulator_count): |
| 327 t = TimeProfile('Emulator launch %d' % n) | 281 t = TimeProfile('Emulator launch %d' % n) |
| 328 avd_name = None | 282 avd_name = None |
| 329 if n > 0: | 283 if n > 0: |
| 330 # Creates a temporary AVD for the extra emulators. | 284 # Creates a temporary AVD for the extra emulators. |
| 331 avd_name = 'run_tests_avd_%d' % n | 285 avd_name = 'run_tests_avd_%d' % n |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 345 if not attached_devices: | 299 if not attached_devices: |
| 346 logging.critical('A device must be attached and online.') | 300 logging.critical('A device must be attached and online.') |
| 347 buildbot_report.PrintError() | 301 buildbot_report.PrintError() |
| 348 return 1 | 302 return 1 |
| 349 | 303 |
| 350 # Reset the test port allocation. It's important to do it before starting | 304 # Reset the test port allocation. It's important to do it before starting |
| 351 # to dispatch any tests. | 305 # to dispatch any tests. |
| 352 if not ports.ResetTestServerPortAllocation(): | 306 if not ports.ResetTestServerPortAllocation(): |
| 353 raise Exception('Failed to reset test server port.') | 307 raise Exception('Failed to reset test server port.') |
| 354 | 308 |
| 355 if (len(attached_devices) > 1 and options.test_suite and | 309 if options.performance_test or options.gtest_filter: |
| 356 not options.gtest_filter and not options.performance_test): | 310 # These configuration can't be split in multiple devices. |
| 357 sharder = TestSharder(attached_devices, options.test_suite, | 311 attached_devices = [attached_devices[0]] |
| 358 options.gtest_filter, options.test_arguments, | 312 sharder = TestSharder(attached_devices, options.test_suite, |
|
nilesh
2012/07/17 18:14:33
We are now using TestSharder in all cases. However
| |
| 359 options.timeout, options.rebaseline, | 313 options.gtest_filter, options.test_arguments, |
| 360 options.performance_test, | 314 options.timeout, options.rebaseline, |
| 361 options.cleanup_test_files, options.tool, | 315 options.performance_test, |
| 362 options.log_dump, options.fast_and_loose) | 316 options.cleanup_test_files, options.tool, |
| 363 test_results = sharder.RunShardedTests() | 317 options.log_dump, options.fast_and_loose) |
| 364 else: | 318 test_results = sharder.RunShardedTests() |
| 365 test_results = RunTests(options.exe, attached_devices[0], | |
| 366 options.test_suite, | |
| 367 options.gtest_filter, options.test_arguments, | |
| 368 options.rebaseline, options.timeout, | |
| 369 options.performance_test, | |
| 370 options.cleanup_test_files, options.tool, | |
| 371 options.log_dump, options.fast_and_loose) | |
| 372 | 319 |
| 373 for buildbot_emulator in buildbot_emulators: | 320 for buildbot_emulator in buildbot_emulators: |
| 374 buildbot_emulator.Shutdown() | 321 buildbot_emulator.Shutdown() |
| 375 | 322 |
| 376 # Another chance if we timed out? At this point It is safe(r) to | 323 # Another chance if we timed out? At this point It is safe(r) to |
| 377 # run fast and loose since we just uploaded all the test data and | 324 # run fast and loose since we just uploaded all the test data and |
| 378 # binary. | 325 # binary. |
| 379 if test_results.timed_out and options.repeat: | 326 if test_results.timed_out and options.repeat: |
| 380 logging.critical('Timed out; repeating in fast_and_loose mode.') | 327 logging.critical('Timed out; repeating in fast_and_loose mode.') |
| 381 options.fast_and_loose = True | 328 options.fast_and_loose = True |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 398 0 if successful, number of failing tests otherwise. | 345 0 if successful, number of failing tests otherwise. |
| 399 """ | 346 """ |
| 400 if options.test_suite == 'help': | 347 if options.test_suite == 'help': |
| 401 ListTestSuites() | 348 ListTestSuites() |
| 402 return 0 | 349 return 0 |
| 403 | 350 |
| 404 if options.use_xvfb: | 351 if options.use_xvfb: |
| 405 xvfb = Xvfb() | 352 xvfb = Xvfb() |
| 406 xvfb.Start() | 353 xvfb.Start() |
| 407 | 354 |
| 408 if options.test_suite: | 355 all_test_suites = FullyQualifiedTestSuites(options.exe, options.test_suite) |
| 409 all_test_suites = FullyQualifiedTestSuites(options.exe, | |
| 410 [options.test_suite]) | |
| 411 else: | |
| 412 all_test_suites = FullyQualifiedTestSuites(options.exe, | |
| 413 _TEST_SUITES) | |
| 414 failures = 0 | 356 failures = 0 |
| 415 for suite in all_test_suites: | 357 for suite in all_test_suites: |
| 416 options.test_suite = suite | 358 options.test_suite = suite |
| 417 failures += _RunATestSuite(options) | 359 failures += _RunATestSuite(options) |
| 418 | 360 |
| 419 if options.use_xvfb: | 361 if options.use_xvfb: |
| 420 xvfb.Stop() | 362 xvfb.Stop() |
| 421 return failures | 363 return failures |
| 422 | 364 |
| 423 | 365 |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 440 help='Rebaseline and update *testsuite_disabled', | 382 help='Rebaseline and update *testsuite_disabled', |
| 441 action='store_true') | 383 action='store_true') |
| 442 option_parser.add_option('-f', '--gtest_filter', dest='gtest_filter', | 384 option_parser.add_option('-f', '--gtest_filter', dest='gtest_filter', |
| 443 help='gtest filter') | 385 help='gtest filter') |
| 444 option_parser.add_option('-a', '--test_arguments', dest='test_arguments', | 386 option_parser.add_option('-a', '--test_arguments', dest='test_arguments', |
| 445 help='Additional arguments to pass to the test') | 387 help='Additional arguments to pass to the test') |
| 446 option_parser.add_option('-p', dest='performance_test', | 388 option_parser.add_option('-p', dest='performance_test', |
| 447 help='Indicator of performance test', | 389 help='Indicator of performance test', |
| 448 action='store_true') | 390 action='store_true') |
| 449 option_parser.add_option('-L', dest='log_dump', | 391 option_parser.add_option('-L', dest='log_dump', |
| 450 help='file name of log dump, which will be put in' | 392 help='file name of log dump, which will be put in ' |
| 451 'subfolder debug_info_dumps under the same directory' | 393 'subfolder debug_info_dumps under the same ' |
| 452 'in where the test_suite exists.') | 394 'directory in where the test_suite exists.') |
| 453 option_parser.add_option('-e', '--emulator', dest='use_emulator', | 395 option_parser.add_option('-e', '--emulator', dest='use_emulator', |
| 454 action='store_true', | 396 action='store_true', |
| 455 help='Run tests in a new instance of emulator') | 397 help='Run tests in a new instance of emulator') |
| 456 option_parser.add_option('-n', '--emulator_count', | 398 option_parser.add_option('-n', '--emulator_count', |
| 457 type='int', default=1, | 399 type='int', default=1, |
| 458 help='Number of emulators to launch for running the ' | 400 help='Number of emulators to launch for running the ' |
| 459 'tests.') | 401 'tests.') |
| 460 option_parser.add_option('-x', '--xvfb', dest='use_xvfb', | 402 option_parser.add_option('-x', '--xvfb', dest='use_xvfb', |
| 461 action='store_true', | 403 action='store_true', |
| 462 help='Use Xvfb around tests (ignored if not Linux)') | 404 help='Use Xvfb around tests (ignored if not Linux)') |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 492 # the batch (this happens because the exit status is a sum of all failures | 434 # the batch (this happens because the exit status is a sum of all failures |
| 493 # from all suites, but the buildbot associates the exit status only with the | 435 # from all suites, but the buildbot associates the exit status only with the |
| 494 # most recent step). | 436 # most recent step). |
| 495 if options.exit_code: | 437 if options.exit_code: |
| 496 return failed_tests_count | 438 return failed_tests_count |
| 497 return 0 | 439 return 0 |
| 498 | 440 |
| 499 | 441 |
| 500 if __name__ == '__main__': | 442 if __name__ == '__main__': |
| 501 sys.exit(main(sys.argv)) | 443 sys.exit(main(sys.argv)) |
| OLD | NEW |