diff options
author | Tim Oram <dev@mitmaro.ca> | 2024-02-12 20:58:05 -0330 |
---|---|---|
committer | Tim Oram <dev@mitmaro.ca> | 2024-02-15 20:27:06 -0330 |
commit | 20fabc4de6baea9a8a07378f4c15f9f14a63dc54 (patch) | |
tree | 41419096dd8dd6c3ab97190bbdd722aae803ed4c | |
parent | 85fc458214aeac22ae2c38478703eac64b946544 (diff) |
Move render testutils to test_helpers
27 files changed, 927 insertions, 869 deletions
diff --git a/src/components/choice/tests.rs b/src/components/choice/tests.rs index 94689b4..1c54b9c 100644 --- a/src/components/choice/tests.rs +++ b/src/components/choice/tests.rs @@ -4,7 +4,8 @@ use super::*; use crate::{ assert_rendered_output, input::StandardEvent, - view::testutil::{with_view_state, AssertRenderOptions}, + test_helpers::assertions::assert_rendered_output::AssertRenderOptions, + view::testutil::with_view_state, }; #[derive(Clone, Debug, PartialEq)] diff --git a/src/components/confirm/tests.rs b/src/components/confirm/tests.rs index 609d2e2..d197d80 100644 --- a/src/components/confirm/tests.rs +++ b/src/components/confirm/tests.rs @@ -4,8 +4,7 @@ use super::*; use crate::{ assert_rendered_output, input::StandardEvent, - test_helpers::create_test_keybindings, - view::testutil::AssertRenderOptions, + test_helpers::{assertions::assert_rendered_output::AssertRenderOptions, create_test_keybindings}, }; #[test] diff --git a/src/components/edit/tests.rs b/src/components/edit/tests.rs index 3991a38..c999f1f 100644 --- a/src/components/edit/tests.rs +++ b/src/components/edit/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{assert_rendered_output, view::testutil::AssertRenderOptions}; +use crate::{assert_rendered_output, test_helpers::assertions::assert_rendered_output::AssertRenderOptions}; #[test] fn with_before_and_after_build() { diff --git a/src/components/shared/editable_line.rs b/src/components/shared/editable_line.rs index 0001879..08088fb 100644 --- a/src/components/shared/editable_line.rs +++ b/src/components/shared/editable_line.rs @@ -209,7 +209,8 @@ mod tests { use super::*; use crate::{ assert_rendered_output, - view::{testutil::AssertRenderOptions, ViewData, ViewLine}, + test_helpers::assertions::assert_rendered_output::AssertRenderOptions, + view::{ViewData, ViewLine}, }; macro_rules! view_data_from_editable_line { diff --git a/src/modules/confirm_abort.rs b/src/modules/confirm_abort.rs index 32c1b40..5666b3f 100644 --- a/src/modules/confirm_abort.rs +++ b/src/modules/confirm_abort.rs @@ -63,8 +63,8 @@ mod tests { assert_results, input::{KeyCode, StandardEvent}, process::Artifact, + test_helpers::assertions::assert_rendered_output::AssertRenderOptions, testutil::module_test, - view::testutil::AssertRenderOptions, }; fn create_confirm_abort(todo_file: TodoFile) -> ConfirmAbort { diff --git a/src/modules/list/tests/search.rs b/src/modules/list/tests/search.rs index 5ba6446..1a33d61 100644 --- a/src/modules/list/tests/search.rs +++ b/src/modules/list/tests/search.rs @@ -5,8 +5,8 @@ use crate::{ assert_results, process::Artifact, render_line, + test_helpers::assertions::assert_rendered_output::AssertRenderOptions, testutil::module_test, - view::testutil::AssertRenderOptions, }; #[test] diff --git a/src/modules/list/tests/toggle_break.rs b/src/modules/list/tests/toggle_break.rs index 196fd19..601dbb7 100644 --- a/src/modules/list/tests/toggle_break.rs +++ b/src/modules/list/tests/toggle_break.rs @@ -4,7 +4,6 @@ use crate::{ assert_rendered_output, testutil::module_test, todo_file::{Action::Pick, ParseError}, - view::testutil::LinePattern, }; #[test] diff --git a/src/modules/list/tests/visual_mode.rs b/src/modules/list/tests/visual_mode.rs index f51b3d2..f74c733 100644 --- a/src/modules/list/tests/visual_mode.rs +++ b/src/modules/list/tests/visual_mode.rs @@ -6,8 +6,8 @@ use crate::{ input::KeyCode, process::Artifact, render_line, + test_helpers::assertions::assert_rendered_output::AssertRenderOptions, testutil::module_test, - view::testutil::AssertRenderOptions, }; fn render_options() -> AssertRenderOptions { diff --git a/src/modules/show_commit/tests.rs b/src/modules/show_commit/tests.rs index f370bc6..4ef4c0e 100644 --- a/src/modules/show_commit/tests.rs +++ b/src/modules/show_commit/tests.rs @@ -10,11 +10,12 @@ use crate::{ process::Artifact, render_line, test_helpers::{ + assertions::assert_rendered_output::AssertRenderOptions, builders::{CommitBuilder, CommitDiffBuilder, FileStatusBuilder}, with_temp_repository, }, testutil::module_test, - view::{testutil::AssertRenderOptions, ViewLine}, + view::ViewLine, }; fn create_show_commit(config: &Config, repository: Repository, todo_file: TodoFile) -> ShowCommit { diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 1bebb48..f5b57d7 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -1,3 +1,4 @@ +pub(crate) mod assertions; pub(crate) mod builders; mod create_commit; mod create_event_reader; diff --git a/src/test_helpers/assertions.rs b/src/test_helpers/assertions.rs new file mode 100644 index 0000000..afd8845 --- /dev/null +++ b/src/test_helpers/assertions.rs @@ -0,0 +1 @@ +pub(crate) mod assert_rendered_output; diff --git a/src/test_helpers/assertions/assert_rendered_output.rs b/src/test_helpers/assertions/assert_rendered_output.rs new file mode 100644 index 0000000..9230932 --- /dev/null +++ b/src/test_helpers/assertions/assert_rendered_output.rs @@ -0,0 +1,330 @@ +mod patterns; +mod render_style; +mod render_view_data; +mod render_view_line; + +use std::fmt::{Debug, Formatter}; + +use bitflags::bitflags; +use itertools::Itertools; + +pub(crate) use self::{ + patterns::{ + ActionPattern, + AllPattern, + AnyLinePattern, + AnyPattern, + ContainsPattern, + EndsWithPattern, + ExactPattern, + LinePattern, + NotPattern, + StartsWithPattern, + }, + render_style::render_style, + render_view_data::render_view_data, + render_view_line::render_view_line, +}; +use crate::{ + test_helpers::shared::replace_invisibles, + view::{ViewData, ViewLine}, +}; + +bitflags! { + /// Options for the `assert_rendered_output!` macro + #[derive(Default, PartialEq, Eq, Debug, Clone, Copy)] + pub(crate) struct AssertRenderOptions: u8 { + /// Ignore trailing whitespace + const INCLUDE_TRAILING_WHITESPACE = 0b0000_0001; + /// Ignore pinned indicator + const INCLUDE_PINNED = 0b0000_0010; + /// Don't include style information + const INCLUDE_STYLE = 0b0000_0100; + /// Only render the body, in this mode {BODY} is also not rendered + const BODY_ONLY = 0b0000_1000; + } +} + +#[allow(clippy::string_slice, clippy::panic)] +pub(crate) fn _assert_rendered_output( + options: AssertRenderOptions, + actual: &[String], + expected_patterns: &[Box<dyn LinePattern>], +) { + let mut mismatch = false; + let mut error_output = vec![ + String::from("\nUnexpected output!"), + String::from("--- Expected"), + String::from("+++ Actual"), + String::from("=========="), + ]; + + for (expected_pattern, output_line) in expected_patterns.iter().zip(actual.iter()) { + let output = if options.contains(AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE) { + output_line.as_str() + } + else { + output_line.trim_end() + }; + + if expected_pattern.matches(output) { + error_output.push(format!(" {}", expected_pattern.expected())); + } + else { + mismatch = true; + error_output.push(format!("-{}", expected_pattern.expected())); + error_output.push(format!("+{}", expected_pattern.actual(output))); + } + } + + match expected_patterns.len() { + a if a > actual.len() => { + mismatch = true; + for expected_pattern in expected_patterns.iter().skip(actual.len()) { + error_output.push(format!("-{}", expected_pattern.expected().as_str())); + } + }, + a if a < actual.len() => { + mismatch = true; + for line in actual.iter().skip(expected_patterns.len()) { + error_output.push(format!("+{}", replace_invisibles(line))); + } + }, + _ => {}, + } + + if mismatch { + error_output.push(String::from("==========\n")); + panic!("{}", error_output.join("\n")); + } +} + +/// Assert the rendered output from a `ViewData`. Generally this function is not used directly, +/// instead use the `assert_rendered_output!` macro. +pub(crate) fn _assert_rendered_output_from_view_data( + view_data: &ViewData, + expected: &[Box<dyn LinePattern>], + options: AssertRenderOptions, + skip_start: Option<usize>, + skip_end: Option<usize>, +) { + let rendered = render_view_data(view_data, options); + let mut length = rendered.len(); + let mut output_iter: Box<dyn Iterator<Item = String>> = Box::new(rendered.into_iter()); + + if let Some(skip) = skip_start { + length = length.saturating_sub(skip); + output_iter = Box::new(output_iter.skip(skip)); + } + + if let Some(skip) = skip_end { + output_iter = Box::new(output_iter.take(length.saturating_sub(skip))); + } + + _assert_rendered_output(options, &output_iter.collect::<Vec<String>>(), expected); +} + +/// Create an assertion on a line. For use in `assert_rendered_output` macro. +#[macro_export] +macro_rules! render_line { + ($line:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::ExactPattern::new($line) + }}; + (Line) => {{ + $crate::test_helpers::assertions::assert_rendered_output::AnyLinePattern::new() + }}; + (StartsWith $line:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::StartsWithPattern::new($line) + }}; + (Not StartsWith $line:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::NotPattern::new( + Box::new( + $crate::test_helpers::assertions::assert_rendered_output::StartsWithPattern::new($line) + ) + ) + }}; + (EndsWith $line:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::EndsWithPattern::new($line) + }}; + (Not EndsWith $line:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::NotPattern::new( + Box::new($crate::test_helpers::assertions::assert_rendered_output::EndsWithPattern::new($line)) + ) + }}; + (Contains $line:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::ContainsPattern::new($line) + }}; + (Not Contains $line:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::NotPattern::new( + Box::new( + $crate::test_helpers::assertions::assert_rendered_output::ContainsPattern::new($line) + ) + ) + }}; + (Not $pattern:expr) => {{ + $crate::test_helpers::assertions::assert_rendered_output::NotPattern::new(Box::new($pattern)) + }}; + (All $($patterns:expr),*) => {{ + let patterns: Vec<Box<dyn LinePattern>> = vec![$( Box::new($patterns), )*]; + $crate::test_helpers::assertions::assert_rendered_output::AllPattern::new(patterns) + }}; + (Any $($patterns:expr),*) => {{ + let patterns: Vec<Box<dyn LinePattern>> = vec![$( Box::new($patterns), )*]; + $crate::test_helpers::assertions::assert_rendered_output::AnyPattern::new(patterns) + }}; +} + +#[macro_export] +macro_rules! action_line { + (Break) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_break(false) + }}; + (Selected Break) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_break(true) + }}; + (Drop $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_drop($hash, $comment, false) + }}; + (Selected Drop $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_drop($hash, $comment, true) + }}; + (Edit $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_edit($hash, $comment, false) + }}; + (Selected Edit $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_edit($hash, $comment, true) + }}; + (Fixup $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_fixup($hash, $comment, false) + }}; + (Selected Fixup $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_fixup($hash, $comment, true) + }}; + (Pick $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_pick($hash, $comment, false) + }}; + (Selected Pick $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_pick($hash, $comment, true) + }}; + (Reword $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_reword($hash, $comment, false) + }}; + (Selected Reword $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_reword($hash, $comment, true) + }}; + (Squash $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_squash($hash, $comment, false) + }}; + (Selected Squash $hash:expr, $comment:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_squash($hash, $comment, true) + }}; + (Exec $command:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_exec($command, false) + }}; + (Selected Exec $command:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_exec($command, true) + }}; + (Label $reference:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_label($reference, false) + }}; + (Selected Label $reference:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_label($reference, true) + }}; + (Reset $reference:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_reset($reference, false) + }}; + (Selected Reset $reference:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_reset($reference, true) + }}; + (Merge $reference:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_merge($reference, false) + }}; + (Selected Merge $reference:expr) => {{ + use $crate::test_helpers::assertions::assert_rendered_output::ActionPattern; + ActionPattern::new_merge($reference, true) + }}; +} + +/// Assert the rendered output from a `ViewData`. +#[macro_export] +macro_rules! assert_rendered_output { + ($view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base AssertRenderOptions::default(), None, None, $view_data, $($arg),* + ) + }; + (Body $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base AssertRenderOptions::BODY_ONLY, None, None, $view_data, $($arg),* + ) + }; + (Style $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base AssertRenderOptions::INCLUDE_STYLE, None, None, $view_data, $($arg),* + ) + }; + (Skip $start:expr, $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base AssertRenderOptions::default(), Some($start), None, $view_data, $($arg),* + ) + }; + (Skip $start:expr;$end:expr, $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base AssertRenderOptions::default(), Some($start), Some($end), $view_data, $($arg),* + ) + }; + (Options $options:expr, $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base $options, None, None, $view_data, $($arg),* + ) + }; + (Body, Skip $start:expr, $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base AssertRenderOptions::BODY_ONLY, Some($start), None, $view_data, $($arg),* + ) + }; + (Body, Skip $start:expr;$end:expr, $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base AssertRenderOptions::BODY_ONLY, Some($start), Some($end), $view_data, $($arg),* + ) + }; + (Options $options:expr, Skip $start:expr, $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base $options, Some($start), None, $view_data, $($arg),* + ) + }; + (Options $options:expr, Skip $start:expr;$end:expr, $view_data:expr, $($arg:expr),*) => { + assert_rendered_output!( + @base $options, Some($start), Some($end), $view_data, $($arg),* + ) + }; + (@base $options:expr, $start:expr, $end:expr, $view_data:expr, $($arg:expr),*) => { + use $crate::test_helpers::assertions::assert_rendered_output::{ + _assert_rendered_output_from_view_data, + AssertRenderOptions,LinePattern + }; + let expected: Vec<Box<dyn LinePattern>> = vec![$( Box::new($arg), )*]; + _assert_rendered_output_from_view_data($view_data, &expected, $options, $start, $end); + }; +} diff --git a/src/test_helpers/assertions/assert_rendered_output/patterns.rs b/src/test_helpers/assertions/assert_rendered_output/patterns.rs new file mode 100644 index 0000000..2ad0d74 --- /dev/null +++ b/src/test_helpers/assertions/assert_rendered_output/patterns.rs @@ -0,0 +1,396 @@ +use std::fmt::{Debug, Formatter}; + +use itertools::Itertools; +use lazy_static::lazy_static; +use regex::Regex; + +use crate::{ + test_helpers::shared::replace_invisibles, + todo_file::{Line, ParseError}, +}; + +/// A pattern matcher for a rendered line +pub(crate) trait LinePattern: Debug { + /// Check if the rendered line matches the matchers pattern + fn matches(&self, rendered: &str) -> bool; + + /// A formatted expected value for the matcher + fn expected(&self) -> String; + + /// A formatted actual value for the matcher + #[must_use] + fn actual(&self, rendered: &str) -> String { + replace_invisibles(rendered) + } + + /// Does this matcher use styles for matching + fn use_styles(&self) -> bool { + true + } +} + +impl LinePattern for String { + fn matches(&self, rendered: &str) -> bool { + rendered == self + } + + fn expected(&self) -> String { + replace_invisibles(self.as_str()) + } +} + +impl LinePattern for &str { + fn matches(&self, rendered: &str) -> bool { + rendered == *self + } + + fn expected(&self) -> String { + replace_invisibles(self) + } +} + +/// A pattern matcher that will match any line +#[derive(Debug, Copy, Clone)] +#[non_exhaustive] +pub(crate) struct AnyLinePattern; + +impl AnyLinePattern { + /// Create a new instance + #[must_use] + pub(crate) fn new() -> Self { + Self + } +} + +impl LinePattern for AnyLinePattern { + fn matches(&self, _: &str) -> bool { + true + } + + fn expected(&self) -> String { + String::from("{{Any}}") + } + + fn actual(&self, _: &str) -> String { + String::from("{{Any}}") + } +} + +/// A pattern matcher that matches that a rendered line is an exact match +#[derive(Debug, Clone)] +#[non_exhaustive] +pub(crate) struct ExactPattern(String); + +impl ExactPattern { + /// Create a new matcher against a line pattern + #[must_use] + pub(crate) fn new(pattern: &str) -> Self { + Self(String::from(pattern)) + } +} + +impl LinePattern for ExactPattern { + fn matches(&self, rendered: &str) -> bool { + rendered == self.0 + } + + fn expected(&self) -> String { + replace_invisibles(self.0.as_str()) + } +} + +/// A pattern that matches that a rendered line starts with a pattern +#[derive(Debug, Clone)] +#[non_exhaustive] +pub(crate) struct StartsWithPattern(String); + +impl StartsWithPattern { + /// Create a new matcher with a pattern + #[must_use] + pub(crate) fn new(pattern: &str) -> Self { + Self(String::from(pattern)) + } +} + +impl LinePattern for StartsWithPattern { + fn matches(&self, rendered: &str) -> bool { + rendered.starts_with(self.0.as_str()) + } + + fn expected(&self) -> String { + format!("StartsWith {}", replace_invisibles(self.0.as_str())) + } + + fn actual(&self, rendered: &str) -> String { + format!( + " {}", + replace_invisibles(rendered.chars().take(self.0.len()).collect::<String>().as_str()) + ) + } +} + +/// A pattern that matches that a rendered line ends with a pattern +#[derive(Debug, Clone)] +#[non_exhaustive] +pub(crate) struct EndsWithPattern(String); + +impl EndsWithPattern { + /// Create a new matcher with a pattern + #[must_use] + pub(crate) fn new(pattern: &str) -> Self { + Self(String::from(pattern)) + } +} + +impl LinePattern for EndsWithPattern { + fn matches(&self, rendered: &str) -> bool { + rendered.ends_with(self.0.as_str()) + } + + fn expected(&self) -> String { + format!("EndsWith {}", replace_invisibles(self.0.as_str())) + } + + #[allow(clippy::string_slice)] + fn actual(&self, rendered: &str) -> String { + format!( + " {}", + replace_invisibles(&rendered[rendered.len() - self.0.len() + 2..]) + ) + } +} + +/// A pattern that matches that a rendered line contains a pattern +#[derive(Debug, Clone)] +#[non_exhaustive] +pub(crate) struct ContainsPattern(String); + +impl ContainsPattern { + /// Create a new matcher with a pattern + #[must_use] + pub(crate) fn new(pattern: &str) -> Self { + Self(String::from(pattern)) + } +} + +/// A pattern that matches that a rendered line matches all patterns +#[derive(Debug)] +#[non_exhaustive] +pub(crate) struct NotPattern(Box<dyn LinePattern>); + +impl LinePattern for ContainsPattern { + fn matches(&self, rendered: &str) -> bool { + rendered.contains(self.0.as_str()) + } + + fn expected(&self) -> String { + format!("Contains {}", replace_invisibles(self.0.as_str())) + } + + #[allow(clippy::string_slice)] + fn actual(&self, rendered: &str) -> String { + format!(" {}", replace_invisibles(rendered)) + } +} + +impl NotPattern { + /// Create a new matcher with a pattern + #[must_use] + pub(crate) fn new(pattern: Box<dyn LinePattern>) -> Self { + Self(pattern) + } +} + +impl LinePattern for NotPattern { + fn matches(&self, rendered: &str) -> bool { + !self.0.matches(rendered) + } + + fn expected(&self) -> String { + format!("Not({})", self.0.expected()) + } + + fn actual(&self, rendered: &str) -> String { + format!("Not({})", self.0.actual(rendered)) + } +} + +/// A pattern that matches that a rendered line matches all of a set of patterns +#[non_exhaustive] +pub(crate) struct AllPattern(Vec<Box<dyn LinePattern>>); + +impl AllPattern { + /// Create a new matcher with patterns + #[must_use] + pub(crate) fn new(patterns: Vec<Box<dyn LinePattern>>) -> Self { + Self(patterns) + } +} + +impl LinePattern for AllPattern { + fn matches(&self, rendered: &str) -> bool { + self.0.iter().all(|pattern| pattern.matches(rendered)) + } + + fn expected(&self) -> String { + format!("All({})", self.0.iter().map(|p| { p.expected() }).join(", ")) + } +} + +impl Debug for AllPattern { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "All({})", self.0.iter().map(|p| format!("{p:?}")).join(", ")) + } +} + +/// A pattern that matches that a rendered line matches any of a set of patterns +#[non_exhaustive] +pub(crate) struct AnyPattern(Vec<Box<dyn LinePattern>>); + +impl AnyPattern { + /// Create a new matcher with patterns + #[must_use] + pub(crate) fn new(patterns: Vec<Box<dyn LinePattern>>) -> Self { + Self(patterns) + } +} + +impl LinePattern for AnyPattern { + fn matches(&self, rendered: &str) -> bool { + self.0.iter().any(|pattern| pattern.matches(rendered)) + } + + fn expected(&self) -> String { + format!("Any({})", self.0.iter().map(|p| { p.expected() }).join(", ")) + } +} + +impl Debug for AnyPattern { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Any({})", self.0.iter().map(|p| format!("{p:?}")).join(", ")) + } +} + +lazy_static! { + pub(crate) static ref FORMAT_REGEX: Regex = Regex::new(r"\{.*?}").unwrap(); +} + +fn parse_rendered_action_line(rendered: &str) -> Result<Line, ParseError> { + let cleaned_line = FORMAT_REGEX.replace_all(rendered, "").replace(" > ", ""); + Line::parse(cleaned_line.as_ref()) +} + +#[derive(Debug)] +pub(crate) struct ActionPattern { + line: Line, + selected: bool, +} + +impl ActionPattern { + fn new(line: &str, selected: bool) -> Self { + Self { + line: Line::parse(line).expect("Expected valid pick"), + selected, + } + } + + pub(crate) fn new_break(selected: bool) -> Self { + Self::new("break", selected) + } + + pub(crate) fn new_drop(hash: &str, comment: &str, selected: bool) -> Self { + Self::new(format!("drop {hash} {comment}").as_str(), selected) + } + + pub(crate) fn new_edit(hash: &str, comment: &str, selected: bool) -> Self { + Self::new(format!("edit {hash} {comment}").as_str(), selected) + } + + pub(crate) fn new_fixup(hash: &str, comment: &str, selected: bool) -> Self { + Self::new(format!("fixup {hash} {comment}").as_str(), selected) + } + + pub(crate) fn new_pick(hash: &str, comment: &str, selected: bool) -> Self { + Self::new(format!("pick {hash} {comment}").as_str(), selected) + } + + pub(crate) fn new_reword(hash: &str, comment: &str, selected: bool) -> Self { + Self::new(format!("reword {hash} {comment}").as_str(), selected) + } + + pub(crate) fn new_squash(hash: &str, comment: &str, selected: bool) -> Self { + Self::new(format!("squash {hash} {comment}").as_str(), selected) + } + + pub(crate) fn new_exec(command: &str, selected: bool) -> Self { + Self::new(format!("exec {command}").as_str(), selected) + } + + pub(crate) fn new_label(reference: &str, selected: bool) -> Self { + Self::new(format!("label {reference}").as_str(), selected) + } + + pub(crate) fn new_reset(reference: &str, selected: bool) -> Self { + Self::new(format!("reset {reference}").as_str(), selected) + } + + pub(crate) fn new_merge(reference: &str, selected: bool) -> Self { + Self::new(format!("merge {reference}").as_str(), selected) + } +} + +impl LinePattern for ActionPattern { + fn matches(&self, rendered: &str) -> bool { + if rendered.contains("{Selected}") { + if !self.selected { + return false; + } + } + else if self.selected { + return false; + } |