fix: retain alternate screen across executable restart
This commit is contained in:
@@ -314,3 +314,124 @@ fn builtin_canonical_and_alias_collisions_reject_the_entire_feature() {
|
||||
assert_eq!(invocation.plugin, "example");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn executable_restart_keeps_the_alternate_screen_and_restores_terminal_on_exit() {
|
||||
for scenario in ["quit", "startup-error"] {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.0.join("startup file.txt");
|
||||
fs::write(&path, "SCREEN_HANDOFF_DOCUMENT\n").unwrap();
|
||||
let output =
|
||||
std::process::Command::new(std::env::var_os("VAMP_RUBY").unwrap_or_else(|| "ruby".into()))
|
||||
.args([
|
||||
"-rpty",
|
||||
"-rtimeout",
|
||||
"-rio/console",
|
||||
"-e",
|
||||
r#"
|
||||
scenario, binary, path = ARGV
|
||||
captured = +"".b
|
||||
status = nil
|
||||
PTY.spawn({"TERM" => "xterm-256color"}, binary, "--no-config", "--", path) do |output, input, pid|
|
||||
phase = 0
|
||||
begin
|
||||
input.winsize = [24, 100]
|
||||
Timeout.timeout(20) do
|
||||
loop do
|
||||
if IO.select([output], nil, nil, 0.05)
|
||||
begin
|
||||
chunk = output.read_nonblock(65536)
|
||||
captured << chunk
|
||||
STDOUT.write(chunk)
|
||||
rescue IO::WaitReadable
|
||||
rescue EOFError, Errno::EIO
|
||||
end
|
||||
end
|
||||
unless status
|
||||
waited = Process.waitpid2(pid, Process::WNOHANG)
|
||||
status = waited[1] if waited
|
||||
end
|
||||
if status
|
||||
# waitpid can win the race with the final output read.
|
||||
loop do
|
||||
begin
|
||||
chunk = output.read_nonblock(65536)
|
||||
captured << chunk
|
||||
STDOUT.write(chunk)
|
||||
rescue IO::WaitReadable, EOFError, Errno::EIO
|
||||
break
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
# A fresh process re-enables input reporting before its first repaint.
|
||||
# Command-overlay frames must not be mistaken for a relaunched editor.
|
||||
sessions = captured.split("\e[?2004h", -1)
|
||||
next unless sessions.length == phase + 2
|
||||
frame = sessions.last
|
||||
next unless frame.include?("\e[?2026l")
|
||||
visible = frame.gsub(/\e\[[0-?]*[ -\/]*[@-~]/, "")
|
||||
next unless visible.include?("SCREEN_HANDOFF_DOCUMENT")
|
||||
raise "alternate screen re-entered during restart" unless captured.scan("\e[?1049h").length == 1
|
||||
raise "normal screen exposed during restart" if captured.include?("\e[?1049l")
|
||||
|
||||
if scenario == "startup-error"
|
||||
File.binwrite(path, "\xff".b)
|
||||
input.write(":restart\r")
|
||||
else
|
||||
input.write([":restart\r", ":restart!\r", ":q\r"].fetch(phase))
|
||||
end
|
||||
input.flush
|
||||
phase += 1
|
||||
end
|
||||
|
||||
expected_phases = scenario == "quit" ? 3 : 1
|
||||
raise "exited before requested restart/quit" unless phase == expected_phases
|
||||
raise "unexpected exit status: #{status.inspect}" unless status.success? == (scenario == "quit")
|
||||
# Inspect the master: macOS revokes slave descriptors when their session exits.
|
||||
raise "terminal echo was not restored" unless input.echo?
|
||||
settings = IO.popen(["stty", "-a"], in: input, &:read)
|
||||
raise "terminal remained noncanonical: #{settings}" unless settings.match?(/(?:^|[;\s])icanon(?:[;\s]|$)/)
|
||||
end
|
||||
ensure
|
||||
unless status
|
||||
begin
|
||||
Process.kill("KILL", pid)
|
||||
rescue Errno::ESRCH
|
||||
end
|
||||
begin
|
||||
Process.waitpid(pid)
|
||||
rescue Errno::ECHILD
|
||||
end
|
||||
end
|
||||
input.close
|
||||
end
|
||||
end
|
||||
"#,
|
||||
"--",
|
||||
scenario,
|
||||
env!("CARGO_BIN_EXE_vamp"),
|
||||
])
|
||||
.arg(&path)
|
||||
.output()
|
||||
.expect("Ruby with stdlib PTY and io/console is required (set VAMP_RUBY)");
|
||||
let captured = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{scenario}: {}\nterminal output: {captured:?}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
assert_eq!(
|
||||
captured.matches("\x1b[?1049h").count(),
|
||||
1,
|
||||
"{scenario}: {captured:?}",
|
||||
);
|
||||
assert_eq!(
|
||||
captured.matches("\x1b[?1049l").count(),
|
||||
1,
|
||||
"{scenario}: {captured:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user