From dbabdbe4f7a12790a45550554c89c6475131eab2 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Sun, 13 Oct 2024 15:24:02 +0300 Subject: [PATCH] Add `Transcript` transforms for tests (#175) ## What? Allows customizing captured `Transcript`s during testing. ## Why? This is useful to filter out / replace variable or env-dependent parts of the test inputs and/or outputs. --- cli/src/main.rs | 7 +++-- cli/src/shell.rs | 9 +++---- cli/src/template.rs | 7 +++-- cli/tests/e2e.rs | 3 +-- e2e-tests/rainbow/src/bin/repl.rs | 3 +-- e2e-tests/rainbow/src/main.rs | 4 +-- e2e-tests/rainbow/tests/integration.rs | 11 ++++---- lib/CHANGELOG.md | 8 +++++- lib/src/lib.rs | 12 ++++++++- lib/src/pty.rs | 10 +++---- lib/src/shell/mod.rs | 1 - lib/src/shell/transcript_impl.rs | 8 +++--- lib/src/svg/helpers.rs | 4 +-- lib/src/svg/mod.rs | 7 +++-- lib/src/svg/palette.rs | 4 +-- lib/src/term/mod.rs | 14 +++++----- lib/src/term/parser.rs | 4 +-- lib/src/term/tests.rs | 5 ++-- lib/src/test/color_diff/mod.rs | 6 ++--- lib/src/test/config_impl.rs | 18 +++++++------ lib/src/test/mod.rs | 33 +++++++++++++++++++---- lib/src/test/parser/mod.rs | 36 ++++++++++++++++++-------- lib/src/test/parser/tests.rs | 25 +++++++++++++++--- lib/src/test/parser/text.rs | 4 +-- lib/src/test/tests.rs | 2 +- lib/src/test/utils.rs | 8 +++--- lib/src/utils.rs | 4 +-- lib/src/write/html.rs | 4 +-- lib/src/write/mod.rs | 4 +-- lib/src/write/svg.rs | 4 +-- lib/src/write/tests.rs | 4 +-- lib/tests/integration.rs | 15 +++++------ 32 files changed, 177 insertions(+), 111 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index f06176e2..aac9e72f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,9 +1,5 @@ //! CLI for the `term-transcript` crate. -use anyhow::Context; -use clap::{Parser, Subcommand, ValueEnum}; -use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; - use std::{ fmt, fs::File, @@ -13,11 +9,14 @@ use std::{ str::FromStr, }; +use anyhow::Context; +use clap::{Parser, Subcommand, ValueEnum}; use term_transcript::{ test::{MatchKind, TestConfig, TestOutputConfig, TestStats}, traits::SpawnShell, Transcript, }; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; mod shell; mod template; diff --git a/cli/src/shell.rs b/cli/src/shell.rs index e5ce6ddf..d1d2c687 100644 --- a/cli/src/shell.rs +++ b/cli/src/shell.rs @@ -1,20 +1,19 @@ //! Shell-related command-line args. -use clap::Args; -use humantime::Duration; - use std::{env, ffi::OsString, io, process::Command}; +use clap::Args; +use humantime::Duration; #[cfg(feature = "portable-pty")] use term_transcript::PtyCommand; use term_transcript::{traits::Echoing, Captured, ExitStatus, ShellOptions, Transcript, UserInput}; #[cfg(feature = "portable-pty")] mod pty { - use anyhow::Context; - use std::str::FromStr; + use anyhow::Context; + #[cfg(feature = "portable-pty")] #[derive(Debug, Clone, Copy)] pub(super) struct PtySize { diff --git a/cli/src/template.rs b/cli/src/template.rs index cf7929cc..b393d443 100644 --- a/cli/src/template.rs +++ b/cli/src/template.rs @@ -1,15 +1,14 @@ //! Templating-related command-line args. -use anyhow::Context; -use clap::{Args, ValueEnum}; -use handlebars::Template as HandlebarsTemplate; - use std::{ fs::{self, File}, io, mem, path::{Path, PathBuf}, }; +use anyhow::Context; +use clap::{Args, ValueEnum}; +use handlebars::Template as HandlebarsTemplate; use term_transcript::{ svg::{self, ScrollOptions, Template, TemplateOptions, WrapOptions}, Transcript, UserInput, diff --git a/cli/tests/e2e.rs b/cli/tests/e2e.rs index b0a53e80..1ba82a1f 100644 --- a/cli/tests/e2e.rs +++ b/cli/tests/e2e.rs @@ -1,12 +1,11 @@ #![cfg(unix)] -use tempfile::{tempdir, TempDir}; - use std::{ path::{Path, PathBuf}, time::Duration, }; +use tempfile::{tempdir, TempDir}; use term_transcript::{ svg::{ScrollOptions, Template, TemplateOptions}, test::{MatchKind, TestConfig}, diff --git a/e2e-tests/rainbow/src/bin/repl.rs b/e2e-tests/rainbow/src/bin/repl.rs index b0de88c1..4e007532 100644 --- a/e2e-tests/rainbow/src/bin/repl.rs +++ b/e2e-tests/rainbow/src/bin/repl.rs @@ -1,10 +1,9 @@ //! Simple REPL application that echoes the input with coloring / styles applied. -use termcolor::{Ansi, Color, ColorSpec, WriteColor}; - use std::io::{self, BufRead}; use term_transcript::svg::RgbColor; +use termcolor::{Ansi, Color, ColorSpec, WriteColor}; fn process_line(writer: &mut impl WriteColor, line: &str) -> io::Result<()> { let parts: Vec<_> = line.split_whitespace().collect(); diff --git a/e2e-tests/rainbow/src/main.rs b/e2e-tests/rainbow/src/main.rs index eca0602d..a64dac14 100644 --- a/e2e-tests/rainbow/src/main.rs +++ b/e2e-tests/rainbow/src/main.rs @@ -1,12 +1,12 @@ //! Simple executable that outputs colored output. Used for testing. -use termcolor::{Ansi, Color, ColorSpec, WriteColor}; - use std::{ env, io::{self, Write}, }; +use termcolor::{Ansi, Color, ColorSpec, WriteColor}; + const BASE_COLORS: &[(&str, Color)] = &[ ("black", Color::Black), ("blue", Color::Blue), diff --git a/e2e-tests/rainbow/tests/integration.rs b/e2e-tests/rainbow/tests/integration.rs index cfeb2b61..0bef59ca 100644 --- a/e2e-tests/rainbow/tests/integration.rs +++ b/e2e-tests/rainbow/tests/integration.rs @@ -1,9 +1,3 @@ -use handlebars::Template as HandlebarsTemplate; -use tempfile::tempdir; -use test_casing::{decorate, decorators::Retry, test_casing}; -use tracing::{subscriber::DefaultGuard, Subscriber}; -use tracing_subscriber::{fmt::format::FmtSpan, FmtSubscriber}; - use std::{ fs::{self, File}, io::{self, BufReader, Read}, @@ -13,6 +7,8 @@ use std::{ time::Duration, }; +use handlebars::Template as HandlebarsTemplate; +use tempfile::tempdir; #[cfg(feature = "portable-pty")] use term_transcript::PtyCommand; use term_transcript::{ @@ -20,6 +16,9 @@ use term_transcript::{ test::{MatchKind, TestConfig, TestOutputConfig, UpdateMode}, ShellOptions, Transcript, UserInput, }; +use test_casing::{decorate, decorators::Retry, test_casing}; +use tracing::{subscriber::DefaultGuard, Subscriber}; +use tracing_subscriber::{fmt::format::FmtSpan, FmtSubscriber}; const PATH_TO_BIN: &str = env!("CARGO_BIN_EXE_rainbow"); const PATH_TO_REPL_BIN: &str = env!("CARGO_BIN_EXE_rainbow-repl"); diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 9c26b22d..aa4d0a36 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -5,6 +5,12 @@ The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) ## [Unreleased] +### Added + +- Allow transforming captured `Transcript`s. This is mostly useful for testing to filter out / replace + variable / env-dependent output parts. Correspondingly, `TestConfig` allows customizing a transform + using `with_transform()` method. + ### Changed - Update `quick-xml` and `handlebars` dependencies. @@ -37,7 +43,7 @@ The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) As an example, this can be used to import fonts using `@import` or `@font-face`. - Add a fallback error message to the default template if HTML-in-SVG embedding is not supported. -- Add [FAQ](FAQ.md) with some tips and troubleshooting advice. +- Add [FAQ](../FAQ.md) with some tips and troubleshooting advice. - Allow hiding `UserInput`s during transcript rendering by calling the `hide()` method. Hidden inputs are supported by the default and pure SVG templates. diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 1433d365..e34d3799 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -256,6 +256,11 @@ impl Transcript { pub fn interactions(&self) -> &[Interaction] { &self.interactions } + + /// Returns a mutable reference to interactions in this transcript. + pub fn interactions_mut(&mut self) -> &mut [Interaction] { + &mut self.interactions + } } impl Transcript { @@ -346,7 +351,7 @@ impl Interaction { pub fn new(input: impl Into, output: impl Into) -> Self { Self { input: input.into(), - output: Captured::new(output.into()), + output: Captured::from(output.into()), exit_status: None, } } @@ -370,6 +375,11 @@ impl Interaction { &self.output } + /// Sets the output for this interaction. + pub fn set_output(&mut self, output: Out) { + self.output = output; + } + /// Returns exit status of the interaction, if available. pub fn exit_status(&self) -> Option { self.exit_status diff --git a/lib/src/pty.rs b/lib/src/pty.rs index 27398be2..8dc8ba0e 100644 --- a/lib/src/pty.rs +++ b/lib/src/pty.rs @@ -2,8 +2,6 @@ // FIXME: Prompt incorrectly read from PTY in some cases (#24) -use portable_pty::{native_pty_system, Child, CommandBuilder, PtyPair, PtySize}; - use std::{ collections::HashMap, error::Error as StdError, @@ -12,6 +10,8 @@ use std::{ path::{Path, PathBuf}, }; +use portable_pty::{native_pty_system, Child, CommandBuilder, PtyPair, PtySize}; + use crate::{ traits::{ConfigureCommand, ShellProcess, SpawnShell, SpawnedShell}, utils::is_recoverable_kill_error, @@ -201,15 +201,15 @@ impl ShellProcess for PtyShell { #[cfg(test)] mod tests { - use super::*; - use crate::{ShellOptions, Transcript, UserInput}; - use std::{ io::{Read, Write}, thread, time::Duration, }; + use super::*; + use crate::{ShellOptions, Transcript, UserInput}; + #[test] fn pty_trait_implementation() -> anyhow::Result<()> { let mut pty_command = PtyCommand::default(); diff --git a/lib/src/shell/mod.rs b/lib/src/shell/mod.rs index c76ac538..9af13efa 100644 --- a/lib/src/shell/mod.rs +++ b/lib/src/shell/mod.rs @@ -14,7 +14,6 @@ mod standard; mod transcript_impl; pub use self::standard::StdShell; - use crate::{ traits::{ConfigureCommand, Echoing, SpawnShell, SpawnedShell}, Captured, ExitStatus, diff --git a/lib/src/shell/transcript_impl.rs b/lib/src/shell/transcript_impl.rs index 43cc2387..a7682e7f 100644 --- a/lib/src/shell/transcript_impl.rs +++ b/lib/src/shell/transcript_impl.rs @@ -82,7 +82,7 @@ impl Transcript { fn read_output( lines_recv: &mpsc::Receiver>, mut timeouts: Timeouts, - line_decoder: &mut impl FnMut(Vec) -> io::Result, + line_decoder: &mut dyn FnMut(Vec) -> io::Result, ) -> io::Result { let mut output = String::new(); @@ -242,7 +242,7 @@ impl Transcript { let output = Self::read_output( lines_recv, Timeouts::new(options), - &mut options.line_decoder, + options.line_decoder.as_mut(), )?; let exit_status = if let Some(status_check) = &options.status_check { @@ -254,9 +254,9 @@ impl Transcript { let response = Self::read_output( lines_recv, Timeouts::new(options), - &mut options.line_decoder, + options.line_decoder.as_mut(), )?; - status_check.check(&Captured::new(response)) + status_check.check(&Captured::from(response)) } else { None }; diff --git a/lib/src/svg/helpers.rs b/lib/src/svg/helpers.rs index de3e0f92..74ef9210 100644 --- a/lib/src/svg/helpers.rs +++ b/lib/src/svg/helpers.rs @@ -1,13 +1,13 @@ //! Custom Handlebars helpers. +use std::sync::Mutex; + use handlebars::{ BlockContext, Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason, Renderable, ScopedJson, StringOutput, }; use serde_json::Value as Json; -use std::sync::Mutex; - /// Tries to convert an `f64` number to `i64` without precision loss. #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn to_i64(value: f64) -> Option { diff --git a/lib/src/svg/mod.rs b/lib/src/svg/mod.rs index 3f9a451f..d245c0ee 100644 --- a/lib/src/svg/mod.rs +++ b/lib/src/svg/mod.rs @@ -11,24 +11,23 @@ //! //! See [`Template`] for examples of usage. +use std::{fmt, io::Write}; + use handlebars::{Handlebars, RenderError, RenderErrorReason, Template as HandlebarsTemplate}; use serde::{Deserialize, Serialize}; -use std::{fmt, io::Write}; - mod data; mod helpers; mod palette; #[cfg(test)] mod tests; +use self::helpers::register_helpers; pub use self::{ data::{CreatorData, HandlebarsData, SerializedInteraction}, palette::{NamedPalette, NamedPaletteParseError, Palette, TermColors}, }; pub use crate::utils::{RgbColor, RgbColorParseError}; - -use self::helpers::register_helpers; use crate::{write::SvgLine, TermError, Transcript}; const DEFAULT_TEMPLATE: &str = include_str!("default.svg.handlebars"); diff --git a/lib/src/svg/palette.rs b/lib/src/svg/palette.rs index 9928f9da..9091db2f 100644 --- a/lib/src/svg/palette.rs +++ b/lib/src/svg/palette.rs @@ -1,9 +1,9 @@ //! `Palette` and other color-related types. -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use std::{error, fmt, str::FromStr}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use crate::utils::RgbColor; /// Palette of [16 standard terminal colors][colors] (8 ordinary colors + 8 intense variations). diff --git a/lib/src/term/mod.rs b/lib/src/term/mod.rs index 49612f17..df3b2e32 100644 --- a/lib/src/term/mod.rs +++ b/lib/src/term/mod.rs @@ -1,7 +1,7 @@ -use termcolor::NoColor; - use std::{borrow::Cow, fmt::Write as WriteStr}; +use termcolor::NoColor; + #[cfg(feature = "svg")] use crate::write::{SvgLine, SvgWriter}; use crate::{ @@ -29,15 +29,17 @@ impl AsRef for Captured { } } -impl Captured { - pub(crate) fn new(term_output: String) -> Self { +impl From for Captured { + fn from(raw: String) -> Self { // Normalize newlines to `\n`. - Self(match normalize_newlines(&term_output) { + Self(match normalize_newlines(&raw) { Cow::Owned(normalized) => normalized, - Cow::Borrowed(_) => term_output, + Cow::Borrowed(_) => raw, }) } +} +impl Captured { pub(crate) fn write_as_html( &self, output: &mut dyn WriteStr, diff --git a/lib/src/term/parser.rs b/lib/src/term/parser.rs index c709880a..978e5ef2 100644 --- a/lib/src/term/parser.rs +++ b/lib/src/term/parser.rs @@ -1,10 +1,10 @@ //! Parser for terminal output that converts it to a sequence of instructions to //! a writer implementing `WriteColor`. -use termcolor::{Color, ColorSpec, WriteColor}; - use std::str; +use termcolor::{Color, ColorSpec, WriteColor}; + use crate::TermError; /// Parses terminal output and issues corresponding commands to the `writer`. diff --git a/lib/src/term/tests.rs b/lib/src/term/tests.rs index b6eb003b..3171d0c7 100644 --- a/lib/src/term/tests.rs +++ b/lib/src/term/tests.rs @@ -1,8 +1,9 @@ -use super::*; - use std::io::Write; + use termcolor::{Ansi, Color, ColorSpec, WriteColor}; +use super::*; + fn prepare_term_output() -> anyhow::Result { let mut writer = Ansi::new(vec![]); writer.set_color( diff --git a/lib/src/test/color_diff/mod.rs b/lib/src/test/color_diff/mod.rs index 097be59f..cc7996f1 100644 --- a/lib/src/test/color_diff/mod.rs +++ b/lib/src/test/color_diff/mod.rs @@ -1,12 +1,12 @@ -use termcolor::{Color, ColorSpec, WriteColor}; -use unicode_width::UnicodeWidthStr; - use std::{ cmp::{self, Ordering}, io, iter::{self, Peekable}, }; +use termcolor::{Color, ColorSpec, WriteColor}; +use unicode_width::UnicodeWidthStr; + #[cfg(test)] mod tests; diff --git a/lib/src/test/config_impl.rs b/lib/src/test/config_impl.rs index b7e17a56..63fd4a21 100644 --- a/lib/src/test/config_impl.rs +++ b/lib/src/test/config_impl.rs @@ -1,7 +1,5 @@ //! Implementation details for `TestConfig`. -use termcolor::{Color, ColorSpec, NoColor, WriteColor}; - use std::{ fmt, fs::File, @@ -10,6 +8,8 @@ use std::{ str, }; +use termcolor::{Color, ColorSpec, NoColor, WriteColor}; + use super::{ color_diff::{ColorDiff, ColorSpan}, parser::Parsed, @@ -18,7 +18,7 @@ use super::{ }; use crate::{traits::SpawnShell, Interaction, TermError, Transcript, UserInput}; -impl TestConfig { +impl TestConfig { /// Tests a snapshot at the specified path with the provided inputs. /// /// If the path is relative, it is resolved relative to the current working dir, @@ -54,7 +54,7 @@ impl TestConfig { /// [inferred]: crate::test::UpdateMode::from_env() #[cfg_attr( feature = "tracing", - tracing::instrument(skip(snapshot_path, inputs), fields(snapshot_path, inputs)) + tracing::instrument(skip_all, fields(snapshot_path, inputs)) )] pub fn test>( &mut self, @@ -136,10 +136,11 @@ impl TestConfig { path: &Path, inputs: impl Iterator, ) -> String { - let reproduced = - Transcript::from_inputs(&mut self.shell_options, inputs).unwrap_or_else(|err| { + let mut reproduced = Transcript::from_inputs(&mut self.shell_options, inputs) + .unwrap_or_else(|err| { panic!("Cannot create a snapshot `{path:?}`: {err}"); }); + (self.transform)(&mut reproduced); self.write_new_snapshot(path, &reproduced) } @@ -211,7 +212,7 @@ impl TestConfig { /// /// - Returns an error if an error occurs during reproducing the transcript or processing /// its output. - #[cfg_attr(feature = "tracing", tracing::instrument(skip(transcript), err))] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, err))] pub fn test_transcript_for_stats( &mut self, transcript: &Transcript, @@ -234,7 +235,8 @@ impl TestConfig { .interactions() .iter() .map(|interaction| interaction.input().clone()); - let reproduced = Transcript::from_inputs(&mut self.shell_options, inputs)?; + let mut reproduced = Transcript::from_inputs(&mut self.shell_options, inputs)?; + (self.transform)(&mut reproduced); let stats = self.compare_transcripts(out, transcript, &reproduced)?; Ok((stats, reproduced)) diff --git a/lib/src/test/mod.rs b/lib/src/test/mod.rs index bc1650eb..7b129710 100644 --- a/lib/src/test/mod.rs +++ b/lib/src/test/mod.rs @@ -52,12 +52,12 @@ //! # } //! ``` -use termcolor::ColorChoice; - use std::process::Command; #[cfg(feature = "svg")] use std::{env, ffi::OsStr}; +use termcolor::ColorChoice; + mod color_diff; mod config_impl; mod parser; @@ -66,10 +66,9 @@ mod tests; mod utils; pub use self::parser::Parsed; - #[cfg(feature = "svg")] use crate::svg::Template; -use crate::{traits::SpawnShell, ShellOptions}; +use crate::{traits::SpawnShell, ShellOptions, Transcript}; /// Configuration of output produced during testing. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -160,7 +159,7 @@ impl UpdateMode { /// /// See the [module docs](crate::test) for the examples of usage. #[derive(Debug)] -pub struct TestConfig { +pub struct TestConfig { shell_options: ShellOptions, match_kind: MatchKind, output: TestOutputConfig, @@ -169,6 +168,7 @@ pub struct TestConfig { update_mode: UpdateMode, #[cfg(feature = "svg")] template: Template, + transform: F, } impl TestConfig { @@ -188,9 +188,32 @@ impl TestConfig { 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(self, transform: F) -> TestConfig + 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 TestConfig { /// Sets the matching kind applied. #[must_use] pub fn with_match_kind(mut self, kind: MatchKind) -> Self { diff --git a/lib/src/test/parser/mod.rs b/lib/src/test/parser/mod.rs index 92e27439..b7fdf946 100644 --- a/lib/src/test/parser/mod.rs +++ b/lib/src/test/parser/mod.rs @@ -1,11 +1,5 @@ //! SVG parsing logic. -use quick_xml::{ - events::{attributes::Attributes, Event}, - Reader as XmlReader, -}; -use termcolor::WriteColor; - use std::{ borrow::Cow, error::Error as StdError, @@ -16,6 +10,12 @@ use std::{ str, }; +use quick_xml::{ + events::{attributes::Attributes, Event}, + Reader as XmlReader, +}; +use termcolor::WriteColor; + #[cfg(test)] mod tests; mod text; @@ -216,15 +216,17 @@ impl StdError for ParseError { #[derive(Debug)] struct UserInputState { exit_status: Option, + is_hidden: bool, text: TextReadingState, prompt: Option>, prompt_open_tags: Option, } impl UserInputState { - fn new(exit_status: Option) -> Self { + fn new(exit_status: Option, is_hidden: bool) -> Self { Self { exit_status, + is_hidden, text: TextReadingState::default(), prompt: None, prompt_open_tags: None, @@ -265,7 +267,7 @@ impl UserInputState { let input = UserInput { text: String::new(), prompt: Some(UserInput::intern_prompt(parsed.plaintext)), - hidden: false, + hidden: self.is_hidden, }; return Ok(Some(Interaction { input, @@ -281,7 +283,7 @@ impl UserInputState { let input = UserInput { text: parsed.into_input_text(), prompt: self.prompt.take(), - hidden: false, + hidden: self.is_hidden, }; Interaction { input, @@ -352,8 +354,14 @@ impl ParserState { if let Event::Start(tag) = event { let classes = parse_classes(tag.attributes())?; if Self::is_input_class(extract_base_class(&classes)) { + let is_hidden = classes + .split(|byte| *byte == b' ') + .any(|chunk| chunk == b"input-hidden"); let exit_status = parse_exit_status(tag.attributes())?; - self.set_state(Self::ReadingUserInput(UserInputState::new(exit_status))); + self.set_state(Self::ReadingUserInput(UserInputState::new( + exit_status, + is_hidden, + ))); } } } @@ -378,7 +386,13 @@ impl ParserState { } else if Self::is_input_class(base_class) { let interaction = mem::replace(interaction, Self::DUMMY_INTERACTION); let exit_status = parse_exit_status(tag.attributes())?; - self.set_state(Self::ReadingUserInput(UserInputState::new(exit_status))); + let is_hidden = classes + .split(|byte| *byte == b' ') + .any(|chunk| chunk == b"input-hidden"); + self.set_state(Self::ReadingUserInput(UserInputState::new( + exit_status, + is_hidden, + ))); return Ok(Some(interaction)); } } diff --git a/lib/src/test/parser/tests.rs b/lib/src/test/parser/tests.rs index 0c4ad416..f12f5b75 100644 --- a/lib/src/test/parser/tests.rs +++ b/lib/src/test/parser/tests.rs @@ -1,9 +1,9 @@ +use std::io::{Cursor, Read}; + use assert_matches::assert_matches; use quick_xml::events::{BytesEnd, BytesStart, BytesText}; use test_casing::test_casing; -use std::io::{Cursor, Read}; - use super::*; use crate::ExitStatus; @@ -127,6 +127,23 @@ fn reading_file_with_exit_code_info() { assert_eq!(interaction.exit_status, Some(ExitStatus(127))); } +#[test] +fn reading_file_with_hidden_input() { + const SVG: &[u8] = br#" + + +
+
$ what
+
+
+
+ "#; + + let transcript = Transcript::from_svg(SVG).unwrap(); + assert_eq!(transcript.interactions.len(), 1); + assert!(transcript.interactions[0].input.hidden); +} + #[test] fn invalid_exit_code_info() { const SVG: &[u8] = br#" @@ -193,7 +210,7 @@ fn reading_file_with_invalid_container(attrs: &str) { #[test] fn reading_user_input_with_manual_events() { - let mut state = UserInputState::new(None); + let mut state = UserInputState::new(None, false); { let event = Event::Start(BytesStart::new("pre")); assert!(state.process(event).unwrap().is_none()); @@ -238,7 +255,7 @@ fn read_user_input(input: &[u8]) -> UserInput { wrapped_input.extend_from_slice(b""); let mut reader = XmlReader::from_reader(wrapped_input.as_slice()); - let mut state = UserInputState::new(None); + let mut state = UserInputState::new(None, false); // Skip the `
` start event. while !matches!(reader.read_event().unwrap(), Event::Start(_)) { diff --git a/lib/src/test/parser/text.rs b/lib/src/test/parser/text.rs index 7cd9c05c..32cb32eb 100644 --- a/lib/src/test/parser/text.rs +++ b/lib/src/test/parser/text.rs @@ -1,10 +1,10 @@ //! Text parsing. +use std::{borrow::Cow, fmt, io::Write, mem, str}; + use quick_xml::events::{BytesStart, Event}; use termcolor::{Color, ColorSpec, WriteColor}; -use std::{borrow::Cow, fmt, io::Write, mem, str}; - use super::{parse_classes, ParseError, Parsed}; use crate::{ test::color_diff::ColorSpansWriter, diff --git a/lib/src/test/tests.rs b/lib/src/test/tests.rs index 61d682aa..4accfc9e 100644 --- a/lib/src/test/tests.rs +++ b/lib/src/test/tests.rs @@ -74,7 +74,7 @@ fn negative_snapshot_testing_with_verbose_output() { } fn diff_snapshot_with_color(expected_capture: &str, actual_capture: &str) -> (TestStats, String) { - let expected_capture = Captured::new(expected_capture.to_owned()); + let expected_capture = Captured::from(expected_capture.to_owned()); let parsed = Transcript { interactions: vec![Interaction { input: UserInput::command("test"), diff --git a/lib/src/test/utils.rs b/lib/src/test/utils.rs index e842d320..2112ebff 100644 --- a/lib/src/test/utils.rs +++ b/lib/src/test/utils.rs @@ -1,10 +1,10 @@ -use termcolor::{Ansi, ColorChoice, ColorSpec, NoColor, StandardStream, WriteColor}; - use std::{ io::{self, IsTerminal, Write}, str, }; +use termcolor::{Ansi, ColorChoice, ColorSpec, NoColor, StandardStream, WriteColor}; + #[cfg(test)] use self::tests::print_to_buffer; @@ -176,10 +176,10 @@ impl WriteColor for ColorPrintlnWriter { #[cfg(test)] mod tests { - use super::*; - use std::{cell::RefCell, fmt, mem}; + use super::*; + thread_local! { static OUTPUT_CAPTURE: RefCell> = RefCell::default(); } diff --git a/lib/src/utils.rs b/lib/src/utils.rs index 0e3d820b..ca3ad82c 100644 --- a/lib/src/utils.rs +++ b/lib/src/utils.rs @@ -152,10 +152,10 @@ mod rgb_color { #[cfg(all(test, any(feature = "svg", feature = "test")))] mod tests { - use super::*; - use assert_matches::assert_matches; + use super::*; + #[test] fn parsing_color() { let RgbColor(r, g, b) = "#fed".parse().unwrap(); diff --git a/lib/src/write/html.rs b/lib/src/write/html.rs index 6847d27c..51e9727f 100644 --- a/lib/src/write/html.rs +++ b/lib/src/write/html.rs @@ -1,9 +1,9 @@ //! `HtmlWriter` and related types. -use termcolor::{ColorSpec, WriteColor}; - use std::{fmt, io}; +use termcolor::{ColorSpec, WriteColor}; + use super::{ fmt_to_io_error, IndexOrRgb, LineBreak, LineSplitter, StyledSpan, WriteLines, WriteStr, }; diff --git a/lib/src/write/mod.rs b/lib/src/write/mod.rs index 2923de2b..212a97d8 100644 --- a/lib/src/write/mod.rs +++ b/lib/src/write/mod.rs @@ -1,10 +1,10 @@ //! Rendering logic for terminal outputs. +use std::{fmt, io, str}; + use termcolor::{Color, ColorSpec}; use unicode_width::UnicodeWidthChar; -use std::{fmt, io, str}; - mod html; #[cfg(feature = "svg")] mod svg; diff --git a/lib/src/write/svg.rs b/lib/src/write/svg.rs index d28ca8a6..11d2054f 100644 --- a/lib/src/write/svg.rs +++ b/lib/src/write/svg.rs @@ -1,8 +1,8 @@ +use std::{fmt, io, iter, mem, str}; + use serde::Serialize; use termcolor::{ColorSpec, WriteColor}; -use std::{fmt, io, iter, mem, str}; - use super::{IndexOrRgb, LineBreak, LineSplitter, StyledSpan, WriteLines, WriteStr}; impl StyledSpan { diff --git a/lib/src/write/tests.rs b/lib/src/write/tests.rs index 79717d34..2f9874a0 100644 --- a/lib/src/write/tests.rs +++ b/lib/src/write/tests.rs @@ -1,7 +1,7 @@ -use termcolor::WriteColor; - use std::io::Write; +use termcolor::WriteColor; + use super::*; #[test] diff --git a/lib/tests/integration.rs b/lib/tests/integration.rs index 532c2168..5b36a49b 100644 --- a/lib/tests/integration.rs +++ b/lib/tests/integration.rs @@ -1,13 +1,5 @@ //! Tests the full lifecycle of `Transcript`s. -use assert_matches::assert_matches; -use test_casing::{decorate, decorators::Retry, test_casing}; -use tracing::{subscriber::DefaultGuard, Subscriber}; -use tracing_capture::{CaptureLayer, CapturedSpan, SharedStorage, Storage}; -use tracing_subscriber::{ - fmt::format::FmtSpan, layer::SubscriberExt, registry::LookupSpan, FmtSubscriber, -}; - use std::{ io, path::Path, @@ -16,10 +8,17 @@ use std::{ time::Duration, }; +use assert_matches::assert_matches; use term_transcript::{ svg::{Template, TemplateOptions}, ShellOptions, Transcript, UserInput, }; +use test_casing::{decorate, decorators::Retry, test_casing}; +use tracing::{subscriber::DefaultGuard, Subscriber}; +use tracing_capture::{CaptureLayer, CapturedSpan, SharedStorage, Storage}; +use tracing_subscriber::{ + fmt::format::FmtSpan, layer::SubscriberExt, registry::LookupSpan, FmtSubscriber, +}; fn create_fmt_subscriber() -> impl Subscriber + for<'a> LookupSpan<'a> { FmtSubscriber::builder()