Search code examples
rustrust-cargorust-tokio

`writeln!(std::io::stdout().lock(), "")` cannot be captured by cargo test


I am trying to implement a logger in my multi-threading program. So I tried using a std::io::stdout to gain a StdoutLock to ensure atomicity. But later I found in that way, all the log write to stdout cannot be capture when cargo test.

I wrote a demo of this:

use std::io::Write as _;

pub fn print() {
    println!("Hello, world! (print)");
}

pub fn write() {
    let mut handle = std::io::stdout().lock();
    writeln!(&mut handle, "Hello, world! (write)").unwrap();
    handle.flush().unwrap();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_print() {
        print();
    }

    #[test]
    fn test_write() {
        write();
    }
}

When running cargo test, it prints:

$ cargo test --lib

running 2 tests
Hello, world! (write)
test tests::test_print ... ok
test tests::test_write ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

I wonder how to avoid "Hello, world! (write)" being printed when running tests.


Solution

  • libtest's capture of stdout indeed does not consider stdout(), this is issue #90785.

    Ideally this will be fixed in std; until then, you can make a wrapper that based on cfg(test) switches between println!() and stdout().lock():

    use std::io::{self, Write};
    
    pub struct Stdout {
        #[cfg(not(test))]
        inner: std::io::StdoutLock<'static>,
    }
    
    impl Stdout {
        pub fn lock() -> Self {
            Self {
                #[cfg(not(test))]
                inner: std::io::stdout().lock(),
            }
        }
    }
    
    impl Write for Stdout {
        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
            #[cfg(not(test))]
            {
                self.inner.write(buf)
            }
            #[cfg(test)]
            {
                println!("{}", std::str::from_utf8(buf).expect("non-UTF8 print"));
                Ok(buf.len())
            }
        }
        
        fn flush(&mut self) -> io::Result<()> {
            #[cfg(not(test))]
            {
                self.inner.flush()
            }
            #[cfg(test)]
            {
                Ok(())
            }
        }
    }
    
    pub fn write() {
        let mut handle = Stdout::lock();
        writeln!(&mut handle, "Hello, world! (write)").unwrap();
        // println!("Hello, world! (write)");
        handle.flush().unwrap();
    }
    

    This doesn't work in integration tests, though, because they don't set cfg(test).