1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 19:17:43 +00:00

config terminal simulation for specific stdios only

This commit is contained in:
Ulrich Hornung 2024-03-24 23:06:00 +01:00
parent eacf53d010
commit 537941b676
5 changed files with 189 additions and 73 deletions

View file

@ -45,7 +45,7 @@ fn test_nohup_with_pseudo_terminal_emulation_on_stdin_stdout_stderr_get_replaced
let result = ts let result = ts
.ucmd() .ucmd()
.terminal_simulation(true) .terminal_simulation(true)
.args(&["sh", "is_atty.sh"]) .args(&["sh", "is_a_tty.sh"])
.succeeds(); .succeeds();
assert_eq!( assert_eq!(
@ -58,6 +58,6 @@ fn test_nohup_with_pseudo_terminal_emulation_on_stdin_stdout_stderr_get_replaced
// this proves that nohup was exchanging the stdio file descriptors // this proves that nohup was exchanging the stdio file descriptors
assert_eq!( assert_eq!(
std::fs::read_to_string(ts.fixtures.plus_as_string("nohup.out")).unwrap(), std::fs::read_to_string(ts.fixtures.plus_as_string("nohup.out")).unwrap(),
"stdin is not atty\nstdout is not atty\nstderr is not atty\n" "stdin is not a tty\nstdout is not a tty\nstderr is not a tty\n"
); );
} }

View file

