Skip to content

Commit 607501a

Browse files
bpo-42789: Don't skip curses tests on non-tty. (GH-24009)
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.
1 parent a25011b commit 607501a

File tree

1 file changed

+62
-38
lines changed

1 file changed

+62
-38
lines changed

Lib/test/test_curses.py

+62-38
Original file line numberDiff line numberDiff line change
@@ -48,37 +48,57 @@ class TestCurses(unittest.TestCase):
4848

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

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

83103
def test_window_funcs(self):
84104
"Test the methods of windows"
@@ -96,7 +116,7 @@ def test_window_funcs(self):
96116
for meth in [stdscr.clear, stdscr.clrtobot,
97117
stdscr.clrtoeol, stdscr.cursyncup, stdscr.delch,
98118
stdscr.deleteln, stdscr.erase, stdscr.getbegyx,
99-
stdscr.getbkgd, stdscr.getkey, stdscr.getmaxyx,
119+
stdscr.getbkgd, stdscr.getmaxyx,
100120
stdscr.getparyx, stdscr.getyx, stdscr.inch,
101121
stdscr.insertln, stdscr.instr, stdscr.is_wintouched,
102122
win.noutrefresh, stdscr.redrawwin, stdscr.refresh,
@@ -207,6 +227,11 @@ def test_window_funcs(self):
207227
if hasattr(stdscr, 'enclose'):
208228
stdscr.enclose(10, 10)
209229

230+
with tempfile.TemporaryFile() as f:
231+
self.stdscr.putwin(f)
232+
f.seek(0)
233+
curses.getwin(f)
234+
210235
self.assertRaises(ValueError, stdscr.getstr, -400)
211236
self.assertRaises(ValueError, stdscr.getstr, 2, 3, -400)
212237
self.assertRaises(ValueError, stdscr.instr, -2)
@@ -225,17 +250,20 @@ def test_embedded_null_chars(self):
225250
def test_module_funcs(self):
226251
"Test module-level functions"
227252
for func in [curses.baudrate, curses.beep, curses.can_change_color,
228-
curses.cbreak, curses.def_prog_mode, curses.doupdate,
229-
curses.flash, curses.flushinp,
253+
curses.doupdate, curses.flash, curses.flushinp,
230254
curses.has_colors, curses.has_ic, curses.has_il,
231255
curses.isendwin, curses.killchar, curses.longname,
232-
curses.nocbreak, curses.noecho, curses.nonl,
233-
curses.noqiflush, curses.noraw,
234-
curses.reset_prog_mode, curses.termattrs,
235-
curses.termname, curses.erasechar,
256+
curses.noecho, curses.nonl, curses.noqiflush,
257+
curses.termattrs, curses.termname, curses.erasechar,
236258
curses.has_extended_color_support]:
237259
with self.subTest(func=func.__qualname__):
238260
func()
261+
if self.isatty:
262+
for func in [curses.cbreak, curses.def_prog_mode,
263+
curses.nocbreak, curses.noraw,
264+
curses.reset_prog_mode]:
265+
with self.subTest(func=func.__qualname__):
266+
func()
239267
if hasattr(curses, 'filter'):
240268
curses.filter()
241269
if hasattr(curses, 'getsyx'):
@@ -247,13 +275,9 @@ def test_module_funcs(self):
247275
curses.delay_output(1)
248276
curses.echo() ; curses.echo(1)
249277

250-
with tempfile.TemporaryFile() as f:
251-
self.stdscr.putwin(f)
252-
f.seek(0)
253-
curses.getwin(f)
254-
255278
curses.halfdelay(1)
256-
curses.intrflush(1)
279+
if self.isatty:
280+
curses.intrflush(1)
257281
curses.meta(1)
258282
curses.napms(100)
259283
curses.newpad(50,50)
@@ -262,7 +286,8 @@ def test_module_funcs(self):
262286
curses.nl() ; curses.nl(1)
263287
curses.putp(b'abc')
264288
curses.qiflush()
265-
curses.raw() ; curses.raw(1)
289+
if self.isatty:
290+
curses.raw() ; curses.raw(1)
266291
curses.set_escdelay(25)
267292
self.assertEqual(curses.get_escdelay(), 25)
268293
curses.set_tabsize(4)
@@ -373,7 +398,6 @@ def test_resize_term(self):
373398

374399
@requires_curses_func('resizeterm')
375400
def test_resizeterm(self):
376-
stdscr = self.stdscr
377401
lines, cols = curses.LINES, curses.COLS
378402
new_lines = lines - 1
379403
new_cols = cols + 1

0 commit comments

Comments
 (0)