run.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. #!/usr/bin/env vpython3
  2. # Copyright 2016 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Run a test.
  6. Sample usage:
  7. ./run.py \
  8. -a src/xcodebuild/Release-iphoneos/base_unittests.app \
  9. -o /tmp/out \
  10. -p iPhone 5s \
  11. -v 9.3 \
  12. -b 9b46
  13. Installs base_unittests.app in an iPhone 5s simulator running iOS 9.3 under
  14. Xcode build version 9b46, runs it, and captures all test data in /tmp/out.
  15. """
  16. import argparse
  17. import json
  18. import logging
  19. import os
  20. import subprocess
  21. import sys
  22. import traceback
  23. import constants
  24. import shard_util
  25. import test_runner
  26. import variations_runner
  27. import wpr_runner
  28. import xcodebuild_runner
  29. import xcode_util as xcode
  30. class Runner():
  31. """
  32. Object to encapsulate iOS test runner execution coordination. Parses
  33. arguments and invokes underlying test runners accordingly.
  34. """
  35. def __init__(self, args=None):
  36. """
  37. args = argparse Namespace object.
  38. test_args = string list of args.
  39. """
  40. self.args = argparse.Namespace()
  41. self.test_args = []
  42. self.should_move_xcode_runtime_to_cache = True
  43. if args:
  44. self.parse_args(args)
  45. def install_xcode(self):
  46. """Installs the requested Xcode build version.
  47. Returns:
  48. (bool, bool)
  49. First bool: True if installation was successful. False otherwise.
  50. Second bool: True if Xcode is legacy package. False if it's new.
  51. """
  52. try:
  53. if not self.args.mac_toolchain_cmd:
  54. raise test_runner.MacToolchainNotFoundError(self.args.mac_toolchain_cmd)
  55. # Guard against incorrect install paths. On swarming, this path
  56. # should be a requested named cache, and it must exist.
  57. if not os.path.exists(self.args.xcode_path):
  58. raise test_runner.XcodePathNotFoundError(self.args.xcode_path)
  59. runtime_cache_folder = None
  60. # Runner script only utilizes runtime cache when it's a simulator task.
  61. if self.args.version:
  62. runtime_cache_folder = xcode.construct_runtime_cache_folder(
  63. self.args.runtime_cache_prefix, self.args.version)
  64. if not os.path.exists(runtime_cache_folder):
  65. # Depending on infra project, runtime named cache might not be
  66. # deployed. Create the dir if it doesn't exist since xcode_util
  67. # assumes it exists.
  68. # TODO(crbug.com/1191260): Raise error instead of creating dirs after
  69. # runtime named cache is deployed everywhere.
  70. os.makedirs(runtime_cache_folder)
  71. # xcode.install() installs the Xcode & iOS runtime, and returns a bool
  72. # indicating if the Xcode version in CIPD is a legacy Xcode package (which
  73. # includes iOS runtimes).
  74. is_legacy_xcode = xcode.install(
  75. self.args.mac_toolchain_cmd,
  76. self.args.xcode_build_version,
  77. self.args.xcode_path,
  78. runtime_cache_folder=runtime_cache_folder,
  79. ios_version=self.args.version)
  80. xcode.select(self.args.xcode_path)
  81. except subprocess.CalledProcessError as e:
  82. # Flush buffers to ensure correct output ordering.
  83. sys.stdout.flush()
  84. sys.stderr.write('Xcode build version %s failed to install: %s\n' %
  85. (self.args.xcode_build_version, e))
  86. sys.stderr.flush()
  87. return (False, False)
  88. else:
  89. return (True, is_legacy_xcode)
  90. def resolve_test_cases(self):
  91. """Forms |self.args.test_cases| considering swarming shard and cmd inputs.
  92. Note:
  93. - Xcode intallation is required before invoking this method since it
  94. requires otool to parse test names from compiled targets.
  95. - It's validated in |parse_args| that test filters won't work in sharding
  96. environment.
  97. """
  98. args_json = json.loads(self.args.args_json)
  99. # GTEST_SHARD_INDEX and GTEST_TOTAL_SHARDS are additional test environment
  100. # variables, set by Swarming, that are only set for a swarming task
  101. # shard count is > 1.
  102. #
  103. # For a given test on a given run, otool should return the same total
  104. # counts and thus, should generate the same sublists. With the shard
  105. # index, each shard would then know the exact test case to run.
  106. gtest_shard_index = shard_util.shard_index()
  107. gtest_total_shards = shard_util.total_shards()
  108. if gtest_total_shards > 1:
  109. self.args.test_cases = shard_util.shard_test_cases(
  110. self.args, gtest_shard_index, gtest_total_shards)
  111. if self.args.test_cases:
  112. assert (
  113. self.args.xcode_parallelization or
  114. self.args.xcodebuild_device_runner
  115. ), 'Only real XCTests can use sharding by shard_util.shard_test_cases()'
  116. else:
  117. self.args.test_cases = self.args.test_cases or []
  118. if self.args.gtest_filter:
  119. self.args.test_cases.extend(self.args.gtest_filter.split(':'))
  120. if self.args.isolated_script_test_filter:
  121. self.args.test_cases.extend(
  122. self.args.isolated_script_test_filter.split('::'))
  123. self.args.test_cases.extend(args_json.get('test_cases', []))
  124. def sharding_env_vars(self):
  125. """Returns env_var arg with GTest sharding env var."""
  126. gtest_total_shards = shard_util.total_shards()
  127. if gtest_total_shards > 1:
  128. assert not any((el.startswith('GTEST_SHARD_INDEX') or
  129. el.startswith('GTEST_TOTAL_SHARDS'))
  130. for el in self.args.env_var
  131. ), 'GTest shard env vars should not be passed in --env-var'
  132. gtest_shard_index = shard_util.shard_index()
  133. return [
  134. 'GTEST_SHARD_INDEX=%d' % gtest_shard_index,
  135. 'GTEST_TOTAL_SHARDS=%d' % gtest_total_shards
  136. ]
  137. return []
  138. def run(self, args):
  139. """
  140. Main coordinating function.
  141. """
  142. self.parse_args(args)
  143. # This logic is run by default before the otool command is invoked such that
  144. # otool has the correct Xcode selected for command line dev tools.
  145. install_success, is_legacy_xcode = self.install_xcode()
  146. if not install_success:
  147. raise test_runner.XcodeVersionNotFoundError(self.args.xcode_build_version)
  148. self.resolve_test_cases()
  149. # Sharding env var is required to shard GTest.
  150. env_vars = self.args.env_var + self.sharding_env_vars()
  151. summary = {}
  152. tr = None
  153. if not os.path.exists(self.args.out_dir):
  154. os.makedirs(self.args.out_dir)
  155. try:
  156. if self.args.xcode_parallelization:
  157. tr = xcodebuild_runner.SimulatorParallelTestRunner(
  158. self.args.app,
  159. self.args.host_app,
  160. self.args.iossim,
  161. self.args.version,
  162. self.args.platform,
  163. out_dir=self.args.out_dir,
  164. readline_timeout=self.args.readline_timeout,
  165. release=self.args.release,
  166. repeat_count=self.args.repeat,
  167. retries=self.args.retries,
  168. shards=self.args.shards,
  169. test_cases=self.args.test_cases,
  170. test_args=self.test_args,
  171. use_clang_coverage=self.args.use_clang_coverage,
  172. env_vars=env_vars)
  173. elif self.args.variations_seed_path != 'NO_PATH':
  174. tr = variations_runner.VariationsSimulatorParallelTestRunner(
  175. self.args.app,
  176. self.args.host_app,
  177. self.args.iossim,
  178. self.args.version,
  179. self.args.platform,
  180. self.args.out_dir,
  181. self.args.variations_seed_path,
  182. readline_timeout=self.args.readline_timeout,
  183. release=self.args.release,
  184. test_cases=self.args.test_cases,
  185. test_args=self.test_args,
  186. env_vars=env_vars)
  187. elif self.args.replay_path != 'NO_PATH':
  188. tr = wpr_runner.WprProxySimulatorTestRunner(
  189. self.args.app,
  190. self.args.host_app,
  191. self.args.iossim,
  192. self.args.replay_path,
  193. self.args.platform,
  194. self.args.version,
  195. self.args.wpr_tools_path,
  196. self.args.out_dir,
  197. env_vars=env_vars,
  198. readline_timeout=self.args.readline_timeout,
  199. retries=self.args.retries,
  200. shards=self.args.shards,
  201. test_args=self.test_args,
  202. test_cases=self.args.test_cases,
  203. xctest=self.args.xctest,
  204. )
  205. elif self.args.iossim and self.args.platform and self.args.version:
  206. tr = test_runner.SimulatorTestRunner(
  207. self.args.app,
  208. self.args.iossim,
  209. self.args.platform,
  210. self.args.version,
  211. self.args.out_dir,
  212. env_vars=env_vars,
  213. readline_timeout=self.args.readline_timeout,
  214. repeat_count=self.args.repeat,
  215. retries=self.args.retries,
  216. shards=self.args.shards,
  217. test_args=self.test_args,
  218. test_cases=self.args.test_cases,
  219. use_clang_coverage=self.args.use_clang_coverage,
  220. wpr_tools_path=self.args.wpr_tools_path,
  221. xctest=self.args.xctest,
  222. )
  223. elif self.args.xcodebuild_device_runner and self.args.xctest:
  224. tr = xcodebuild_runner.DeviceXcodeTestRunner(
  225. app_path=self.args.app,
  226. host_app_path=self.args.host_app,
  227. out_dir=self.args.out_dir,
  228. readline_timeout=self.args.readline_timeout,
  229. release=self.args.release,
  230. repeat_count=self.args.repeat,
  231. retries=self.args.retries,
  232. test_cases=self.args.test_cases,
  233. test_args=self.test_args,
  234. env_vars=env_vars)
  235. else:
  236. tr = test_runner.DeviceTestRunner(
  237. self.args.app,
  238. self.args.out_dir,
  239. env_vars=env_vars,
  240. readline_timeout=self.args.readline_timeout,
  241. repeat_count=self.args.repeat,
  242. restart=self.args.restart,
  243. retries=self.args.retries,
  244. test_args=self.test_args,
  245. test_cases=self.args.test_cases,
  246. xctest=self.args.xctest,
  247. )
  248. logging.info("Using test runner %s" % type(tr).__name__)
  249. return 0 if tr.launch() else 1
  250. except test_runner.DeviceError as e:
  251. sys.stderr.write(traceback.format_exc())
  252. summary['step_text'] = '%s%s' % (e.__class__.__name__,
  253. ': %s' % e.args[0] if e.args else '')
  254. # Swarming infra marks device status unavailable for any device related
  255. # issue using this return code.
  256. return 3
  257. except test_runner.SimulatorNotFoundError as e:
  258. # This means there's probably some issue in simulator runtime so we don't
  259. # want to cache it anymore (when it's in new Xcode format).
  260. self.should_move_xcode_runtime_to_cache = False
  261. sys.stderr.write(traceback.format_exc())
  262. summary['step_text'] = '%s%s' % (e.__class__.__name__,
  263. ': %s' % e.args[0] if e.args else '')
  264. return 2
  265. except test_runner.TestRunnerError as e:
  266. sys.stderr.write(traceback.format_exc())
  267. summary['step_text'] = '%s%s' % (e.__class__.__name__,
  268. ': %s' % e.args[0] if e.args else '')
  269. # test_runner.Launch returns 0 on success, 1 on failure, so return 2
  270. # on exception to distinguish between a test failure, and a failure
  271. # to launch the test at all.
  272. return 2
  273. finally:
  274. if tr:
  275. summary['logs'] = tr.logs
  276. with open(os.path.join(self.args.out_dir, 'summary.json'), 'w') as f:
  277. json.dump(summary, f)
  278. if tr:
  279. with open(os.path.join(self.args.out_dir, 'full_results.json'),
  280. 'w') as f:
  281. json.dump(tr.test_results, f)
  282. # The value of test-launcher-summary-output is set by the recipe
  283. # and passed here via swarming.py. This argument defaults to
  284. # ${ISOLATED_OUTDIR}/output.json. out-dir is set to ${ISOLATED_OUTDIR}
  285. # TODO(crbug.com/1031338) - the content of this output.json will
  286. # work with Chromium recipe because we use the noop_merge merge script,
  287. # but will require structural changes to support the default gtest
  288. # merge script (ref: //testing/merge_scripts/standard_gtest_merge.py)
  289. output_json_path = (
  290. self.args.test_launcher_summary_output or
  291. os.path.join(self.args.out_dir, 'output.json'))
  292. with open(output_json_path, 'w') as f:
  293. json.dump(tr.test_results, f)
  294. # Move the iOS runtime back to cache dir if the Xcode package is not
  295. # legacy (i.e. Xcode program & runtimes are in different CIPD packages.)
  296. # and it's a simulator task.
  297. if not is_legacy_xcode and self.args.version:
  298. if self.should_move_xcode_runtime_to_cache:
  299. runtime_cache_folder = xcode.construct_runtime_cache_folder(
  300. self.args.runtime_cache_prefix, self.args.version)
  301. xcode.move_runtime(runtime_cache_folder, self.args.xcode_path, False)
  302. else:
  303. xcode.remove_runtimes(self.args.xcode_path)
  304. test_runner.defaults_delete('com.apple.CoreSimulator',
  305. 'FramebufferServerRendererPolicy')
  306. def parse_args(self, args):
  307. """Parse the args into args and test_args.
  308. Note: test_cases related arguments are handled in |resolve_test_cases|
  309. instead of this function.
  310. """
  311. parser = argparse.ArgumentParser()
  312. parser.add_argument(
  313. '-x',
  314. '--xcode-parallelization',
  315. help='Run tests using xcodebuild\'s parallelization.',
  316. action='store_true',
  317. )
  318. parser.add_argument(
  319. '-a',
  320. '--app',
  321. help='Compiled .app to run for EG1, Compiled -Runner.app for EG2',
  322. metavar='app',
  323. )
  324. parser.add_argument(
  325. '-b',
  326. '--xcode-build-version',
  327. help='Xcode build version to install.',
  328. required=True,
  329. metavar='build_id',
  330. )
  331. parser.add_argument(
  332. '-e',
  333. '--env-var',
  334. action='append',
  335. help='Environment variable to pass to the test itself.',
  336. metavar='ENV=val',
  337. )
  338. parser.add_argument(
  339. '--gtest_filter',
  340. help='List of test names to run. Expected to be in GTest filter format,'
  341. 'which should be a colon delimited list. Note: Specifying test cases '
  342. 'is not supported in multiple swarming shards environment. Will be '
  343. 'merged with tests specified in --test-cases, --args-json and '
  344. '--isolated-script-test-filter.',
  345. metavar='gtest_filter',
  346. )
  347. parser.add_argument(
  348. '--isolated-script-test-filter',
  349. help='A double-colon-separated ("::") list of test names to run. '
  350. 'Note: Specifying test cases is not supported in multiple swarming '
  351. 'shards environment. Will be merged with tests specified in '
  352. '--test-cases, --args-json and --gtest_filter.',
  353. metavar='isolated_test_filter',
  354. )
  355. parser.add_argument(
  356. '--gtest_repeat',
  357. '--isolated-script-test-repeat',
  358. help='Number of times to repeat each test case.',
  359. metavar='repeat',
  360. dest='repeat',
  361. type=int,
  362. )
  363. parser.add_argument(
  364. '--host-app',
  365. help='Compiled host .app to run.',
  366. default='NO_PATH',
  367. metavar='host_app',
  368. )
  369. parser.add_argument(
  370. '-i',
  371. '--iossim',
  372. help='Compiled iossim to run the app on.',
  373. metavar='iossim',
  374. )
  375. parser.add_argument(
  376. '-j',
  377. '--args-json',
  378. default='{}',
  379. help=
  380. 'Specify "env_var": [...] and "test_args": [...] using a JSON dict.',
  381. metavar='{}',
  382. )
  383. parser.add_argument(
  384. '--mac-toolchain-cmd',
  385. help='Command to run mac_toolchain tool. Default: %(default)s.',
  386. default='mac_toolchain',
  387. metavar='mac_toolchain',
  388. )
  389. parser.add_argument(
  390. '-o',
  391. '--out-dir',
  392. help='Directory to store all test data in.',
  393. metavar='dir',
  394. required=True,
  395. )
  396. parser.add_argument(
  397. '-p',
  398. '--platform',
  399. help='Platform to simulate.',
  400. metavar='sim',
  401. )
  402. parser.add_argument(
  403. '--readline-timeout',
  404. help='Timeout to kill a test process when it doesn\'t'
  405. 'have output (in seconds).',
  406. metavar='n',
  407. type=int,
  408. default=constants.READLINE_TIMEOUT,
  409. )
  410. #TODO(crbug.com/1056887): Implement this arg in infra.
  411. parser.add_argument(
  412. '--release',
  413. help='Indicates if this is a release build.',
  414. action='store_true',
  415. )
  416. parser.add_argument(
  417. '--replay-path',
  418. help=('Path to a directory containing WPR replay and recipe files, for '
  419. 'use with WprProxySimulatorTestRunner to replay a test suite '
  420. 'against multiple saved website interactions. '
  421. 'Default: %(default)s'),
  422. default='NO_PATH',
  423. metavar='replay-path',
  424. )
  425. parser.add_argument(
  426. '--restart',
  427. action='store_true',
  428. help=argparse.SUPPRESS,
  429. )
  430. parser.add_argument(
  431. '-r',
  432. '--retries',
  433. help=('Number of times to retry failed test cases. Note: This will be '
  434. 'overwritten as 0 if test repeat argument value > 1.'),
  435. metavar='n',
  436. type=int,
  437. )
  438. parser.add_argument(
  439. '--runtime-cache-prefix',
  440. metavar='PATH',
  441. help=(
  442. 'Path prefix for runtime cache folder. The prefix will be appended '
  443. 'with iOS version to construct the path. iOS simulator will be '
  444. 'installed to the path and further copied into Xcode. Default: '
  445. '%(default)s. WARNING: this folder will be overwritten! This '
  446. 'folder is intended to be a cached CIPD installation.'),
  447. default='Runtime-ios-',
  448. )
  449. parser.add_argument(
  450. '-s',
  451. '--shards',
  452. help='Number of shards to split test cases.',
  453. metavar='n',
  454. type=int,
  455. )
  456. parser.add_argument(
  457. '-t',
  458. '--test-cases',
  459. action='append',
  460. help=('Tests that should be included in the test run. All other tests '
  461. 'will be excluded from this run. If unspecified, run all tests. '
  462. 'Note: Specifying test cases is not supported in multiple '
  463. 'swarming shards environment. Will be merged with tests '
  464. 'specified in --gtest_filter and --args-json.'),
  465. metavar='testcase',
  466. )
  467. parser.add_argument(
  468. '--use-clang-coverage',
  469. help='Enable code coverage related steps in test runner scripts.',
  470. action='store_true',
  471. )
  472. parser.add_argument(
  473. '--use-trusted-cert',
  474. action='store_true',
  475. help=('Whether to install a cert to the simulator to allow for local '
  476. 'HTTPS testing.'),
  477. )
  478. parser.add_argument(
  479. '-v',
  480. '--version',
  481. help='Version of iOS the simulator should run.',
  482. metavar='ver',
  483. )
  484. parser.add_argument(
  485. '--variations-seed-path',
  486. help=('Path to a JSON file with variations seed used in variations '
  487. 'smoke testing. Default: %(default)s'),
  488. default='NO_PATH',
  489. metavar='variations-seed-path',
  490. )
  491. parser.add_argument(
  492. '--wpr-tools-path',
  493. help=(
  494. 'Location of WPR test tools (should be preinstalled, e.g. as part '
  495. 'of a swarming task requirement). Default: %(default)s.'),
  496. default='NO_PATH',
  497. metavar='wpr-tools-path',
  498. )
  499. parser.add_argument(
  500. '--xcode-path',
  501. metavar='PATH',
  502. help=('Path to <Xcode>.app folder where contents of the app will be '
  503. 'installed. Default: %(default)s. WARNING: this folder will be '
  504. 'overwritten! This folder is intended to be a cached CIPD '
  505. 'installation.'),
  506. default='Xcode.app',
  507. )
  508. parser.add_argument(
  509. '--xcodebuild-device-runner',
  510. help='Run tests using xcodebuild\'s on real device.',
  511. action='store_true',
  512. )
  513. parser.add_argument(
  514. '--xctest',
  515. action='store_true',
  516. help='Whether or not the given app should be run as an XCTest.',
  517. )
  518. parser.add_argument(
  519. '--test-launcher-summary-output',
  520. default=None,
  521. help='Full path to output.json file. output.json is consumed by both '
  522. 'collect_task.py and merge scripts.')
  523. def load_from_json(args):
  524. """Loads and sets arguments from args_json.
  525. Note: |test_cases| in --args-json is handled in
  526. |Runner.resolve_test_cases()| instead of this function.
  527. """
  528. args_json = json.loads(args.args_json)
  529. args.env_var = args.env_var or []
  530. args.env_var.extend(args_json.get('env_var', []))
  531. args.restart = args_json.get('restart', args.restart)
  532. args.xctest = args_json.get('xctest', args.xctest)
  533. args.xcode_parallelization = args_json.get('xcode_parallelization',
  534. args.xcode_parallelization)
  535. args.xcodebuild_device_runner = (
  536. args_json.get('xcodebuild_device_runner',
  537. args.xcodebuild_device_runner))
  538. args.shards = args_json.get('shards', args.shards)
  539. test_args.extend(args_json.get('test_args', []))
  540. def validate(args):
  541. """
  542. Runs argument validation
  543. """
  544. if (not (args.xcode_parallelization or args.xcodebuild_device_runner) and
  545. (args.iossim or args.platform or args.version)):
  546. # If any of --iossim, --platform, or --version
  547. # are specified then they must all be specified.
  548. if not (args.iossim and args.platform and args.version):
  549. parser.error('must specify all or none of '
  550. '-i/--iossim, -p/--platform, -v/--version')
  551. if args.xcode_parallelization and not (args.platform and args.version):
  552. parser.error('--xcode-parallelization also requires '
  553. 'both -p/--platform and -v/--version')
  554. # Do not retry when repeat
  555. if args.repeat and args.repeat > 1:
  556. args.retries = 0
  557. args_json = json.loads(args.args_json)
  558. if (args.gtest_filter or args.test_cases or
  559. args_json.get('test_cases')) and shard_util.total_shards() > 1:
  560. parser.error(
  561. 'Specifying test cases is not supported in multiple swarming '
  562. 'shards environment.')
  563. args, test_args = parser.parse_known_args(args)
  564. load_from_json(args)
  565. validate(args)
  566. # TODO(crbug.com/1056820): |app| won't contain "Debug" or "Release" after
  567. # recipe migrations.
  568. args.release = args.release or (args.app and "Release" in args.app)
  569. self.args = args
  570. self.test_args = test_args
  571. def main(args):
  572. logging.basicConfig(
  573. format='[%(asctime)s:%(levelname)s] %(message)s',
  574. level=logging.DEBUG,
  575. datefmt='%I:%M:%S')
  576. test_runner.defaults_delete('com.apple.CoreSimulator',
  577. 'FramebufferServerRendererPolicy')
  578. runner = Runner()
  579. logging.debug("Arg values passed for this run: %s" % args)
  580. return runner.run(args)
  581. if __name__ == '__main__':
  582. sys.exit(main(sys.argv[1:]))