Repository navigation
Feature Request: wait and/or then methods after writing to stdin #106
Description
I came across this crate recently, but I cannot remember where...maybe from the CLI-WG? Anyways, it provides a lot of the functionality I wrote in the past for testing some internal projects, so I am excited to start using it in the future, and add tests for some of my Rust CLI applications. Great Work!
I am currently working on a project where I am writing messages into stdin and reading from stdout. I have some tests that deliberately result in relatively long execution times (on the order of 10's of seconds). The general test follows: (1) write message to stdin, (2) wait some amount of time depending on expected processing time, (3) read stdout and check for correctness. For these tests, where a long execution time occurs, I have not found a way to use this crate. I believe the problem is that the stdin handle from the internal Command is being dropped after writing to stdin but before the command has had time to write to stdout, which leaves the contents of stdout from the wait_with_output method empty. Any correctness assertions on the output then fail.
Instead of using this crate, here is a non-working example of the test I implemented:
#[test]
fn slow_reply_works() {
const RESPONSE_TIME: u64 = 10; // seconds
let mut child = Command::new("some_cmd") // <-- Not actual command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Child command");
{
let stdin = child.stdin.as_mut().expect("Stdin");
stdin.write_all("slowReplyMessage".as_bytes()).expect("Write to stdin");
// Without this, the `stdin` handle is dropped. My CLI Under Test (CUT) exits when
// stdin reaches EOF or a Broken Pipe occurs. So, there is no time for
// the cmd to complete the long/slow computation and write anything to stdout
// when the output is later retrieved with the `wait_with_output` method. The
// output is empty and any assertion for correctness fails.
thread::sleep(Duration::from_secs(RESPONSE_TIME));
}
let output = child.wait_with_output().expect("Wait on child");
// Correctness assertions for the output follow here, but without the
// `thread::sleep`, the output is empty. Omitted for clarity.
// ...
}I would like to see a wait, hold, or delay method added to the Assert struct as I believe the Assert struct is essentially doing the same time as my non-working example test, but does not have the thread::sleep line. If such a method existed, the above test could be rewritten more succinctly using this crate as follows:
#[test]
fn slow_reply_works() {
const RESPONSE_TIME: u64 = 10; // seconds
Assert::command(&["some_cmd"])
.stdin("slowReplyMessage").wait(RESPONSE_TIME)
.succeeds()
.and()
.stdout().contains("expected message after delay")
.unwrap();
}I believe this is cleaner and easier to read, but I understand wait might be confusing since there is the wait and wait_with_output methods for the Command struct in std::process. So, maybe hold or delay would be better, with a slight personal preference at the moment towards "hold"?
However, this got me thinking that maybe a wait/hold/delay method is too restrictive? There might be other tests in the future that need to do more than just wait for some time after writing to stdin. So, maybe a then method with a predicate would be better? The above example with the wait/hold/delay method could be re-implemented as follows:
#[test]
fn slow_reply_works() {
const RESPONSE_TIME: u64 = 10; // seconds
Assert::command(&["some_cmd"])
// Not sure if anything needs to be passed to the closure, handle to stdin?
.stdin("slowReplyMessage").then(|| {
// Waits for the slow reply to complete, just like the previous example,
// but other actions can be executed.
thread::sleep(Duration::from_secs(RESPONSE_TIME);
})
.succeeds()
.and()
.stdout().contains("expected message after delay")
.unwrap();
}Of course, both a wait/hold/delay and then methods could be added because the then implementation of wait/hold/delay is a little more verbose/noisy. I am open to alternatives for the then method name as well.
Any thoughts or comments about this proposed feature/enhancement would be greatly appreciated.
Thanks again for the great crate and work!
Activity
epage commentedon May 15, 2018
So the crux of the issue is this:
So is
Commandsending an EOF oncewait_with_outputis called? I sure hope it isn't breaking the pipe; that'd be bad. I assume this command is doing RPC and doesn't know when it should wait for replies? This seems like an obscure use case which means I probably won't prioritize this among my different rust efforts. I'm also trying to decide how much benefit it would be to include in the API (the hard life of a maintainer, trying to decide when to say "no").If we move forward with this, it seems the most ideal would be a
wait_for(pipe, pred: IntoOutputPredicate)that reads from stdout or stderr and looks for specific content because waiting for timeouts is brittle at worst and slows your tests at best.(
IntoOutputPredicatewould be a trait within assert_cli I'm planning on making when I integrate predicates-rust to make it easy for using aPredicate<[u8]>orPredicate<str>, maybe even accepting a&strthough choosing the right default will be interesting).volks73 commentedon May 15, 2018
I believe so. According to the documentation for the
wait_with_outputmethod (emphasis mine):The current implementation for my application is to exit on stdin EOF. Maybe I am missing something in my implementations to avoid this problem?
You are correct. The CUT implements a RPC server over stdio and/or TCP depending on configuration, and it continuously reads from stdin and writes to stdout from two separate threads to mimic the independent, bi-directional communication of a TCP socket. I implemented RPC communication over stdio for embedding but also specifically for testing the application. If I use the TCP server, then I would potentially need hundreds, or possibly even thousands, of simultaneous TCP connections, or I would have to run the tests in serial as opposed to parallel (increasing test time). Using stdio communication, I can avoid running out of TCP connections for testing and still run in parallel. I know there are other ways to test RPCs without having to "spin up" a server for each one, but there are times when I want to test the whole process and have confidence the framing, (de)serialization, routing, etc. all work as expected.
Yes, after writing the above discussion and thinking about it some more, I agree that this is probably an edge case. I don't know if enough developers have encountered, or will encounter, this quirk when working with stdio and the
Commandtype to warrant the expansion in the API of this crate and I appreciate the honesty and difficulty in saying "No". I just really liked the cleanliness, readability, and succinctness of my tests where I could use this crate and wanted to apply it to all of my tests.So, I understand leaving out a
wait/hold/delaymethod, but what about the more "generic"thenandand_thenmethods for stdin? More broadly, implementing chaining for stdin? The API already has chaining for stdout and stderr. Something like:An
IntoInputPredicatesimilar to theIntoOutputPredicatetrait could be provided/implemented. Right now, a test needs to compose the message or data into one big input before the test.epage commentedon May 15, 2018
To be clear, I'm not outright saying no, just wanting to better understand. Additionally, I'm grateful we at least have the issue so even if we close it, others might find it in the future.
That chaining is just combining predicates, its not doing much else. If there is a use case for chaining but right now, I'm not seeing it beyond your case.
btw would something like rexpect be useful? See https://crates.io/crates/rexpect
Something else to note, #98 is us experimenting with making the API more of a toolbox so you can create your own solutions with it.
volks73 commentedon May 18, 2018
I will take a look at rexpect, thanks for the recommendation.
In the mean time, I was experimenting the other day and came up with a change that keeps the public API usage unchanged, but allows "optional" access to the
stdinhandle of the child process. I thought I would share and created PR #107 that includes this experiment and some examples for thestdinmethod with the new implementation.volks73 commentedon Jun 17, 2018
Okay, this issue/feature request and related PR (#107) can be closed. I have a solution for my use case using the assert_cmd crate that does not need any changes to this crate. Instead, I used
StdInCommandandStdInCommandExtfrom the assert_cmd crate as templates and the changes in PR #107 to createInputCommand,InputCommandExt, andStdinWriter, which together adds theinputandthenmethods toprocess::Command. I put all of this in a sub-module for my integration tests and used them with other features from the assert_cmd crate to create readable and compact tests for my CLI application and its examples.The assert_cmd crate made it really easy to create a custom test fixture. I believe this is the intention of the crate. Thank you for the all of the work and information.
I have created a gist of my implementation for reference and in case anyone else is interested. I am open to comments on it.
epage commentedon Jun 18, 2018
Glad the new
assert_cmdis working out!