1import contextlib 2import faulthandler 3import locale 4import math 5import os.path 6import platform 7import random 8import re 9import shlex 10import signal 11import subprocess 12import sys 13import sysconfig 14import tempfile 15import textwrap 16from collections.abc import Callable, Iterable 17 18from test import support 19from test.support import os_helper 20from test.support import threading_helper 21 22 23# All temporary files and temporary directories created by libregrtest should 24# use TMP_PREFIX so cleanup_temp_dir() can remove them all. 25TMP_PREFIX = 'test_python_' 26WORK_DIR_PREFIX = TMP_PREFIX 27WORKER_WORK_DIR_PREFIX = WORK_DIR_PREFIX + 'worker_' 28 29# bpo-38203: Maximum delay in seconds to exit Python (call Py_Finalize()). 30# Used to protect against threading._shutdown() hang. 31# Must be smaller than buildbot "1200 seconds without output" limit. 32EXIT_TIMEOUT = 120.0 33 34 35ALL_RESOURCES = ('audio', 'curses', 'largefile', 'network', 36 'decimal', 'cpu', 'subprocess', 'urlfetch', 'gui', 'walltime') 37 38# Other resources excluded from --use=all: 39# 40# - extralagefile (ex: test_zipfile64): really too slow to be enabled 41# "by default" 42# - tzdata: while needed to validate fully test_datetime, it makes 43# test_datetime too slow (15-20 min on some buildbots) and so is disabled by 44# default (see bpo-30822). 45RESOURCE_NAMES = ALL_RESOURCES + ('extralargefile', 'tzdata') 46 47 48# Types for types hints 49StrPath = str 50TestName = str 51StrJSON = str 52TestTuple = tuple[TestName, ...] 53TestList = list[TestName] 54# --match and --ignore options: list of patterns 55# ('*' joker character can be used) 56TestFilter = list[tuple[TestName, bool]] 57FilterTuple = tuple[TestName, ...] 58FilterDict = dict[TestName, FilterTuple] 59 60 61def format_duration(seconds): 62 ms = math.ceil(seconds * 1e3) 63 seconds, ms = divmod(ms, 1000) 64 minutes, seconds = divmod(seconds, 60) 65 hours, minutes = divmod(minutes, 60) 66 67 parts = [] 68 if hours: 69 parts.append('%s hour' % hours) 70 if minutes: 71 parts.append('%s min' % minutes) 72 if seconds: 73 if parts: 74 # 2 min 1 sec 75 parts.append('%s sec' % seconds) 76 else: 77 # 1.0 sec 78 parts.append('%.1f sec' % (seconds + ms / 1000)) 79 if not parts: 80 return '%s ms' % ms 81 82 parts = parts[:2] 83 return ' '.join(parts) 84 85 86def strip_py_suffix(names: list[str] | None) -> None: 87 if not names: 88 return 89 for idx, name in enumerate(names): 90 basename, ext = os.path.splitext(name) 91 if ext == '.py': 92 names[idx] = basename 93 94 95def plural(n, singular, plural=None): 96 if n == 1: 97 return singular 98 elif plural is not None: 99 return plural 100 else: 101 return singular + 's' 102 103 104def count(n, word): 105 if n == 1: 106 return f"{n} {word}" 107 else: 108 return f"{n} {word}s" 109 110 111def printlist(x, width=70, indent=4, file=None): 112 """Print the elements of iterable x to stdout. 113 114 Optional arg width (default 70) is the maximum line length. 115 Optional arg indent (default 4) is the number of blanks with which to 116 begin each line. 117 """ 118 119 blanks = ' ' * indent 120 # Print the sorted list: 'x' may be a '--random' list or a set() 121 print(textwrap.fill(' '.join(str(elt) for elt in sorted(x)), width, 122 initial_indent=blanks, subsequent_indent=blanks), 123 file=file) 124 125 126def print_warning(msg): 127 support.print_warning(msg) 128 129 130orig_unraisablehook = None 131 132 133def regrtest_unraisable_hook(unraisable): 134 global orig_unraisablehook 135 support.environment_altered = True 136 support.print_warning("Unraisable exception") 137 old_stderr = sys.stderr 138 try: 139 support.flush_std_streams() 140 sys.stderr = support.print_warning.orig_stderr 141 orig_unraisablehook(unraisable) 142 sys.stderr.flush() 143 finally: 144 sys.stderr = old_stderr 145 146 147def setup_unraisable_hook(): 148 global orig_unraisablehook 149 orig_unraisablehook = sys.unraisablehook 150 sys.unraisablehook = regrtest_unraisable_hook 151 152 153orig_threading_excepthook = None 154 155 156def regrtest_threading_excepthook(args): 157 global orig_threading_excepthook 158 support.environment_altered = True 159 support.print_warning(f"Uncaught thread exception: {args.exc_type.__name__}") 160 old_stderr = sys.stderr 161 try: 162 support.flush_std_streams() 163 sys.stderr = support.print_warning.orig_stderr 164 orig_threading_excepthook(args) 165 sys.stderr.flush() 166 finally: 167 sys.stderr = old_stderr 168 169 170def setup_threading_excepthook(): 171 global orig_threading_excepthook 172 import threading 173 orig_threading_excepthook = threading.excepthook 174 threading.excepthook = regrtest_threading_excepthook 175 176 177def clear_caches(): 178 # Clear the warnings registry, so they can be displayed again 179 for mod in sys.modules.values(): 180 if hasattr(mod, '__warningregistry__'): 181 del mod.__warningregistry__ 182 183 # Flush standard output, so that buffered data is sent to the OS and 184 # associated Python objects are reclaimed. 185 for stream in (sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__): 186 if stream is not None: 187 stream.flush() 188 189 try: 190 re = sys.modules['re'] 191 except KeyError: 192 pass 193 else: 194 re.purge() 195 196 try: 197 _strptime = sys.modules['_strptime'] 198 except KeyError: 199 pass 200 else: 201 _strptime._regex_cache.clear() 202 203 try: 204 urllib_parse = sys.modules['urllib.parse'] 205 except KeyError: 206 pass 207 else: 208 urllib_parse.clear_cache() 209 210 try: 211 urllib_request = sys.modules['urllib.request'] 212 except KeyError: 213 pass 214 else: 215 urllib_request.urlcleanup() 216 217 try: 218 linecache = sys.modules['linecache'] 219 except KeyError: 220 pass 221 else: 222 linecache.clearcache() 223 224 try: 225 mimetypes = sys.modules['mimetypes'] 226 except KeyError: 227 pass 228 else: 229 mimetypes._default_mime_types() 230 231 try: 232 filecmp = sys.modules['filecmp'] 233 except KeyError: 234 pass 235 else: 236 filecmp._cache.clear() 237 238 try: 239 struct = sys.modules['struct'] 240 except KeyError: 241 pass 242 else: 243 struct._clearcache() 244 245 try: 246 doctest = sys.modules['doctest'] 247 except KeyError: 248 pass 249 else: 250 doctest.master = None 251 252 try: 253 ctypes = sys.modules['ctypes'] 254 except KeyError: 255 pass 256 else: 257 ctypes._reset_cache() 258 259 try: 260 typing = sys.modules['typing'] 261 except KeyError: 262 pass 263 else: 264 for f in typing._cleanups: 265 f() 266 267 import inspect 268 abs_classes = filter(inspect.isabstract, typing.__dict__.values()) 269 for abc in abs_classes: 270 for obj in abc.__subclasses__() + [abc]: 271 obj._abc_caches_clear() 272 273 try: 274 fractions = sys.modules['fractions'] 275 except KeyError: 276 pass 277 else: 278 fractions._hash_algorithm.cache_clear() 279 280 try: 281 inspect = sys.modules['inspect'] 282 except KeyError: 283 pass 284 else: 285 inspect._shadowed_dict_from_weakref_mro_tuple.cache_clear() 286 inspect._filesbymodname.clear() 287 inspect.modulesbyfile.clear() 288 289 try: 290 importlib_metadata = sys.modules['importlib.metadata'] 291 except KeyError: 292 pass 293 else: 294 importlib_metadata.FastPath.__new__.cache_clear() 295 296 297def get_build_info(): 298 # Get most important configure and build options as a list of strings. 299 # Example: ['debug', 'ASAN+MSAN'] or ['release', 'LTO+PGO']. 300 301 config_args = sysconfig.get_config_var('CONFIG_ARGS') or '' 302 cflags = sysconfig.get_config_var('PY_CFLAGS') or '' 303 cflags += ' ' + (sysconfig.get_config_var('PY_CFLAGS_NODIST') or '') 304 ldflags_nodist = sysconfig.get_config_var('PY_LDFLAGS_NODIST') or '' 305 306 build = [] 307 308 # --disable-gil 309 if sysconfig.get_config_var('Py_GIL_DISABLED'): 310 if not sys.flags.ignore_environment: 311 PYTHON_GIL = os.environ.get('PYTHON_GIL', None) 312 if PYTHON_GIL: 313 PYTHON_GIL = (PYTHON_GIL == '1') 314 else: 315 PYTHON_GIL = None 316 317 free_threading = "free_threading" 318 if PYTHON_GIL is not None: 319 free_threading = f"{free_threading} GIL={int(PYTHON_GIL)}" 320 build.append(free_threading) 321 322 if hasattr(sys, 'gettotalrefcount'): 323 # --with-pydebug 324 build.append('debug') 325 326 if '-DNDEBUG' in cflags: 327 build.append('without_assert') 328 else: 329 build.append('release') 330 331 if '--with-assertions' in config_args: 332 build.append('with_assert') 333 elif '-DNDEBUG' not in cflags: 334 build.append('with_assert') 335 336 # --enable-experimental-jit 337 tier2 = re.search('-D_Py_TIER2=([0-9]+)', cflags) 338 if tier2: 339 tier2 = int(tier2.group(1)) 340 341 if not sys.flags.ignore_environment: 342 PYTHON_JIT = os.environ.get('PYTHON_JIT', None) 343 if PYTHON_JIT: 344 PYTHON_JIT = (PYTHON_JIT != '0') 345 else: 346 PYTHON_JIT = None 347 348 if tier2 == 1: # =yes 349 if PYTHON_JIT == False: 350 jit = 'JIT=off' 351 else: 352 jit = 'JIT' 353 elif tier2 == 3: # =yes-off 354 if PYTHON_JIT: 355 jit = 'JIT' 356 else: 357 jit = 'JIT=off' 358 elif tier2 == 4: # =interpreter 359 if PYTHON_JIT == False: 360 jit = 'JIT-interpreter=off' 361 else: 362 jit = 'JIT-interpreter' 363 elif tier2 == 6: # =interpreter-off (Secret option!) 364 if PYTHON_JIT: 365 jit = 'JIT-interpreter' 366 else: 367 jit = 'JIT-interpreter=off' 368 elif '-D_Py_JIT' in cflags: 369 jit = 'JIT' 370 else: 371 jit = None 372 if jit: 373 build.append(jit) 374 375 # --enable-framework=name 376 framework = sysconfig.get_config_var('PYTHONFRAMEWORK') 377 if framework: 378 build.append(f'framework={framework}') 379 380 # --enable-shared 381 shared = int(sysconfig.get_config_var('PY_ENABLE_SHARED') or '0') 382 if shared: 383 build.append('shared') 384 385 # --with-lto 386 optimizations = [] 387 if '-flto=thin' in ldflags_nodist: 388 optimizations.append('ThinLTO') 389 elif '-flto' in ldflags_nodist: 390 optimizations.append('LTO') 391 392 if support.check_cflags_pgo(): 393 # PGO (--enable-optimizations) 394 optimizations.append('PGO') 395 396 if support.check_bolt_optimized(): 397 # BOLT (--enable-bolt) 398 optimizations.append('BOLT') 399 400 if optimizations: 401 build.append('+'.join(optimizations)) 402 403 # --with-address-sanitizer 404 sanitizers = [] 405 if support.check_sanitizer(address=True): 406 sanitizers.append("ASAN") 407 # --with-memory-sanitizer 408 if support.check_sanitizer(memory=True): 409 sanitizers.append("MSAN") 410 # --with-undefined-behavior-sanitizer 411 if support.check_sanitizer(ub=True): 412 sanitizers.append("UBSAN") 413 # --with-thread-sanitizer 414 if support.check_sanitizer(thread=True): 415 sanitizers.append("TSAN") 416 if sanitizers: 417 build.append('+'.join(sanitizers)) 418 419 # --with-trace-refs 420 if hasattr(sys, 'getobjects'): 421 build.append("TraceRefs") 422 # --enable-pystats 423 if hasattr(sys, '_stats_on'): 424 build.append("pystats") 425 # --with-valgrind 426 if sysconfig.get_config_var('WITH_VALGRIND'): 427 build.append("valgrind") 428 # --with-dtrace 429 if sysconfig.get_config_var('WITH_DTRACE'): 430 build.append("dtrace") 431 432 return build 433 434 435def get_temp_dir(tmp_dir: StrPath | None = None) -> StrPath: 436 if tmp_dir: 437 tmp_dir = os.path.expanduser(tmp_dir) 438 else: 439 # When tests are run from the Python build directory, it is best practice 440 # to keep the test files in a subfolder. This eases the cleanup of leftover 441 # files using the "make distclean" command. 442 if sysconfig.is_python_build(): 443 if not support.is_wasi: 444 tmp_dir = sysconfig.get_config_var('abs_builddir') 445 if tmp_dir is None: 446 tmp_dir = sysconfig.get_config_var('abs_srcdir') 447 if not tmp_dir: 448 # gh-74470: On Windows, only srcdir is available. Using 449 # abs_builddir mostly matters on UNIX when building 450 # Python out of the source tree, especially when the 451 # source tree is read only. 452 tmp_dir = sysconfig.get_config_var('srcdir') 453 if not tmp_dir: 454 raise RuntimeError( 455 "Could not determine the correct value for tmp_dir" 456 ) 457 tmp_dir = os.path.join(tmp_dir, 'build') 458 else: 459 # WASI platform 460 tmp_dir = sysconfig.get_config_var('projectbase') 461 if not tmp_dir: 462 raise RuntimeError( 463 "sysconfig.get_config_var('projectbase') " 464 f"unexpectedly returned {tmp_dir!r} on WASI" 465 ) 466 tmp_dir = os.path.join(tmp_dir, 'build') 467 468 # When get_temp_dir() is called in a worker process, 469 # get_temp_dir() path is different than in the parent process 470 # which is not a WASI process. So the parent does not create 471 # the same "tmp_dir" than the test worker process. 472 os.makedirs(tmp_dir, exist_ok=True) 473 else: 474 tmp_dir = tempfile.gettempdir() 475 476 return os.path.abspath(tmp_dir) 477 478 479def fix_umask(): 480 if support.is_emscripten: 481 # Emscripten has default umask 0o777, which breaks some tests. 482 # see https://github.com/emscripten-core/emscripten/issues/17269 483 old_mask = os.umask(0) 484 if old_mask == 0o777: 485 os.umask(0o027) 486 else: 487 os.umask(old_mask) 488 489 490def get_work_dir(parent_dir: StrPath, worker: bool = False) -> StrPath: 491 # Define a writable temp dir that will be used as cwd while running 492 # the tests. The name of the dir includes the pid to allow parallel 493 # testing (see the -j option). 494 # Emscripten and WASI have stubbed getpid(), Emscripten has only 495 # millisecond clock resolution. Use randint() instead. 496 if support.is_emscripten or support.is_wasi: 497 nounce = random.randint(0, 1_000_000) 498 else: 499 nounce = os.getpid() 500 501 if worker: 502 work_dir = WORK_DIR_PREFIX + str(nounce) 503 else: 504 work_dir = WORKER_WORK_DIR_PREFIX + str(nounce) 505 work_dir += os_helper.FS_NONASCII 506 work_dir = os.path.join(parent_dir, work_dir) 507 return work_dir 508 509 510@contextlib.contextmanager 511def exit_timeout(): 512 try: 513 yield 514 except SystemExit as exc: 515 # bpo-38203: Python can hang at exit in Py_Finalize(), especially 516 # on threading._shutdown() call: put a timeout 517 if threading_helper.can_start_thread: 518 faulthandler.dump_traceback_later(EXIT_TIMEOUT, exit=True) 519 sys.exit(exc.code) 520 521 522def remove_testfn(test_name: TestName, verbose: int) -> None: 523 # Try to clean up os_helper.TESTFN if left behind. 524 # 525 # While tests shouldn't leave any files or directories behind, when a test 526 # fails that can be tedious for it to arrange. The consequences can be 527 # especially nasty on Windows, since if a test leaves a file open, it 528 # cannot be deleted by name (while there's nothing we can do about that 529 # here either, we can display the name of the offending test, which is a 530 # real help). 531 name = os_helper.TESTFN 532 if not os.path.exists(name): 533 return 534 535 nuker: Callable[[str], None] 536 if os.path.isdir(name): 537 import shutil 538 kind, nuker = "directory", shutil.rmtree 539 elif os.path.isfile(name): 540 kind, nuker = "file", os.unlink 541 else: 542 raise RuntimeError(f"os.path says {name!r} exists but is neither " 543 f"directory nor file") 544 545 if verbose: 546 print_warning(f"{test_name} left behind {kind} {name!r}") 547 support.environment_altered = True 548 549 try: 550 import stat 551 # fix possible permissions problems that might prevent cleanup 552 os.chmod(name, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) 553 nuker(name) 554 except Exception as exc: 555 print_warning(f"{test_name} left behind {kind} {name!r} " 556 f"and it couldn't be removed: {exc}") 557 558 559def abs_module_name(test_name: TestName, test_dir: StrPath | None) -> TestName: 560 if test_name.startswith('test.') or test_dir: 561 return test_name 562 else: 563 # Import it from the test package 564 return 'test.' + test_name 565 566 567# gh-90681: When rerunning tests, we might need to rerun the whole 568# class or module suite if some its life-cycle hooks fail. 569# Test level hooks are not affected. 570_TEST_LIFECYCLE_HOOKS = frozenset(( 571 'setUpClass', 'tearDownClass', 572 'setUpModule', 'tearDownModule', 573)) 574 575def normalize_test_name(test_full_name, *, is_error=False): 576 short_name = test_full_name.split(" ")[0] 577 if is_error and short_name in _TEST_LIFECYCLE_HOOKS: 578 if test_full_name.startswith(('setUpModule (', 'tearDownModule (')): 579 # if setUpModule() or tearDownModule() failed, don't filter 580 # tests with the test file name, don't use use filters. 581 return None 582 583 # This means that we have a failure in a life-cycle hook, 584 # we need to rerun the whole module or class suite. 585 # Basically the error looks like this: 586 # ERROR: setUpClass (test.test_reg_ex.RegTest) 587 # or 588 # ERROR: setUpModule (test.test_reg_ex) 589 # So, we need to parse the class / module name. 590 lpar = test_full_name.index('(') 591 rpar = test_full_name.index(')') 592 return test_full_name[lpar + 1: rpar].split('.')[-1] 593 return short_name 594 595 596def adjust_rlimit_nofile(): 597 """ 598 On macOS the default fd limit (RLIMIT_NOFILE) is sometimes too low (256) 599 for our test suite to succeed. Raise it to something more reasonable. 1024 600 is a common Linux default. 601 """ 602 try: 603 import resource 604 except ImportError: 605 return 606 607 fd_limit, max_fds = resource.getrlimit(resource.RLIMIT_NOFILE) 608 609 desired_fds = 1024 610 611 if fd_limit < desired_fds and fd_limit < max_fds: 612 new_fd_limit = min(desired_fds, max_fds) 613 try: 614 resource.setrlimit(resource.RLIMIT_NOFILE, 615 (new_fd_limit, max_fds)) 616 print(f"Raised RLIMIT_NOFILE: {fd_limit} -> {new_fd_limit}") 617 except (ValueError, OSError) as err: 618 print_warning(f"Unable to raise RLIMIT_NOFILE from {fd_limit} to " 619 f"{new_fd_limit}: {err}.") 620 621 622def get_host_runner(): 623 if (hostrunner := os.environ.get("_PYTHON_HOSTRUNNER")) is None: 624 hostrunner = sysconfig.get_config_var("HOSTRUNNER") 625 return hostrunner 626 627 628def is_cross_compiled(): 629 return ('_PYTHON_HOST_PLATFORM' in os.environ) 630 631 632def format_resources(use_resources: Iterable[str]): 633 use_resources = set(use_resources) 634 all_resources = set(ALL_RESOURCES) 635 636 # Express resources relative to "all" 637 relative_all = ['all'] 638 for name in sorted(all_resources - use_resources): 639 relative_all.append(f'-{name}') 640 for name in sorted(use_resources - all_resources): 641 relative_all.append(f'{name}') 642 all_text = ','.join(relative_all) 643 all_text = f"resources: {all_text}" 644 645 # List of enabled resources 646 text = ','.join(sorted(use_resources)) 647 text = f"resources ({len(use_resources)}): {text}" 648 649 # Pick the shortest string (prefer relative to all if lengths are equal) 650 if len(all_text) <= len(text): 651 return all_text 652 else: 653 return text 654 655 656def display_header(use_resources: tuple[str, ...], 657 python_cmd: tuple[str, ...] | None): 658 # Print basic platform information 659 print("==", platform.python_implementation(), *sys.version.split()) 660 print("==", platform.platform(aliased=True), 661 "%s-endian" % sys.byteorder) 662 print("== Python build:", ' '.join(get_build_info())) 663 print("== cwd:", os.getcwd()) 664 665 cpu_count: object = os.cpu_count() 666 if cpu_count: 667 # The function is new in Python 3.13; mypy doesn't know about it yet: 668 process_cpu_count = os.process_cpu_count() # type: ignore[attr-defined] 669 if process_cpu_count and process_cpu_count != cpu_count: 670 cpu_count = f"{process_cpu_count} (process) / {cpu_count} (system)" 671 print("== CPU count:", cpu_count) 672 print("== encodings: locale=%s FS=%s" 673 % (locale.getencoding(), sys.getfilesystemencoding())) 674 675 if use_resources: 676 text = format_resources(use_resources) 677 print(f"== {text}") 678 else: 679 print("== resources: all test resources are disabled, " 680 "use -u option to unskip tests") 681 682 cross_compile = is_cross_compiled() 683 if cross_compile: 684 print("== cross compiled: Yes") 685 if python_cmd: 686 cmd = shlex.join(python_cmd) 687 print(f"== host python: {cmd}") 688 689 get_cmd = [*python_cmd, '-m', 'platform'] 690 proc = subprocess.run( 691 get_cmd, 692 stdout=subprocess.PIPE, 693 text=True, 694 cwd=os_helper.SAVEDCWD) 695 stdout = proc.stdout.replace('\n', ' ').strip() 696 if stdout: 697 print(f"== host platform: {stdout}") 698 elif proc.returncode: 699 print(f"== host platform: <command failed with exit code {proc.returncode}>") 700 else: 701 hostrunner = get_host_runner() 702 if hostrunner: 703 print(f"== host runner: {hostrunner}") 704 705 # This makes it easier to remember what to set in your local 706 # environment when trying to reproduce a sanitizer failure. 707 asan = support.check_sanitizer(address=True) 708 msan = support.check_sanitizer(memory=True) 709 ubsan = support.check_sanitizer(ub=True) 710 tsan = support.check_sanitizer(thread=True) 711 sanitizers = [] 712 if asan: 713 sanitizers.append("address") 714 if msan: 715 sanitizers.append("memory") 716 if ubsan: 717 sanitizers.append("undefined behavior") 718 if tsan: 719 sanitizers.append("thread") 720 if sanitizers: 721 print(f"== sanitizers: {', '.join(sanitizers)}") 722 for sanitizer, env_var in ( 723 (asan, "ASAN_OPTIONS"), 724 (msan, "MSAN_OPTIONS"), 725 (ubsan, "UBSAN_OPTIONS"), 726 (tsan, "TSAN_OPTIONS"), 727 ): 728 options= os.environ.get(env_var) 729 if sanitizer and options is not None: 730 print(f"== {env_var}={options!r}") 731 732 print(flush=True) 733 734 735def cleanup_temp_dir(tmp_dir: StrPath): 736 import glob 737 738 path = os.path.join(glob.escape(tmp_dir), TMP_PREFIX + '*') 739 print("Cleanup %s directory" % tmp_dir) 740 for name in glob.glob(path): 741 if os.path.isdir(name): 742 print("Remove directory: %s" % name) 743 os_helper.rmtree(name) 744 else: 745 print("Remove file: %s" % name) 746 os_helper.unlink(name) 747 748WINDOWS_STATUS = { 749 0xC0000005: "STATUS_ACCESS_VIOLATION", 750 0xC00000FD: "STATUS_STACK_OVERFLOW", 751 0xC000013A: "STATUS_CONTROL_C_EXIT", 752} 753 754def get_signal_name(exitcode): 755 if exitcode < 0: 756 signum = -exitcode 757 try: 758 return signal.Signals(signum).name 759 except ValueError: 760 pass 761 762 # Shell exit code (ex: WASI build) 763 if 128 < exitcode < 256: 764 signum = exitcode - 128 765 try: 766 return signal.Signals(signum).name 767 except ValueError: 768 pass 769 770 try: 771 return WINDOWS_STATUS[exitcode] 772 except KeyError: 773 pass 774 775 return None 776 777 778ILLEGAL_XML_CHARS_RE = re.compile( 779 '[' 780 # Control characters; newline (\x0A and \x0D) and TAB (\x09) are legal 781 '\x00-\x08\x0B\x0C\x0E-\x1F' 782 # Surrogate characters 783 '\uD800-\uDFFF' 784 # Special Unicode characters 785 '\uFFFE' 786 '\uFFFF' 787 # Match multiple sequential invalid characters for better efficiency 788 ']+') 789 790def _sanitize_xml_replace(regs): 791 text = regs[0] 792 return ''.join(f'\\x{ord(ch):02x}' if ch <= '\xff' else ascii(ch)[1:-1] 793 for ch in text) 794 795def sanitize_xml(text): 796 return ILLEGAL_XML_CHARS_RE.sub(_sanitize_xml_replace, text) 797