rustyline icon indicating copy to clipboard operation
rustyline copied to clipboard

How to test an app that uses rustyline?

Open seanballais opened this issue 1 year ago • 5 comments

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?

seanballais avatar Sep 26 '22 17:09 seanballais

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

gwenn avatar Oct 02 '22 07:10 gwenn

I'll try that one out tomorrow.

seanballais avatar Oct 02 '22 07:10 seanballais

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.

rikhuijzer avatar Feb 07 '23 19:02 rikhuijzer

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

gruuya avatar Dec 01 '23 20:12 gruuya

@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.

gwenn avatar Dec 02 '23 08:12 gwenn