Skip to content

Commit 645174a

Browse files
[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 607501a) (cherry picked from commit 0303008)
1 parent 2e8b1c9 commit 645174a

File tree

1 file changed

+63
-39
lines changed

1 file changed

+63
-39
lines changed

Lib/test/test_curses.py

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -47,37 +47,57 @@ class TestCurses(unittest.TestCase):
4747

4848
@classmethod
4949
def setUpClass(cls):
50-
if not sys.__stdout__.isatty():
51-
# Temporary skip tests on non-tty
52-
raise unittest.SkipTest('sys.__stdout__ is not a tty')
53-
cls.tmp = tempfile.TemporaryFile()
54-
fd = cls.tmp.fileno()
55-
else:
56-
cls.tmp = None
57-
fd = sys.__stdout__.fileno()
5850
# testing setupterm() inside initscr/endwin
5951
# causes terminal breakage
60-
curses.setupterm(fd=fd)
61-
62-
@classmethod
63-
def tearDownClass(cls):
64-
if cls.tmp:
65-
cls.tmp.close()
66-
del cls.tmp
52+
stdout_fd = sys.__stdout__.fileno()
53+
curses.setupterm(fd=stdout_fd)
6754

6855
def setUp(self):
56+
self.isatty = True
57+
self.output = sys.__stdout__
58+
stdout_fd = sys.__stdout__.fileno()
59+
if not sys.__stdout__.isatty():
60+
# initstr() unconditionally uses C stdout.
61+
# If it is redirected to file or pipe, try to attach it
62+
# to terminal.
63+
# First, save a copy of the file descriptor of stdout, so it
64+
# can be restored after finishing the test.
65+
dup_fd = os.dup(stdout_fd)
66+
self.addCleanup(os.close, dup_fd)
67+
self.addCleanup(os.dup2, dup_fd, stdout_fd)
68+
69+
if sys.__stderr__.isatty():
70+
# If stderr is connected to terminal, use it.
71+
tmp = sys.__stderr__
72+
self.output = sys.__stderr__
73+
else:
74+
try:
75+
# Try to open the terminal device.
76+
tmp = open('/dev/tty', 'wb', buffering=0)
77+
except OSError:
78+
# As a fallback, use regular file to write control codes.
79+
# Some functions (like savetty) will not work, but at
80+
# least the garbage control sequences will not be mixed
81+
# with the testing report.
82+
tmp = tempfile.TemporaryFile(mode='wb', buffering=0)
83+
self.isatty = False
84+
self.addCleanup(tmp.close)
85+
self.output = None
86+
os.dup2(tmp.fileno(), stdout_fd)
87+
6988
self.save_signals = SaveSignals()
7089
self.save_signals.save()
71-
if verbose:
90+
self.addCleanup(self.save_signals.restore)
91+
if verbose and self.output is not None:
7292
# just to make the test output a little more readable
73-
print()
93+
sys.stderr.flush()
94+
sys.stdout.flush()
95+
print(file=self.output, flush=True)
7496
self.stdscr = curses.initscr()
75-
curses.savetty()
76-
77-
def tearDown(self):
78-
curses.resetty()
79-
curses.endwin()
80-
self.save_signals.restore()
97+
if self.isatty:
98+
curses.savetty()
99+
self.addCleanup(curses.endwin)
100+
self.addCleanup(curses.resetty)
81101

