From 25b51c7d9f756101bf7d1331bbacb4fe65cfa083 Mon Sep 17 00:00:00 2001 From: Bradford Hovinen Date: Fri, 21 Jul 2023 15:41:50 +0200 Subject: [PATCH] Add support for using `is_terminal` and environment variables to determine whether to output ANSI colour sequences. This makes use of the trait [`IsTerminal`](https://doc.rust-lang.org/stable/std/io/trait.IsTerminal.html), stabilised in Rust 1.70. If the Rust version is at least 1.70, then the library uses this trait to detect whether stdout is a terminal and only outputs colours by default if that is the case. When used on prior Rust versions, the library does not detect whether the output is a terminal and defaults to not outputing colours. This also introduces support for two environment variables to override the default behaviour defined above: * `NO_COLOR`, described in . * `FORCE_COLOR`, which is supported by a range of tools such as [Node.js](https://nodejs.org/api/cli.html#force_color1-2-3). The environment variable `GTEST_RUST_NO_COLOR` is hereby no longer supported. This also removes the ANSI colour sequences from nearly all tests. It creates two integration tests which specifically test the colourised output respectively the behaviour of the environment variable `NO_COLOR`. Otherwise, it globally sets `NO_COLOR` to suppress colourised output. This greatly reduces the noise in the existing test assertions and makes sure the tests continue to be compatible with all environments. Since the use of `IsTerminal` is optional and conditional on the Rust version, this does not change the minimum supported Rust version. --- README.md | 7 +- googletest/Cargo.toml | 3 +- googletest/config.toml | 2 + .../src/matcher_support/summarize_diff.rs | 82 ++++++------------- googletest/src/matchers/display_matcher.rs | 6 +- .../src/matchers/eq_deref_of_matcher.rs | 10 +-- googletest/src/matchers/eq_matcher.rs | 72 ++++++++-------- googletest/src/matchers/str_matcher.rs | 66 +++++++-------- googletest/tests/colourised_diff_test.rs | 52 ++++++++++++ googletest/tests/lib.rs | 1 + googletest/tests/no_color_test.rs | 54 ++++++++++++ 11 files changed, 214 insertions(+), 141 deletions(-) create mode 100644 googletest/config.toml create mode 100644 googletest/tests/colourised_diff_test.rs create mode 100644 googletest/tests/no_color_test.rs diff --git a/README.md b/README.md index 67afb619..c51423ec 100644 --- a/README.md +++ b/README.md @@ -297,9 +297,10 @@ displayed, we recommend setting those variables in the personal ### Configuration variable list -| Variable name | Description | -| ------------------- | -------------------------------------------------- | -| GTEST_RUST_NO_COLOR | If set to any value, disables ANSI output from the failure message. This is useful when the failure description is piped to a file or another process. | +| Variable name | Description | +| ------------- | ------------------------------------------------------- | +| NO_COLOR | Disables coloured output. See . | +| FORCE_COLOR | Forces colours even when the output is piped to a file. | ## Contributing Changes diff --git a/googletest/Cargo.toml b/googletest/Cargo.toml index 62778dcb..601c3ceb 100644 --- a/googletest/Cargo.toml +++ b/googletest/Cargo.toml @@ -34,8 +34,9 @@ authors = [ googletest_macro = { path = "../googletest_macro", version = "0.9.0" } anyhow = { version = "1", optional = true } num-traits = "0.2.15" -regex = "1.6.0" proptest = { version = "1.2.0", optional = true } +regex = "1.6.0" +rustversion = "1.0.14" [dev-dependencies] indoc = "2" diff --git a/googletest/config.toml b/googletest/config.toml new file mode 100644 index 00000000..7bc2412e --- /dev/null +++ b/googletest/config.toml @@ -0,0 +1,2 @@ +[env] +NO_COLOR = "1" diff --git a/googletest/src/matcher_support/summarize_diff.rs b/googletest/src/matcher_support/summarize_diff.rs index 6981496f..53f43807 100644 --- a/googletest/src/matcher_support/summarize_diff.rs +++ b/googletest/src/matcher_support/summarize_diff.rs @@ -14,14 +14,13 @@ #![doc(hidden)] -use std::borrow::Cow; -use std::fmt::{Display, Write}; - use crate::matcher_support::edit_distance; - -/// Environment variable controlling the usage of ansi color in difference -/// summary. -const NO_COLOR_VAR: &str = "GTEST_RUST_NO_COLOR"; +#[rustversion::since(1.70)] +use std::io::IsTerminal; +use std::{ + borrow::Cow, + fmt::{Display, Write}, +}; /// Returns a string describing how the expected and actual lines differ. /// @@ -195,7 +194,7 @@ struct StyledLine<'a> { impl<'a> Display for StyledLine<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if std::env::var(NO_COLOR_VAR).is_err() { + if stdout_supports_colour() { write!( f, "{}{}{}{}", @@ -207,36 +206,29 @@ impl<'a> Display for StyledLine<'a> { } } +#[rustversion::since(1.70)] +fn stdout_supports_colour() -> bool { + match (is_env_var_set("NO_COLOR"), is_env_var_set("FORCE_COLOR")) { + (true, _) => false, + (false, true) => true, + (false, false) => std::io::stdout().is_terminal(), + } +} + +#[rustversion::not(since(1.70))] +fn stdout_supports_colour() -> bool { + is_env_var_set("FORCE_COLOR") +} + +fn is_env_var_set(var: &'static str) -> bool { + std::env::var(var).map(|s| !s.is_empty()).unwrap_or(false) +} + #[cfg(test)] mod tests { use super::*; use crate::{matcher_support::edit_distance::Mode, prelude::*}; use indoc::indoc; - use serial_test::serial; - - #[must_use] - fn remove_var() -> TempVar { - let old_value = std::env::var(NO_COLOR_VAR); - std::env::remove_var(NO_COLOR_VAR); - TempVar(old_value.ok()) - } - - #[must_use] - fn set_var(var: &str) -> TempVar { - let old_value = std::env::var(NO_COLOR_VAR); - std::env::set_var(NO_COLOR_VAR, var); - TempVar(old_value.ok()) - } - struct TempVar(Option); - - impl Drop for TempVar { - fn drop(&mut self) { - match &self.0 { - Some(old_var) => std::env::set_var(NO_COLOR_VAR, old_var), - None => std::env::remove_var(NO_COLOR_VAR), - } - } - } // Make a long text with each element of the iterator on one line. // `collection` must contains at least one element. @@ -277,30 +269,8 @@ mod tests { } #[test] - #[serial] - fn create_diff_exact_small_difference() -> Result<()> { - let _cleanup = remove_var(); - - verify_that!( - create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact), - eq(indoc! { - " - - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): - 1 - 2 - \x1B[3m<---- 45 common lines omitted ---->\x1B[0m - 48 - 49 - +\x1B[1;32m50\x1B[0m" - }) - ) - } - - #[test] - #[serial] fn create_diff_exact_small_difference_no_color() -> Result<()> { - let _cleanup = set_var("NO_COLOR"); + std::env::set_var("NO_COLOR", "1"); verify_that!( create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact), diff --git a/googletest/src/matchers/display_matcher.rs b/googletest/src/matchers/display_matcher.rs index 3c110acb..628769be 100644 --- a/googletest/src/matchers/display_matcher.rs +++ b/googletest/src/matchers/display_matcher.rs @@ -108,10 +108,10 @@ mod tests { err(displays_as(contains_substring(indoc!( " which displays as a string which isn't equal to \"123\\n345\" - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): 123 - -\x1B[1;31m234\x1B[0m - +\x1B[1;32m345\x1B[0m + -234 + +345 " )))) ) diff --git a/googletest/src/matchers/eq_deref_of_matcher.rs b/googletest/src/matchers/eq_deref_of_matcher.rs index cb810ead..15409058 100644 --- a/googletest/src/matchers/eq_deref_of_matcher.rs +++ b/googletest/src/matchers/eq_deref_of_matcher.rs @@ -138,12 +138,12 @@ mod tests { " Actual: Strukt { int: 123, string: \"something\" }, which isn't equal to Strukt { int: 321, string: \"someone\" } - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): Strukt { - -\x1B[1;31m int: 123,\x1B[0m - +\x1B[1;32m int: 321,\x1B[0m - -\x1B[1;31m string: \"something\",\x1B[0m - +\x1B[1;32m string: \"someone\",\x1B[0m + - int: 123, + + int: 321, + - string: \"something\", + + string: \"someone\", } "}))) ) diff --git a/googletest/src/matchers/eq_matcher.rs b/googletest/src/matchers/eq_matcher.rs index fc52ecb2..42684c97 100644 --- a/googletest/src/matchers/eq_matcher.rs +++ b/googletest/src/matchers/eq_matcher.rs @@ -178,12 +178,12 @@ mod tests { " Actual: Strukt { int: 123, string: \"something\" }, which isn't equal to Strukt { int: 321, string: \"someone\" } - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): Strukt { - -\x1B[1;31m int: 123,\x1B[0m - +\x1B[1;32m int: 321,\x1B[0m - -\x1B[1;31m string: \"something\",\x1B[0m - +\x1B[1;32m string: \"someone\",\x1B[0m + - int: 123, + + int: 321, + - string: \"something\", + + string: \"someone\", } "}))) ) @@ -200,12 +200,12 @@ mod tests { Expected: is equal to [1, 3, 4] Actual: [1, 2, 3], which isn't equal to [1, 3, 4] - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): [ 1, - -\x1B[1;31m 2,\x1B[0m + - 2, 3, - +\x1B[1;32m 4,\x1B[0m + + 4, ] "}))) ) @@ -222,12 +222,12 @@ mod tests { Expected: is equal to [1, 3, 5] Actual: [1, 2, 3, 4, 5], which isn't equal to [1, 3, 5] - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): [ 1, - -\x1B[1;31m 2,\x1B[0m + - 2, 3, - -\x1B[1;31m 4,\x1B[0m + - 4, 5, ] "}))) @@ -241,17 +241,17 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): [ - -\x1B[1;31m 1,\x1B[0m - -\x1B[1;31m 2,\x1B[0m + - 1, + - 2, 3, 4, - \x1B[3m<---- 43 common lines omitted ---->\x1B[0m + <---- 43 common lines omitted ----> 48, 49, - +\x1B[1;32m 50,\x1B[0m - +\x1B[1;32m 51,\x1B[0m + + 50, + + 51, ]"}))) ) } @@ -263,17 +263,17 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): [ - -\x1B[1;31m 1,\x1B[0m - -\x1B[1;31m 2,\x1B[0m + - 1, + - 2, 3, 4, 5, 6, 7, - +\x1B[1;32m 8,\x1B[0m - +\x1B[1;32m 9,\x1B[0m + + 8, + + 9, ]"}))) ) } @@ -285,14 +285,14 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): [ 1, - \x1B[3m<---- 46 common lines omitted ---->\x1B[0m + <---- 46 common lines omitted ----> 48, 49, - +\x1B[1;32m 50,\x1B[0m - +\x1B[1;32m 51,\x1B[0m + + 50, + + 51, ]"}))) ) } @@ -304,13 +304,13 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + Difference(-actual / +expected): [ - -\x1B[1;31m 1,\x1B[0m - -\x1B[1;31m 2,\x1B[0m + - 1, + - 2, 3, 4, - \x1B[3m<---- 46 common lines omitted ---->\x1B[0m + <---- 46 common lines omitted ----> 51, ]"}))) ) @@ -357,8 +357,8 @@ mod tests { err(displays_as(contains_substring(indoc!( " First line - -\x1B[1;31mSecond line\x1B[0m - +\x1B[1;32mSecond lines\x1B[0m + -Second line + +Second lines Third line " )))) @@ -380,9 +380,7 @@ mod tests { verify_that!( result, - err(displays_as(not(contains_substring( - "Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):" - )))) + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) ) } @@ -401,9 +399,7 @@ mod tests { verify_that!( result, - err(displays_as(not(contains_substring( - "Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):" - )))) + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) ) } } diff --git a/googletest/src/matchers/str_matcher.rs b/googletest/src/matchers/str_matcher.rs index c8a510e3..3a4e2e99 100644 --- a/googletest/src/matchers/str_matcher.rs +++ b/googletest/src/matchers/str_matcher.rs @@ -974,8 +974,8 @@ mod tests { err(displays_as(contains_substring(indoc!( " First line - -\x1B[1;31mSecond line\x1B[0m - +\x1B[1;32mSecond lines\x1B[0m + -Second line + +Second lines Third line " )))) @@ -1007,10 +1007,10 @@ mod tests { err(displays_as(contains_substring(indoc!( " First line - -\x1B[1;31mSecond line\x1B[0m - +\x1B[1;32mSecond lines\x1B[0m + -Second line + +Second lines Third line - \x1B[3m<---- remaining lines omitted ---->\x1B[0m + <---- remaining lines omitted ----> " )))) ) @@ -1040,9 +1040,9 @@ mod tests { err(displays_as(contains_substring(indoc!( " First line - -\x1B[1;31mSecond line\x1B[0m - +\x1B[1;32mSecond lines\x1B[0m - \x1B[3m<---- remaining lines omitted ---->\x1B[0m + -Second line + +Second lines + <---- remaining lines omitted ----> " )))) ) @@ -1072,11 +1072,11 @@ mod tests { result, err(displays_as(contains_substring(indoc!( " - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): - \x1B[3m<---- remaining lines omitted ---->\x1B[0m + Difference(-actual / +expected): + <---- remaining lines omitted ----> Second line - +\x1B[1;32mThird lines\x1B[0m - -\x1B[1;31mThird line\x1B[0m + +Third lines + -Third line Fourth line " )))) @@ -1109,13 +1109,13 @@ mod tests { result, err(displays_as(contains_substring(indoc!( " - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): - \x1B[3m<---- remaining lines omitted ---->\x1B[0m + Difference(-actual / +expected): + <---- remaining lines omitted ----> Second line - +\x1B[1;32mThird lines\x1B[0m - -\x1B[1;31mThird line\x1B[0m + +Third lines + -Third line Fourth line - \x1B[3m<---- remaining lines omitted ---->\x1B[0m" + <---- remaining lines omitted ---->" )))) ) } @@ -1146,16 +1146,16 @@ mod tests { result, err(displays_as(contains_substring(indoc!( " - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): - \x1B[3m<---- remaining lines omitted ---->\x1B[0m - +\x1B[1;32mline\x1B[0m - -\x1B[1;31mSecond line\x1B[0m + Difference(-actual / +expected): + <---- remaining lines omitted ----> + +line + -Second line Third line - +\x1B[1;32mFoorth line\x1B[0m - -\x1B[1;31mFourth line\x1B[0m - +\x1B[1;32mFifth\x1B[0m - -\x1B[1;31mFifth line\x1B[0m - \x1B[3m<---- remaining lines omitted ---->\x1B[0m + +Foorth line + -Fourth line + +Fifth + -Fifth line + <---- remaining lines omitted ----> " )))) ) @@ -1186,10 +1186,10 @@ mod tests { err(displays_as(contains_substring(indoc!( " First line - -\x1B[1;31mSecond line\x1B[0m - +\x1B[1;32mSecond lines\x1B[0m + -Second line + +Second lines Third line - -\x1B[1;31mFourth line\x1B[0m + -Fourth line " )))) ) @@ -1209,9 +1209,7 @@ mod tests { verify_that!( result, - err(displays_as(not(contains_substring( - "Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):" - )))) + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) ) } @@ -1230,9 +1228,7 @@ mod tests { verify_that!( result, - err(displays_as(not(contains_substring( - "Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):" - )))) + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) ) } } diff --git a/googletest/tests/colourised_diff_test.rs b/googletest/tests/colourised_diff_test.rs new file mode 100644 index 00000000..d1b4ecbd --- /dev/null +++ b/googletest/tests/colourised_diff_test.rs @@ -0,0 +1,52 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use googletest::prelude::*; +use indoc::indoc; +use std::fmt::{Display, Write}; + +// Make a long text with each element of the iterator on one line. +// `collection` must contains at least one element. +fn build_text(mut collection: impl Iterator) -> String { + let mut text = String::new(); + write!(&mut text, "{}", collection.next().expect("Provided collection without elements")) + .unwrap(); + for item in collection { + write!(&mut text, "\n{}", item).unwrap(); + } + text +} + +#[test] +fn colors_appear_when_no_color_is_no_set_and_force_color_is_set() -> Result<()> { + std::env::remove_var("NO_COLOR"); + std::env::set_var("FORCE_COLOR", "1"); + + let result = verify_that!(build_text(1..50), eq(build_text(1..51))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + + Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + 1 + 2 + \x1B[3m<---- 45 common lines omitted ---->\x1B[0m + 48 + 49 + +\x1B[1;32m50\x1B[0m" + }))) + ) +} diff --git a/googletest/tests/lib.rs b/googletest/tests/lib.rs index f6b3ec10..6cb158e5 100644 --- a/googletest/tests/lib.rs +++ b/googletest/tests/lib.rs @@ -14,6 +14,7 @@ mod all_matcher_test; mod any_matcher_test; +mod colourised_diff_test; mod composition_test; mod elements_are_matcher_test; mod field_matcher_test; diff --git a/googletest/tests/no_color_test.rs b/googletest/tests/no_color_test.rs new file mode 100644 index 00000000..86c40f47 --- /dev/null +++ b/googletest/tests/no_color_test.rs @@ -0,0 +1,54 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(feature = "supports-color")] + +use googletest::prelude::*; +use indoc::indoc; +use std::fmt::{Display, Write}; + +// Make a long text with each element of the iterator on one line. +// `collection` must contains at least one element. +fn build_text(mut collection: impl Iterator) -> String { + let mut text = String::new(); + write!(&mut text, "{}", collection.next().expect("Provided collection without elements")) + .unwrap(); + for item in collection { + write!(&mut text, "\n{}", item).unwrap(); + } + text +} + +#[test] +fn colours_suppressed_when_both_no_color_and_force_color_are_set() -> Result<()> { + std::env::set_var("NO_COLOR", "1"); + std::env::set_var("FORCE_COLOR", "1"); + + let result = verify_that!(build_text(1..50), eq(build_text(1..51))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + + Difference(-actual / +expected): + 1 + 2 + <---- 45 common lines omitted ----> + 48 + 49 + +50" + }))) + ) +}