kkawakam / rustyline

Readline Implementation in Rust
https://crates.io/crates/rustyline/
MIT License
1.53k stars 176 forks source link

How to test an app that uses rustyline? #652

Open seanballais opened 1 year ago

seanballais commented 1 year ago

I have been figuring out how to test the following basic CLI app that uses rustyline using std::process::Command but I could not figure out how to pull it off right.

The following is the code for CLI app:

use std::io::Write;

use rustyline::error::ReadlineError;
use rustyline::Editor;

fn main() {
    let mut line_editor = Editor::<()>::new().unwrap();
    let mut counter = 0;

    loop {
        let readline = line_editor.readline("> ");
        match readline {
            Ok(line) => {
                counter += 1;
                println!("Line: {}", line);
                line_editor.add_history_entry(line.clone());

                if line == "clear" {
                    print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
                    std::io::stdout().flush().unwrap();
                }
            }
            Err(ReadlineError::Interrupted) => {
                println!("Ctrl+C");
                break;
            }
            Err(ReadlineError::Eof) => {
                println!("Ctrl+D | Lines counted: {}", counter);
                break;
            }
            Err(err) => {
                println!("Error: {:?}", err);
                break;
            }
        }
    }
}

And code for the test:

#[test]
fn clear_screen() -> Result<(), Box<dyn std::error::Error>> {
    let mut cmd = Command::cargo_bin("control-cli")?;
    let mut cmd_child = cmd.stdin(Stdio::piped()).spawn()?;
    let mut child_stdin = cmd_child.stdin.take().unwrap();

    child_stdin.write_all(b"history\n").unwrap();

    signal::kill(Pid::from_raw(cmd_child.id() as i32), Signal::SIGINT)?;

    let output = cmd.output()?;

    println!("{}", String::from_utf8_lossy(&output.stdout));

    Ok(())
}

When I run the test with cargo test -- --nocapture, I only get the following output:

Ctrl+D | Lines counted: 0

I was expecting something like:

Line: history
Ctrl+D | Lines counted: 1

I believe #504 may be related to this problem. Nevertheless, how can I achieve the second output?

gwenn commented 1 year ago

Not tested: https://crates.io/crates/pty-process

seanballais commented 1 year ago

I'll try that one out tomorrow.

rikhuijzer commented 1 year ago

You could call functions inside the readline matches cases and test those on their own:

use std::io::Write;

use rustyline::error::ReadlineError;
use rustyline::Editor;

fn handle_line<W>(mut dest: W, line: String) where W: Write {
    write!(&mut dest, "Line: {line}\n").unwrap();

    if line == "clear" {
        write!(&mut dest, "{esc}[2J{esc}[1;1H", esc = 27 as char).unwrap();
        dest.flush().unwrap();
    }
}

#[test]
fn line_is_handled() {
    let mut dest = Vec::new();
    handle_line(&mut dest, "something".to_string());
    let actual = String::from_utf8(dest).unwrap();
    let expected = "Line: something\n".to_string();
    assert_eq!(actual, expected);
}

fn main() {
    let mut line_editor = Editor::<()>::new().unwrap();
    let mut counter = 0;
    let mut dest = std::io::stdout();

    loop {
        let readline = line_editor.readline("> ");
        match readline {
            Ok(line) => {
                line_editor.add_history_entry(line.clone());
                counter += 1;
                handle_line(&mut dest, line);
            }
            Err(ReadlineError::Interrupted) => {
                println!("Ctrl+C");
                break;
            }
            Err(ReadlineError::Eof) => {
                println!("Ctrl+D | Lines counted: {}", counter);
                break;
            }
            Err(err) => {
                println!("Error: {:?}", err);
                break;
            }
        }
    }
}

This means that the logic inside main is not tested, but that is arguably not so bad since it's mostly declarative and already tested by rustyline. The complex logic will most likely be inside your own logic.

gruuya commented 9 months ago

FWIW, here's a working example of end-to-end testing (facilitated via the assert_cmd crate) for a CLI app that uses rustyline: https://github.com/splitgraph/seafowl/blob/main/tests/cli/basic.rs

gwenn commented 9 months ago

@gruuya I guess that you are testing the non-interactive mode because stdin/stdout are not tty. And there are some issues if you want to actually test the interactive mode like #703.