Skip to content

Commit

Permalink
Support capturing for cargo test (#127)
Browse files Browse the repository at this point in the history
* allow logs to be captured for cargo test
  • Loading branch information
KodrAus authored Mar 3, 2019
1 parent 811db32 commit 8fa6240
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 38 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Tests can use the `env_logger` crate to see log messages generated during that t
log = "0.4.0"

[dev-dependencies]
env_logger = { version = "0.6.0", default-features = false }
env_logger = "0.6.0"
```

```rust
Expand All @@ -69,16 +69,22 @@ mod tests {
use super::*;
extern crate env_logger;

fn init() {
let _ = env_logger::builder().is_test(true).try_init();
}

#[test]
fn it_adds_one() {
let _ = env_logger::try_init();
init();

info!("can log from the test too");
assert_eq!(3, add_one(2));
}

#[test]
fn it_handles_negative_numbers() {
let _ = env_logger::try_init();
init();

info!("logging from another test");
assert_eq!(-7, add_one(-8));
}
Expand Down
22 changes: 15 additions & 7 deletions src/fmt/writer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,23 @@ impl Writer {
pub(crate) struct Builder {
target: Target,
write_style: WriteStyle,
is_test: bool,
built: bool,
}

impl Builder {
/// Initialize the writer builder with defaults.
pub fn new() -> Self {
pub(crate) fn new() -> Self {
Builder {
target: Default::default(),
write_style: Default::default(),
is_test: false,
built: false,
}
}

/// Set the target to write to.
pub fn target(&mut self, target: Target) -> &mut Self {
pub(crate) fn target(&mut self, target: Target) -> &mut Self {
self.target = target;
self
}
Expand All @@ -94,18 +96,24 @@ impl Builder {
/// See the [Disabling colors] section for more details.
///
/// [Disabling colors]: ../index.html#disabling-colors
pub fn parse(&mut self, write_style: &str) -> &mut Self {
pub(crate) fn parse_write_style(&mut self, write_style: &str) -> &mut Self {
self.write_style(parse_write_style(write_style))
}

/// Whether or not to print style characters when writing.
pub fn write_style(&mut self, write_style: WriteStyle) -> &mut Self {
pub(crate) fn write_style(&mut self, write_style: WriteStyle) -> &mut Self {
self.write_style = write_style;
self
}

/// Whether or not to capture logs for `cargo test`.
pub(crate) fn is_test(&mut self, is_test: bool) -> &mut Self {
self.is_test = is_test;
self
}

/// Build a terminal writer.
pub fn build(&mut self) -> Writer {
pub(crate) fn build(&mut self) -> Writer {
assert!(!self.built, "attempt to re-use consumed builder");
self.built = true;

Expand All @@ -124,8 +132,8 @@ impl Builder {
};

let writer = match self.target {
Target::Stderr => BufferWriter::stderr(color_choice),
Target::Stdout => BufferWriter::stdout(color_choice),
Target::Stderr => BufferWriter::stderr(self.is_test, color_choice),
Target::Stdout => BufferWriter::stdout(self.is_test, color_choice),
};

Writer {
Expand Down
79 changes: 63 additions & 16 deletions src/fmt/writer/termcolor/extern_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use log::Level;
use termcolor::{self, ColorChoice, ColorSpec, WriteColor};

use ::WriteStyle;
use ::fmt::Formatter;
use ::fmt::{Formatter, Target};

pub(in ::fmt::writer) mod glob {
pub use super::*;
Expand Down Expand Up @@ -69,51 +69,98 @@ impl Formatter {
}
}

pub(in ::fmt::writer) struct BufferWriter(termcolor::BufferWriter);
pub(in ::fmt) struct Buffer(termcolor::Buffer);
pub(in ::fmt::writer) struct BufferWriter {
inner: termcolor::BufferWriter,
test_target: Option<Target>,
}

pub(in ::fmt) struct Buffer {
inner: termcolor::Buffer,
test_target: Option<Target>,
}

impl BufferWriter {
pub(in ::fmt::writer) fn stderr(write_style: WriteStyle) -> Self {
BufferWriter(termcolor::BufferWriter::stderr(write_style.into_color_choice()))
pub(in ::fmt::writer) fn stderr(is_test: bool, write_style: WriteStyle) -> Self {
BufferWriter {
inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()),
test_target: if is_test {
Some(Target::Stderr)
} else {
None
},
}
}

pub(in ::fmt::writer) fn stdout(write_style: WriteStyle) -> Self {
BufferWriter(termcolor::BufferWriter::stdout(write_style.into_color_choice()))
pub(in ::fmt::writer) fn stdout(is_test: bool, write_style: WriteStyle) -> Self {
BufferWriter {
inner: termcolor::BufferWriter::stdout(write_style.into_color_choice()),
test_target: if is_test {
Some(Target::Stdout)
} else {
None
},
}
}

pub(in ::fmt::writer) fn buffer(&self) -> Buffer {
Buffer(self.0.buffer())
Buffer {
inner: self.inner.buffer(),
test_target: self.test_target,
}
}

pub(in ::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> {
self.0.print(&buf.0)
if let Some(target) = self.test_target {
// This impl uses the `eprint` and `print` macros
// instead of `termcolor`'s buffer.
// This is so their output can be captured by `cargo test`
let log = String::from_utf8_lossy(buf.bytes());

match target {
Target::Stderr => eprint!("{}", log),
Target::Stdout => print!("{}", log),
}

Ok(())
} else {
self.inner.print(&buf.inner)
}
}
}

impl Buffer {
pub(in ::fmt) fn clear(&mut self) {
self.0.clear()
self.inner.clear()
}

pub(in ::fmt) fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
self.inner.write(buf)
}

pub(in ::fmt) fn flush(&mut self) -> io::Result<()> {
self.0.flush()
self.inner.flush()
}

#[cfg(test)]
pub(in ::fmt) fn bytes(&self) -> &[u8] {
self.0.as_slice()
self.inner.as_slice()
}

fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
self.0.set_color(spec)
// Ignore styles for test captured logs because they can't be printed
if self.test_target.is_none() {
self.inner.set_color(spec)
} else {
Ok(())
}
}

fn reset(&mut self) -> io::Result<()> {
self.0.reset()
// Ignore styles for test captured logs because they can't be printed
if self.test_target.is_none() {
self.inner.reset()
} else {
Ok(())
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/fmt/writer/termcolor/shim_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ pub(in ::fmt::writer) struct BufferWriter {
pub(in ::fmt) struct Buffer(Vec<u8>);

impl BufferWriter {
pub(in ::fmt::writer) fn stderr(_: WriteStyle) -> Self {
pub(in ::fmt::writer) fn stderr(_is_test: bool, _write_style: WriteStyle) -> Self {
BufferWriter {
target: Target::Stderr,
}
}

pub(in ::fmt::writer) fn stdout(_: WriteStyle) -> Self {
pub(in ::fmt::writer) fn stdout(_is_test: bool, _write_style: WriteStyle) -> Self {
BufferWriter {
target: Target::Stdout,
}
Expand Down
78 changes: 69 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,33 @@
//! * `error,hello=warn/[0-9]scopes` turn on global error logging and also
//! warn for hello. In both cases the log message must include a single digit
//! number followed by 'scopes'.
//!
//! ## Capturing logs in tests
//!
//! Records logged during `cargo test` will not be captured by the test harness by default.
//! The [`Builder::is_test`] method can be used in unit tests to ensure logs will be captured:
//!
//! ```
//! # #[macro_use] extern crate log;
//! # extern crate env_logger;
//! # fn main() {}
//! #[cfg(test)]
//! mod tests {
//! fn init() {
//! let _ = env_logger::builder().is_test(true).try_init();
//! }
//!
//! #[test]
//! fn it_works() {
//! info!("This record will be captured by `cargo test`");
//!
//! assert_eq!(2, 1 + 1);
//! }
//! }
//! ```
//!
//! Enabling test capturing comes at the expense of color and other style support
//! and may have performance implications.
//!
//! ## Disabling colors
//!
Expand All @@ -157,9 +184,7 @@
//! The following example excludes the timestamp from the log output:
//!
//! ```
//! use env_logger::Builder;
//!
//! Builder::from_default_env()
//! env_logger::builder()
//! .default_format_timestamp(false)
//! .init();
//! ```
Expand All @@ -180,9 +205,8 @@
//!
//! ```
//! use std::io::Write;
//! use env_logger::Builder;
//!
//! Builder::from_default_env()
//! env_logger::builder()
//! .format(|buf, record| {
//! writeln!(buf, "{}: {}", record.level(), record.args())
//! })
Expand All @@ -199,13 +223,14 @@
//! isn't set:
//!
//! ```
//! use env_logger::{Builder, Env};
//! use env_logger::Env;
//!
//! Builder::from_env(Env::default().default_filter_or("warn")).init();
//! env_logger::from_env(Env::default().default_filter_or("warn")).init();
//! ```
//!
//! [log-crate-url]: https://docs.rs/log/
//! [`Builder`]: struct.Builder.html
//! [`Builder::is_test`]: struct.Builder.html#method.is_test
//! [`Env`]: struct.Env.html
//! [`fmt`]: fmt/index.html

Expand Down Expand Up @@ -404,7 +429,7 @@ impl Builder {
let env = env.into();

if let Some(s) = env.get_filter() {
builder.parse(&s);
builder.parse_filters(&s);
}

if let Some(s) = env.get_write_style() {
Expand Down Expand Up @@ -579,7 +604,16 @@ impl Builder {
/// environment variable.
///
/// See the module documentation for more details.
#[deprecated(since = "0.6.0", note = "use `parse_filters` instead.")]
pub fn parse(&mut self, filters: &str) -> &mut Self {
self.parse_filters(filters)
}

/// Parses the directives string in the same form as the `RUST_LOG`
/// environment variable.
///
/// See the module documentation for more details.
pub fn parse_filters(&mut self, filters: &str) -> &mut Self {
self.filter.parse(filters);
self
}
Expand Down Expand Up @@ -630,7 +664,16 @@ impl Builder {
///
/// See the module documentation for more details.
pub fn parse_write_style(&mut self, write_style: &str) -> &mut Self {
self.writer.parse(write_style);
self.writer.parse_write_style(write_style);
self
}

/// Sets whether or not the logger will be used in unit tests.
///
/// If `is_test` is `true` then the logger will allow the testing framework to
/// capture log records rather than printing them to the terminal directly.
pub fn is_test(&mut self, is_test: bool) -> &mut Self {
self.writer.is_test(is_test);
self
}

Expand Down Expand Up @@ -1068,6 +1111,23 @@ where
try_init_from_env(env).expect("env_logger::init_from_env should not be called after logger initialized");
}

/// Create a new builder with the default environment variables.
///
/// The builder can be configured before being initialized.
pub fn builder() -> Builder {
Builder::from_default_env()
}

/// Create a builder from the given environment variables.
///
/// The builder can be configured before being initialized.
pub fn from_env<'a, E>(env: E) -> Builder
where
E: Into<Env<'a>>
{
Builder::from_env(env)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion tests/init-twice-retains-filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fn main() {
// Init again using a different max level
// This shouldn't clobber the level that was previously set
env_logger::Builder::new()
.parse("info")
.parse_filters("info")
.try_init()
.unwrap_err();

Expand Down

0 comments on commit 8fa6240

Please sign in to comment.