82102
def test_window_funcs(self):
83103
"Test the methods of windows"
@@ -95,7 +115,7 @@ def test_window_funcs(self):
95115
for meth in [stdscr.clear, stdscr.clrtobot,
96116
stdscr.clrtoeol, stdscr.cursyncup, stdscr.delch,
97117
stdscr.deleteln, stdscr.erase, stdscr.getbegyx,
98-
stdscr.getbkgd, stdscr.getkey, stdscr.getmaxyx,
118+
stdscr.getbkgd, stdscr.getmaxyx,
99119
stdscr.getparyx, stdscr.getyx, stdscr.inch,
100120
stdscr.insertln, stdscr.instr, stdscr.is_wintouched,
101121
win.noutrefresh, stdscr.redrawwin, stdscr.refresh,
@@ -206,6 +226,11 @@ def test_window_funcs(self):
206226
if hasattr(stdscr, 'enclose'):
207227
stdscr.enclose(10, 10)
208228

229+
with tempfile.TemporaryFile() as f:
230+
self.stdscr.putwin(f)
231+
f.seek(0)
232+
curses.getwin(f)
233+
209234
self.assertRaises(ValueError, stdscr.getstr, -400)
210235
self.assertRaises(ValueError, stdscr.getstr, 2, 3, -400)
211236
self.assertRaises(ValueError, stdscr.instr, -2)
@@ -224,16 +249,19 @@ def test_embedded_null_chars(self):
224249
def test_module_funcs(self):
225250
"Test module-level functions"
226251
for func in [curses.baudrate, curses.beep, curses.can_change_color,
227-
curses.cbreak, curses.def_prog_mode, curses.doupdate,
228-
curses.flash, curses.flushinp,
252+
curses.doupdate, curses.flash, curses.flushinp,
229253
curses.has_colors, curses.has_ic, curses.has_il,
230254
curses.isendwin, curses.killchar, curses.longname,
231-
curses.nocbreak, curses.noecho, curses.nonl,
232-
curses.noqiflush, curses.noraw,
233-
curses.reset_prog_mode, curses.termattrs,
234-
curses.termname, curses.erasechar]:
255+
curses.noecho, curses.nonl, curses.noqiflush,
256+
curses.termattrs, curses.termname, curses.erasechar]:
235257
with self.subTest(func=func.__qualname__):
236258
func()
259+
if self.isatty:
260+
for func in [curses.cbreak, curses.def_prog_mode,
261+
curses.nocbreak, curses.noraw,
262+
curses.reset_prog_mode]:
263+
with self.subTest(func=func.__qualname__):
264+
func()
237265
if hasattr(curses, 'filter'):
238266
curses.filter()
239267
if hasattr(curses, 'getsyx'):
@@ -245,13 +273,9 @@ def test_module_funcs(self):
245273
curses.delay_output(1)
246274
curses.echo() ; curses.echo(1)
247275

248-
with tempfile.TemporaryFile() as f:
249-
self.stdscr.putwin(f)
250-
f.seek(0)
251-
curses.getwin(f)
252-
253276
curses.halfdelay(1)
254-
curses.intrflush(1)
277+
if self.isatty:
278+
curses.intrflush(1)
255279
curses.meta(1)
256280
curses.napms(100)
257281
curses.newpad(50,50)
@@ -260,7 +284,8 @@ def test_module_funcs(self):
260284
curses.nl() ; curses.nl(1)
261285
curses.putp(b'abc')
262286
curses.qiflush()
263-
curses.raw() ; curses.raw(1)
287+
if self.isatty:
288+
curses.raw() ; curses.raw(1)
264289
if hasattr(curses, 'setsyx'):
265290
curses.setsyx(5,5)
266291
curses.tigetflag('hc')
@@ -282,7 +307,7 @@ def test_colors_funcs(self):
282307
curses.init_pair(2, 1,1)
283308
curses.color_content(1)
284309
curses.color_pair(2)
285-
curses.pair_content(curses.COLOR_PAIRS - 1)
310+
curses.pair_content(min(curses.COLOR_PAIRS - 1, 0x7fff))
286311
curses.pair_number(0)
287312

288313
if hasattr(curses, 'use_default_colors'):
@@ -354,7 +379,6 @@ def test_resize_term(self):
354379

355380
@requires_curses_func('resizeterm')
356381
def test_resizeterm(self):
357-
stdscr = self.stdscr
358382
lines, cols = curses.LINES, curses.COLS
359383
new_lines = lines - 1
360384
new_cols = cols + 1

0 commit comments

Comments
 (0)