]> git.proxmox.com Git - rustc.git/blob - vendor/snapbox/src/substitutions.rs
New upstream version 1.70.0+dfsg2
[rustc.git] / vendor / snapbox / src / substitutions.rs
1 use std::borrow::Cow;
2
3 /// Match pattern expressions, see [`Assert`][crate::Assert]
4 ///
5 /// Built-in expressions:
6 /// - `...` on a line of its own: match multiple complete lines
7 /// - `[..]`: match multiple characters within a line
8 #[derive(Default, Clone, Debug, PartialEq, Eq)]
9 pub struct Substitutions {
10 vars: std::collections::BTreeMap<&'static str, Cow<'static, str>>,
11 unused: std::collections::BTreeSet<&'static str>,
12 }
13
14 impl Substitutions {
15 pub fn new() -> Self {
16 Default::default()
17 }
18
19 pub(crate) fn with_exe() -> Self {
20 let mut substitutions = Self::new();
21 substitutions
22 .insert("[EXE]", std::env::consts::EXE_SUFFIX)
23 .unwrap();
24 substitutions
25 }
26
27 /// Insert an additional match pattern
28 ///
29 /// `key` must be enclosed in `[` and `]`.
30 ///
31 /// ```rust
32 /// let mut subst = snapbox::Substitutions::new();
33 /// subst.insert("[EXE]", std::env::consts::EXE_SUFFIX);
34 /// ```
35 pub fn insert(
36 &mut self,
37 key: &'static str,
38 value: impl Into<Cow<'static, str>>,
39 ) -> Result<(), crate::Error> {
40 let key = validate_key(key)?;
41 let value = value.into();
42 if value.is_empty() {
43 self.unused.insert(key);
44 } else {
45 self.vars
46 .insert(key, crate::utils::normalize_text(value.as_ref()).into());
47 }
48 Ok(())
49 }
50
51 /// Insert additional match patterns
52 ///
53 /// keys must be enclosed in `[` and `]`.
54 pub fn extend(
55 &mut self,
56 vars: impl IntoIterator<Item = (&'static str, impl Into<Cow<'static, str>>)>,
57 ) -> Result<(), crate::Error> {
58 for (key, value) in vars {
59 self.insert(key, value)?;
60 }
61 Ok(())
62 }
63
64 /// Apply match pattern to `input`
65 ///
66 /// If `pattern` matches `input`, then `pattern` is returned.
67 ///
68 /// Otherwise, `input`, with as many patterns replaced as possible, will be returned.
69 ///
70 /// ```rust
71 /// let subst = snapbox::Substitutions::new();
72 /// let output = subst.normalize("Hello World!", "Hello [..]!");
73 /// assert_eq!(output, "Hello [..]!");
74 /// ```
75 pub fn normalize(&self, input: &str, pattern: &str) -> String {
76 normalize(input, pattern, self)
77 }
78
79 fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str> {
80 let mut value = Cow::Borrowed(value);
81 for (var, replace) in self.vars.iter() {
82 debug_assert!(!replace.is_empty());
83 value = Cow::Owned(value.replace(replace.as_ref(), var));
84 }
85 value
86 }
87
88 fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> {
89 if pattern.contains('[') {
90 let mut pattern = Cow::Borrowed(pattern);
91 for var in self.unused.iter() {
92 pattern = Cow::Owned(pattern.replace(var, ""));
93 }
94 pattern
95 } else {
96 Cow::Borrowed(pattern)
97 }
98 }
99 }
100
101 fn validate_key(key: &'static str) -> Result<&'static str, crate::Error> {
102 if !key.starts_with('[') || !key.ends_with(']') {
103 return Err(format!("Key `{}` is not enclosed in []", key).into());
104 }
105
106 if key[1..(key.len() - 1)]
107 .find(|c: char| !c.is_ascii_uppercase())
108 .is_some()
109 {
110 return Err(format!("Key `{}` can only be A-Z but ", key).into());
111 }
112
113 Ok(key)
114 }
115
116 fn normalize(input: &str, pattern: &str, substitutions: &Substitutions) -> String {
117 if input == pattern {
118 return input.to_owned();
119 }
120
121 let mut normalized: Vec<Cow<str>> = Vec::new();
122 let input_lines: Vec<_> = crate::utils::LinesWithTerminator::new(input).collect();
123 let pattern_lines: Vec<_> = crate::utils::LinesWithTerminator::new(pattern).collect();
124
125 let mut input_index = 0;
126 let mut pattern_index = 0;
127 'outer: loop {
128 let pattern_line = if let Some(pattern_line) = pattern_lines.get(pattern_index) {
129 *pattern_line
130 } else {
131 normalized.extend(
132 input_lines[input_index..]
133 .iter()
134 .copied()
135 .map(|s| substitutions.substitute(s)),
136 );
137 break 'outer;
138 };
139 let next_pattern_index = pattern_index + 1;
140
141 let input_line = if let Some(input_line) = input_lines.get(input_index) {
142 *input_line
143 } else {
144 break 'outer;
145 };
146 let next_input_index = input_index + 1;
147
148 if line_matches(input_line, pattern_line, substitutions) {
149 pattern_index = next_pattern_index;
150 input_index = next_input_index;
151 normalized.push(Cow::Borrowed(pattern_line));
152 continue 'outer;
153 } else if is_line_elide(pattern_line) {
154 let next_pattern_line: &str =
155 if let Some(pattern_line) = pattern_lines.get(next_pattern_index) {
156 pattern_line
157 } else {
158 normalized.push(Cow::Borrowed(pattern_line));
159 break 'outer;
160 };
161 if let Some(future_input_index) = input_lines[input_index..]
162 .iter()
163 .enumerate()
164 .find(|(_, l)| **l == next_pattern_line)
165 .map(|(i, _)| input_index + i)
166 {
167 normalized.push(Cow::Borrowed(pattern_line));
168 pattern_index = next_pattern_index;
169 input_index = future_input_index;
170 continue 'outer;
171 } else {
172 normalized.extend(
173 input_lines[input_index..]
174 .iter()
175 .copied()
176 .map(|s| substitutions.substitute(s)),
177 );
178 break 'outer;
179 }
180 } else {
181 // Find where we can pick back up for normalizing
182 for future_input_index in next_input_index..input_lines.len() {
183 let future_input_line = input_lines[future_input_index];
184 if let Some(future_pattern_index) = pattern_lines[next_pattern_index..]
185 .iter()
186 .enumerate()
187 .find(|(_, l)| **l == future_input_line || is_line_elide(**l))
188 .map(|(i, _)| next_pattern_index + i)
189 {
190 normalized.extend(
191 input_lines[input_index..future_input_index]
192 .iter()
193 .copied()
194 .map(|s| substitutions.substitute(s)),
195 );
196 pattern_index = future_pattern_index;
197 input_index = future_input_index;
198 continue 'outer;
199 }
200 }
201
202 normalized.extend(
203 input_lines[input_index..]
204 .iter()
205 .copied()
206 .map(|s| substitutions.substitute(s)),
207 );
208 break 'outer;
209 }
210 }
211
212 normalized.join("")
213 }
214
215 fn is_line_elide(line: &str) -> bool {
216 line == "...\n" || line == "..."
217 }
218
219 fn line_matches(line: &str, pattern: &str, substitutions: &Substitutions) -> bool {
220 if line == pattern {
221 return true;
222 }
223
224 let subbed = substitutions.substitute(line);
225 let mut line = subbed.as_ref();
226
227 let pattern = substitutions.clear(pattern);
228
229 let mut sections = pattern.split("[..]").peekable();
230 while let Some(section) = sections.next() {
231 if let Some(remainder) = line.strip_prefix(section) {
232 if let Some(next_section) = sections.peek() {
233 if next_section.is_empty() {
234 line = "";
235 } else if let Some(restart_index) = remainder.find(next_section) {
236 line = &remainder[restart_index..];
237 }
238 } else {
239 return remainder.is_empty();
240 }
241 } else {
242 return false;
243 }
244 }
245
246 false
247 }
248
249 #[cfg(test)]
250 mod test {
251 use super::*;
252
253 #[test]
254 fn empty() {
255 let input = "";
256 let pattern = "";
257 let expected = "";
258 let actual = normalize(input, pattern, &Substitutions::new());
259 assert_eq!(expected, actual);
260 }
261
262 #[test]
263 fn literals_match() {
264 let input = "Hello\nWorld";
265 let pattern = "Hello\nWorld";
266 let expected = "Hello\nWorld";
267 let actual = normalize(input, pattern, &Substitutions::new());
268 assert_eq!(expected, actual);
269 }
270
271 #[test]
272 fn pattern_shorter() {
273 let input = "Hello\nWorld";
274 let pattern = "Hello\n";
275 let expected = "Hello\nWorld";
276 let actual = normalize(input, pattern, &Substitutions::new());
277 assert_eq!(expected, actual);
278 }
279
280 #[test]
281 fn input_shorter() {
282 let input = "Hello\n";
283 let pattern = "Hello\nWorld";
284 let expected = "Hello\n";
285 let actual = normalize(input, pattern, &Substitutions::new());
286 assert_eq!(expected, actual);
287 }
288
289 #[test]
290 fn all_different() {
291 let input = "Hello\nWorld";
292 let pattern = "Goodbye\nMoon";
293 let expected = "Hello\nWorld";
294 let actual = normalize(input, pattern, &Substitutions::new());
295 assert_eq!(expected, actual);
296 }
297
298 #[test]
299 fn middles_diverge() {
300 let input = "Hello\nWorld\nGoodbye";
301 let pattern = "Hello\nMoon\nGoodbye";
302 let expected = "Hello\nWorld\nGoodbye";
303 let actual = normalize(input, pattern, &Substitutions::new());
304 assert_eq!(expected, actual);
305 }
306
307 #[test]
308 fn leading_elide() {
309 let input = "Hello\nWorld\nGoodbye";
310 let pattern = "...\nGoodbye";
311 let expected = "...\nGoodbye";
312 let actual = normalize(input, pattern, &Substitutions::new());
313 assert_eq!(expected, actual);
314 }
315
316 #[test]
317 fn trailing_elide() {
318 let input = "Hello\nWorld\nGoodbye";
319 let pattern = "Hello\n...";
320 let expected = "Hello\n...";
321 let actual = normalize(input, pattern, &Substitutions::new());
322 assert_eq!(expected, actual);
323 }
324
325 #[test]
326 fn middle_elide() {
327 let input = "Hello\nWorld\nGoodbye";
328 let pattern = "Hello\n...\nGoodbye";
329 let expected = "Hello\n...\nGoodbye";
330 let actual = normalize(input, pattern, &Substitutions::new());
331 assert_eq!(expected, actual);
332 }
333
334 #[test]
335 fn post_elide_diverge() {
336 let input = "Hello\nSun\nAnd\nWorld";
337 let pattern = "Hello\n...\nMoon";
338 let expected = "Hello\nSun\nAnd\nWorld";
339 let actual = normalize(input, pattern, &Substitutions::new());
340 assert_eq!(expected, actual);
341 }
342
343 #[test]
344 fn post_diverge_elide() {
345 let input = "Hello\nWorld\nGoodbye\nSir";
346 let pattern = "Hello\nMoon\nGoodbye\n...";
347 let expected = "Hello\nWorld\nGoodbye\n...";
348 let actual = normalize(input, pattern, &Substitutions::new());
349 assert_eq!(expected, actual);
350 }
351
352 #[test]
353 fn inline_elide() {
354 let input = "Hello\nWorld\nGoodbye\nSir";
355 let pattern = "Hello\nW[..]d\nGoodbye\nSir";
356 let expected = "Hello\nW[..]d\nGoodbye\nSir";
357 let actual = normalize(input, pattern, &Substitutions::new());
358 assert_eq!(expected, actual);
359 }
360
361 #[test]
362 fn line_matches_cases() {
363 let cases = [
364 ("", "", true),
365 ("", "[..]", true),
366 ("hello", "hello", true),
367 ("hello", "goodbye", false),
368 ("hello", "[..]", true),
369 ("hello", "he[..]", true),
370 ("hello", "go[..]", false),
371 ("hello", "[..]o", true),
372 ("hello", "[..]e", false),
373 ("hello", "he[..]o", true),
374 ("hello", "he[..]e", false),
375 ("hello", "go[..]o", false),
376 ("hello", "go[..]e", false),
377 (
378 "hello world, goodbye moon",
379 "hello [..], goodbye [..]",
380 true,
381 ),
382 (
383 "hello world, goodbye moon",
384 "goodbye [..], goodbye [..]",
385 false,
386 ),
387 (
388 "hello world, goodbye moon",
389 "goodbye [..], hello [..]",
390 false,
391 ),
392 ("hello world, goodbye moon", "hello [..], [..] moon", true),
393 (
394 "hello world, goodbye moon",
395 "goodbye [..], [..] moon",
396 false,
397 ),
398 ("hello world, goodbye moon", "hello [..], [..] world", false),
399 ];
400 for (line, pattern, expected) in cases {
401 let actual = line_matches(line, pattern, &Substitutions::new());
402 assert_eq!(expected, actual, "line={:?} pattern={:?}", line, pattern);
403 }
404 }
405
406 #[test]
407 fn test_validate_key() {
408 let cases = [
409 ("[HELLO", false),
410 ("HELLO]", false),
411 ("[HELLO]", true),
412 ("[hello]", false),
413 ("[HE O]", false),
414 ];
415 for (key, expected) in cases {
416 let actual = validate_key(key).is_ok();
417 assert_eq!(expected, actual, "key={:?}", key);
418 }
419 }
420 }