From f5b64f6b665edd39c7cfd951b24b1058007408d6 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Wed, 19 Feb 2025 00:56:30 +0500 Subject: [PATCH 01/10] Add test for changing stdout from thread --- Lib/test/test_sys_getattr.py | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Lib/test/test_sys_getattr.py diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py new file mode 100644 index 00000000000000..1456ae08ae066d --- /dev/null +++ b/Lib/test/test_sys_getattr.py @@ -0,0 +1,47 @@ +import test.support +from test.support.script_helper import assert_python_failure, assert_python_ok +import textwrap +import unittest + + + + +@test.support.cpython_only +class PySysGetAttrTest(unittest.TestCase): + + + def test_changing_stdout_from_thread(self): + # print should use strong reference to the stdout. + code = textwrap.dedent(''' + from contextlib import redirect_stdout + from io import StringIO + from threading import Thread + import time + + class Foo: + def __repr__(self): + time.sleep(0.2) + return "Foo" + + + def thread1(): + text = StringIO() + with redirect_stdout(text): + time.sleep(0.2) + + def main(): + t1 = Thread(target=thread1, args=()) + t1.start() + time.sleep(0.1) + print(Foo()) + + + if __name__ == "__main__": + main() + ''') + rc, out, err = assert_python_ok('-c', code) + self.assertEqual(out, b"") + self.assertEqual(err, b"") + +if __name__ == "__main__": + unittest.main() From 97e0b2e6606e35d045fe3bc24e6bc6dd4defbea1 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Thu, 20 Feb 2025 19:05:44 +0500 Subject: [PATCH 02/10] Add tests for faulthandler --- Lib/test/test_sys_getattr.py | 75 +++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index 1456ae08ae066d..92500f82263a88 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -1,3 +1,5 @@ +from pathlib import Path +import tempfile import test.support from test.support.script_helper import assert_python_failure, assert_python_ok import textwrap @@ -9,10 +11,44 @@ @test.support.cpython_only class PySysGetAttrTest(unittest.TestCase): + common_faulthandler_code = textwrap.dedent(''' + from contextlib import redirect_stderr + from threading import Thread + import time + from faulthandler import dump_traceback, enable, dump_traceback_later - def test_changing_stdout_from_thread(self): + class FakeFile: + def __init__(self): + self.f = open("{0}", "w") + def write(self, s): + self.f.write(s) + def flush(self): + time.sleep(0.2) + def fileno(self): + time.sleep(0.2) + return self.f.fileno() + + def thread1(): + text = FakeFile() + with redirect_stderr(text): + time.sleep(0.2) + + def main(): + enable(None, True) + t1 = Thread(target=thread1, args=()) + t1.start() + time.sleep(0.1) + {1} + + if __name__ == "__main__": + main() + ''') + + + def test_print_deleted_stdout(self): # print should use strong reference to the stdout. code = textwrap.dedent(''' + # from https://gist.github.com/colesbury/c48f50e95d5d68e24814a56e2664e587 from contextlib import redirect_stdout from io import StringIO from threading import Thread @@ -23,7 +59,6 @@ def __repr__(self): time.sleep(0.2) return "Foo" - def thread1(): text = StringIO() with redirect_stdout(text): @@ -43,5 +78,41 @@ def main(): self.assertEqual(out, b"") self.assertEqual(err, b"") + def test_faulthandler_enable_deleted_stderr(self): + # faulthandler should use strong reference to the stderr + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir, "test_faulthandler_enable") + test_code = self.common_faulthandler_code.format( + path.as_posix(), + "enable(None, True)" + ) + rc, out, err = assert_python_ok('-c', test_code) + self.assertEqual(out, b"") + self.assertEqual(err, b"") + + def test_faulthandler_dump_traceback_deleted_stderr(self): + # faulthandler should use strong reference to the stderr + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir, "test_faulthandler_dump_traceback") + test_code = self.common_faulthandler_code.format( + path.as_posix(), + "dump_traceback(None, False)" + ) + rc, out, err = assert_python_ok('-c', test_code) + self.assertEqual(out, b"") + self.assertEqual(err, b"") + + def test_faulthandler_dump_traceback_later_deleted_stderr(self): + # faulthandler should use strong reference to the stderr + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir, "test_faulthandler_dump_traceback_later") + test_code = self.common_faulthandler_code.format( + path.as_posix(), + "dump_traceback_later(0.1, True, None, False)" + ) + rc, out, err = assert_python_ok('-c', test_code) + self.assertEqual(out, b"") + self.assertEqual(err, b"") + if __name__ == "__main__": unittest.main() From 1987fb27365302945f0eba03a6e94817c2597994 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 21 Feb 2025 01:30:00 +0500 Subject: [PATCH 03/10] Add tests for warnings and simplify for print - also add checks of exit code --- Lib/test/test_sys_getattr.py | 95 ++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index 92500f82263a88..09a6eb9ea355a4 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -13,9 +13,9 @@ class PySysGetAttrTest(unittest.TestCase): common_faulthandler_code = textwrap.dedent(''' from contextlib import redirect_stderr + from faulthandler import dump_traceback, enable, dump_traceback_later from threading import Thread import time - from faulthandler import dump_traceback, enable, dump_traceback_later class FakeFile: def __init__(self): @@ -42,41 +42,63 @@ def main(): if __name__ == "__main__": main() + exit(0) + ''') + + common_warnings_code = textwrap.dedent(''' + from io import StringIO + import sys + import warnings + + class Foo: + def __init__(self): + self.x = sys.stderr + setattr(sys, "stderr", StringIO()) + + def __repr__(self): + x = sys.stderr + setattr(sys, "stderr", self.x) + del x + return "Foo" + + def main(): + del warnings._showwarnmsg + {0} + + if __name__ == "__main__": + main() + exit(0) ''') def test_print_deleted_stdout(self): # print should use strong reference to the stdout. code = textwrap.dedent(''' - # from https://gist.github.com/colesbury/c48f50e95d5d68e24814a56e2664e587 - from contextlib import redirect_stdout from io import StringIO - from threading import Thread - import time + import sys - class Foo: - def __repr__(self): - time.sleep(0.2) - return "Foo" + class Bar: + def __init__(self): + self.x = sys.stdout + setattr(sys, "stdout", StringIO()) - def thread1(): - text = StringIO() - with redirect_stdout(text): - time.sleep(0.2) + def __repr__(self): + x = sys.stdout + setattr(sys, "stdout", self.x) + del x + return "Bar" def main(): - t1 = Thread(target=thread1, args=()) - t1.start() - time.sleep(0.1) - print(Foo()) - + print(Bar()) if __name__ == "__main__": main() + exit(0) ''') rc, out, err = assert_python_ok('-c', code) - self.assertEqual(out, b"") - self.assertEqual(err, b"") + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) def test_faulthandler_enable_deleted_stderr(self): # faulthandler should use strong reference to the stderr @@ -87,8 +109,9 @@ def test_faulthandler_enable_deleted_stderr(self): "enable(None, True)" ) rc, out, err = assert_python_ok('-c', test_code) - self.assertEqual(out, b"") - self.assertEqual(err, b"") + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) def test_faulthandler_dump_traceback_deleted_stderr(self): # faulthandler should use strong reference to the stderr @@ -99,8 +122,9 @@ def test_faulthandler_dump_traceback_deleted_stderr(self): "dump_traceback(None, False)" ) rc, out, err = assert_python_ok('-c', test_code) - self.assertEqual(out, b"") - self.assertEqual(err, b"") + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) def test_faulthandler_dump_traceback_later_deleted_stderr(self): # faulthandler should use strong reference to the stderr @@ -111,8 +135,27 @@ def test_faulthandler_dump_traceback_later_deleted_stderr(self): "dump_traceback_later(0.1, True, None, False)" ) rc, out, err = assert_python_ok('-c', test_code) - self.assertEqual(out, b"") - self.assertEqual(err, b"") + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + + def test_warnings_warn(self): + test_code = self.common_warnings_code.format( + "warnings.warn(Foo())" + ) + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + + def test_warnings_warn_explicit(self): + test_code = self.common_warnings_code.format( + "warnings.warn_explicit(Foo(), UserWarning, 'filename', 0)" + ) + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) if __name__ == "__main__": unittest.main() From f13cdbb453d7ab7e5dd091a71fcf527c69ddcfc3 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 22 Feb 2025 00:37:51 +0500 Subject: [PATCH 04/10] Add tests for input --- Lib/test/test_sys_getattr.py | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index 09a6eb9ea355a4..b74674dc5b02c5 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -70,6 +70,63 @@ def main(): exit(0) ''') + common_input_code = textwrap.dedent(''' + import sys + + class FakeIO: + def write(self, str): + pass + def flush(self): + pass + def fileno(self): + return 0 + + class CrashStdin: + def __init__(self): + self.stdin = sys.stdin + setattr(sys, "stdin", FakeIO()) + + def __repr__(self): + stdin = sys.stdin + setattr(sys, "stdin", self.stdin) + del stdin + return "CrashStdin" + + class CrashStdout: + def __init__(self): + self.stdout = sys.stdout + setattr(sys, "stdout", FakeIO()) + + def __repr__(self): + stdout = sys.stdout + setattr(sys, "stdout", self.stdout) + del stdout + return "CrashStdout" + + class CrashStderr: + def __init__(self): + self.stderr = sys.stderr + setattr(sys, "stderr", FakeIO()) + + def __repr__(self): + stderr = sys.stderr + setattr(sys, "stderr", self.stderr) + del stderr + return "CrashStderr" + + def audit(event, args): + if event == 'builtins.input': + repr(args) + + def main(): + {0} + input({1}) + + if __name__ == "__main__": + main() + + ''') + def test_print_deleted_stdout(self): # print should use strong reference to the stdout. @@ -157,5 +214,35 @@ def test_warnings_warn_explicit(self): self.assertNotIn(b"Segmentation fault", err) self.assertNotIn(b"access violation", err) + def test_input_stdin(self): + test_code = self.common_input_code.format( + "", + "CrashStdin()" + ) + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + + def test_input_stdout(self): + test_code = self.common_input_code.format( + "", + "CrashStdout()" + ) + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + + def test_input_stderr(self): + test_code = self.common_input_code.format( + "sys.addaudithook(audit)", + "CrashStderr()" + ) + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + if __name__ == "__main__": unittest.main() From 1d0b0e057b07b7416028362755fdf892df47ecb3 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 22 Feb 2025 00:53:04 +0500 Subject: [PATCH 05/10] Add test for unraisablehook --- Lib/test/test_sys_getattr.py | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index b74674dc5b02c5..ad7f3a5844b588 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -127,6 +127,37 @@ def main(): ''') + unraisable_hook_code = textwrap.dedent(''' + import sys + + class UnraisableHookInitiator: + def __del__(self): + raise Exception('1') + + class UnraisableHook: + def __call__(self, *args, **kwds): + print('X', *args) + + def __repr__(self): + h = sys.unraisablehook + setattr(sys, 'unraisablehook', sys.__unraisablehook__) + del h + return 'UnraisableHook' + + def audit(event, args): + repr(args) + + def main(): + sys.addaudithook(audit) + setattr(sys, 'unraisablehook', UnraisableHook()) + x = UnraisableHookInitiator() + del x + + if __name__ == "__main__": + main() + + ''') + def test_print_deleted_stdout(self): # print should use strong reference to the stdout. @@ -244,5 +275,12 @@ def test_input_stderr(self): self.assertNotIn(b"Segmentation fault", err) self.assertNotIn(b"access violation", err) + def test_errors_unraisablehook(self): + test_code = self.unraisable_hook_code + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + if __name__ == "__main__": unittest.main() From 9cf4173404640f73884d01777eb3acea6999a381 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 22 Feb 2025 15:28:15 +0500 Subject: [PATCH 06/10] Add tests for pylifecycle/flush_std_files --- Lib/test/test_sys_getattr.py | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index ad7f3a5844b588..391aa44b448288 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -158,6 +158,33 @@ def main(): ''') + flush_std_files_common_code = textwrap.dedent(''' + import sys + + class FakeIO: + def __init__(self, what): + self.what = what + def write(self, str): + pass + def flush(self): + pass + def fileno(self): + return 0 + + @property + def closed(self): + stdfile = getattr(sys, self.what) + setattr(sys, self.what, getattr(sys, "__" + self.what + "__")) + del stdfile + return False + + def main(): + setattr(sys, '{0}', FakeIO('{0}')) + + if __name__ == "__main__": + main() + ''') + def test_print_deleted_stdout(self): # print should use strong reference to the stdout. @@ -282,5 +309,19 @@ def test_errors_unraisablehook(self): self.assertNotIn(b"Segmentation fault", err) self.assertNotIn(b"access violation", err) + def test_py_finalize_flush_std_files_stdout(self): + test_code = self.flush_std_files_common_code.format("stdout") + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + + def test_py_finalize_flush_std_files_stderr(self): + test_code = self.flush_std_files_common_code.format("stderr") + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + if __name__ == "__main__": unittest.main() From e65b17c4d13fe6af41f2674e04b66d80b0649a7c Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 22 Feb 2025 15:28:24 +0500 Subject: [PATCH 07/10] Fix tests for input --- Lib/test/test_sys_getattr.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index 391aa44b448288..7d56920b19413b 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -103,20 +103,20 @@ def __repr__(self): del stdout return "CrashStdout" - class CrashStderr: - def __init__(self): - self.stderr = sys.stderr - setattr(sys, "stderr", FakeIO()) + class CrashStderr: + def __init__(self): + self.stderr = sys.stderr + setattr(sys, "stderr", FakeIO()) - def __repr__(self): - stderr = sys.stderr - setattr(sys, "stderr", self.stderr) - del stderr - return "CrashStderr" - - def audit(event, args): - if event == 'builtins.input': - repr(args) + def __repr__(self): + stderr = sys.stderr + setattr(sys, "stderr", self.stderr) + del stderr + return "CrashStderr" + + def audit(event, args): + if event == 'builtins.input': + repr(args) def main(): {0} From 93d9fb765ff17e1a4e2f7c5af590bf783dd10a02 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sun, 23 Feb 2025 00:09:00 +0500 Subject: [PATCH 08/10] Add test for _PyErr_PrintEx --- Lib/test/test_sys_getattr.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index 7d56920b19413b..6e8b7a52521b6a 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -185,6 +185,31 @@ def main(): main() ''') + pyerr_printex_code = textwrap.dedent(''' + import sys + + class Hook: + def __call__(self, *args, **kwds): + pass + + def __repr__(self): + h = sys.excepthook + setattr(sys, 'excepthook', sys.__excepthook__) + del h + return 'Hook' + + def audit(event, args): + repr(args) + + def main(): + sys.addaudithook(audit) + setattr(sys, 'excepthook', Hook()) + raise + + if __name__ == "__main__": + main() + ''') + def test_print_deleted_stdout(self): # print should use strong reference to the stdout. @@ -323,5 +348,12 @@ def test_py_finalize_flush_std_files_stderr(self): self.assertNotIn(b"Segmentation fault", err) self.assertNotIn(b"access violation", err) + def test_pyerr_printex_excepthook(self): + test_code = self.pyerr_printex_code + rc, _, err = assert_python_ok('-c', test_code) + self.assertEqual(rc, 0) + self.assertNotIn(b"Segmentation fault", err) + self.assertNotIn(b"access violation", err) + if __name__ == "__main__": unittest.main() From 413940935f48019fb08c6aac51d43f31262be9f1 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sun, 23 Feb 2025 16:58:00 +0500 Subject: [PATCH 09/10] Simplify tests for print and add _check to unify tests --- Lib/test/test_sys_getattr.py | 194 ++++++++++++++--------------------- 1 file changed, 75 insertions(+), 119 deletions(-) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index 6e8b7a52521b6a..34041e20373883 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -12,37 +12,37 @@ class PySysGetAttrTest(unittest.TestCase): common_faulthandler_code = textwrap.dedent(''' - from contextlib import redirect_stderr + import sys from faulthandler import dump_traceback, enable, dump_traceback_later - from threading import Thread - import time - class FakeFile: - def __init__(self): - self.f = open("{0}", "w") - def write(self, s): - self.f.write(s) + class FakeIO: + def __init__(self, what): + self.what = what + def write(self, str): + pass def flush(self): - time.sleep(0.2) + pass def fileno(self): - time.sleep(0.2) - return self.f.fileno() + self.restore_std('stderr') + return 0 + + @staticmethod + def restore_std(what): + stdfile = getattr(sys, what) + setattr(sys, what, getattr(sys, "__" + what + "__")) + del stdfile - def thread1(): - text = FakeFile() - with redirect_stderr(text): - time.sleep(0.2) + @staticmethod + def set_std(what): + setattr(sys, what, FakeIO(what)) def main(): enable(None, True) - t1 = Thread(target=thread1, args=()) - t1.start() - time.sleep(0.1) - {1} + FakeIO.set_std('stderr') + {0} if __name__ == "__main__": main() - exit(0) ''') common_warnings_code = textwrap.dedent(''' @@ -210,150 +210,106 @@ def main(): main() ''') + print_code = textwrap.dedent(''' + from io import StringIO + import sys - def test_print_deleted_stdout(self): - # print should use strong reference to the stdout. - code = textwrap.dedent(''' - from io import StringIO - import sys - - class Bar: - def __init__(self): - self.x = sys.stdout - setattr(sys, "stdout", StringIO()) - - def __repr__(self): - x = sys.stdout - setattr(sys, "stdout", self.x) - del x - return "Bar" - - def main(): - print(Bar()) - - if __name__ == "__main__": - main() - exit(0) - ''') + class Bar: + def __init__(self): + self.x = sys.stdout + setattr(sys, "stdout", StringIO()) + + def __repr__(self): + x = sys.stdout + setattr(sys, "stdout", self.x) + del x + return "Bar" + + def main(): + print(Bar()) + + if __name__ == "__main__": + main() + exit(0) + ''') + + def _check(self, code): rc, out, err = assert_python_ok('-c', code) self.assertEqual(rc, 0) self.assertNotIn(b"Segmentation fault", err) self.assertNotIn(b"access violation", err) + def test_print_deleted_stdout(self): + # print should use strong reference to the stdout. + self._check(self.print_code) + def test_faulthandler_enable_deleted_stderr(self): # faulthandler should use strong reference to the stderr - with tempfile.TemporaryDirectory() as tmpdir: - path = Path(tmpdir, "test_faulthandler_enable") - test_code = self.common_faulthandler_code.format( - path.as_posix(), - "enable(None, True)" - ) - rc, out, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + code = self.common_faulthandler_code.format( + "enable(None, True)" + ) + self._check(code) def test_faulthandler_dump_traceback_deleted_stderr(self): # faulthandler should use strong reference to the stderr - with tempfile.TemporaryDirectory() as tmpdir: - path = Path(tmpdir, "test_faulthandler_dump_traceback") - test_code = self.common_faulthandler_code.format( - path.as_posix(), - "dump_traceback(None, False)" - ) - rc, out, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + code = self.common_faulthandler_code.format( + "dump_traceback(None, False)" + ) + self._check(code) def test_faulthandler_dump_traceback_later_deleted_stderr(self): # faulthandler should use strong reference to the stderr - with tempfile.TemporaryDirectory() as tmpdir: - path = Path(tmpdir, "test_faulthandler_dump_traceback_later") - test_code = self.common_faulthandler_code.format( - path.as_posix(), - "dump_traceback_later(0.1, True, None, False)" - ) - rc, out, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + code = self.common_faulthandler_code.format( + "dump_traceback_later(0.1, True, None, False)" + ) + self._check(code) def test_warnings_warn(self): - test_code = self.common_warnings_code.format( + code = self.common_warnings_code.format( "warnings.warn(Foo())" ) - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + self._check(code) def test_warnings_warn_explicit(self): - test_code = self.common_warnings_code.format( + code = self.common_warnings_code.format( "warnings.warn_explicit(Foo(), UserWarning, 'filename', 0)" ) - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + self._check(code) def test_input_stdin(self): - test_code = self.common_input_code.format( + code = self.common_input_code.format( "", "CrashStdin()" ) - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + self._check(code) def test_input_stdout(self): - test_code = self.common_input_code.format( + code = self.common_input_code.format( "", "CrashStdout()" ) - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + self._check(code) def test_input_stderr(self): - test_code = self.common_input_code.format( + code = self.common_input_code.format( "sys.addaudithook(audit)", "CrashStderr()" ) - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + self._check(code) def test_errors_unraisablehook(self): - test_code = self.unraisable_hook_code - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + self._check(self.unraisable_hook_code) def test_py_finalize_flush_std_files_stdout(self): - test_code = self.flush_std_files_common_code.format("stdout") - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + code = self.flush_std_files_common_code.format("stdout") + self._check(code) def test_py_finalize_flush_std_files_stderr(self): - test_code = self.flush_std_files_common_code.format("stderr") - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + code = self.flush_std_files_common_code.format("stderr") + self._check(code) def test_pyerr_printex_excepthook(self): - test_code = self.pyerr_printex_code - rc, _, err = assert_python_ok('-c', test_code) - self.assertEqual(rc, 0) - self.assertNotIn(b"Segmentation fault", err) - self.assertNotIn(b"access violation", err) + self._check(self.pyerr_printex_code) if __name__ == "__main__": unittest.main() From 9220dd20ba5ae80d5291b61fd9b87f7f2e088ff9 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Tue, 25 Feb 2025 01:55:27 +0500 Subject: [PATCH 10/10] Add comments to tests --- Lib/test/test_sys_getattr.py | 83 +++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/Lib/test/test_sys_getattr.py b/Lib/test/test_sys_getattr.py index 34041e20373883..c9956ad66e4442 100644 --- a/Lib/test/test_sys_getattr.py +++ b/Lib/test/test_sys_getattr.py @@ -15,6 +15,10 @@ class PySysGetAttrTest(unittest.TestCase): import sys from faulthandler import dump_traceback, enable, dump_traceback_later + # The faulthandler_get_fileno calls stderr functions twice - the first + # call is a fileno and the second one is a flush. So, if the stderr + # changed while fileno is called, then we get a segfault when we + # call flush after that. class FakeIO: def __init__(self, what): self.what = what @@ -50,6 +54,12 @@ def main(): import sys import warnings + # First of all, we have to delete _showwarnmsg to get into show_warning + # function. The show_warning do series of calls of PyFile_WriteObject + # and PyFile_WriteString functions. And one of the call is for __repr__ + # of warning's message. So if we change stderr while calling __repr__ + # (or concurently) we can get segfault from one of the consequence call + # to write functions. class Foo: def __init__(self): self.x = sys.stderr @@ -78,45 +88,28 @@ def write(self, str): pass def flush(self): pass - def fileno(self): - return 0 - - class CrashStdin: - def __init__(self): - self.stdin = sys.stdin - setattr(sys, "stdin", FakeIO()) - - def __repr__(self): - stdin = sys.stdin - setattr(sys, "stdin", self.stdin) - del stdin - return "CrashStdin" - - class CrashStdout: - def __init__(self): - self.stdout = sys.stdout - setattr(sys, "stdout", FakeIO()) - def __repr__(self): - stdout = sys.stdout - setattr(sys, "stdout", self.stdout) - del stdout - return "CrashStdout" - - class CrashStderr: - def __init__(self): - self.stderr = sys.stderr - setattr(sys, "stderr", FakeIO()) + # The input function gets borrowed refs for stdin, stdout and stderr. + # As we use FakeIO without fileno the input functions thinks that we + # are not tty and following happens: + # audit is called, stderr is flushed, prompt's __repr__ is printed to + # stdout and line is read from stdin. For stdin and stdout we can just + # replace stdin and stdout from prompt's __repr__ and get segfault. But + # for stderr we should do this from audit function. + class CrashWhat: + def __init__(self, what): + self.what = what + self.std = getattr(sys, what) + setattr(sys, what, FakeIO()) def __repr__(self): - stderr = sys.stderr - setattr(sys, "stderr", self.stderr) - del stderr - return "CrashStderr" + std = getattr(sys, self.what) + setattr(sys, self.what, self.std) + del std + return "Crash" def audit(event, args): - if event == 'builtins.input': - repr(args) + repr(args) def main(): {0} @@ -134,6 +127,10 @@ class UnraisableHookInitiator: def __del__(self): raise Exception('1') + # To get into unraisablehook we need to raise an exception from __del__ + # function. So, format_unraisable_v gets hook, calls audit + # function and calls hook. If we revert back unraisablehook from audit + # function we will get segfault when calling hook. class UnraisableHook: def __call__(self, *args, **kwds): print('X', *args) @@ -161,6 +158,9 @@ def main(): flush_std_files_common_code = textwrap.dedent(''' import sys + # The flush_std_files function gets stdout and stderr. And then checks + # if both of them are closed. And if so calls flush for them. + # If we restore stdfile from FakeIO.closed property we can get segfault. class FakeIO: def __init__(self, what): self.what = what @@ -168,8 +168,6 @@ def write(self, str): pass def flush(self): pass - def fileno(self): - return 0 @property def closed(self): @@ -188,6 +186,10 @@ def main(): pyerr_printex_code = textwrap.dedent(''' import sys + # To get into excepthook we should run invalid statement. + # Then _PyErr_PrintEx gets excepthook, calls audit function and tries + # to call hook. If we replace hook from audit (or concurently) we get + # segfault. class Hook: def __call__(self, *args, **kwds): pass @@ -214,6 +216,9 @@ def main(): from io import StringIO import sys + # The print function gets stdout and does a series of calls write + # functions. One of the function calls __repr__ and if we replace + # stdout from __repr__ (or concurently) we get segfault. class Bar: def __init__(self): self.x = sys.stdout @@ -279,21 +284,21 @@ def test_warnings_warn_explicit(self): def test_input_stdin(self): code = self.common_input_code.format( "", - "CrashStdin()" + "CrashWhat('stdin')" ) self._check(code) def test_input_stdout(self): code = self.common_input_code.format( "", - "CrashStdout()" + "CrashWhat('stdout')" ) self._check(code) def test_input_stderr(self): code = self.common_input_code.format( "sys.addaudithook(audit)", - "CrashStderr()" + "CrashWhat('stderr')" ) self._check(code)