From: Serhiy Storchaka Date: Thu, 2 Jul 2026 14:07:30 +0000 (+0300) Subject: gh-152789: Skip curses tests needing newterm() on ncurses before 6.5 (GH-152880) X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=3428959a3c67138ab56b181953f429b7e1a4ab8b;p=thirdparty%2FPython%2Fcpython.git gh-152789: Skip curses tests needing newterm() on ncurses before 6.5 (GH-152880) 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 --- diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 5015b053ff29..5fe285046071 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -80,6 +80,12 @@ def requires_colors(test): 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 @@ class TestCurses(unittest.TestCase): 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 @@ class TestCurses(unittest.TestCase): 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 @@ class TestCurses(unittest.TestCase): @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 @@ class NewtermTestBase(unittest.TestCase): @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 @@ class ScreenTests(NewtermTestBase): @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",