[Python-checkins] [3.8] [3.9] bpo-42789: Don't skip curses tests on non-tty. (GH-24009) (GH-24076) (GH-24078)

serhiy-storchaka webhook-mailer at python.org
Sun Jan 3 17:54:27 EST 2021


https://github.com/python/cpython/commit/645174abe0d13cce2cb339cc80b095ad484428ea
commit: 645174abe0d13cce2cb339cc80b095ad484428ea
branch: 3.8
author: Serhiy Storchaka <storchaka at gmail.com>
committer: serhiy-storchaka <storchaka at gmail.com>
date: 2021-01-04T00:54:13+02:00
summary:

[3.8] [3.9] bpo-42789: Don't skip curses tests on non-tty. (GH-24009) (GH-24076) (GH-24078)

If __stdout__ is not attached to terminal, try to use __stderr__
if it is attached to terminal, or open the terminal device, or
use regular file as terminal, but some functions will be untested
in the latter case.
(cherry picked from commit 607501abb488fb37e33cf9d35260ab7baefa192f)
(cherry picked from commit 0303008ebceb6ac6035cd9722d1393267304171d)

files:
M Lib/test/test_curses.py

diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index 09738c8a41c94..b7349d9e42a5d 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -47,37 +47,57 @@ class TestCurses(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        if not sys.__stdout__.isatty():
-            # Temporary skip tests on non-tty
-            raise unittest.SkipTest('sys.__stdout__ is not a tty')
-            cls.tmp = tempfile.TemporaryFile()
-            fd = cls.tmp.fileno()
-        else:
-            cls.tmp = None
-            fd = sys.__stdout__.fileno()
         # testing setupterm() inside initscr/endwin
         # causes terminal breakage
-        curses.setupterm(fd=fd)
-
-    @classmethod
-    def tearDownClass(cls):
-        if cls.tmp:
-            cls.tmp.close()
-            del cls.tmp
+        stdout_fd = sys.__stdout__.fileno()
+        curses.setupterm(fd=stdout_fd)
 
     def setUp(self):
+        self.isatty = True
+        self.output = sys.__stdout__
+        stdout_fd = sys.__stdout__.fileno()
+        if not sys.__stdout__.isatty():
+            # initstr() unconditionally uses C stdout.
+            # If it is redirected to file or pipe, try to attach it
+            # to terminal.
+            # First, save a copy of the file descriptor of stdout, so it
+            # can be restored after finishing the test.
+            dup_fd = os.dup(stdout_fd)
+            self.addCleanup(os.close, dup_fd)
+            self.addCleanup(os.dup2, dup_fd, stdout_fd)
+
+            if sys.__stderr__.isatty():
+                # If stderr is connected to terminal, use it.
+                tmp = sys.__stderr__
+                self.output = sys.__stderr__
+            else:
+                try:
+                    # Try to open the terminal device.
+                    tmp = open('/dev/tty', 'wb', buffering=0)
+                except OSError:
+                    # As a fallback, use regular file to write control codes.
+                    # Some functions (like savetty) will not work, but at
+                    # least the garbage control sequences will not be mixed
+                    # with the testing report.
+                    tmp = tempfile.TemporaryFile(mode='wb', buffering=0)
+                    self.isatty = False
+                self.addCleanup(tmp.close)
+                self.output = None
+            os.dup2(tmp.fileno(), stdout_fd)
+
         self.save_signals = SaveSignals()
         self.save_signals.save()
-        if verbose:
+        self.addCleanup(self.save_signals.restore)
+        if verbose and self.output is not None:
             # just to make the test output a little more readable
-            print()
+            sys.stderr.flush()
+            sys.stdout.flush()
+            print(file=self.output, flush=True)
         self.stdscr = curses.initscr()
-        curses.savetty()
-
-    def tearDown(self):
-        curses.resetty()
-        curses.endwin()
-        self.save_signals.restore()
+        if self.isatty:
+            curses.savetty()
+            self.addCleanup(curses.endwin)
+            self.addCleanup(curses.resetty)
 
     def test_window_funcs(self):
         "Test the methods of windows"
@@ -95,7 +115,7 @@ def test_window_funcs(self):
         for meth in [stdscr.clear, stdscr.clrtobot,
                      stdscr.clrtoeol, stdscr.cursyncup, stdscr.delch,
                      stdscr.deleteln, stdscr.erase, stdscr.getbegyx,
-                     stdscr.getbkgd, stdscr.getkey, stdscr.getmaxyx,
+                     stdscr.getbkgd, stdscr.getmaxyx,
                      stdscr.getparyx, stdscr.getyx, stdscr.inch,
                      stdscr.insertln, stdscr.instr, stdscr.is_wintouched,
                      win.noutrefresh, stdscr.redrawwin, stdscr.refresh,
@@ -206,6 +226,11 @@ def test_window_funcs(self):
         if hasattr(stdscr, 'enclose'):
             stdscr.enclose(10, 10)
 
+        with tempfile.TemporaryFile() as f:
+            self.stdscr.putwin(f)
+            f.seek(0)
+            curses.getwin(f)
+
         self.assertRaises(ValueError, stdscr.getstr, -400)
         self.assertRaises(ValueError, stdscr.getstr, 2, 3, -400)
         self.assertRaises(ValueError, stdscr.instr, -2)
@@ -224,16 +249,19 @@ def test_embedded_null_chars(self):
     def test_module_funcs(self):
         "Test module-level functions"
         for func in [curses.baudrate, curses.beep, curses.can_change_color,
-                     curses.cbreak, curses.def_prog_mode, curses.doupdate,
-                     curses.flash, curses.flushinp,
+                     curses.doupdate, curses.flash, curses.flushinp,
                      curses.has_colors, curses.has_ic, curses.has_il,
                      curses.isendwin, curses.killchar, curses.longname,
-                     curses.nocbreak, curses.noecho, curses.nonl,
-                     curses.noqiflush, curses.noraw,
-                     curses.reset_prog_mode, curses.termattrs,
-                     curses.termname, curses.erasechar]:
+                     curses.noecho, curses.nonl, curses.noqiflush,
+                     curses.termattrs, curses.termname, curses.erasechar]:
             with self.subTest(func=func.__qualname__):
                 func()
+        if self.isatty:
+            for func in [curses.cbreak, curses.def_prog_mode,
+                         curses.nocbreak, curses.noraw,
+                         curses.reset_prog_mode]:
+                with self.subTest(func=func.__qualname__):
+                    func()
         if hasattr(curses, 'filter'):
             curses.filter()
         if hasattr(curses, 'getsyx'):
@@ -245,13 +273,9 @@ def test_module_funcs(self):
         curses.delay_output(1)
         curses.echo() ; curses.echo(1)
 
-        with tempfile.TemporaryFile() as f:
-            self.stdscr.putwin(f)
-            f.seek(0)
-            curses.getwin(f)
-
         curses.halfdelay(1)
-        curses.intrflush(1)
+        if self.isatty:
+            curses.intrflush(1)
         curses.meta(1)
         curses.napms(100)
         curses.newpad(50,50)
@@ -260,7 +284,8 @@ def test_module_funcs(self):
         curses.nl() ; curses.nl(1)
         curses.putp(b'abc')
         curses.qiflush()
-        curses.raw() ; curses.raw(1)
+        if self.isatty:
+            curses.raw() ; curses.raw(1)
         if hasattr(curses, 'setsyx'):
             curses.setsyx(5,5)
         curses.tigetflag('hc')
@@ -282,7 +307,7 @@ def test_colors_funcs(self):
         curses.init_pair(2, 1,1)
         curses.color_content(1)
         curses.color_pair(2)
-        curses.pair_content(curses.COLOR_PAIRS - 1)
+        curses.pair_content(min(curses.COLOR_PAIRS - 1, 0x7fff))
         curses.pair_number(0)
 
         if hasattr(curses, 'use_default_colors'):
@@ -354,7 +379,6 @@ def test_resize_term(self):
 
     @requires_curses_func('resizeterm')
     def test_resizeterm(self):
-        stdscr = self.stdscr
         lines, cols = curses.LINES, curses.COLS
         new_lines = lines - 1
         new_cols = cols + 1



More information about the Python-checkins mailing list