1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
//! Snapshot testing tools for [`Transcript`](crate::Transcript)s.
//!
//! # Examples
//!
//! Simple scenario in which the tested transcript calls to one or more Cargo binaries / examples
//! by their original names.
//!
//! ```no_run
//! use term_transcript::{
//! ShellOptions, Transcript,
//! test::{MatchKind, TestConfig, TestOutputConfig},
//! };
//!
//! // Test configuration that can be shared across tests.
//! fn config() -> TestConfig {
//! let shell_options = ShellOptions::default().with_cargo_path();
//! TestConfig::new(shell_options)
//! .with_match_kind(MatchKind::Precise)
//! .with_output(TestOutputConfig::Verbose)
//! }
//!
//! // Usage in tests:
//! #[test]
//! fn help_command() {
//! config().test("tests/__snapshots__/help.svg", &["my-command --help"]);
//! }
//! ```
//!
//! Use [`TestConfig::test_transcript()`] for more complex scenarios or increased control:
//!
//! ```
//! use term_transcript::{test::TestConfig, ShellOptions, Transcript, UserInput};
//! # use term_transcript::svg::{Template, TemplateOptions};
//! use std::io;
//!
//! fn read_svg_file() -> anyhow::Result<impl io::BufRead> {
//! // snipped...
//! # let transcript = Transcript::from_inputs(
//! # &mut ShellOptions::default(),
//! # vec![UserInput::command(r#"echo "Hello world!""#)],
//! # )?;
//! # let mut writer = vec![];
//! # Template::new(TemplateOptions::default()).render(&transcript, &mut writer)?;
//! # Ok(io::Cursor::new(writer))
//! }
//!
//! # fn main() -> anyhow::Result<()> {
//! let reader = read_svg_file()?;
//! let transcript = Transcript::from_svg(reader)?;
//! TestConfig::new(ShellOptions::default()).test_transcript(&transcript);
//! # Ok(())
//! # }
//! ```
use std::process::Command;
#[cfg(feature = "svg")]
use std::{env, ffi::OsStr};
use termcolor::ColorChoice;
mod color_diff;
mod config_impl;
mod parser;
#[cfg(test)]
mod tests;
mod utils;
pub use self::parser::Parsed;
#[cfg(feature = "svg")]
use crate::svg::Template;
use crate::{traits::SpawnShell, ShellOptions, Transcript};
/// Configuration of output produced during testing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TestOutputConfig {
/// Do not output anything.
Quiet,
/// Output normal amount of details.
Normal,
/// Output more details.
Verbose,
}
impl Default for TestOutputConfig {
fn default() -> Self {
Self::Normal
}
}
/// Strategy for saving a new snapshot on a test failure within [`TestConfig::test()`] and
/// related methods.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg(feature = "svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
pub enum UpdateMode {
/// Never create a new snapshot on test failure.
Never,
/// Always create a new snapshot on test failure.
Always,
}
#[cfg(feature = "svg")]
impl UpdateMode {
/// Reads the update mode from the `TERM_TRANSCRIPT_UPDATE` env variable.
///
/// If the `TERM_TRANSCRIPT_UPDATE` variable is not set, the output depends on whether
/// the executable is running in CI (which is detected by the presence of
/// the `CI` env variable):
///
/// - In CI, the method returns [`Self::Never`].
/// - Otherwise, the method returns [`Self::Always`].
///
/// # Panics
///
/// If the `TERM_TRANSCRIPT_UPDATE` env variable is set to an unrecognized value
/// (something other than `never` or `always`), this method will panic.
pub fn from_env() -> Self {
const ENV_VAR: &str = "TERM_TRANSCRIPT_UPDATE";
match env::var_os(ENV_VAR) {
Some(s) => Self::from_os_str(&s).unwrap_or_else(|| {
panic!(
"Cannot read update mode from env variable {ENV_VAR}: `{}` is not a valid value \
(use one of `never` or `always`)",
s.to_string_lossy()
);
}),
None => {
if env::var_os("CI").is_some() {
Self::Never
} else {
Self::Always
}
}
}
}
fn from_os_str(s: &OsStr) -> Option<Self> {
match s {
s if s == "never" => Some(Self::Never),
s if s == "always" => Some(Self::Always),
_ => None,
}
}
fn should_create_snapshot(self) -> bool {
match self {
Self::Always => true,
Self::Never => false,
}
}
}
/// Testing configuration.
///
/// # Examples
///
/// See the [module docs](crate::test) for the examples of usage.
#[derive(Debug)]
pub struct TestConfig<Cmd = Command, F = fn(&mut Transcript)> {
shell_options: ShellOptions<Cmd>,
match_kind: MatchKind,
output: TestOutputConfig,
color_choice: ColorChoice,
#[cfg(feature = "svg")]
update_mode: UpdateMode,
#[cfg(feature = "svg")]
template: Template,
transform: F,
}
impl<Cmd: SpawnShell> TestConfig<Cmd> {
/// Creates a new config.
///
/// # Panics
///
/// - Panics if the `svg` crate feature is enabled and the `TERM_TRANSCRIPT_UPDATE` variable
/// is set to an incorrect value. See [`UpdateMode::from_env()`] for more details.
pub fn new(shell_options: ShellOptions<Cmd>) -> Self {
Self {
shell_options,
match_kind: MatchKind::TextOnly,
output: TestOutputConfig::Normal,
color_choice: ColorChoice::Auto,
#[cfg(feature = "svg")]
update_mode: UpdateMode::from_env(),
#[cfg(feature = "svg")]
template: Template::default(),
transform: |_| { /* do nothing */ },
}
}
/// Sets the transcript transform for these options. This can be used to transform the captured transcript
/// (e.g., to remove / replace uncontrollably varying data) before it's compared to the snapshot.
#[must_use]
pub fn with_transform<F>(self, transform: F) -> TestConfig<Cmd, F>
where
F: FnMut(&mut Transcript),
{
TestConfig {
shell_options: self.shell_options,
match_kind: self.match_kind,
output: self.output,
color_choice: self.color_choice,
#[cfg(feature = "svg")]
update_mode: self.update_mode,
#[cfg(feature = "svg")]
template: self.template,
transform,
}
}
}
impl<Cmd: SpawnShell, F: FnMut(&mut Transcript)> TestConfig<Cmd, F> {
/// Sets the matching kind applied.
#[must_use]
pub fn with_match_kind(mut self, kind: MatchKind) -> Self {
self.match_kind = kind;
self
}
/// Sets coloring of the output.
///
/// On Windows, `color_choice` has slightly different semantics than its usage
/// in the `termcolor` crate. Namely, if colors can be used (stdout is a tty with
/// color support), ANSI escape sequences will always be used.
#[must_use]
pub fn with_color_choice(mut self, color_choice: ColorChoice) -> Self {
self.color_choice = color_choice;
self
}
/// Configures test output.
#[must_use]
pub fn with_output(mut self, output: TestOutputConfig) -> Self {
self.output = output;
self
}
/// Sets the template for rendering new snapshots.
#[cfg(feature = "svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
#[must_use]
pub fn with_template(mut self, template: Template) -> Self {
self.template = template;
self
}
/// Overrides the strategy for saving new snapshots for failed tests.
///
/// By default, the strategy is determined from the execution environment
/// using [`UpdateMode::from_env()`].
#[cfg(feature = "svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
#[must_use]
pub fn with_update_mode(mut self, update_mode: UpdateMode) -> Self {
self.update_mode = update_mode;
self
}
}
/// Kind of terminal output matching.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum MatchKind {
/// Relaxed matching: compare only output text, but not coloring.
TextOnly,
/// Precise matching: compare output together with colors.
Precise,
}
/// Stats of a single snapshot test output by [`TestConfig::test_transcript_for_stats()`].
#[derive(Debug, Clone)]
pub struct TestStats {
// Match kind per each user input.
matches: Vec<Option<MatchKind>>,
}
impl TestStats {
/// Returns the number of successfully matched user inputs with at least the specified
/// `match_level`.
pub fn passed(&self, match_level: MatchKind) -> usize {
self.matches
.iter()
.filter(|&&kind| kind >= Some(match_level))
.count()
}
/// Returns the number of user inputs that do not match with at least the specified
/// `match_level`.
pub fn errors(&self, match_level: MatchKind) -> usize {
self.matches.len() - self.passed(match_level)
}
/// Returns match kinds per each user input of the tested [`Transcript`]. `None` values
/// mean no match.
///
/// [`Transcript`]: crate::Transcript
pub fn matches(&self) -> &[Option<MatchKind>] {
&self.matches
}
/// Panics if these stats contain errors.
#[allow(clippy::missing_panics_doc)]
pub fn assert_no_errors(&self, match_level: MatchKind) {
assert_eq!(self.errors(match_level), 0, "There were test errors");
}
}