Cargo doesn't capture stderr of running tests?

I am trying to log information through stderr. It seems 'cargo test' does not capture stderr in the same way as it does to stdout. So when I run tests, the logging from stderr messes up with the output information from cargo. Is there any options that can control this?

2 Likes

You should probably not be using stderr, but the log crate, which is much more expressive

Not that I know of. AFAIKT cargo test does print on stdout though, so you should be able to filter it out.
Maybe others will know better than me.

Yes. I am using log crate along with stderrlog crate, which outputs logs through stderr. I could use something like env_logger or simple_logger which outputs to stdout. However, I would like to keep stdout clean. But stderr output messes up with cargo test.

Currently I am using shell output redirection (2> /dev/null). However, it is not the best solution I guess, since it will omit any stderr output from not only the tested program but also cargo. I am wondering if cargo provides any options to filter out stderr of tested programs.

This may be bug. libtest.rs does stipulate that the test harness should capture both stdout and stderr. The functions responsible for that are std::io::{set_print, set_panic} but they may be a bit unreliable (see #31334).
There seems to be a difference of behavior between the print{ln}! and write{ln}! macros because println!(...) is captured in the tests but writeln!(io::stdout(), ...) is not. Nor is writeln!(io::stderr(), ...).

1 Like

Thanks a lot. That well explained my question. I checked stderrlog, and it does use writeln!() for stderr output.

I am wondering, if libtest uses set_print() to capture the print!() output, does that mean I am not supposed to use set_print() in my code? Also how should I output to stderr that can be captured by cargo test, is there a viable way?

Well they're unstable so you/your users will need a nightly compiler. Also you can't promise stability given that unstable APIs can break at any time. Whether that's a problem depends on what you have in mind. I wouldn't expect them to disappear any time soon though, given that the compiler relies on them. Not until another proper capture mechanism is written.

If I understand the code properly, I don't think that's possible at the moment. write! will use .write_fmt() directly on stdout() / stderr() whereas print! and panic! will try to use the LOCAL_STDOUT and LOCAL_STDERR sinks. What set_print and set_panic do is they swap out those sinks for their own so they get to decide what to do with the contents and wether to actually print them or not. write! completely bypasses that.
AFAICT with a quick search in the rust repo, the only way to use LOCAL_STDOUT is through print! and the only way to use LOCAL_STDERR is through panic!. There's no way to write to stderr and have it captured.

Hopefully it shouldn't take too long to get eprint! and then stabilize it. Since the proposed implementation uses LOCAL_STDERR that should solve your problem.

1 Like

Thank you very much. It is a bit surprising that the implementation around this is so hacky. I guess there is no good solution for me then. I will just work around that.

I implemented customizable log targets for env_logger: https://github.com/rust-lang-nursery/log/pull/105

When setting it to stdout, the log output is captured by Cargo.

1 Like

In the meantime the PR was merged, but later the code was changed again to use write! which doesn't seem to be captured by libtest.

In case you use log4rs, this appender seems to work:

/// An appender that uses println! for logging
/// so that the calls are captured by libtest.
#[derive(Debug)]
struct CapturedConsoleAppender {
    encoder: Box<Encode>,
}

impl CapturedConsoleAppender {
    fn new() -> Self {
        CapturedConsoleAppender {
            encoder: Box::new(PatternEncoder::default()),
        }
    }
}

impl Append for CapturedConsoleAppender {
    fn append(&self, record: &Record) -> Result<(), Box<StdError + Sync + Send>> {
        let mut writer = SimpleWriter(Vec::<u8>::new());
        self.encoder.encode(&mut writer, record)?;
        let line = str::from_utf8(&writer.0).unwrap();
        println!("{}", line);
        Ok(())
    }
    fn flush(&self) {}
}