Skip to content

Commit 3fe7a97

Browse files
committed
Keep the job-control test's terminal setup out of bash's way
Two intermittent failures of test_pipeline_stops_with_cmd2_and_returns_terminal under parallel load, neither caused by cmd2: - Interactive bash intermittently stops a job it has just started on a TOSTOP terminal, before the job runs any code of its own. The test set TOSTOP before starting bash in its exit_sigint cases, so the launch of the application, or of printf or stty during a stop, was sometimes reported Stopped. Reproduced without cmd2: across 14 parallel bash sessions launching Python, 10 of 2,240 launches stopped with TOSTOP set and none of 2,240 without it. The test now sets TOSTOP only once the pager owns the terminal, and again after fg: while the job is stopped, bash restores its own terminal modes, so its commands run without it. TOSTOP is still in effect when Ctrl-C is sent, which is what the test needs it for. - The pseudo-terminal occasionally reported its old size after the test resized it while the job was stopped, though every process of the job was stopped and nothing else sets a size. The test now confirms the new size through the shell and resizes again if the shell saw the old one, reporting every attempt if it never takes.
1 parent 50fab1f commit 3fe7a97

1 file changed

Lines changed: 35 additions & 11 deletions

File tree

‎tests/test_pipeline_job_control.py‎

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,6 @@ def test_pipeline_stops_with_cmd2_and_returns_terminal(
183183
)
184184
master, slave = pty.openpty()
185185
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
186-
if finish == "exit_sigint":
187-
settings = termios.tcgetattr(slave)
188-
settings[3] |= termios.TOSTOP
189-
termios.tcsetattr(slave, termios.TCSANOW, settings)
190186
# Establish a controlling terminal in a fresh interpreter, avoiding preexec_fn
191187
# (unsafe when pytest or its plugins have started threads).
192188
bootstrap = (
@@ -216,6 +212,18 @@ def terminal_size() -> tuple[int, int]:
216212
def send(data):
217213
os.write(master, data.encode())
218214

215+
def stop_background_output():
216+
"""Set TOSTOP, so that a write from the background stops the writer.
217+
218+
Ctrl-C's handler may write while cmd2 has lent the terminal to the pager. Set it only
219+
while cmd2's command runs: bash intermittently stops a job it has just started on a
220+
TOSTOP terminal, before the job runs a line of its own. When the job stops, bash
221+
restores its own terminal modes, so its commands during the stop run without it.
222+
"""
223+
settings = termios.tcgetattr(master)
224+
settings[3] |= termios.TOSTOP
225+
termios.tcsetattr(master, termios.TCSANOW, settings)
226+
219227
def wait_until(predicate):
220228
nonlocal transcript
221229
deadline = time.monotonic() + 10
@@ -317,6 +325,8 @@ def stopped(*pids: int) -> bool:
317325
# Readiness output can precede the foreground handoff. Send terminal
318326
# signals and keystrokes only once the pipeline can receive them.
319327
wait_until(lambda: os.tcgetpgrp(master) == pipeline_group)
328+
if finish == "exit_sigint":
329+
stop_background_output()
320330
if launcher == "exec":
321331
# There is no outer shell to run fg: Ctrl-Z must leave the pager
322332
# usable. Require a fresh read acknowledgement, not a SIGCONT.
@@ -332,18 +342,32 @@ def stopped(*pids: int) -> bool:
332342
# A child left running can steal these keystrokes from the shell.
333343
send("printf 'SHELL_%s\\n' OWNS_INPUT\n")
334344
wait_until(lambda start=start: "SHELL_OWNS_INPUT" in transcript[start:])
335-
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", rows, 80, 0, 0))
336-
resizes.append(f"requested {rows}x80, read back {terminal_size()}")
337345
screen.resize(lines=rows, columns=80)
338-
start = len(transcript)
339-
send("stty size\n")
340-
# Bash 5.1+ turns bracketed paste off with "\x1b[?2004l\r" before running the
341-
# command, so the reply may follow a bare "\r" rather than "\r\n".
342-
wait_until(lambda start=start, rows=rows: re.search(rf"[\r\n]{rows} 80\r\n", transcript[start:]) is not None)
346+
# The shell sees the resize while the job is stopped. Under a loaded parallel run the
347+
# pseudo-terminal occasionally reports its old size again, though every process of
348+
# the job is stopped and nothing here sets a size. So confirm the size through the
349+
# shell, and resize again if it saw the old one. Every attempt is reported on failure.
350+
for _ in range(5):
351+
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", rows, 80, 0, 0))
352+
resizes.append(f"requested {rows}x80, read back {terminal_size()}")
353+
start = len(transcript)
354+
send("stty size\n")
355+
# Bash 5.1+ turns bracketed paste off with "\x1b[?2004l\r" before running the
356+
# command, so the reply may follow a bare "\r" rather than "\r\n".
357+
wait_until(lambda start=start: re.search(r"[\r\n]\d+ \d+\r\n", transcript[start:]) is not None)
358+
reply = re.search(r"[\r\n](\d+) (\d+)\r\n", transcript[start:])
359+
assert reply is not None
360+
resizes.append(f"the shell saw {reply.group(1)}x{reply.group(2)}")
361+
if reply.groups() == (str(rows), "80"):
362+
break
363+
else:
364+
pytest.fail(f"the shell never saw a {rows}x80 terminal: {resizes}")
343365
start = len(transcript)
344366
send("fg\n")
345367
wait_until(lambda start=start: "PAGER_RESUMED\r\n" in transcript[start:])
346368
assert os.tcgetpgrp(master) == pipeline_group
369+
if finish == "exit_sigint":
370+
stop_background_output()
347371
if finish == "exit_sigint":
348372
send("\x03")
349373
elif finish == "interrupts":

0 commit comments

Comments
 (0)