summaryrefslogtreecommitdiffstats
path: root/vendor/snapbox/src/substitutions.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
commit10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch)
treebdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/snapbox/src/substitutions.rs
parentReleasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff)
downloadrustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz
rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/snapbox/src/substitutions.rs')
-rw-r--r--vendor/snapbox/src/substitutions.rs420
1 files changed, 420 insertions, 0 deletions
diff --git a/vendor/snapbox/src/substitutions.rs b/vendor/snapbox/src/substitutions.rs
new file mode 100644
index 000000000..9c228172b
--- /dev/null
+++ b/vendor/snapbox/src/substitutions.rs
@@ -0,0 +1,420 @@
+use std::borrow::Cow;
+
+/// Match pattern expressions, see [`Assert`][crate::Assert]
+///
+/// Built-in expressions:
+/// - `...` on a line of its own: match multiple complete lines
+/// - `[..]`: match multiple characters within a line
+#[derive(Default, Clone, Debug, PartialEq, Eq)]
+pub struct Substitutions {
+ vars: std::collections::BTreeMap<&'static str, Cow<'static, str>>,
+ unused: std::collections::BTreeSet<&'static str>,
+}
+
+impl Substitutions {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub(crate) fn with_exe() -> Self {
+ let mut substitutions = Self::new();
+ substitutions
+ .insert("[EXE]", std::env::consts::EXE_SUFFIX)
+ .unwrap();
+ substitutions
+ }
+
+ /// Insert an additional match pattern
+ ///
+ /// `key` must be enclosed in `[` and `]`.
+ ///
+ /// ```rust
+ /// let mut subst = snapbox::Substitutions::new();
+ /// subst.insert("[EXE]", std::env::consts::EXE_SUFFIX);
+ /// ```
+ pub fn insert(
+ &mut self,
+ key: &'static str,
+ value: impl Into<Cow<'static, str>>,
+ ) -> Result<(), crate::Error> {
+ let key = validate_key(key)?;
+ let value = value.into();
+ if value.is_empty() {
+ self.unused.insert(key);
+ } else {
+ self.vars
+ .insert(key, crate::utils::normalize_text(value.as_ref()).into());
+ }
+ Ok(())
+ }
+
+ /// Insert additional match patterns
+ ///
+ /// keys must be enclosed in `[` and `]`.
+ pub fn extend(
+ &mut self,
+ vars: impl IntoIterator<Item = (&'static str, impl Into<Cow<'static, str>>)>,
+ ) -> Result<(), crate::Error> {
+ for (key, value) in vars {
+ self.insert(key, value)?;
+ }
+ Ok(())
+ }
+
+ /// Apply match pattern to `input`
+ ///
+ /// If `pattern` matches `input`, then `pattern` is returned.
+ ///
+ /// Otherwise, `input`, with as many patterns replaced as possible, will be returned.
+ ///
+ /// ```rust
+ /// let subst = snapbox::Substitutions::new();
+ /// let output = subst.normalize("Hello World!", "Hello [..]!");
+ /// assert_eq!(output, "Hello [..]!");
+ /// ```
+ pub fn normalize(&self, input: &str, pattern: &str) -> String {
+ normalize(input, pattern, self)
+ }
+
+ fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str> {
+ let mut value = Cow::Borrowed(value);
+ for (var, replace) in self.vars.iter() {
+ debug_assert!(!replace.is_empty());
+ value = Cow::Owned(value.replace(replace.as_ref(), var));
+ }
+ value
+ }
+
+ fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> {
+ if pattern.contains('[') {
+ let mut pattern = Cow::Borrowed(pattern);
+ for var in self.unused.iter() {
+ pattern = Cow::Owned(pattern.replace(var, ""));
+ }
+ pattern
+ } else {
+ Cow::Borrowed(pattern)
+ }
+ }
+}
+
+fn validate_key(key: &'static str) -> Result<&'static str, crate::Error> {
+ if !key.starts_with('[') || !key.ends_with(']') {
+ return Err(format!("Key `{}` is not enclosed in []", key).into());
+ }
+
+ if key[1..(key.len() - 1)]
+ .find(|c: char| !c.is_ascii_uppercase())
+ .is_some()
+ {
+ return Err(format!("Key `{}` can only be A-Z but ", key).into());
+ }
+
+ Ok(key)
+}
+
+fn normalize(input: &str, pattern: &str, substitutions: &Substitutions) -> String {
+ if input == pattern {
+ return input.to_owned();
+ }
+
+ let mut normalized: Vec<Cow<str>> = Vec::new();
+ let input_lines: Vec<_> = crate::utils::LinesWithTerminator::new(input).collect();
+ let pattern_lines: Vec<_> = crate::utils::LinesWithTerminator::new(pattern).collect();
+
+ let mut input_index = 0;
+ let mut pattern_index = 0;
+ 'outer: loop {
+ let pattern_line = if let Some(pattern_line) = pattern_lines.get(pattern_index) {
+ *pattern_line
+ } else {
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ };
+ let next_pattern_index = pattern_index + 1;
+
+ let input_line = if let Some(input_line) = input_lines.get(input_index) {
+ *input_line
+ } else {
+ break 'outer;
+ };
+ let next_input_index = input_index + 1;
+
+ if line_matches(input_line, pattern_line, substitutions) {
+ pattern_index = next_pattern_index;
+ input_index = next_input_index;
+ normalized.push(Cow::Borrowed(pattern_line));
+ continue 'outer;
+ } else if is_line_elide(pattern_line) {
+ let next_pattern_line: &str =
+ if let Some(pattern_line) = pattern_lines.get(next_pattern_index) {
+ pattern_line
+ } else {
+ normalized.push(Cow::Borrowed(pattern_line));
+ break 'outer;
+ };
+ if let Some(future_input_index) = input_lines[input_index..]
+ .iter()
+ .enumerate()
+ .find(|(_, l)| **l == next_pattern_line)
+ .map(|(i, _)| input_index + i)
+ {
+ normalized.push(Cow::Borrowed(pattern_line));
+ pattern_index = next_pattern_index;
+ input_index = future_input_index;
+ continue 'outer;
+ } else {
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ }
+ } else {
+ // Find where we can pick back up for normalizing
+ for future_input_index in next_input_index..input_lines.len() {
+ let future_input_line = input_lines[future_input_index];
+ if let Some(future_pattern_index) = pattern_lines[next_pattern_index..]
+ .iter()
+ .enumerate()
+ .find(|(_, l)| **l == future_input_line || is_line_elide(**l))
+ .map(|(i, _)| next_pattern_index + i)
+ {
+ normalized.extend(
+ input_lines[input_index..future_input_index]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ pattern_index = future_pattern_index;
+ input_index = future_input_index;
+ continue 'outer;
+ }
+ }
+
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ }
+ }
+
+ normalized.join("")
+}
+
+fn is_line_elide(line: &str) -> bool {
+ line == "...\n" || line == "..."
+}
+
+fn line_matches(line: &str, pattern: &str, substitutions: &Substitutions) -> bool {
+ if line == pattern {
+ return true;
+ }
+
+ let subbed = substitutions.substitute(line);
+ let mut line = subbed.as_ref();
+
+ let pattern = substitutions.clear(pattern);
+
+ let mut sections = pattern.split("[..]").peekable();
+ while let Some(section) = sections.next() {
+ if let Some(remainder) = line.strip_prefix(section) {
+ if let Some(next_section) = sections.peek() {
+ if next_section.is_empty() {
+ line = "";
+ } else if let Some(restart_index) = remainder.find(next_section) {
+ line = &remainder[restart_index..];
+ }
+ } else {
+ return remainder.is_empty();
+ }
+ } else {
+ return false;
+ }
+ }
+
+ false
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn empty() {
+ let input = "";
+ let pattern = "";
+ let expected = "";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn literals_match() {
+ let input = "Hello\nWorld";
+ let pattern = "Hello\nWorld";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn pattern_shorter() {
+ let input = "Hello\nWorld";
+ let pattern = "Hello\n";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn input_shorter() {
+ let input = "Hello\n";
+ let pattern = "Hello\nWorld";
+ let expected = "Hello\n";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn all_different() {
+ let input = "Hello\nWorld";
+ let pattern = "Goodbye\nMoon";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn middles_diverge() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\nMoon\nGoodbye";
+ let expected = "Hello\nWorld\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn leading_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "...\nGoodbye";
+ let expected = "...\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn trailing_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\n...";
+ let expected = "Hello\n...";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn middle_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\n...\nGoodbye";
+ let expected = "Hello\n...\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn post_elide_diverge() {
+ let input = "Hello\nSun\nAnd\nWorld";
+ let pattern = "Hello\n...\nMoon";
+ let expected = "Hello\nSun\nAnd\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn post_diverge_elide() {
+ let input = "Hello\nWorld\nGoodbye\nSir";
+ let pattern = "Hello\nMoon\nGoodbye\n...";
+ let expected = "Hello\nWorld\nGoodbye\n...";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn inline_elide() {
+ let input = "Hello\nWorld\nGoodbye\nSir";
+ let pattern = "Hello\nW[..]d\nGoodbye\nSir";
+ let expected = "Hello\nW[..]d\nGoodbye\nSir";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn line_matches_cases() {
+ let cases = [
+ ("", "", true),
+ ("", "[..]", true),
+ ("hello", "hello", true),
+ ("hello", "goodbye", false),
+ ("hello", "[..]", true),
+ ("hello", "he[..]", true),
+ ("hello", "go[..]", false),
+ ("hello", "[..]o", true),
+ ("hello", "[..]e", false),
+ ("hello", "he[..]o", true),
+ ("hello", "he[..]e", false),
+ ("hello", "go[..]o", false),
+ ("hello", "go[..]e", false),
+ (
+ "hello world, goodbye moon",
+ "hello [..], goodbye [..]",
+ true,
+ ),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], goodbye [..]",
+ false,
+ ),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], hello [..]",
+ false,
+ ),
+ ("hello world, goodbye moon", "hello [..], [..] moon", true),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], [..] moon",
+ false,
+ ),
+ ("hello world, goodbye moon", "hello [..], [..] world", false),
+ ];
+ for (line, pattern, expected) in cases {
+ let actual = line_matches(line, pattern, &Substitutions::new());
+ assert_eq!(expected, actual, "line={:?} pattern={:?}", line, pattern);
+ }
+ }
+
+ #[test]
+ fn test_validate_key() {
+ let cases = [
+ ("[HELLO", false),
+ ("HELLO]", false),
+ ("[HELLO]", true),
+ ("[hello]", false),
+ ("[HE O]", false),
+ ];
+ for (key, expected) in cases {
+ let actual = validate_key(key).is_ok();
+ assert_eq!(expected, actual, "key={:?}", key);
+ }
+ }
+}