]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-152789: Skip curses tests needing newterm() on ncurses before 6.5 (GH-152880)
authorSerhiy Storchaka <storchaka@gmail.com>
Thu, 2 Jul 2026 14:07:30 +0000 (17:07 +0300)
committerGitHub <noreply@github.com>
Thu, 2 Jul 2026 14:07:30 +0000 (17:07 +0300)
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 <noreply@anthropic.com>
Lib/test/test_curses.py

index 5015b053ff29fac75327034280847117ce28e0ba..5fe2850460717b804b63d0360618d359d378d97d 100644 (file)
@@ -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",