diff options
Diffstat (limited to 'test_util/src/lib.rs')
-rw-r--r-- | test_util/src/lib.rs | 403 |
1 files changed, 351 insertions, 52 deletions
diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index fa0cb8a84..0600867d7 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -2410,57 +2410,266 @@ impl<'a> CheckOutputIntegrationTest<'a> { } } -pub fn wildcard_match(pattern: &str, s: &str) -> bool { - pattern_match(pattern, s, "[WILDCARD]") +pub fn wildcard_match(pattern: &str, text: &str) -> bool { + match wildcard_match_detailed(pattern, text) { + WildcardMatchResult::Success => true, + WildcardMatchResult::Fail(debug_output) => { + eprintln!("{}", debug_output); + false + } + } } -pub fn pattern_match(pattern: &str, s: &str, wildcard: &str) -> bool { - // Normalize line endings - let mut s = s.replace("\r\n", "\n"); - let pattern = pattern.replace("\r\n", "\n"); +pub enum WildcardMatchResult { + Success, + Fail(String), +} - if pattern == wildcard { - return true; +pub fn wildcard_match_detailed( + pattern: &str, + text: &str, +) -> WildcardMatchResult { + fn annotate_whitespace(text: &str) -> String { + text.replace('\t', "\u{2192}").replace(' ', "\u{00B7}") } - let parts = pattern.split(wildcard).collect::<Vec<&str>>(); - if parts.len() == 1 { - return pattern == s; - } + // Normalize line endings + let original_text = text.replace("\r\n", "\n"); + let mut current_text = original_text.as_str(); + let pattern = pattern.replace("\r\n", "\n"); + let mut output_lines = Vec::new(); - if !s.starts_with(parts[0]) { - return false; - } + let parts = parse_wildcard_pattern_text(&pattern).unwrap(); - // If the first line of the pattern is just a wildcard the newline character - // needs to be pre-pended so it can safely match anything or nothing and - // continue matching. - if pattern.lines().next() == Some(wildcard) { - s.insert(0, '\n'); + let mut was_last_wildcard = false; + for (i, part) in parts.iter().enumerate() { + match part { + WildcardPatternPart::Wildcard => { + output_lines.push("<WILDCARD />".to_string()); + } + WildcardPatternPart::Text(search_text) => { + let is_last = i + 1 == parts.len(); + let search_index = if is_last && was_last_wildcard { + // search from the end of the file + current_text.rfind(search_text) + } else { + current_text.find(search_text) + }; + match search_index { + Some(found_index) if was_last_wildcard || found_index == 0 => { + output_lines.push(format!( + "<FOUND>{}</FOUND>", + colors::gray(annotate_whitespace(search_text)) + )); + current_text = ¤t_text[found_index + search_text.len()..]; + } + Some(index) => { + output_lines.push( + "==== FOUND SEARCH TEXT IN WRONG POSITION ====".to_string(), + ); + output_lines.push(colors::gray(annotate_whitespace(search_text))); + output_lines + .push("==== HAD UNKNOWN PRECEEDING TEXT ====".to_string()); + output_lines + .push(colors::red(annotate_whitespace(¤t_text[..index]))); + return WildcardMatchResult::Fail(output_lines.join("\n")); + } + None => { + let mut max_found_index = 0; + for (index, _) in search_text.char_indices() { + let sub_string = &search_text[..index]; + if let Some(found_index) = current_text.find(sub_string) { + if was_last_wildcard || found_index == 0 { + max_found_index = index; + } else { + break; + } + } else { + break; + } + } + if !was_last_wildcard && max_found_index > 0 { + output_lines.push(format!( + "<FOUND>{}</FOUND>", + colors::gray(annotate_whitespace( + &search_text[..max_found_index] + )) + )); + } + output_lines + .push("==== COULD NOT FIND SEARCH TEXT ====".to_string()); + output_lines.push(colors::green(annotate_whitespace( + if was_last_wildcard { + search_text + } else { + &search_text[max_found_index..] + }, + ))); + if was_last_wildcard && max_found_index > 0 { + output_lines.push(format!( + "==== MAX FOUND ====\n{}", + colors::red(annotate_whitespace( + &search_text[..max_found_index] + )) + )); + } + return WildcardMatchResult::Fail(output_lines.join("\n")); + } + } + } + WildcardPatternPart::UnorderedLines(expected_lines) => { + assert!(!was_last_wildcard, "unsupported"); + let mut actual_lines = Vec::with_capacity(expected_lines.len()); + for _ in 0..expected_lines.len() { + match current_text.find('\n') { + Some(end_line_index) => { + actual_lines.push(¤t_text[..end_line_index]); + current_text = ¤t_text[end_line_index + 1..]; + } + None => { + break; + } + } + } + actual_lines.sort_unstable(); + let mut expected_lines = expected_lines.clone(); + expected_lines.sort_unstable(); + + if actual_lines.len() != expected_lines.len() { + output_lines + .push("==== HAD WRONG NUMBER OF UNORDERED LINES ====".to_string()); + output_lines.push("# ACTUAL".to_string()); + output_lines.extend( + actual_lines + .iter() + .map(|l| colors::green(annotate_whitespace(l))), + ); + output_lines.push("# EXPECTED".to_string()); + output_lines.extend( + expected_lines + .iter() + .map(|l| colors::green(annotate_whitespace(l))), + ); + return WildcardMatchResult::Fail(output_lines.join("\n")); + } + for (actual, expected) in actual_lines.iter().zip(expected_lines.iter()) + { + if actual != expected { + output_lines + .push("==== UNORDERED LINE DID NOT MATCH ====".to_string()); + output_lines.push(format!( + " ACTUAL: {}", + colors::red(annotate_whitespace(actual)) + )); + output_lines.push(format!( + "EXPECTED: {}", + colors::green(annotate_whitespace(expected)) + )); + return WildcardMatchResult::Fail(output_lines.join("\n")); + } + } + output_lines.push("# Found matching unordered lines".to_string()); + } + } + was_last_wildcard = matches!(part, WildcardPatternPart::Wildcard); } - let mut t = s.split_at(parts[0].len()); + if was_last_wildcard || current_text.is_empty() { + WildcardMatchResult::Success + } else { + output_lines.push("==== HAD TEXT AT END OF FILE ====".to_string()); + output_lines.push(colors::red(annotate_whitespace(current_text))); + WildcardMatchResult::Fail(output_lines.join("\n")) + } +} + +#[derive(Debug)] +enum WildcardPatternPart<'a> { + Wildcard, + Text(&'a str), + UnorderedLines(Vec<&'a str>), +} + +fn parse_wildcard_pattern_text( + text: &str, +) -> Result<Vec<WildcardPatternPart>, monch::ParseErrorFailureError> { + use monch::*; + + fn parse_unordered_lines(input: &str) -> ParseResult<Vec<&str>> { + const END_TEXT: &str = "\n[UNORDERED_END]\n"; + let (input, _) = tag("[UNORDERED_START]\n")(input)?; + match input.find(END_TEXT) { + Some(end_index) => ParseResult::Ok(( + &input[end_index + END_TEXT.len()..], + input[..end_index].lines().collect::<Vec<_>>(), + )), + None => ParseError::fail(input, "Could not find [UNORDERED_END]"), + } + } + + enum InnerPart<'a> { + Wildcard, + UnorderedLines(Vec<&'a str>), + Char, + } + + struct Parser<'a> { + current_input: &'a str, + last_text_input: &'a str, + parts: Vec<WildcardPatternPart<'a>>, + } + + impl<'a> Parser<'a> { + fn parse(mut self) -> ParseResult<'a, Vec<WildcardPatternPart<'a>>> { + while !self.current_input.is_empty() { + let (next_input, inner_part) = or3( + map(tag("[WILDCARD]"), |_| InnerPart::Wildcard), + map(parse_unordered_lines, |lines| { + InnerPart::UnorderedLines(lines) + }), + map(next_char, |_| InnerPart::Char), + )(self.current_input)?; + match inner_part { + InnerPart::Wildcard => { + self.queue_previous_text(next_input); + self.parts.push(WildcardPatternPart::Wildcard); + } + InnerPart::UnorderedLines(expected_lines) => { + self.queue_previous_text(next_input); + self + .parts + .push(WildcardPatternPart::UnorderedLines(expected_lines)); + } + InnerPart::Char => { + // ignore + } + } + self.current_input = next_input; + } - for (i, part) in parts.iter().enumerate() { - if i == 0 { - continue; - } - dbg!(part, i); - if i == parts.len() - 1 && (part.is_empty() || *part == "\n") { - dbg!("exit 1 true", i); - return true; + self.queue_previous_text(""); + + ParseResult::Ok(("", self.parts)) } - if let Some(found) = t.1.find(*part) { - dbg!("found ", found); - t = t.1.split_at(found + part.len()); - } else { - dbg!("exit false ", i); - return false; + + fn queue_previous_text(&mut self, next_input: &'a str) { + let previous_text = &self.last_text_input + [..self.last_text_input.len() - self.current_input.len()]; + if !previous_text.is_empty() { + self.parts.push(WildcardPatternPart::Text(previous_text)); + } + self.last_text_input = next_input; } } - dbg!("end ", t.1.len()); - t.1.is_empty() + with_failure_handling(|input| { + Parser { + current_input: input, + last_text_input: input, + parts: Vec::new(), + } + .parse() + })(text) } pub fn with_pty(deno_args: &[&str], action: impl FnMut(Pty)) { @@ -2604,6 +2813,67 @@ pub fn parse_max_mem(output: &str) -> Option<u64> { None } +pub(crate) mod colors { + use std::io::Write; + + use termcolor::Ansi; + use termcolor::Color; + use termcolor::ColorSpec; + use termcolor::WriteColor; + + pub fn bold<S: AsRef<str>>(s: S) -> String { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true); + style(s, style_spec) + } + + pub fn red<S: AsRef<str>>(s: S) -> String { + fg_color(s, Color::Red) + } + + pub fn bold_red<S: AsRef<str>>(s: S) -> String { + bold_fg_color(s, Color::Red) + } + + pub fn green<S: AsRef<str>>(s: S) -> String { + fg_color(s, Color::Green) + } + + pub fn bold_green<S: AsRef<str>>(s: S) -> String { + bold_fg_color(s, Color::Green) + } + + pub fn bold_blue<S: AsRef<str>>(s: S) -> String { + bold_fg_color(s, Color::Blue) + } + + pub fn gray<S: AsRef<str>>(s: S) -> String { + fg_color(s, Color::Ansi256(245)) + } + + fn bold_fg_color<S: AsRef<str>>(s: S, color: Color) -> String { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true); + style_spec.set_fg(Some(color)); + style(s, style_spec) + } + + fn fg_color<S: AsRef<str>>(s: S, color: Color) -> String { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(color)); + style(s, style_spec) + } + + fn style<S: AsRef<str>>(s: S, colorspec: ColorSpec) -> String { + let mut v = Vec::new(); + let mut ansi_writer = Ansi::new(&mut v); + ansi_writer.set_color(&colorspec).unwrap(); + ansi_writer.write_all(s.as_ref().as_bytes()).unwrap(); + ansi_writer.reset().unwrap(); + String::from_utf8_lossy(&v).into_owned() + } +} + #[cfg(test)] mod tests { use super::*; @@ -2690,6 +2960,15 @@ mod tests { } #[test] + fn parse_parse_wildcard_match_text() { + let result = + parse_wildcard_pattern_text("[UNORDERED_START]\ntesting\ntesting") + .err() + .unwrap(); + assert_contains!(result.to_string(), "Could not find [UNORDERED_END]"); + } + + #[test] fn test_wildcard_match() { let fixtures = vec![ ("foobarbaz", "foobarbaz", true), @@ -2733,16 +3012,15 @@ mod tests { } #[test] - fn test_pattern_match() { + fn test_wildcard_match2() { // foo, bar, baz, qux, quux, quuz, corge, grault, garply, waldo, fred, plugh, xyzzy - let wildcard = "[BAR]"; - assert!(pattern_match("foo[BAR]baz", "foobarbaz", wildcard)); - assert!(!pattern_match("foo[BAR]baz", "foobazbar", wildcard)); + assert!(wildcard_match("foo[WILDCARD]baz", "foobarbaz")); + assert!(!wildcard_match("foo[WILDCARD]baz", "foobazbar")); - let multiline_pattern = "[BAR] + let multiline_pattern = "[WILDCARD] foo: -[BAR]baz[BAR]"; +[WILDCARD]baz[WILDCARD]"; fn multi_line_builder(input: &str, leading_text: Option<&str>) -> String { // If there is leading text add a newline so it's on it's own line @@ -2767,31 +3045,52 @@ grault", ); // Correct input & leading line - assert!(pattern_match( + assert!(wildcard_match( multiline_pattern, &multi_line_builder("baz", Some("QUX=quux")), - wildcard )); - // Correct input & no leading line - assert!(pattern_match( + // Should fail when leading line + assert!(!wildcard_match( multiline_pattern, &multi_line_builder("baz", None), - wildcard )); // Incorrect input & leading line - assert!(!pattern_match( + assert!(!wildcard_match( multiline_pattern, &multi_line_builder("garply", Some("QUX=quux")), - wildcard )); // Incorrect input & no leading line - assert!(!pattern_match( + assert!(!wildcard_match( multiline_pattern, &multi_line_builder("garply", None), - wildcard + )); + } + + #[test] + fn test_wildcard_match_unordered_lines() { + // matching + assert!(wildcard_match( + concat!("[UNORDERED_START]\n", "B\n", "A\n", "[UNORDERED_END]\n"), + concat!("A\n", "B\n",) + )); + // different line + assert!(!wildcard_match( + concat!("[UNORDERED_START]\n", "Ba\n", "A\n", "[UNORDERED_END]\n"), + concat!("A\n", "B\n",) + )); + // different number of lines + assert!(!wildcard_match( + concat!( + "[UNORDERED_START]\n", + "B\n", + "A\n", + "C\n", + "[UNORDERED_END]\n" + ), + concat!("A\n", "B\n",) )); } |