ito_test_support/pty/
mod.rs

1//! PTY-based command runners for interactive integration tests.
2
3use std::io::{Read, Write};
4use std::path::Path;
5
6use portable_pty::{CommandBuilder, PtySize, native_pty_system};
7
8use crate::CmdOutput;
9
10/// Runs a command in a PTY and captures output.
11///
12/// Notes:
13/// - PTY output is a merged stream (stdout+stderr); we populate `stdout`.
14/// - This helper is intentionally minimal; interactive parity tests can extend
15///   it to incremental reads/writes.
16pub fn run_pty(program: &Path, args: &[&str], cwd: &Path, home: &Path, input: &str) -> CmdOutput {
17    run_pty_with_interactive_env(program, args, cwd, home, input, false, &[])
18}
19
20/// Runs a command in a PTY with `ITO_INTERACTIVE=1`.
21///
22/// This is useful for testing that interactive code paths behave as expected
23/// under a pseudo-terminal.
24pub fn run_pty_interactive(
25    program: &Path,
26    args: &[&str],
27    cwd: &Path,
28    home: &Path,
29    input: &str,
30) -> CmdOutput {
31    run_pty_with_interactive_env(program, args, cwd, home, input, true, &[])
32}
33
34/// Runs a command in a PTY with `ITO_INTERACTIVE=1` and extra environment variables.
35pub fn run_pty_interactive_with_env(
36    program: &Path,
37    args: &[&str],
38    cwd: &Path,
39    home: &Path,
40    input: &str,
41    env: &[(&str, &str)],
42) -> CmdOutput {
43    run_pty_with_interactive_env(program, args, cwd, home, input, true, env)
44}
45
46fn run_pty_with_interactive_env(
47    program: &Path,
48    args: &[&str],
49    cwd: &Path,
50    home: &Path,
51    input: &str,
52    interactive: bool,
53    env: &[(&str, &str)],
54) -> CmdOutput {
55    let pty_system = native_pty_system();
56    let pair = pty_system
57        .openpty(PtySize {
58            rows: 24,
59            cols: 120,
60            pixel_width: 0,
61            pixel_height: 0,
62        })
63        .expect("openpty");
64
65    let mut cmd = CommandBuilder::new(program);
66    cmd.args(args);
67    cmd.cwd(cwd);
68    cmd.env("CI", "1");
69    cmd.env("NO_COLOR", "1");
70    let interactive_value = match interactive {
71        true => "1",
72        false => "0",
73    };
74    cmd.env("ITO_INTERACTIVE", interactive_value);
75    cmd.env("TERM", "dumb");
76    cmd.env("HOME", home);
77    cmd.env("XDG_CONFIG_HOME", home.join(".config"));
78    cmd.env("XDG_DATA_HOME", home);
79
80    for (k, v) in env {
81        cmd.env(k, v);
82    }
83
84    let mut child = pair.slave.spawn_command(cmd).expect("spawn_command");
85    drop(pair.slave);
86
87    if !input.is_empty() {
88        let mut writer = pair.master.take_writer().expect("take_writer");
89        writer.write_all(input.as_bytes()).expect("write input");
90        writer.flush().ok();
91    }
92
93    // Read until EOF.
94    let mut reader = pair.master.try_clone_reader().expect("clone_reader");
95    let mut out = String::new();
96    reader.read_to_string(&mut out).ok();
97
98    let status = child.wait().expect("wait");
99    let code = status.exit_code() as i32;
100
101    CmdOutput {
102        code,
103        stdout: out,
104        stderr: String::new(),
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    // Uses `cat` which is not available on Windows
113    #[test]
114    #[cfg(unix)]
115    fn pty_can_echo_input_via_cat() {
116        // Smoke test to prove PTY wiring works.
117        let home = tempfile::tempdir().expect("home");
118        let cwd = tempfile::tempdir().expect("cwd");
119
120        let out = run_pty(Path::new("cat"), &[], cwd.path(), home.path(), "hello\n");
121        assert_eq!(out.code, 0);
122        assert!(out.stdout.contains("hello"));
123    }
124}