1#!/usr/bin/env python 2 3# Copyright (C) 2016 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17'''Main test suite execution script.''' 18import argparse 19import inspect 20import logging 21import os 22import signal 23import subprocess 24import sys 25import time 26import collections 27import xml.etree.ElementTree as ET 28 29from config import Config 30from tests.harness import util_constants 31from tests.harness.exception import TestSuiteException, FailFastException 32from tests.harness import UtilAndroid 33from tests.harness import UtilBundle 34from tests.harness import util_log 35from tests.harness.util_functions import load_py_module 36from tests.harness.decorators import deprecated 37 38# For some reason pylint is not able to understand the class returned by 39# from util_log.get_logger() and generates a lot of false warnings 40#pylint: disable=maybe-no-member 41 42EMU_PROC = None 43 44def _parse_args(): 45 '''Parse the command line arguments. 46 47 Returns: 48 A namespace object that contains the options specified to run_tests on 49 the command line. 50 ''' 51 52 parser = argparse.ArgumentParser(description='Run the test suite.') 53 54 parser.add_argument('--config', '-c', 55 metavar='path', 56 help='Path to a custom config file.') 57 parser.add_argument('--device', '-d', 58 help='Specify the device id of the device to test on.') 59 parser.add_argument('--test', '-t', 60 metavar='path', 61 help='Specify a specific test to run.') 62 group = parser.add_mutually_exclusive_group() 63 group.add_argument('--wimpy', '-w', 64 action='store_true', 65 default=None, 66 help='Test only a core subset of features.') 67 group.add_argument('--app-types', 68 default=['java', 'cpp', 'jni'], 69 nargs='*', 70 help='Specify a list of Android app types against which' 71 ' to run the tests', 72 dest='bundle_types') 73 parser.add_argument('--install-only', 74 action='store_true', 75 default=False, 76 help='It only runs the pre-run stage of the test suite.' 77 ' It installs the required APKs but does not ' 78 'execute the tests.', 79 dest='install_only') 80 parser.add_argument('--no-install', '-n', 81 action='store_true', 82 default=False, 83 help='Stop the test suite installing apks to device.', 84 dest='noinstall') 85 parser.add_argument('--no-uninstall', 86 action='store_true', 87 default=False, 88 help='Stop the test suite uninstalling apks after ' 89 'completion.', 90 dest='nouninstall') 91 parser.add_argument('--print-to-stdout', 92 action='store_true', 93 default=False, 94 help='Print all logging information to standard out.', 95 dest='print_to_stdout') 96 parser.add_argument('--verbose', '-v', 97 action='store_true', 98 default=None, 99 help='Store extra info in the log.') 100 parser.add_argument('--fail-fast', 101 action='store_true', 102 default=False, 103 help='Exit the test suite immediately on the first failure.') 104 parser.add_argument('--run-emu', 105 action='store_true', 106 default=None, 107 help='Spawn an emulator and run the test suite on that.' 108 ' Specify the emulator command line in the config' 109 ' file or with -emu-cmd.', 110 dest='run_emu') 111 112 # Get the properties of the Config class and add a command line argument 113 # for each. 114 this_module = sys.modules[__name__] 115 for member_name, member_obj in inspect.getmembers(Config): 116 if (inspect.isdatadescriptor(member_obj) and 117 member_name not in ['__weakref__', 'device', 'verbose']): 118 119 # List type properties can take one or more arguments 120 num_args = None 121 if (isinstance(member_obj, property) 122 and isinstance(member_obj.fget(Config), list)): 123 num_args = '+' 124 125 opt_name = member_name.replace('_', '-') 126 127 setattr(this_module, opt_name, '') 128 129 parser.add_argument('--' + opt_name, 130 nargs=num_args, 131 help=member_obj.__doc__, 132 dest=member_name) 133 134 return parser.parse_args() 135 136 137def _choice(first_choice, second_choice): 138 '''Return first_choice if it is not None otherwise return second_choice. 139 140 Args: 141 first_choice: The first choice value. 142 second_choice: The alternative value. 143 144 Returns: 145 The first argument if it is not None, and the second otherwise. 146 ''' 147 return first_choice if first_choice else second_choice 148 149 150class State(object): 151 '''This class manages all objects required by the test suite.''' 152 153 # pylint: disable=too-many-instance-attributes 154 # Since this is a state class many attributes are expected. 155 156 def __init__(self): 157 '''State constructor. 158 159 Raises: 160 TestSuiteException: When unable to load config file. 161 162 AssertionError: When assertions fail. 163 ''' 164 165 # Parse the command line options 166 args = _parse_args() 167 168 # create a config instance 169 if args.config: 170 # use the user supplied 171 config = State.load_user_configuration(args.config) 172 else: 173 # use the default configuration 174 config = Config() 175 176 # save the test denylist 177 self.blocklist = _choice(args.blocklist, config.blocklist) 178 179 # Allow any of the command line arguments to override the 180 # values in the config file. 181 self.adb_path = _choice(args.adb_path, config.adb_path) 182 183 self.host_port = int(_choice(args.host_port, config.host_port)) 184 185 self.device = _choice(args.device, config.device) 186 187 self.user_specified_device = self.device 188 189 self.device_port = int(_choice(args.device_port, config.device_port)) 190 191 self.lldb_server_path_device = _choice(args.lldb_server_path_device, 192 config.lldb_server_path_device) 193 194 self.lldb_server_path_host = _choice(args.lldb_server_path_host, 195 config.lldb_server_path_host) 196 197 self.aosp_product_path = _choice(args.aosp_product_path, 198 config.aosp_product_path) 199 200 self.log_file_path = _choice(args.log_file_path, config.log_file_path) 201 202 self.results_file_path = _choice(args.results_file_path, 203 config.results_file_path) 204 205 self.lldb_path = _choice(args.lldb_path, config.lldb_path) 206 self.print_to_stdout = args.print_to_stdout 207 self.verbose = _choice(args.verbose, config.verbose) 208 self.timeout = int(_choice(args.timeout, config.timeout)) 209 self.emu_cmd = _choice(args.emu_cmd, config.emu_cmd) 210 self.run_emu = args.run_emu 211 self.wimpy = args.wimpy 212 self.bundle_types = args.bundle_types if not self.wimpy else ['java'] 213 self.fail_fast = args.fail_fast 214 215 # validate the param "verbose" 216 if not isinstance(self.verbose, bool): 217 raise TestSuiteException('The parameter "verbose" should be a ' 218 'boolean: {0}'.format(self.verbose)) 219 220 # create result array 221 self.results = dict() 222 self.single_test = args.test 223 224 # initialise the logging facility 225 log_level = logging.INFO if not self.verbose else logging.DEBUG 226 util_log.initialise("driver", 227 print_to_stdout=self.print_to_stdout, 228 level=log_level, 229 file_mode='w', # open for write 230 file_path=self.log_file_path 231 ) 232 log = util_log.get_logger() 233 234 if self.run_emu and not self.emu_cmd: 235 log.TestSuiteException('Need to specify --emu-cmd (or specify a' 236 ' value in the config file) if using --run-emu.') 237 238 # create a results file 239 self.results_file = open(self.results_file_path, 'w') 240 241 # create an android helper object 242 self.android = UtilAndroid(self.adb_path, 243 self.lldb_server_path_device, 244 self.device) 245 assert self.android 246 247 # create a test bundle 248 self.bundle = UtilBundle(self.android, 249 self.aosp_product_path) 250 assert self.bundle 251 252 # save the no pushing option 253 assert isinstance(args.noinstall, bool) 254 self.noinstall = args.noinstall 255 256 assert isinstance(args.nouninstall, bool) 257 self.nouninstall = args.nouninstall 258 259 # install only option 260 assert type(args.install_only) is bool 261 self.install_only = args.install_only 262 if self.install_only: 263 log.log_and_print('Option --install-only set. The test APKs will ' 264 'be installed on the device but the tests will ' 265 'not be executed.') 266 if self.noinstall: 267 raise TestSuiteException('Conflicting options given: ' 268 '--install-only and --no-install') 269 270 # TCP port modifier which is used to increment the port number used for 271 # each test case to avoid collisions. 272 self.port_mod = 0 273 274 # total number of test files that have been executed 275 self.test_count = 0 276 277 def get_android(self): 278 '''Return the android ADB helper instance. 279 280 Returns: 281 The android ADB helper, instance of UtilAndroid. 282 ''' 283 assert self.android 284 return self.android 285 286 def get_bundle(self): 287 '''Return the test executable bundle. 288 289 Returns: 290 The test exectable collection, instance of UtilBundle. 291 ''' 292 return self.bundle 293 294 def add_result(self, name, app_type, result): 295 '''Add a test result to the collection. 296 297 Args: 298 name: String name of the test that has executed. 299 app_type: type of app i.e. java, jni, or cpp 300 result: String result of the test, "pass", "fail", "error". 301 ''' 302 key = (name, app_type) 303 assert key not in self.results 304 self.results[key] = result 305 306 def get_single_test(self): 307 '''Get the name of the single test to run. 308 309 Returns: 310 A string that is the name of the python file containing the test to 311 be run. If all tests are to be run this returns None. 312 ''' 313 return self.single_test 314 315 @staticmethod 316 def load_user_configuration(path): 317 '''Load the test suite config from the give path. 318 319 Instantiate the Config class found in the module at the given path. 320 If no suitable class is available, it raises a TestSuiteException. 321 322 Args: 323 path: String location of the module. 324 325 Returns: 326 an instance of the Config class, defined in the module. 327 328 Raises: 329 TestSuiteException: when unable to import the module or when a 330 subclass of Config is not found inside it. 331 ''' 332 333 # load the module 334 config_module = load_py_module(path) 335 if not config_module: 336 raise TestSuiteException('Unable to import the module from "%s"' 337 % (path)) 338 339 # look for a subclass of Config 340 for name, value in inspect.getmembers(config_module): 341 if (inspect.isclass(value) 342 and name != 'Config' 343 and issubclass(value, Config)): 344 # that's our candidate 345 return value() 346 347 # otherwise there are no valid candidates 348 raise TestSuiteException('The provided user configuration is not ' 349 'valid. The module must define a subclass ' 350 'of Config') 351 352 353def _kill_emulator(): 354 ''' Kill the emulator process. ''' 355 global EMU_PROC 356 if EMU_PROC: 357 try: 358 EMU_PROC.terminate() 359 except OSError: 360 # can't kill a dead proc 361 log = util_log.get_logger() 362 log.debug('Trying to kill an emulator but it is already dead.') 363 364 365def _check_emulator_terminated(): 366 ''' Throw an exception if the emulator process has ended. 367 368 Raises: 369 TestSuiteException: If the emulator process has ended. 370 ''' 371 global EMU_PROC 372 assert EMU_PROC 373 if EMU_PROC.poll(): 374 stdout, stderr = EMU_PROC.communicate() 375 raise TestSuiteException('The emulator terminated with output:' 376 '\nstderr: {0}\nstdout: {1}.'.format(stderr, stdout)) 377 378 379@deprecated() 380def _launch_emulator(state): 381 '''Launch the emulator and wait for it to boot. 382 383 Args: 384 emu_cmd: The command line to run the emulator. 385 386 Raises: 387 TestSuiteException: If an emulator already exists or the emulator 388 process terminated before we could connect to it, or 389 we failed to copy lldb-server to the emulator. 390 ''' 391 global EMU_PROC 392 android = state.android 393 if state.user_specified_device: 394 if android.device_with_substring_exists(state.user_specified_device): 395 raise TestSuiteException( 396 'A device with name {0} already exists.', 397 state.user_specified_device) 398 else: 399 if android.device_with_substring_exists('emulator'): 400 raise TestSuiteException('An emulator already exists.') 401 402 assert state.emu_cmd 403 EMU_PROC = subprocess.Popen(state.emu_cmd.split(), 404 stdout=None, 405 stderr=subprocess.STDOUT) 406 407 log = util_log.get_logger() 408 log.info('Launching emulator with command line {0}'.format(state.emu_cmd)) 409 410 tries_number = 180 411 tries = tries_number 412 found_device = False 413 while not found_device: 414 try: 415 android.validate_device(False, 'emulator') 416 found_device = True 417 except TestSuiteException as ex: 418 tries -= 1 419 if tries == 0: 420 # Avoid infinitely looping if the emulator won't boot 421 log.warning( 422 'Giving up trying to validate device after {0} tries.' 423 .format(tries_number)) 424 raise ex 425 _check_emulator_terminated() 426 # wait a bit and try again, maybe it has now booted 427 time.sleep(10) 428 429 tries = 500 430 while not android.is_booted(): 431 tries -= 1 432 if tries == 0: 433 # Avoid infinitely looping if the emulator won't boot 434 raise TestSuiteException('The emulator has failed to boot.') 435 _check_emulator_terminated() 436 time.sleep(5) 437 438 # Need to be root before we can push lldb-server 439 android.adb_root() 440 android.wait_for_device() 441 442 # Push the lldb-server executable to the device. 443 output = android.adb('push {0} {1}'.format(state.lldb_server_path_host, 444 state.lldb_server_path_device)) 445 446 if 'failed to copy' in output or 'No such file or directory' in output: 447 raise TestSuiteException( 448 'unable to push lldb-server to the emulator: {0}.' 449 .format(output)) 450 451 output = android.shell('chmod a+x {0}' 452 .format(state.lldb_server_path_device)) 453 454 if 'No such file or directory' in output: 455 raise TestSuiteException('Failed to copy lldb-server to the emulator.') 456 457 458def _restart_emulator(state): 459 '''Kill the emulator and start a new instance. 460 461 Args: 462 state: Test suite state collection, instance of State. 463 ''' 464 _kill_emulator() 465 _launch_emulator(state) 466 467 468def _run_test(state, name, bundle_type): 469 '''Execute a single test case. 470 471 Args: 472 state: Test suite state collection, instance of State. 473 name: String file name of the test to execute. 474 bundle_type: string for the installed app type (cpp|jni|java) 475 476 Raises: 477 AssertionError: When assertion fails. 478 ''' 479 assert isinstance(name, str) 480 481 try: 482 state.android.check_adb_alive() 483 except TestSuiteException as expt: 484 global EMU_PROC 485 if EMU_PROC: 486 _restart_emulator(state) 487 else: 488 raise expt 489 490 log = util_log.get_logger() 491 sys.stdout.write('Running {0}\r'.format(name)) 492 sys.stdout.flush() 493 log.info('Running {0}'.format(name)) 494 495 run_tests_dir = os.path.dirname(os.path.realpath(__file__)) 496 run_test_path = os.path.join(run_tests_dir, 'tests', 'run_test.py') 497 498 # Forward port for lldb-server on the device to our host 499 hport = int(state.host_port) + state.port_mod 500 dport = int(state.device_port) + state.port_mod 501 state.android.forward_port(hport, dport) 502 state.port_mod += 1 503 504 log.debug('Giving up control to {0}...'.format(name)) 505 506 params = map(str, [ 507 sys.executable, 508 run_test_path, 509 name, 510 state.log_file_path, 511 state.adb_path, 512 state.lldb_server_path_device, 513 state.aosp_product_path, 514 dport, 515 state.android.get_device_id(), 516 state.print_to_stdout, 517 state.verbose, 518 state.wimpy, 519 state.timeout, 520 bundle_type 521 ]) 522 523 return_code = subprocess.call(params) 524 state.test_count += 1 525 state.android.remove_port_forwarding() 526 log.seek_to_end() 527 528 # report in sys.stdout the result 529 success = return_code == util_constants.RC_TEST_OK 530 status_handlers = collections.defaultdict(lambda: ('error', log.error), ( 531 (util_constants.RC_TEST_OK, ('pass', log.info)), 532 (util_constants.RC_TEST_TIMEOUT, ('timeout', log.error)), 533 (util_constants.RC_TEST_IGNORED, ('ignored', log.info)), 534 (util_constants.RC_TEST_FAIL, ('fail', log.critical)) 535 ) 536 ) 537 status_name, status_logger = status_handlers[return_code] 538 log.info('Running %s: %s', name, status_name.upper()) 539 status_logger("Test %r: %s", name, status_name) 540 541 # Special case for ignored tests - just return now 542 if return_code == util_constants.RC_TEST_IGNORED: 543 return 544 545 state.add_result(name, bundle_type, status_name) 546 547 if state.fail_fast and not success: 548 raise FailFastException(name) 549 550 # print a running total pass rate 551 passes = sum(1 for key, value in state.results.items() if value == 'pass') 552 log.info('Current pass rate: %s of %s executed.', passes, len(state.results)) 553 554 555def _check_lldbserver_exists(state): 556 '''Check lldb-server exists on the target device and it is executable. 557 558 Raises: 559 TestSuiteError: If lldb-server does not exist on the target. 560 ''' 561 assert state 562 563 message = 'Unable to verify valid lldb-server on target' 564 565 android = state.get_android() 566 assert android 567 568 cmd = state.lldb_server_path_device 569 out = android.shell(cmd, False) 570 if not isinstance(out, str): 571 raise TestSuiteException(message) 572 if out.find('Usage:') < 0: 573 raise TestSuiteException(message) 574 575 576def _suite_pre_run(state): 577 '''This function is executed before the test cases are run (setup). 578 579 Args: 580 state: Test suite state collection, instance of State. 581 582 Return: 583 True if the pre_run step completes without error. 584 Checks made: 585 - Validating that adb exists and runs. 586 - Validating that a device is attached. 587 - We have root access to the device. 588 - All test binaries were pushed to the device. 589 - The port for lldb-server was forwarded correctly. 590 591 Raises: 592 AssertionError: When assertions fail. 593 ''' 594 assert state 595 log = util_log.get_logger() 596 597 try: 598 android = state.get_android() 599 bundle = state.get_bundle() 600 assert android 601 assert bundle 602 603 # validate ADB helper class 604 android.validate_adb() 605 log.log_and_print('Located ADB') 606 607 if state.run_emu: 608 log.log_and_print('Launching emulator...') 609 _launch_emulator(state) 610 log.log_and_print('Started emulator ' + android.device) 611 else: 612 android.validate_device() 613 log.log_and_print('Located device ' + android.device) 614 615 if state.noinstall and not state.single_test: 616 bundle.check_apps_installed(state.wimpy) 617 618 # elevate to root user 619 android.adb_root() 620 android.wait_for_device() 621 # check that lldb-server exists on device 622 android.kill_servers() 623 _check_lldbserver_exists(state) 624 625 if not state.noinstall: 626 # push all tests to the device 627 log.log_and_print('Pushing all tests...') 628 bundle.push_all() 629 log.log_and_print('Pushed all tests') 630 log.log_and_print('Pre run complete') 631 632 except TestSuiteException as expt: 633 log.exception('Test suite pre run failure') 634 635 # Even if we are logging the error, it may be helpful and more 636 # immediate to find out the error into the terminal 637 log.log_and_print('ERROR: Unable to set up the test suite: %s\n' 638 % expt.message, logging.ERROR) 639 640 return False 641 return True 642 643 644def _suite_post_run(state): 645 '''This function is executed after the test cases have run (teardown). 646 647 Args: 648 state: Test suite state collection, instance of State. 649 Returns: 650 Number of failures 651 ''' 652 log = util_log.get_logger() 653 654 if not state.noinstall and not state.nouninstall: 655 if state.wimpy: 656 state.bundle.uninstall_all_apk() 657 else: 658 state.bundle.uninstall_all() 659 log.log_and_print('Uninstalled/Deleted all tests') 660 661 total = 0 662 passes = 0 663 failures = 0 664 665 results = ET.Element('testsuite') 666 results.attrib['name'] = 'LLDB RS Test Suite' 667 668 for key, value in state.results.items(): 669 total += 1 670 if value == 'pass': 671 passes += 1 672 else: 673 failures += 1 674 675 # test case name, followed by pass, failure or error elements 676 testcase = ET.Element('testcase') 677 testcase.attrib['name'] = "%s:%s" % key 678 result_element = ET.Element(value) 679 result_element.text = "%s:%s" % key 680 testcase.append(result_element) 681 results.append(testcase) 682 683 assert passes + failures == total, 'Invalid test results status' 684 if failures: 685 log.log_and_print( 686 'The following failures occurred:\n%s\n' % 687 '\n'.join('failed: %s:%s' % test_spec 688 for test_spec, result in state.results.items() if result != 'pass' 689 )) 690 691 log.log_and_print('{0} of {1} passed'.format(passes, total)) 692 if total: 693 log.log_and_print('{0}% rate'.format((passes*100)/total)) 694 695 results.attrib['tests'] = str(total) 696 state.results_file.write(ET.tostring(results, encoding='iso-8859-1')) 697 698 return failures 699 700 701def _discover_tests(state): 702 '''Discover all tests in the tests directory. 703 704 Returns: 705 List of strings, test file names from the 'tests' directory. 706 ''' 707 tests = [] 708 709 single_test = state.get_single_test() 710 if single_test is None: 711 file_dir = os.path.dirname(os.path.realpath(__file__)) 712 tests_dir = os.path.join(file_dir, 'tests') 713 714 for sub_dir in os.listdir(tests_dir): 715 current_test_dir = os.path.join(tests_dir, sub_dir) 716 if os.path.isdir(current_test_dir): 717 dir_name = os.path.basename(current_test_dir) 718 719 if dir_name == 'harness': 720 continue 721 722 for item in os.listdir(current_test_dir): 723 if (item.startswith('test') 724 and item.endswith('.py') 725 and not item in state.blocklist): 726 tests.append(item) 727 else: 728 if single_test.endswith('.py'): 729 tests.append(single_test) 730 else: 731 tests.append(single_test + '.py') 732 733 return tests 734 735 736def _deduce_python_path(state): 737 '''Try to deduce the PYTHONPATH environment variable via the LLDB binary. 738 739 Args: 740 state: Test suite state collection, instance of State. 741 742 Returns: 743 True if PYTHONPATH has been updated, False otherwise. 744 745 Raises: 746 TestSuiteException: If lldb path provided in the config or command line 747 is incorrect. 748 AssertionError: If an assertion fails. 749 ''' 750 751 lldb_path = state.lldb_path 752 if not lldb_path: 753 # lldb may not be provided in preference of a manual $PYTHONPATH 754 return False 755 756 params = [lldb_path, '-P'] 757 758 try: 759 proc = subprocess.Popen(params, stdout=subprocess.PIPE) 760 except OSError as err: 761 error_string = 'Could not run lldb at %s: %s' % (lldb_path, str(err)) 762 raise TestSuiteException(error_string) 763 764 stdout = proc.communicate()[0] 765 if stdout: 766 os.environ['PYTHONPATH'] = stdout.strip() 767 return True 768 769 return False 770 771 772def main(): 773 '''The lldb-renderscript test suite entry point.''' 774 log = None 775 776 try: 777 # parse the command line 778 state = State() 779 assert state 780 781 # logging is initialised in State() 782 log = util_log.get_logger() 783 784 # if we can, set PYTHONPATH for lldb bindings 785 if not _deduce_python_path(state): 786 log.log_and_print('Unable to deduce PYTHONPATH', logging.WARN) 787 788 # pre run step 789 if not _suite_pre_run(state): 790 raise TestSuiteException('Test suite pre-run step failed') 791 # discover all tests and execute them 792 tests = _discover_tests(state) 793 log.log_and_print('Found {0} tests'.format(len(tests))) 794 if state.install_only: 795 log.log_and_print('Test applications installed. Terminating due to ' 796 '--install-only option') 797 else: 798 # run the tests 799 for bundle_type in state.bundle_types: 800 log.info("Running bundle type '%s'", bundle_type) 801 for item in tests: 802 _run_test(state, item, bundle_type) 803 # post run step 804 quit(0 if _suite_post_run(state) == 0 else 1) 805 806 except AssertionError: 807 if log: 808 log.exception('Internal test suite error') 809 810 print('Internal test suite error') 811 quit(1) 812 813 except FailFastException: 814 log.exception('Early exit after first test failure') 815 quit(1) 816 817 except TestSuiteException as error: 818 if log: 819 log.exception('Test suite exception') 820 821 print('{0}'.format(str(error))) 822 quit(2) 823 824 finally: 825 _kill_emulator() 826 logging.shutdown() 827 828def signal_handler(_, _unused): 829 '''Signal handler for SIGINT, caused by the user typing Ctrl-C.''' 830 # pylint: disable=unused-argument 831 # pylint: disable=protected-access 832 print('Ctrl+C!') 833 os._exit(1) 834 835 836# execution trampoline 837if __name__ == '__main__': 838 signal.signal(signal.SIGINT, signal_handler) 839 main() 840