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

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 607501abb4)
This commit is contained in:
Serhiy Storchaka 2021-01-03 22:54:44 +02:00 committed by GitHub
parent bfc413ce4f
commit 0303008ebc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 63 additions and 39 deletions

View File

@ -47,37 +47,57 @@ class TestCurses(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): 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 # testing setupterm() inside initscr/endwin
# causes terminal breakage # causes terminal breakage
curses.setupterm(fd=fd) stdout_fd = sys.__stdout__.fileno()
curses.setupterm(fd=stdout_fd)
@classmethod
def tearDownClass(cls):
if cls.tmp:
cls.tmp.close()
del cls.tmp
def setUp(self): 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 = SaveSignals()
self.save_signals.save() 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 # 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() self.stdscr = curses.initscr()
if self.isatty:
curses.savetty() curses.savetty()
self.addCleanup(curses.endwin)
def tearDown(self): self.addCleanup(curses.resetty)
curses.resetty()
curses.endwin()
self.save_signals.restore()
def test_window_funcs(self): def test_window_funcs(self):
"Test the methods of windows" "Test the methods of windows"
@ -95,7 +115,7 @@ class TestCurses(unittest.TestCase):
for meth in [stdscr.clear, stdscr.clrtobot, for meth in [stdscr.clear, stdscr.clrtobot,
stdscr.clrtoeol, stdscr.cursyncup, stdscr.delch, stdscr.clrtoeol, stdscr.cursyncup, stdscr.delch,
stdscr.deleteln, stdscr.erase, stdscr.getbegyx, stdscr.deleteln, stdscr.erase, stdscr.getbegyx,
stdscr.getbkgd, stdscr.getkey, stdscr.getmaxyx, stdscr.getbkgd, stdscr.getmaxyx,
stdscr.getparyx, stdscr.getyx, stdscr.inch, stdscr.getparyx, stdscr.getyx, stdscr.inch,
stdscr.insertln, stdscr.instr, stdscr.is_wintouched, stdscr.insertln, stdscr.instr, stdscr.is_wintouched,
win.noutrefresh, stdscr.redrawwin, stdscr.refresh, win.noutrefresh, stdscr.redrawwin, stdscr.refresh,
@ -206,6 +226,11 @@ class TestCurses(unittest.TestCase):
if hasattr(stdscr, 'enclose'): if hasattr(stdscr, 'enclose'):
stdscr.enclose(10, 10) 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, -400)
self.assertRaises(ValueError, stdscr.getstr, 2, 3, -400) self.assertRaises(ValueError, stdscr.getstr, 2, 3, -400)
self.assertRaises(ValueError, stdscr.instr, -2) self.assertRaises(ValueError, stdscr.instr, -2)
@ -224,14 +249,17 @@ class TestCurses(unittest.TestCase):
def test_module_funcs(self): def test_module_funcs(self):
"Test module-level functions" "Test module-level functions"
for func in [curses.baudrate, curses.beep, curses.can_change_color, for func in [curses.baudrate, curses.beep, curses.can_change_color,
curses.cbreak, curses.def_prog_mode, curses.doupdate, curses.doupdate, curses.flash, curses.flushinp,
curses.flash, curses.flushinp,
curses.has_colors, curses.has_ic, curses.has_il, curses.has_colors, curses.has_ic, curses.has_il,
curses.isendwin, curses.killchar, curses.longname, curses.isendwin, curses.killchar, curses.longname,
curses.nocbreak, curses.noecho, curses.nonl, curses.noecho, curses.nonl, curses.noqiflush,
curses.noqiflush, curses.noraw, curses.termattrs, curses.termname, curses.erasechar]:
curses.reset_prog_mode, curses.termattrs, with self.subTest(func=func.__qualname__):
curses.termname, curses.erasechar]: 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__): with self.subTest(func=func.__qualname__):
func() func()
if hasattr(curses, 'filter'): if hasattr(curses, 'filter'):
@ -245,12 +273,8 @@ class TestCurses(unittest.TestCase):
curses.delay_output(1) curses.delay_output(1)
curses.echo() ; curses.echo(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.halfdelay(1)
if self.isatty:
curses.intrflush(1) curses.intrflush(1)
curses.meta(1) curses.meta(1)
curses.napms(100) curses.napms(100)
@ -260,6 +284,7 @@ class TestCurses(unittest.TestCase):
curses.nl() ; curses.nl(1) curses.nl() ; curses.nl(1)
curses.putp(b'abc') curses.putp(b'abc')
curses.qiflush() curses.qiflush()
if self.isatty:
curses.raw() ; curses.raw(1) curses.raw() ; curses.raw(1)
curses.set_escdelay(25) curses.set_escdelay(25)
self.assertEqual(curses.get_escdelay(), 25) self.assertEqual(curses.get_escdelay(), 25)
@ -286,7 +311,7 @@ class TestCurses(unittest.TestCase):
curses.init_pair(2, 1,1) curses.init_pair(2, 1,1)
curses.color_content(1) curses.color_content(1)
curses.color_pair(2) curses.color_pair(2)
curses.pair_content(curses.COLOR_PAIRS - 1) curses.pair_content(min(curses.COLOR_PAIRS - 1, 0x7fff))
curses.pair_number(0) curses.pair_number(0)
if hasattr(curses, 'use_default_colors'): if hasattr(curses, 'use_default_colors'):
@ -358,7 +383,6 @@ class TestCurses(unittest.TestCase):
@requires_curses_func('resizeterm') @requires_curses_func('resizeterm')
def test_resizeterm(self): def test_resizeterm(self):
stdscr = self.stdscr
lines, cols = curses.LINES, curses.COLS lines, cols = curses.LINES, curses.COLS
new_lines = lines - 1 new_lines = lines - 1
new_cols = cols + 1 new_cols = cols + 1