diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py index 5596e5b16..c98152c50 100644 --- a/Lib/test/test_faulthandler.py +++ b/Lib/test/test_faulthandler.py @@ -22,6 +22,16 @@ if not support.has_subprocess_support: TIMEOUT = 0.5 +STACK_HEADER_STR = r'Stack (most recent call first):' + +# Regular expressions +STACK_HEADER = re.escape(STACK_HEADER_STR) +THREAD_NAME = r'( \[.*\])?' +THREAD_ID = fr'Thread 0x[0-9a-f]+{THREAD_NAME}' +THREAD_HEADER = fr'{THREAD_ID} \(most recent call first\):' +CURRENT_THREAD_ID = fr'Current thread 0x[0-9a-f]+{THREAD_NAME}' +CURRENT_THREAD_HEADER = fr'{CURRENT_THREAD_ID} \(most recent call first\):' + def expected_traceback(lineno1, lineno2, header, min_count=1): regex = header @@ -45,6 +55,13 @@ def temporary_filename(): finally: os_helper.unlink(filename) + +ADDRESS_EXPR = "0x[0-9a-f]+" +C_STACK_REGEX = [ + r"Current thread's C stack trace \(most recent call first\):", + fr'( Binary file ".+"(, at .*(\+|-){ADDRESS_EXPR})? \[{ADDRESS_EXPR}\])|(<.+>)' +] + class FaultHandlerTests(unittest.TestCase): def get_output(self, code, filename=None, fd=None): @@ -93,6 +110,7 @@ class FaultHandlerTests(unittest.TestCase): fd=None, know_current_thread=True, py_fatal_error=False, garbage_collecting=False, + c_stack=True, function=''): """ Check that the fault handler for fatal errors is enabled and check the @@ -100,21 +118,32 @@ class FaultHandlerTests(unittest.TestCase): Raise an error if the output doesn't match the expected format. """ - if all_threads: + all_threads_disabled = ( + all_threads + and (not sys._is_gil_enabled()) + ) + if all_threads and not all_threads_disabled: if know_current_thread: - header = 'Current thread 0x[0-9a-f]+' + header = CURRENT_THREAD_HEADER else: - header = 'Thread 0x[0-9a-f]+' + header = THREAD_HEADER else: - header = 'Stack' + header = STACK_HEADER regex = [f'^{fatal_error}'] if py_fatal_error: regex.append("Python runtime state: initialized") regex.append('') - regex.append(fr'{header} \(most recent call first\):') - if garbage_collecting: - regex.append(' Garbage-collecting') - regex.append(fr' File "", line {lineno} in {function}') + if all_threads_disabled and not py_fatal_error: + regex.append("") + regex.append(fr'{header}') + if support.Py_GIL_DISABLED and py_fatal_error and not know_current_thread: + regex.append(" ") + else: + if garbage_collecting and not all_threads_disabled: + regex.append(' Garbage-collecting') + regex.append(fr' File "", line {lineno} in {function}') + if c_stack: + regex.extend(C_STACK_REGEX) regex = '\n'.join(regex) if other_regex: @@ -137,8 +166,6 @@ class FaultHandlerTests(unittest.TestCase): fatal_error = 'Windows fatal exception: %s' % name_regex self.check_error(code, line_number, fatal_error, **kw) - # TODO: RUSTPYTHON - @unittest.expectedFailure @unittest.skipIf(sys.platform.startswith('aix'), "the first page of memory is a mapped read-only on AIX") def test_read_null(self): @@ -162,8 +189,6 @@ class FaultHandlerTests(unittest.TestCase): 3, 'access violation') - # TODO: RUSTPYTHON, AssertionError: Regex didn't match - @unittest.expectedFailure @skip_segfault_on_android def test_sigsegv(self): self.check_fatal_error(""" @@ -174,8 +199,7 @@ class FaultHandlerTests(unittest.TestCase): 3, 'Segmentation fault') - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '(?m)^Fatal Python error: Segmentation fault\n\n\nStack\\ \\(most\\ recent\\ call\\ first\\):\n File "", line 9 in __del__\nCurrent thread\'s C stack trace \\(most recent call first\\):\n( Binary file ".+"(, at .*(\\+|-)0x[0-9a-f]+)? \\[0x[0-9a-f]+\\])|(<.+>)' not found in 'exit' @skip_segfault_on_android def test_gc(self): # bpo-44466: Detect if the GC is running @@ -212,8 +236,7 @@ class FaultHandlerTests(unittest.TestCase): function='__del__', garbage_collecting=True) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 == 0 def test_fatal_error_c_thread(self): self.check_fatal_error(""" import faulthandler @@ -226,8 +249,7 @@ class FaultHandlerTests(unittest.TestCase): func='faulthandler_fatal_error_thread', py_fatal_error=True) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @support.skip_if_sanitizer("TSAN itercepts SIGABRT", thread=True) def test_sigabrt(self): self.check_fatal_error(""" import faulthandler @@ -237,10 +259,9 @@ class FaultHandlerTests(unittest.TestCase): 3, 'Aborted') - # TODO: RUSTPYTHON - @unittest.expectedFailure @unittest.skipIf(sys.platform == 'win32', "SIGFPE cannot be caught on Windows") + @support.skip_if_sanitizer("TSAN itercepts SIGFPE", thread=True) def test_sigfpe(self): self.check_fatal_error(""" import faulthandler @@ -252,6 +273,7 @@ class FaultHandlerTests(unittest.TestCase): @unittest.skipIf(_testcapi is None, 'need _testcapi') @unittest.skipUnless(hasattr(signal, 'SIGBUS'), 'need signal.SIGBUS') + @support.skip_if_sanitizer("TSAN itercepts SIGBUS", thread=True) @skip_segfault_on_android def test_sigbus(self): self.check_fatal_error(""" @@ -266,6 +288,7 @@ class FaultHandlerTests(unittest.TestCase): @unittest.skipIf(_testcapi is None, 'need _testcapi') @unittest.skipUnless(hasattr(signal, 'SIGILL'), 'need signal.SIGILL') + @support.skip_if_sanitizer("TSAN itercepts SIGILL", thread=True) @skip_segfault_on_android def test_sigill(self): self.check_fatal_error(""" @@ -291,13 +314,9 @@ class FaultHandlerTests(unittest.TestCase): func='_testcapi_fatal_error_impl', py_fatal_error=True) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_fatal_error(self): self.check_fatal_error_func(False) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_fatal_error_without_gil(self): self.check_fatal_error_func(True) @@ -316,8 +335,6 @@ class FaultHandlerTests(unittest.TestCase): '(?:Segmentation fault|Bus error)', other_regex='unable to raise a stack overflow') - # TODO: RUSTPYTHON - @unittest.expectedFailure @skip_segfault_on_android def test_gil_released(self): self.check_fatal_error(""" @@ -328,8 +345,6 @@ class FaultHandlerTests(unittest.TestCase): 3, 'Segmentation fault') - # TODO: RUSTPYTHON - @unittest.expectedFailure @skip_segfault_on_android def test_enable_file(self): with temporary_filename() as filename: @@ -343,8 +358,6 @@ class FaultHandlerTests(unittest.TestCase): 'Segmentation fault', filename=filename) - # TODO: RUSTPYTHON - @unittest.expectedFailure @unittest.skipIf(sys.platform == "win32", "subprocess doesn't support pass_fds on Windows") @skip_segfault_on_android @@ -361,8 +374,6 @@ class FaultHandlerTests(unittest.TestCase): 'Segmentation fault', fd=fd) - # TODO: RUSTPYTHON - @unittest.expectedFailure @skip_segfault_on_android def test_enable_single_thread(self): self.check_fatal_error(""" @@ -389,8 +400,7 @@ class FaultHandlerTests(unittest.TestCase): "%r is present in %r" % (not_expected, stderr)) self.assertNotEqual(exitcode, 0) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Cannot find 'Extension modules:' in 'Fatal Python error: Segmentation fault\n\nCurrent thread 0x0000000000004284 (most recent call first):\n File "", line 6 in ' @skip_segfault_on_android def test_dump_ext_modules(self): code = """ @@ -511,7 +521,7 @@ class FaultHandlerTests(unittest.TestCase): else: lineno = 14 expected = [ - 'Stack (most recent call first):', + f'{STACK_HEADER_STR}', ' File "", line %s in funcB' % lineno, ' File "", line 17 in funcA', ' File "", line 19 in ' @@ -523,14 +533,11 @@ class FaultHandlerTests(unittest.TestCase): def test_dump_traceback(self): self.check_dump_traceback() - # TODO: RUSTPYTHON - binary file write needs different handling - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; - binary file write needs different handling def test_dump_traceback_file(self): with temporary_filename() as filename: self.check_dump_traceback(filename=filename) - # TODO: RUSTPYTHON - @unittest.expectedFailure @unittest.skipIf(sys.platform == "win32", "subprocess doesn't support pass_fds on Windows") def test_dump_traceback_fd(self): @@ -553,7 +560,7 @@ class FaultHandlerTests(unittest.TestCase): func_name=func_name, ) expected = [ - 'Stack (most recent call first):', + f'{STACK_HEADER_STR}', ' File "", line 4 in %s' % truncated, ' File "", line 6 in ' ] @@ -607,28 +614,26 @@ class FaultHandlerTests(unittest.TestCase): lineno = 10 # When the traceback is dumped, the waiter thread may be in the # `self.running.set()` call or in `self.stop.wait()`. - regex = r""" - ^Thread 0x[0-9a-f]+ \(most recent call first\): + regex = fr""" + ^{THREAD_HEADER} (?: File ".*threading.py", line [0-9]+ in [_a-z]+ ){{1,3}} File "", line (?:22|23) in run File ".*threading.py", line [0-9]+ in _bootstrap_inner File ".*threading.py", line [0-9]+ in _bootstrap - Current thread 0x[0-9a-f]+ \(most recent call first\): + {CURRENT_THREAD_HEADER} File "", line {lineno} in dump File "", line 28 in $ """ - regex = dedent(regex.format(lineno=lineno)).strip() + regex = dedent(regex).strip() self.assertRegex(output, regex) self.assertEqual(exitcode, 0) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '^Thread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\n(?: File ".*threading.py", line [0-9]+ in [_a-z]+\n){1,3} File "", line (?:22|23) in run\n File ".*threading.py", line [0-9]+ in _bootstrap_inner\n File ".*threading.py", line [0-9]+ in _bootstrap\n\nCurrent thread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\n File "", line 10 in dump\n File "", line 28 in $' not found in 'Stack (most recent call first):\n File "", line 10 in dump\n File "", line 28 in ' def test_dump_traceback_threads(self): self.check_dump_traceback_threads(None) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; - TypeError: a bytes-like object is required, not 'str' def test_dump_traceback_threads_file(self): with temporary_filename() as filename: self.check_dump_traceback_threads(filename) @@ -688,44 +693,38 @@ class FaultHandlerTests(unittest.TestCase): count = loops if repeat: count *= 2 - header = r'Timeout \(%s\)!\nThread 0x[0-9a-f]+ \(most recent call first\):\n' % timeout_str + header = (fr'Timeout \({timeout_str}\)!\n' + fr'{THREAD_HEADER}\n') regex = expected_traceback(17, 26, header, min_count=count) self.assertRegex(trace, regex) else: self.assertEqual(trace, '') self.assertEqual(exitcode, 0) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '^Timeout \\(0:00:00.500000\\)!\\nThread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\\n File "", line 17 in func\n File "", line 26 in $' not found in 'Traceback (most recent call last):\n File "", line 26, in \n File "", line 14, in func\nAttributeError: \'NoneType\' object has no attribute \'fileno\'' def test_dump_traceback_later(self): self.check_dump_traceback_later() - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '^Timeout \\(0:00:00.500000\\)!\\nThread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\\n File "", line 17 in func\n File "", line 26 in \nTimeout \\(0:00:00.500000\\)!\\nThread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\\n File "", line 17 in func\n File "", line 26 in ' not found in 'Traceback (most recent call last):\n File "", line 26, in \n File "", line 14, in func\nAttributeError: \'NoneType\' object has no attribute \'fileno\'' def test_dump_traceback_later_repeat(self): self.check_dump_traceback_later(repeat=True) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; - AttributeError: 'NoneType' object has no attribute 'fileno' def test_dump_traceback_later_cancel(self): self.check_dump_traceback_later(cancel=True) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '^Timeout \\(0:00:00.500000\\)!\\nThread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\\n File "", line 17 in func\n File "", line 26 in $' not found in 'Timeout (00:00:00.500000)!\n' def test_dump_traceback_later_file(self): with temporary_filename() as filename: self.check_dump_traceback_later(filename=filename) - # TODO: RUSTPYTHON - @unittest.expectedFailure @unittest.skipIf(sys.platform == "win32", "subprocess doesn't support pass_fds on Windows") def test_dump_traceback_later_fd(self): with tempfile.TemporaryFile('wb+') as fp: self.check_dump_traceback_later(fd=fp.fileno()) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '^Timeout \\(0:00:00.500000\\)!\\nThread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\\n File "", line 17 in func\n File "", line 26 in \nTimeout \\(0:00:00.500000\\)!\\nThread 0x[0-9a-f]+( \\[.*\\])? \\(most recent call first\\):\\n File "", line 17 in func\n File "", line 26 in ' not found in 'Traceback (most recent call last):\n File "", line 26, in \n File "", line 14, in func\nAttributeError: \'NoneType\' object has no attribute \'fileno\'' @support.requires_resource('walltime') def test_dump_traceback_later_twice(self): self.check_dump_traceback_later(loops=2) @@ -801,9 +800,9 @@ class FaultHandlerTests(unittest.TestCase): trace = '\n'.join(trace) if not unregister: if all_threads: - regex = r'Current thread 0x[0-9a-f]+ \(most recent call first\):\n' + regex = fr'{CURRENT_THREAD_HEADER}\n' else: - regex = r'Stack \(most recent call first\):\n' + regex = fr'{STACK_HEADER}\n' regex = expected_traceback(14, 32, regex) self.assertRegex(trace, regex) else: @@ -813,37 +812,26 @@ class FaultHandlerTests(unittest.TestCase): else: self.assertEqual(exitcode, 0) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_register(self): self.check_register() - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_unregister(self): self.check_register(unregister=True) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_register_file(self): with temporary_filename() as filename: self.check_register(filename=filename) - # TODO: RUSTPYTHON - @unittest.expectedFailure @unittest.skipIf(sys.platform == "win32", "subprocess doesn't support pass_fds on Windows") def test_register_fd(self): with tempfile.TemporaryFile('wb+') as fp: self.check_register(fd=fp.fileno()) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_register_threads(self): self.check_register(all_threads=True) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @support.skip_if_sanitizer("gh-129825: hangs under TSAN", thread=True) def test_register_chain(self): self.check_register(chain=True) @@ -871,8 +859,6 @@ class FaultHandlerTests(unittest.TestCase): with self.check_stderr_none(): faulthandler.register(signal.SIGUSR1) - # TODO: RUSTPYTHON, AttributeError: module 'msvcrt' has no attribute 'GetErrorMode' - @unittest.expectedFailure @unittest.skipUnless(MS_WINDOWS, 'specific to Windows') def test_raise_exception(self): for exc, name in ( @@ -985,5 +971,37 @@ class FaultHandlerTests(unittest.TestCase): _, exitcode = self.get_output(code) self.assertEqual(exitcode, 0) + def check_c_stack(self, output): + starting_line = output.pop(0) + self.assertRegex(starting_line, C_STACK_REGEX[0]) + self.assertGreater(len(output), 0) + + for line in output: + with self.subTest(line=line): + if line != '': # Ignore trailing or leading newlines + self.assertRegex(line, C_STACK_REGEX[1]) + + + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 0 + def test_dump_c_stack(self): + code = dedent(""" + import faulthandler + faulthandler.dump_c_stack() + """) + output, exitcode = self.get_output(code) + self.assertEqual(exitcode, 0) + self.check_c_stack(output) + + + @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'faulthandler' has no attribute 'dump_c_stack' + def test_dump_c_stack_file(self): + import tempfile + + with tempfile.TemporaryFile("w+") as tmp: + faulthandler.dump_c_stack(file=tmp) + tmp.flush() # Just in case + tmp.seek(0) + self.check_c_stack(tmp.read().split("\n")) + if __name__ == "__main__": unittest.main()