From f1f9922f7f36ec48e356b606df9e2a0a2ec4872d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 2 Jul 2026 15:16:12 +0300 Subject: [PATCH] gh-152789: Skip curses tests needing newterm() on ncurses before 6.5 ncurses before 6.5 can crash on repeated newterm(). Fall back to initscr() in TestCurses and skip ScreenTests and SLKTests, which need several screens. The shared initscr() screen also needs its rendition and background reset between tests. Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_curses.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 5015b053ff29fa..5fe2850460717b 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -80,6 +80,12 @@ def wrapped(self, *args, **kwargs): term = os.environ.get('TERM') SHORT_MAX = 0x7fff +# ncurses before 6.5 can crash on repeated newterm(). Fall back to initscr() +# and skip the tests that need several screens. +_ncurses_version = getattr(curses, 'ncurses_version', None) +BROKEN_NEWTERM = _ncurses_version is not None and _ncurses_version < (6, 5) +USE_NEWTERM = hasattr(curses, 'newterm') and not BROKEN_NEWTERM + # newterm() is used when available (it reports errors instead of exiting), but # initscr() is still the fallback, and an unusable $TERM has no terminal to # drive either way. @@ -139,7 +145,7 @@ def setUp(self): sys.stderr.flush() sys.stdout.flush() print(file=self.output, flush=True) - if hasattr(curses, 'newterm'): + if USE_NEWTERM: # Use newterm() rather than initscr(): it reports errors instead of # exiting, and gives each test a fresh screen, which also lets # ScreenTests run newterm()/set_term() in the same process. @@ -157,7 +163,11 @@ def setUp(self): self.addCleanup(setattr, self, 'screen', None) self.addCleanup(setattr, self, 'stdscr', None) else: + # Tests share one initscr() screen; clear the rendition and + # background so a previous test's does not bleed in. self.stdscr = curses.initscr() + self.stdscr.attrset(curses.A_NORMAL) + self.stdscr.bkgdset(' ') if self.isatty: curses.savetty() self.addCleanup(curses.endwin) @@ -2026,6 +2036,7 @@ def test_use_window(self): @unittest.skipUnless(hasattr(curses.screen, 'use'), 'requires screen.use()') + @unittest.skipUnless(USE_NEWTERM, 'no screen object without newterm()') def test_use_screen(self): screen = self.screen self.assertEqual( @@ -2913,6 +2924,7 @@ def stop_reader(): @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') +@unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') @unittest.skipIf(not term or term == 'unknown', f"$TERM={term!r}, newterm() may not work") @unittest.skipIf(sys.platform == "cygwin", @@ -2994,6 +3006,7 @@ def test_disallow_instantiation(self): @unittest.skipUnless(hasattr(curses, 'slk_init'), 'requires curses.slk_init()') @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') +@unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') @unittest.skipIf(not term or term == 'unknown', f"$TERM={term!r}, newterm() may not work") @unittest.skipIf(sys.platform == "cygwin",