1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright (c) 2021-2024 Huawei Device Co., Ltd. 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import argparse 17import os 18import re 19import sys 20import traceback 21from clade import Clade 22from clang.cindex import Index 23from clang.cindex import TokenKind 24from clang.cindex import CursorKind 25from clang.cindex import TranslationUnitLoadError 26import yamale 27import yaml 28 29BINDINGS_FILE_NAME = "bindings.yml" 30 31# File describing structure of bindings.yml files and other restrictions on their content 32# This file is used to validate bindings.yml with yamale module 33BINDINGS_SCHEMA_FILE = '%s/%s' % (os.path.dirname(os.path.realpath(__file__)), "bindings_schema.yml") 34 35CPP_CODE_TO_GET_TEST_ID_IN_PANDA = "IntrusiveTest::GetId()" 36CPP_HEADER_COMPILE_OPTION_LIST = ["-x", "c++-header"] 37CLANG_CUR_WORKING_DIR_OPT = "-working-directory" 38HEADER_FILE_EXTENSION = ".h" 39 40 41# Parser of arguments and options 42class ArgsParser: 43 44 def __init__(self): 45 parser = argparse.ArgumentParser(description="Source code instrumentation for intrusive testing") 46 parser.add_argument( 47 'src_dir', 48 metavar='SRC_DIR', 49 nargs=1, 50 type=str, 51 help="Directory with source code files for instrumentation") 52 53 parser.add_argument( 54 'clade_file', 55 metavar='CLADE_FILE', 56 nargs=1, 57 type=str, 58 help="File with build commands intercepted by 'clade' tool") 59 60 parser.add_argument( 61 'test_suite_dir', 62 metavar='TEST_SUITE_DIR', 63 nargs=1, 64 type=str, 65 help="Directory of intrusive test suite") 66 67 self.__args = parser.parse_args() 68 self.__src_dir = None 69 self.__clade_file = None 70 self.__test_suite_dir = None 71 72 # raise: FatalError 73 def get_src_dir(self): 74 if(self.__src_dir is None): 75 self.__src_dir = os.path.abspath(self.__args.src_dir[0]) 76 if(not os.path.isdir(self.__src_dir)): 77 err_msg = "%s argument %s is not an existing directory: %s"\ 78 % ("1st", "SRC_DIR", self.__args.src_dir[0]) 79 raise FatalError(err_msg) 80 return self.__src_dir 81 82 # raise: FatalError 83 def get_clade_file(self): 84 if(self.__clade_file is None): 85 self.__clade_file = os.path.abspath(self.__args.clade_file[0]) 86 if(not os.path.isfile(self.__clade_file)): 87 err_msg = "%s argument %s is not an existing file: %s"\ 88 % ("2nd", "CLADE_FIlE", self.__args.clade_file[0]) 89 raise FatalError(err_msg) 90 return self.__clade_file 91 92 # raise: FatalError 93 def get_test_suite_dir(self): 94 if(self.__test_suite_dir is None): 95 self.__test_suite_dir = os.path.abspath(self.__args.test_suite_dir[0]) 96 if(not os.path.isdir(self.__test_suite_dir)): 97 err_msg = "%s argument %s is not an existing directory: %s"\ 98 % ("3d", "TEST_SUITE_DIR", self.__args.test_suite_dir[0]) 99 raise FatalError(err_msg) 100 return self.__test_suite_dir 101 102 103# User defined exception class 104class FatalError(Exception): 105 106 def __init__(self, msg, *args): 107 super().__init__(args) 108 self.__msg = msg 109 110 def __str__(self): 111 return self.get_err_msg() 112 113 def get_err_msg(self): 114 return self.__msg 115 116 117# Class for auxillary functions 118class Util: 119 120 # raise: OSError 121 @staticmethod 122 def find_files_by_name(directory, file_name, file_set): 123 for name in os.listdir(directory): 124 path = os.path.join(directory, name) 125 if(os.path.isfile(path) and (name == file_name)): 126 file_set.add(path) 127 elif(os.path.isdir(path) and (name != "..") and (name != ".")): 128 Util.find_files_by_name(path, file_name, file_set) 129 130 # raise: OSError 131 @staticmethod 132 def find_files_by_extension(directory, extension, is_recursive, file_set): 133 for name in os.listdir(directory): 134 path = os.path.join(directory, name) 135 if(os.path.isfile(path)): 136 root, ext = os.path.splitext(path) 137 if(ext == extension): 138 file_set.add(path) 139 elif(is_recursive \ 140 and os.path.isdir(path) \ 141 and (name != "..") \ 142 and (name != ".")): 143 Util.find_files_by_extension( 144 path, 145 extension, 146 is_recursive, 147 file_set) 148 149 # raise: OSError 150 @staticmethod 151 def read_file_lines(file_path): 152 with open(file_path, "r") as fd: 153 lines = fd.readlines() 154 return lines 155 156 # raise: OSError 157 @staticmethod 158 def write_lines_to_file(file_path, lines): 159 fd = os.open(file_path, os.O_RDWR, 0o777) 160 fo = os.fdopen(fd, "w+") 161 try: 162 fo.writelines(lines) 163 finally: 164 fo.close() 165 166 @staticmethod 167 def blank_string_to_none(s): 168 if(s is not None): 169 match = re.match(r'^\s*$', s) 170 if(match): 171 return None 172 return s 173 174 175# Class describing location of a synchronization point 176class SyncPoint: 177 178 def __init__(self, file, index, cl=None, method=None, source=None): 179 self._file = file 180 self._index = index 181 # In case the key is defined but with empty value, we set 'None' constant to encode that the key is not provided 182 self._cl = Util.blank_string_to_none(cl) 183 self._method = Util.blank_string_to_none(method) 184 self._source = Util.blank_string_to_none(source) 185 186 def __hash__(self): 187 return hash((self.file, self.index, self.cl, self.method, self.source)) 188 189 def __eq__(self, other): 190 if not isinstance(other, SyncPoint): 191 return False 192 return (self.file == other.file) \ 193 and (self.index == other.index) \ 194 and (self.cl == other.cl) \ 195 and (self.method == other.method) \ 196 and (self.source == other.source) 197 198 @property 199 def file(self): 200 return self._file 201 202 @property 203 def index(self): 204 return self._index 205 206 @property 207 def cl(self): 208 return self._cl 209 210 @property 211 def method(self): 212 return self._method 213 214 @property 215 def source(self): 216 return self._source 217 218 def has_class(self): 219 return self._cl is not None 220 221 def has_method(self): 222 return self._method is not None 223 224 def has_source(self): 225 return self._source is not None 226 227 228 def to_string(self): 229 s = ["file: %s" % (self.file), "index: %s" % (str(self.index))] 230 if(self.has_class()): 231 s.append("class: %s" % (str(self.cl))) 232 if(self.has_method()): 233 s.append("method: %s" % (str(self.method))) 234 if(self.has_source()): 235 s.append("source: %s" % (str(self.source))) 236 return ", ".join(s) 237 238 239# Class describing a synchronization action (code) 240# of a single test at one synchronization point 241class SyncAction: 242 243 ID = 0 244 245 def __init__(self, code): 246 self._code = code 247 self._id = SyncAction.ID 248 SyncAction.ID += 1 249 250 def __hash__(self): 251 return hash(self._id) 252 253 def __eq__(self, other): 254 if not isinstance(other, SyncAction): 255 return False 256 return self._id == other.id 257 258 @property 259 def code(self): 260 return self._code 261 262 @property 263 def id(self): 264 return self._id 265 266 267# Abstraction for a source code file 268class SourceFile: 269 270 def __init__(self, path): 271 self._path = path 272 273 def __hash__(self): 274 return hash(self._path) 275 276 def __eq__(self, other): 277 if not isinstance(other, SourceFile): 278 return False 279 return self.path == other.path 280 281 @property 282 def path(self): 283 return self._path 284 285 286# Abstraction for a header file, i.e. 287# file included by another source or header file 288class HeaderFile(SourceFile): 289 def __init__(self, path, including_source_file_path): 290 self._including_source_file_path = including_source_file_path 291 super().__init__(path) 292 293 def __hash__(self): 294 return hash((self.path, self._including_source_file_path)) 295 296 def __eq__(self, other): 297 if not isinstance(other, HeaderFile): 298 return False 299 return super().__eq__(other) \ 300 and (self.including_source_file_path == other.including_source_file_path) 301 302 @property 303 def including_source_file_path(self): 304 return self._including_source_file_path 305 306 307# Parser of bindings.yml files 308class BindingsFileParser: 309 310 # raise: OSError 311 def __init__(self, file): 312 self.__file = file 313 with open(file, "r") as f: 314 self.__obj = yaml.safe_load(f) 315 316 #raise: yamale.YamaleError 317 def validate(self, schema_file): 318 schema = yamale.make_schema(schema_file) 319 data = yamale.make_data(self.__file) 320 yamale.validate(schema, data) 321 322 # Set of header files with declarations referenced in the code 323 # of synchronization actions, e.g. function calls, constants 324 # raise: FatalError 325 def get_declaration_set(self, test_dir): 326 l = self.__obj["declaration"].split(",") 327 i = 0 328 while(i < len(l)): 329 l[i] = l[i].lstrip().rstrip() 330 abs_path = os.path.join(test_dir, l[i]) 331 if(not os.path.isfile(abs_path)): 332 err_msg = "[%s] in declaration list from [%s] is not an existing file"\ 333 % (l[i], self.__file) 334 raise FatalError(err_msg) 335 l[i] = abs_path 336 i += 1 337 return set(l) 338 339 # raise: FatalError 340 def get_mapping(self, src_dir): 341 m = {} 342 l = self.__obj["mapping"] 343 for e in l: 344 f = e.get("file") 345 abs_f = os.path.join(src_dir, f) 346 if(not os.path.isfile(abs_f)): 347 err_msg = "file attribute [%s] from [%s] is not an existing file"\ 348 % (f, self.__file) 349 raise FatalError(err_msg) 350 351 s = e.get("source") 352 if(s is None): 353 abs_s = None 354 else: 355 abs_s = os.path.join(src_dir, s) 356 if(not os.path.isfile(abs_s)): 357 err_msg = "source attribute [%s] from [%s] is not an existing file"\ 358 % (s, self.__file) 359 raise FatalError(err_msg) 360 361 ref = SyncPoint( 362 file=abs_f, 363 index=e.get("index"), 364 cl=e.get("class"), 365 method=e.get("method"), 366 source=abs_s) 367 m[ref] = SyncAction(e.get("code")) 368 return m 369 370 371# Class implementing all instrumentation logic (algorithm) 372class Instrumentator: 373 374 def __init__(self, args_parser): 375 # arguments parser 376 self.__args_parser = args_parser 377 378 # set of bindings.yml files 379 self.__bindings_file_set = set() 380 381 # dictionary from synchronization action 382 # to set of header files declaring functions, 383 # constants and etc. used in that synchronization 384 # action 385 self.__sync_action_to_declaration_set = {} 386 387 # dictionary from synchronization action 388 # to test identifier 389 self.__sync_action_to_test_id = {} 390 391 # dictionary from synchronization point 392 # to list of synchronization actions for that point 393 self.__sync_point_to_sync_action_list = {} 394 395 # dictionary from synchronization point to 396 # line number in which the synchronization comment 397 # for that synchronization point finishes 398 self.__sync_point_to_line = {} 399 400 # dictionary from file to set of synchronization 401 # points from that file 402 self.__file_to_sync_point_set = {} 403 404 # dictionary from file to list of compile options 405 # used to parse that file 406 self.__file_to_compile_option_list = {} 407 408 409 def run(self): 410 Util.find_files_by_name( 411 self.__args_parser.get_test_suite_dir(), 412 BINDINGS_FILE_NAME, 413 self.__bindings_file_set) 414 # go over all bindings.yml files, parse them and fill data structures 415 self.__bindings_file_set_work() 416 self.__lookup_compile_opts_in_clade_db() 417 framework_header_file_set = set() 418 Util.find_files_by_extension( 419 self.__args_parser.get_test_suite_dir(), 420 HEADER_FILE_EXTENSION, 421 False, 422 framework_header_file_set) 423 424 # Instrumentator includes all headers from runtime/tests/intrusive-tests directory 425 # in all places where code is needed to be synced (according to bindings.yml). 426 # That is why it includes intrusive_test_option.h file, which contains RuntimeOptions, 427 # so framework cannot be used in libpandabase, compiler and etc, but actually 428 # intrusive_test_option.h is only needed to initialize testsuite (see Runtime::Create) 429 for header in framework_header_file_set: 430 if "intrusive_test_option.h" in header: 431 framework_header_file_set.remove(header) 432 break 433 434 # go over map elements (from instrumented file to sync points in that file) 435 for file in self.__file_to_sync_point_set: 436 # find line numbers of synchronization points 437 # in the source code and header files 438 self.__lookup_sync_points_with_libclang(file) 439 lines = Util.read_file_lines(file.path) 440 try: 441 sync_point_list = list(self.__file_to_sync_point_set[file]) 442 except KeyError: 443 print(f"no key {file}") 444 sys.exit(-1) 445 try: 446 sync_point_list.sort(key=lambda sp_ref : self.__sync_point_to_line[sp_ref]) 447 except KeyError: 448 print(f"no keys in {self.__sync_point_to_line}") 449 sys.exit(-1) 450 451 # Header files that should be '#included' in the instrumented file 452 header_file_set = set(framework_header_file_set) 453 self.__add_changes_to_files(header_file_set, sync_point_list, lines) 454 incl_text_list = [] 455 for header in header_file_set: 456 incl_text_list.append("#include \"") 457 incl_text_list.append(header) 458 incl_text_list.append("\"\n") 459 lines.insert(0, ''.join(incl_text_list)) 460 Util.write_lines_to_file(file.path, lines) 461 462 def __add_changes_to_files(self, header_file_set, sync_point_list, lines): 463 sync_point_idx = 0 464 # go over sync points in a file 465 while(sync_point_idx < len(sync_point_list)): 466 try: 467 sp_ref = sync_point_list[sync_point_idx] 468 except KeyError: 469 print(f"no key {sync_point_idx}") 470 sys.exit(-1) 471 sync_point_code = [] 472 # go over synchronization actions for a synchronization point 473 # and insert source code of the synchronization actions into 474 # file lines 475 476 try: 477 sync_action_list = self.__sync_point_to_sync_action_list[sp_ref] 478 except KeyError: 479 print(f"no key {sp_ref}") 480 sys.exit(-1) 481 482 for sync_action in sync_action_list: 483 self.__rep_sync_actions_process(sp_ref, sync_point_code, sync_action, header_file_set) 484 sync_point_code.insert(0, "#if defined(INTRUSIVE_TESTING)\n") 485 sync_point_code.append("#endif\n") 486 487 try: 488 lines.insert( 489 self.__sync_point_to_line[sp_ref] - 1 + sync_point_idx, 490 ''.join(sync_point_code)) 491 except KeyError: 492 print(f"no key {sp_ref}") 493 sys.exit(-1) 494 495 sync_point_idx += 1 496 497 def __rep_sync_actions_process(self, sp_ref, sync_point_code, sync_action, header_file_set): 498 if(sp_ref.has_method()): 499 if(len(sync_point_code) > 0): 500 sync_point_code.append("\nelse ") 501 sync_point_code.append("if(") 502 sync_point_code.append(CPP_CODE_TO_GET_TEST_ID_IN_PANDA) 503 sync_point_code.append(" == (uint32_t)") 504 try: 505 sync_point_code.append(self.__sync_action_to_test_id[sync_action]) 506 except KeyError: 507 print(f"no key {sync_action}") 508 sys.exit(-1) 509 sync_point_code.append("){\n") 510 sync_point_code.append(sync_action.code) 511 if(sp_ref.has_method()): 512 sync_point_code.append("\n}") 513 sync_point_code.append("\n") 514 try: 515 header_file_set.update(self.__sync_action_to_declaration_set[sync_action]) 516 except KeyError: 517 print(f"no key {sync_action}") 518 sys.exit(-1) 519 520 def __bindings_file_set_work(self): 521 for bindings_file in self.__bindings_file_set: 522 bindings_parser = BindingsFileParser(bindings_file) 523 bindings_parser.validate(BINDINGS_SCHEMA_FILE) 524 test_dir = self.__get_test_dir(bindings_file) 525 test_id = self.__get_test_id(test_dir) 526 declaration_set = bindings_parser.get_declaration_set(test_dir) 527 mapping = bindings_parser.get_mapping(self.__args_parser.get_src_dir()) 528 # go over mapping from sync point to sync action in a single bindings.yml 529 for r in mapping: 530 a = mapping[r] 531 self.__sync_action_to_declaration_set[a] = declaration_set 532 self.__sync_action_to_test_id[a] = test_id 533 if(r not in self.__sync_point_to_sync_action_list): 534 self.__sync_point_to_sync_action_list[r] = [] 535 self.__sync_point_to_sync_action_list[r].append(a) 536 if(r.has_source()): 537 f = HeaderFile(r.file, r.source) 538 else: 539 f = SourceFile(r.file) 540 if(f not in self.__file_to_sync_point_set): 541 self.__file_to_sync_point_set[f] = set() 542 self.__file_to_sync_point_set[f].add(r) 543 544 def __get_test_dir(self, bindings_file): 545 return os.path.dirname(bindings_file) 546 547 def __get_test_id(self, test_dir): 548 return os.path.basename(test_dir).upper() 549 550 # parse clade database file and find compilation options 551 # for source code modules 552 def __lookup_compile_opts_in_clade_db(self): 553 clade_cmds_file = self.__args_parser.get_clade_file() 554 c = Clade(work_dir=os.path.dirname(clade_cmds_file), cmds_file=clade_cmds_file) 555 f_list = list(self.__file_to_sync_point_set.keys()) 556 f_list.sort(key=lambda f : f.including_source_file_path if isinstance(f, HeaderFile) else f.path) 557 # go over types of compilation commands 558 # (emitted by C and C++ compilers, 2 items in the list) 559 for cmd_type in ["CXX", "CC"]: 560 for cmd in c.get_all_cmds_by_type(cmd_type): 561 # go over input files of compilation commands 562 # i.e. parsed files (usually 1 item in the list) 563 for compiled_file_path in cmd["in"]: 564 self.__filter_file(f_list, compiled_file_path, c, cmd) 565 if(len(f_list) > 0): 566 err_msg = ["Failed to find compilation command for source code modules:"] 567 for f in f_list: 568 if isinstance(f, HeaderFile): 569 src_file_path = f.including_source_file_path 570 else: 571 src_file_path = f.path 572 err_msg.append("\n") 573 err_msg.append(src_file_path) 574 raise FatalError(''.join(err_msg)) 575 576 def __is_kind_of_libclang_function(self, kind): 577 return kind == CursorKind.FUNCTION_DECL \ 578 or kind == CursorKind.CXX_METHOD \ 579 or kind == CursorKind.FUNCTION_TEMPLATE \ 580 or kind == CursorKind.CONSTRUCTOR \ 581 or kind == CursorKind.DESTRUCTOR \ 582 or kind == CursorKind.CONVERSION_FUNCTION 583 584 def __is_kind_of_libclang_class(self, kind): 585 return kind == CursorKind.CLASS_DECL \ 586 or kind == CursorKind.CLASS_TEMPLATE \ 587 or kind == CursorKind.CLASS_TEMPLATE_PARTIAL_SPECIALIZATION \ 588 or kind == CursorKind.STRUCT_DECL 589 590 def __filter_file(self, f_list, compiled_file_path, c, cmd): 591 i = 0 592 # go over sorted file List 593 while(i < len(f_list)): 594 f = f_list[i] 595 if isinstance(f, HeaderFile): 596 src_file_path = f.including_source_file_path 597 else: 598 src_file_path = f.path 599 if(compiled_file_path == src_file_path): 600 compile_option_list = c.get_cmd_opts(cmd["id"]) 601 compile_option_list.append('%s=%s' % (CLANG_CUR_WORKING_DIR_OPT, cmd["cwd"])) 602 if isinstance(f, HeaderFile): 603 compile_option_list.extend(CPP_HEADER_COMPILE_OPTION_LIST) 604 self.__file_to_compile_option_list[f] = compile_option_list 605 f_list.pop(i) 606 elif(compiled_file_path < src_file_path): 607 break 608 else: 609 i += 1 610 611 # Find line numbers of synchronization points 612 # in source code modules and header files 613 def __lookup_sync_points_with_libclang(self, file): 614 try: 615 not_found = list(self.__file_to_sync_point_set[file]) 616 except KeyError: 617 print(f"no key {file}") 618 sys.exit(-1) 619 620 candidates = [] 621 622 clang_index = Index.create() 623 try: 624 tu = clang_index.parse(file.path, args=self.__file_to_compile_option_list[file]) 625 except KeyError: 626 print(f"no key {file}") 627 sys.exit(-1) 628 for token in tu.get_tokens(extent=tu.cursor.extent): 629 if(token.kind != TokenKind.COMMENT): 630 continue 631 match = re.match(r'\s*/\*\s*@sync\s+([0-9]+).*', token.spelling) 632 if(not match): 633 continue 634 635 candidates.clear() 636 candidates.extend(not_found) 637 638 self.__clear_candidates_by_match(candidates, match) 639 self.__clear_candidates_by_token(candidates, token) 640 641 for sync_point in candidates: 642 self.__sync_point_to_line[sync_point] = token.extent.end.line + 1 643 not_found.remove(sync_point) 644 645 if(len(not_found) > 0): 646 err_msg = ["Failed to find synchronization points:"] 647 for sync_point in not_found: 648 err_msg.append("\n") 649 err_msg.append(sync_point.to_string()) 650 raise FatalError(''.join(err_msg)) 651 652 def __clear_candidates_by_match(self, candidates, match): 653 i = 0 654 while(i < len(candidates)): 655 if(int(match.group(1)) != candidates[i].index): 656 candidates.pop(i) 657 else: 658 i += 1 659 660 def __clear_candidates_by_token(self, candidates, token): 661 i = 0 662 while(i < len(candidates)): 663 need_continue = False 664 if(candidates[i].has_method()): 665 need_continue = self.__check_for_method(candidates, token, i) 666 667 elif(candidates[i].has_class()): 668 if(not(self.__is_kind_of_libclang_class(token.cursor.kind)) \ 669 or (token.cursor.spelling != candidates[i].cl)): 670 candidates.pop(i) 671 continue 672 if(need_continue): 673 continue 674 i += 1 675 676 def __check_for_method(self, candidates, token, i): 677 if(not(hasattr(token.cursor, "semantic_parent"))): 678 candidates.pop(i) 679 return True 680 sem_parent = token.cursor.semantic_parent 681 if(not(self.__is_kind_of_libclang_function(sem_parent.kind)) \ 682 or (sem_parent.spelling != candidates[i].method)): 683 candidates.pop(i) 684 return True 685 686 if(candidates[i].has_class()): 687 if(not(hasattr(sem_parent, "semantic_parent"))): 688 candidates.pop(i) 689 return True 690 sem_grand_parent = sem_parent.semantic_parent 691 if(not(self.__is_kind_of_libclang_class(sem_grand_parent.kind)) \ 692 or (sem_grand_parent.spelling != candidates[i].cl)): 693 candidates.pop(i) 694 return True 695 return False 696 697 698if __name__ == '__main__': 699 err_msg_suffix = "Error! Source code instrumentation failed. Reason:" 700 try: 701 exit_code = 0 702 argument_parser = ArgsParser() 703 instrumentator = Instrumentator(argument_parser) 704 instrumentator.run() 705 except yaml.YAMLError as yaml_err: 706 exit_code = 1 707 print(err_msg_suffix) 708 traceback.print_exc(file=sys.stdout, limit=0) 709 except yamale.YamaleError as yamale_err: 710 exit_code = 2 711 print(err_msg_suffix) 712 for res in yamale_err.results: 713 print("Error validating '%s' against schema '%s'\n\t" % (res.data, res.schema)) 714 for er in res.errors: 715 print('\t%s' % er) 716 except OSError as os_err: 717 exit_code = 3 718 err_message = [os_err.strerror] 719 if(hasattr(os_err, 'filename') and (os_err.filename is not None)): 720 err_message.append(": ") 721 err_message.append(os_err.filename) 722 if(hasattr(os_err, 'filename2') and (os_err.filename2 is not None)): 723 err_message.append(", ") 724 err_message.append(os_err.filename2) 725 print(''.join(err_message)) 726 except FatalError as ferr: 727 exit_code = 4 728 print(err_msg_suffix, ferr.get_err_msg()) 729 except Exception as err: 730 exit_code = 5 731 print(err_msg_suffix) 732 traceback.print_exc(file=sys.stdout) 733 sys.exit(exit_code) 734