@ -1207,6 +1207,15 @@ impl TestScenario {
} }
} }
#[cfg(unix)]
#[derive(Debug, Default)]
pub struct TerminalSimulation {
size: Option<libc::winsize>,
stdin: bool,
stdout: bool,
stderr: bool,
}
/// A `UCommand` is a builder wrapping an individual Command that provides several additional features: /// A `UCommand` is a builder wrapping an individual Command that provides several additional features:
/// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command /// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command
/// and asserting on the results. /// and asserting on the results.
@ -1242,9 +1251,7 @@ pub struct UCommand {
stderr_to_stdout: bool, stderr_to_stdout: bool,
timeout: Option<Duration>, timeout: Option<Duration>,
#[cfg(unix)] #[cfg(unix)]
terminal_simulation: bool, terminal_simulation: Option<TerminalSimulation>,
#[cfg(unix)]
terminal_size: Option<libc::winsize>,
tmpd: Option<Rc<TempDir>>, // drop last tmpd: Option<Rc<TempDir>>, // drop last
} }
@ -1425,22 +1432,32 @@ impl UCommand {
/// Set if process should be run in a simulated terminal /// Set if process should be run in a simulated terminal
/// ///
/// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`]. /// This is useful to test behavior that is only active if e.g. [`stdout.is_terminal()`] is [`true`].
/// This function uses default terminal size and attaches stdin, stdout and stderr to that terminal.
/// For more control over the terminal simulation, use `terminal_sim_stdio`
/// (unix: pty, windows: ConPTY[not yet supported]) /// (unix: pty, windows: ConPTY[not yet supported])
#[cfg(unix)] #[cfg(unix)]
pub fn terminal_simulation(&mut self, enable: bool) -> &mut Self { pub fn terminal_simulation(&mut self, enable: bool) -> &mut Self {
self.terminal_simulation = enable; if enable {
self.terminal_simulation = Some(TerminalSimulation {
stdin: true,
stdout: true,
stderr: true,
..Default::default()
});
} else {
self.terminal_simulation = None;
}
self self
} }
/// Set if process should be run in a simulated terminal with specific size /// Allows to simulate a terminal use-case with specific properties.
/// ///
/// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`]. /// This is useful to test behavior that is only active if e.g. [`stdout.is_terminal()`] is [`true`].
/// And the size of the terminal matters additionally. /// This function allows to set a specific size and to attach the terminal to only parts of the in/out.
#[cfg(unix)] #[cfg(unix)]
pub fn terminal_size(&mut self, win_size: libc::winsize) -> &mut Self { pub fn terminal_sim_stdio(&mut self, config: TerminalSimulation) -> &mut Self {
self.terminal_simulation(true); self.terminal_simulation = Some(config);
self.terminal_size = Some(win_size);
self self
} }
@ -1628,35 +1645,48 @@ impl UCommand {
}; };
#[cfg(unix)] #[cfg(unix)]
if self.terminal_simulation { if let Some(simulated_terminal) = &self.terminal_simulation {
let terminal_size = self.terminal_size.unwrap_or(libc::winsize { let terminal_size = simulated_terminal.size.unwrap_or(libc::winsize {
ws_col: 80, ws_col: 80,
ws_row: 30, ws_row: 30,
ws_xpixel: 80 * 8, ws_xpixel: 80 * 8,
ws_ypixel: 30 * 10, ws_ypixel: 30 * 10,
}); });
let OpenptyResult { if simulated_terminal.stdin {
slave: pi_slave, let OpenptyResult {
master: pi_master, slave: pi_slave,
} = nix::pty::openpty(&terminal_size, None).unwrap(); master: pi_master,
let OpenptyResult { } = nix::pty::openpty(&terminal_size, None).unwrap();
slave: po_slave, stdin_pty = Some(File::from(pi_master));
master: po_master, command.stdin(pi_slave);
} = nix::pty::openpty(&terminal_size, None).unwrap(); }
let OpenptyResult {
slave: pe_slave,
master: pe_master,
} = nix::pty::openpty(&terminal_size, None).unwrap();
stdin_pty = Some(File::from(pi_master)); if simulated_terminal.stdout {
let OpenptyResult {
slave: po_slave,
master: po_master,
} = nix::pty::openpty(&terminal_size, None).unwrap();
captured_stdout = self.spawn_reader_thread(
captured_stdout,
po_master,
"stdout_reader".to_string(),
);
command.stdout(po_slave);
}
captured_stdout = if simulated_terminal.stderr {
self.spawn_reader_thread(captured_stdout, po_master, "stdout_reader".to_string()); let OpenptyResult {
captured_stderr = slave: pe_slave,
self.spawn_reader_thread(captured_stderr, pe_master, "stderr_reader".to_string()); master: pe_master,
} = nix::pty::openpty(&terminal_size, None).unwrap();
command.stdin(pi_slave).stdout(po_slave).stderr(pe_slave); captured_stderr = self.spawn_reader_thread(
captured_stderr,
pe_master,
"stderr_reader".to_string(),
);
command.stderr(pe_slave);
}
} }
#[cfg(unix)] #[cfg(unix)]
@ -3609,10 +3639,10 @@ mod tests {
fn test_simulation_of_terminal_false() { fn test_simulation_of_terminal_false() {
let scene = TestScenario::new("util"); let scene = TestScenario::new("util");
let out = scene.ccmd("env").arg("sh").arg("is_atty.sh").succeeds(); let out = scene.ccmd("env").arg("sh").arg("is_a_tty.sh").succeeds();
std::assert_eq!( std::assert_eq!(
String::from_utf8_lossy(out.stdout()), String::from_utf8_lossy(out.stdout()),
"stdin is not atty\nstdout is not atty\nstderr is not atty\n" "stdin is not a tty\nstdout is not a tty\nstderr is not a tty\n"
); );
std::assert_eq!( std::assert_eq!(
String::from_utf8_lossy(out.stderr()), String::from_utf8_lossy(out.stderr()),
@ -3629,12 +3659,93 @@ mod tests {
let out = scene let out = scene
.ccmd("env") .ccmd("env")
.arg("sh") .arg("sh")
.arg("is_atty.sh") .arg("is_a_tty.sh")
.terminal_simulation(true) .terminal_simulation(true)
.succeeds(); .succeeds();
std::assert_eq!( std::assert_eq!(
String::from_utf8_lossy(out.stdout()), String::from_utf8_lossy(out.stdout()),
"stdin is atty\r\nstdout is atty\r\nstderr is atty\r\nterminal size: 30 80\r\n" "stdin is a tty\r\nterminal size: 30 80\r\nstdout is a tty\r\nstderr is a tty\r\n"
);
std::assert_eq!(
String::from_utf8_lossy(out.stderr()),
"This is an error message.\r\n"
);
}
#[cfg(unix)]
#[cfg(feature = "env")]
#[test]
fn test_simulation_of_terminal_for_stdin_only() {
let scene = TestScenario::new("util");
let out = scene
.ccmd("env")
.arg("sh")
.arg("is_a_tty.sh")
.terminal_sim_stdio(TerminalSimulation {
stdin: true,
stdout: false,
stderr: false,
..Default::default()
})
.succeeds();
std::assert_eq!(
String::from_utf8_lossy(out.stdout()),
"stdin is a tty\nterminal size: 30 80\nstdout is not a tty\nstderr is not a tty\n"
);
std::assert_eq!(
String::from_utf8_lossy(out.stderr()),
"This is an error message.\n"
);
}
#[cfg(unix)]
#[cfg(feature = "env")]
#[test]
fn test_simulation_of_terminal_for_stdout_only() {
let scene = TestScenario::new("util");
let out = scene
.ccmd("env")
.arg("sh")
.arg("is_a_tty.sh")
.terminal_sim_stdio(TerminalSimulation {
stdin: false,
stdout: true,
stderr: false,
..Default::default()
})
.succeeds();
std::assert_eq!(
String::from_utf8_lossy(out.stdout()),
"stdin is not a tty\r\nstdout is a tty\r\nstderr is not a tty\r\n"
);
std::assert_eq!(
String::from_utf8_lossy(out.stderr()),
"This is an error message.\n"
);
}
#[cfg(unix)]
#[cfg(feature = "env")]
#[test]
fn test_simulation_of_terminal_for_stderr_only() {
let scene = TestScenario::new("util");
let out = scene
.ccmd("env")
.arg("sh")
.arg("is_a_tty.sh")
.terminal_sim_stdio(TerminalSimulation {
stdin: false,
stdout: false,
stderr: true,
..Default::default()
})
.succeeds();
std::assert_eq!(
String::from_utf8_lossy(out.stdout()),
"stdin is not a tty\nstdout is not a tty\nstderr is a tty\n"
); );
std::assert_eq!( std::assert_eq!(
String::from_utf8_lossy(out.stderr()), String::from_utf8_lossy(out.stderr()),
@ -3651,17 +3762,22 @@ mod tests {
let out = scene let out = scene
.ccmd("env") .ccmd("env")
.arg("sh") .arg("sh")
.arg("is_atty.sh") .arg("is_a_tty.sh")
.terminal_size(libc::winsize { .terminal_sim_stdio(TerminalSimulation {
ws_col: 40, size: Some(libc::winsize {
ws_row: 10, ws_col: 40,
ws_xpixel: 40 * 8, ws_row: 10,
ws_ypixel: 10 * 10, ws_xpixel: 40 * 8,
ws_ypixel: 10 * 10,
}),
stdout: true,
stdin: true,
stderr: true,
}) })
.succeeds(); .succeeds();
std::assert_eq!( std::assert_eq!(
String::from_utf8_lossy(out.stdout()), String::from_utf8_lossy(out.stdout()),
"stdin is atty\r\nstdout is atty\r\nstderr is atty\r\nterminal size: 10 40\r\n" "stdin is a tty\r\nterminal size: 10 40\r\nstdout is a tty\r\nstderr is a tty\r\n"
); );
std::assert_eq!( std::assert_eq!(
String::from_utf8_lossy(out.stderr()), String::from_utf8_lossy(out.stderr()),

21
tests/fixtures/nohup/is_a_tty.sh vendored Normal file
View file

@ -0,0 +1,21 @@
#!/bin/bash
if [ -t 0 ] ; then
echo "stdin is a tty"
else
echo "stdin is not a tty"
fi
if [ -t 1 ] ; then
echo "stdout is a tty"
else
echo "stdout is not a tty"
fi
if [ -t 2 ] ; then
echo "stderr is a tty"
else
echo "stderr is not a tty"
fi
true

View file

@ -1,21 +0,0 @@
#!/bin/bash
if [ -t 0 ] ; then
echo "stdin is atty"
else
echo "stdin is not atty"
fi
if [ -t 1 ] ; then
echo "stdout is atty"
else
echo "stdout is not atty"
fi
if [ -t 2 ] ; then
echo "stderr is atty"
else
echo "stderr is not atty"
fi
true

View file

@ -1,22 +1,22 @@
#!/bin/bash #!/bin/bash
if [ -t 0 ] ; then if [ -t 0 ] ; then
echo "stdin is atty" echo "stdin is a tty"
echo "terminal size: $(stty size)"
else else
echo "stdin is not atty" echo "stdin is not a tty"
fi fi
if [ -t 1 ] ; then if [ -t 1 ] ; then
echo "stdout is atty" echo "stdout is a tty"
else else
echo "stdout is not atty" echo "stdout is not a tty"
fi fi
if [ -t 2 ] ; then if [ -t 2 ] ; then
echo "stderr is atty" echo "stderr is a tty"
echo "terminal size: $(stty size)"
else else
echo "stderr is not atty" echo "stderr is not a tty"
fi fi
>&2 echo "This is an error message." >&2 echo "This is an error message."