Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fine control over signals, stdin, etc #94

Open
epage opened this issue Mar 27, 2020 · 1 comment
Open

Fine control over signals, stdin, etc #94

epage opened this issue Mar 27, 2020 · 1 comment
Labels
enhancement Improve the expected

Comments

@epage
Copy link
Contributor

epage commented Mar 27, 2020

As food for thoughts, here's how I'm using assert_cmd.

The guiding idea is that I build a struct describing programs to run/delay/kill, a set of input files, and a set of post-run checks that operate on created files (including the program's stdout/stderr). I can run() on that struct once it is set up, and it takes care of setting up the TempDir, scheduling program start, and running the checks.

/// Main struct described above. It's got builder-pattern methods,
/// and a `run` method that will do all the work and `assert` upon failure.
pub struct Exec {
    cmds: Vec<Cmd>,
    timeout: Duration,
    files: Vec<FileIn>,
    checks: Vec<FilePredicate>,
}

pub struct Cmd {
    /// Program to run.
    bin: Command,
    /// Command name for `after()`, std/err filname prefix, and logs.
    name: String,
    /// Expected exit status. Some(0)=success, Some(n)=failure, None=timeout.
    exit: Option<i32>,
    /// Fail if the cmd exits too early.
    mintime: Duration,
    /// Fail if the cmd run for too long.
    maxtime: Duration,
    /// Current state.
    state: CmdState,
    /// List of signals to send to the process after startup.
    signals: Vec<(CmdCond, c_int)>,
}

enum CmdState {
    Wait(CmdCond),
    Started(Child, Instant),
    Done,
}

pub enum CmdCond {
    /// Immediately true
    None,
    /// Duration elapsed
    Delay(Duration),
    /// Other Cmd exited
    Cmd(String),
    /// File-based predicate
    Predicate(FilePredicate),
}

pub struct FilePredicate {
    /// Desciption for assert-logging purpose.
    desc: String,
    /// Which file to operate on.
    file: String,
    /// Closure that tests the content of the file.
    pred: Box<dyn Fn(&str) -> bool>,
}

pub enum FileIn {
    FromFs(&'static str, &'static str),
    Bin(&'static str, Vec<u8>),
}

With that in place, I have a pretty powerful way to write a unittest, using my crate's main binary and a few ancillary processes:

    // As basic as it gets.
    exec().cmd(Cmd::any("echo", "echo", "-n a")).check("echo a", "echo.out", |s| s == "a").run();
    // File from hardcoded data.
    exec().inbytes("a", "a")
          .cmd(Cmd::any("cat", "cat", "a"))
          .check("cat a", "cat.out", |s| s == "a")
          .run();
    // File from file in test/ directory.
    exec().infile("j", "input.basic.json")
          .cmd(Cmd::any("cat", "cat", "j"))
          .check("cat j", "cat.out", |s| s.starts_with("{"))
          .run();
    // run sequentially
    let start = Instant::now();
    exec().cmd(Cmd::any("s1", "sleep", "0.3"))
          .cmd(Cmd::any("s2", "sleep", "0.3").after("s1"))
          .cmd(Cmd::any("s3", "sleep", "0.3").after("s2"))
          .cmd(Cmd::any("cat", "cat", "s1.out s2.out s3.out").after("s3"))
          .run();
    assert!(Instant::now() - start > Duration::from_millis(900));
    // delayed start
    exec().cmd(Cmd::any("0", "cat", "1.out 2.out").after(20))
          .cmd(Cmd::any("1", "echo", "a"))
          .cmd(Cmd::any("2", "echo", "b"))
          .check("cat", "0.out", |s| s == "a\nb\n")
          .run();
    // timeout
    exec().cmd(Cmd::any("s", "sleep", "1").maxtime(100).exit(None)).run();
    exec().cmd(Cmd::any("s", "sleep", "0.05").mintime(50).maxtime(70)).run();
    // Multiple signals, ordered by Instant.
    exec().cmd(Cmd::any("s", "sleep", "1").signal(SIGINT, 70) // Added first but triggers last
                                          .signal(SIGCONT, 10) // Triggers first but doesn't terminate
                                          .signal(SIGTERM, 30) // Actual terminator
                                          .exit(SIGTERM))
          .run();
    // Signal after another cmd exit.
    exec().cmd(Cmd::any("s1", "sleep", "1").signal(SIGINT, "s2").maxtime(50).exit(SIGINT))
          .cmd(Cmd::any("s2", "sleep", "0.01"))
          .run();
    // Signal after file content matches
    exec().cmd(Cmd::any("s1", "sleep", "1").signal(SIGINT, ("s2.out", "4"))
                                           .maxtime(100)
                                           .exit(SIGINT))
          .cmd(Cmd::bash("s2", "for i in $(seq 10);do echo $i;sleep 0.01;done"))
          .timeout(1000)
          .run();
    // Main binary
    exec().cmd(Cmd::main("m", "-V")).check("progname", "0.out", |s| s.starts_with("mybin"));
    // Set/unset env
    exec().cmd(Cmd::any("env", "env", "").env("foo", "bar")
                                         .env("PATH", &format!("/ut:{}", env::var("PATH").unwrap()))
                                         .env_remove("HOME"))
          .check("added_foo", "env.out", |s| s.lines().any(|l| l == "foo=bar"))
          .check("changed_path", "env.out", |s| s.lines().any(|l| l.starts_with("PATH=/ut:")))
          .check("removed_home", "env.out", |s| !s.lines().any(|l| l.starts_with("HOME=")))
          .run();

If there's enough interest, I could try to get this cleared for upstreaming. Currently there are a few extensions and API decisions that are specific to my project, and the code is a bit ad-hoc in places.

Originally posted by @vincentdephily in #74 (comment)

@epage
Copy link
Contributor Author

epage commented Mar 27, 2020

I've done a quick filtering out of the private code and got the thumbs up to share https://gist.github.com/vincentdephily/592546c41ff9713adff234e5535aa6d4 which you can treat as public domain.

I've added a few UPSTREAMING: comments, as you probably don't want to take it all as-is. Hope that helps.

#74 (comment)

@epage epage added the enhancement Improve the expected label Mar 27, 2020
epage pushed a commit to epage/assert_cmd that referenced this issue May 29, 2021
In the tree view, we already show the original and the current value, we
shouldnt show an entire Diff that is only parseable by color.

In changing this, we removed the more cosmetic atom selector.  We also
removed the edit distance, since there isn't a known case for it.  Let
us know if you needed this!

Fixes assert-rs#94
Fixes assert-rs#105
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improve the expected
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant