1use crate::cross_compile::try_alternate;
45use crate::paths;
46use crate::rustc_host;
47use anyhow::{bail, Result};
48use snapbox::Data;
49use snapbox::IntoData;
50use std::fmt;
51use std::path::Path;
52use std::path::PathBuf;
53use std::str;
54
55macro_rules! regex {
58 ($re:literal $(,)?) => {{
59 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
60 RE.get_or_init(|| regex::Regex::new($re).unwrap())
61 }};
62}
63
64pub fn assert_ui() -> snapbox::Assert {
109 let mut subs = snapbox::Redactions::new();
110 subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned())
111 .unwrap();
112 add_test_support_redactions(&mut subs);
113 add_regex_redactions(&mut subs);
114
115 snapbox::Assert::new()
116 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
117 .redact_with(subs)
118}
119
120pub fn assert_e2e() -> snapbox::Assert {
164 let mut subs = snapbox::Redactions::new();
165 subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned())
166 .unwrap();
167 subs.extend(E2E_LITERAL_REDACTIONS.into_iter().cloned())
168 .unwrap();
169 add_test_support_redactions(&mut subs);
170 add_regex_redactions(&mut subs);
171
172 snapbox::Assert::new()
173 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
174 .redact_with(subs)
175}
176
177fn add_test_support_redactions(subs: &mut snapbox::Redactions) {
178 let root = paths::root();
179 let root_url = url::Url::from_file_path(&root).unwrap().to_string();
182
183 subs.insert("[ROOT]", root).unwrap();
184 subs.insert("[ROOTURL]", root_url).unwrap();
185 subs.insert("[HOST_TARGET]", rustc_host()).unwrap();
186 if let Some(alt_target) = try_alternate() {
187 subs.insert("[ALT_TARGET]", alt_target).unwrap();
188 }
189}
190
191fn add_regex_redactions(subs: &mut snapbox::Redactions) {
192 subs.insert(
194 "[ELAPSED]",
195 regex!(r"\[FINISHED\].*in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"),
196 )
197 .unwrap();
198 subs.insert(
200 "[ELAPSED]",
201 regex!(r"Finished.*in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"),
202 )
203 .unwrap();
204 subs.insert(
206 "[ELAPSED]",
207 regex!(r"; finished in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"),
208 )
209 .unwrap();
210 subs.insert(
211 "[FILE_NUM]",
212 regex!(r"\[(REMOVED|SUMMARY)\] (?<redacted>[1-9][0-9]*) files"),
213 )
214 .unwrap();
215 subs.insert(
216 "[FILE_SIZE]",
217 regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?([a-zA-Z]i)?)B\s"),
218 )
219 .unwrap();
220 subs.insert(
221 "[HASH]",
222 regex!(r"home/\.cargo/registry/(cache|index|src)/-(?<redacted>[a-z0-9]+)"),
223 )
224 .unwrap();
225 subs.insert(
226 "[HASH]",
227 regex!(r"\.cargo/target/(?<redacted>[0-9a-f]{2}/[0-9a-f]{14})"),
228 )
229 .unwrap();
230 subs.insert("[HASH]", regex!(r"/[a-z0-9\-_]+-(?<redacted>[0-9a-f]{16})"))
231 .unwrap();
232 subs.insert(
233 "[AVG_ELAPSED]",
234 regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?) ns/iter"),
235 )
236 .unwrap();
237 subs.insert(
238 "[JITTER]",
239 regex!(r"ns/iter \(\+/- (?<redacted>[0-9]+(\.[0-9]+)?)\)"),
240 )
241 .unwrap();
242
243 subs.insert(
248 "[TIME_DIFF_AFTER_LAST_BUILD]",
249 regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?s, (\s?[0-9]+(\.[0-9]+)?(s|ns|h))+ after last build at [0-9]+(\.[0-9]+)?s)"),
250 )
251 .unwrap();
252}
253
254static MIN_LITERAL_REDACTIONS: &[(&str, &str)] = &[
255 ("[EXE]", std::env::consts::EXE_SUFFIX),
256 ("[BROKEN_PIPE]", "Broken pipe (os error 32)"),
257 ("[BROKEN_PIPE]", "The pipe is being closed. (os error 232)"),
258 ("[NOT_FOUND]", "No such file or directory (os error 2)"),
260 (
262 "[NOT_FOUND]",
263 "The system cannot find the file specified. (os error 2)",
264 ),
265 (
266 "[NOT_FOUND]",
267 "The system cannot find the path specified. (os error 3)",
268 ),
269 ("[NOT_FOUND]", "Access is denied. (os error 5)"),
270 ("[NOT_FOUND]", "program not found"),
271 ("[EXIT_STATUS]", "exit status"),
273 ("[EXIT_STATUS]", "exit code"),
275];
276static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[
277 ("[RUNNING]", " Running"),
278 ("[COMPILING]", " Compiling"),
279 ("[CHECKING]", " Checking"),
280 ("[COMPLETED]", " Completed"),
281 ("[CREATED]", " Created"),
282 ("[CREATING]", " Creating"),
283 ("[CREDENTIAL]", " Credential"),
284 ("[DOWNGRADING]", " Downgrading"),
285 ("[FINISHED]", " Finished"),
286 ("[ERROR]", "error:"),
287 ("[WARNING]", "warning:"),
288 ("[NOTE]", "note:"),
289 ("[HELP]", "help:"),
290 ("[DOCUMENTING]", " Documenting"),
291 ("[SCRAPING]", " Scraping"),
292 ("[FRESH]", " Fresh"),
293 ("[DIRTY]", " Dirty"),
294 ("[LOCKING]", " Locking"),
295 ("[UPDATING]", " Updating"),
296 ("[UPGRADING]", " Upgrading"),
297 ("[ADDING]", " Adding"),
298 ("[REMOVING]", " Removing"),
299 ("[REMOVED]", " Removed"),
300 ("[UNCHANGED]", " Unchanged"),
301 ("[DOCTEST]", " Doc-tests"),
302 ("[PACKAGING]", " Packaging"),
303 ("[PACKAGED]", " Packaged"),
304 ("[DOWNLOADING]", " Downloading"),
305 ("[DOWNLOADED]", " Downloaded"),
306 ("[UPLOADING]", " Uploading"),
307 ("[UPLOADED]", " Uploaded"),
308 ("[VERIFYING]", " Verifying"),
309 ("[ARCHIVING]", " Archiving"),
310 ("[INSTALLING]", " Installing"),
311 ("[REPLACING]", " Replacing"),
312 ("[UNPACKING]", " Unpacking"),
313 ("[SUMMARY]", " Summary"),
314 ("[FIXED]", " Fixed"),
315 ("[FIXING]", " Fixing"),
316 ("[IGNORED]", " Ignored"),
317 ("[INSTALLED]", " Installed"),
318 ("[REPLACED]", " Replaced"),
319 ("[BUILDING]", " Building"),
320 ("[LOGIN]", " Login"),
321 ("[LOGOUT]", " Logout"),
322 ("[YANK]", " Yank"),
323 ("[OWNER]", " Owner"),
324 ("[MIGRATING]", " Migrating"),
325 ("[EXECUTABLE]", " Executable"),
326 ("[SKIPPING]", " Skipping"),
327 ("[WAITING]", " Waiting"),
328 ("[PUBLISHED]", " Published"),
329 ("[BLOCKING]", " Blocking"),
330 ("[GENERATED]", " Generated"),
331 ("[OPENING]", " Opening"),
332];
333
334pub(crate) fn match_contains(
339 expected: &str,
340 actual: &str,
341 redactions: &snapbox::Redactions,
342) -> Result<()> {
343 let expected = normalize_expected(expected, redactions);
344 let actual = normalize_actual(actual, redactions);
345 let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
346 let a: Vec<_> = actual.lines().collect();
347 if e.len() == 0 {
348 bail!("expected length must not be zero");
349 }
350 for window in a.windows(e.len()) {
351 if e == window {
352 return Ok(());
353 }
354 }
355 bail!(
356 "expected to find:\n\
357 {}\n\n\
358 did not find in output:\n\
359 {}",
360 expected,
361 actual
362 );
363}
364
365pub(crate) fn match_does_not_contain(
370 expected: &str,
371 actual: &str,
372 redactions: &snapbox::Redactions,
373) -> Result<()> {
374 if match_contains(expected, actual, redactions).is_ok() {
375 bail!(
376 "expected not to find:\n\
377 {}\n\n\
378 but found in output:\n\
379 {}",
380 expected,
381 actual
382 );
383 } else {
384 Ok(())
385 }
386}
387
388pub(crate) fn match_with_without(
396 actual: &str,
397 with: &[String],
398 without: &[String],
399 redactions: &snapbox::Redactions,
400) -> Result<()> {
401 let actual = normalize_actual(actual, redactions);
402 let norm = |s: &String| format!("[..]{}[..]", normalize_expected(s, redactions));
403 let with: Vec<_> = with.iter().map(norm).collect();
404 let without: Vec<_> = without.iter().map(norm).collect();
405 let with_wild: Vec<_> = with.iter().map(|w| WildStr::new(w)).collect();
406 let without_wild: Vec<_> = without.iter().map(|w| WildStr::new(w)).collect();
407
408 let matches: Vec<_> = actual
409 .lines()
410 .filter(|line| with_wild.iter().all(|with| with == line))
411 .filter(|line| !without_wild.iter().any(|without| without == line))
412 .collect();
413 match matches.len() {
414 0 => bail!(
415 "Could not find expected line in output.\n\
416 With contents: {:?}\n\
417 Without contents: {:?}\n\
418 Actual stderr:\n\
419 {}\n",
420 with,
421 without,
422 actual
423 ),
424 1 => Ok(()),
425 _ => bail!(
426 "Found multiple matching lines, but only expected one.\n\
427 With contents: {:?}\n\
428 Without contents: {:?}\n\
429 Matching lines:\n\
430 {}\n",
431 with,
432 without,
433 itertools::join(matches, "\n")
434 ),
435 }
436}
437
438fn normalize_actual(content: &str, redactions: &snapbox::Redactions) -> String {
440 use snapbox::filter::Filter as _;
441 let content = snapbox::filter::FilterPaths.filter(content.into_data());
442 let content = snapbox::filter::FilterNewlines.filter(content);
443 let content = content.render().expect("came in as a String");
444 let content = redactions.redact(&content);
445 content
446}
447
448fn normalize_expected(content: &str, redactions: &snapbox::Redactions) -> String {
450 use snapbox::filter::Filter as _;
451 let content = snapbox::filter::FilterPaths.filter(content.into_data());
452 let content = snapbox::filter::FilterNewlines.filter(content);
453 let content = content.render().expect("came in as a String");
455 let content = redactions.clear_unused(&content);
456 content.into_owned()
457}
458
459struct WildStr<'a> {
461 has_meta: bool,
462 line: &'a str,
463}
464
465impl<'a> WildStr<'a> {
466 fn new(line: &'a str) -> WildStr<'a> {
467 WildStr {
468 has_meta: line.contains("[..]"),
469 line,
470 }
471 }
472}
473
474impl PartialEq<&str> for WildStr<'_> {
475 fn eq(&self, other: &&str) -> bool {
476 if self.has_meta {
477 meta_cmp(self.line, other)
478 } else {
479 self.line == *other
480 }
481 }
482}
483
484fn meta_cmp(a: &str, mut b: &str) -> bool {
485 for (i, part) in a.split("[..]").enumerate() {
486 match b.find(part) {
487 Some(j) => {
488 if i == 0 && j != 0 {
489 return false;
490 }
491 b = &b[j + part.len()..];
492 }
493 None => return false,
494 }
495 }
496 b.is_empty() || a.ends_with("[..]")
497}
498
499impl fmt::Display for WildStr<'_> {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 f.write_str(&self.line)
502 }
503}
504
505impl fmt::Debug for WildStr<'_> {
506 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507 write!(f, "{:?}", self.line)
508 }
509}
510
511pub struct InMemoryDir {
512 files: Vec<(PathBuf, Data)>,
513}
514
515impl InMemoryDir {
516 pub fn paths(&self) -> impl Iterator<Item = &Path> {
517 self.files.iter().map(|(p, _)| p.as_path())
518 }
519
520 #[track_caller]
521 pub fn assert_contains(&self, expected: &Self) {
522 use std::fmt::Write as _;
523 let assert = assert_e2e();
524 let mut errs = String::new();
525 for (path, expected_data) in &expected.files {
526 let actual_data = self
527 .files
528 .iter()
529 .find_map(|(p, d)| (path == p).then(|| d.clone()))
530 .unwrap_or_else(|| Data::new());
531 if let Err(err) =
532 assert.try_eq(Some(&path.display()), actual_data, expected_data.clone())
533 {
534 let _ = write!(&mut errs, "{err}");
535 }
536 }
537 if !errs.is_empty() {
538 panic!("{errs}")
539 }
540 }
541}
542
543impl<P, D> FromIterator<(P, D)> for InMemoryDir
544where
545 P: Into<std::path::PathBuf>,
546 D: IntoData,
547{
548 fn from_iter<I: IntoIterator<Item = (P, D)>>(files: I) -> Self {
549 let files = files
550 .into_iter()
551 .map(|(p, d)| (p.into(), d.into_data()))
552 .collect();
553 Self { files }
554 }
555}
556
557impl<const N: usize, P, D> From<[(P, D); N]> for InMemoryDir
558where
559 P: Into<PathBuf>,
560 D: IntoData,
561{
562 fn from(files: [(P, D); N]) -> Self {
563 let files = files
564 .into_iter()
565 .map(|(p, d)| (p.into(), d.into_data()))
566 .collect();
567 Self { files }
568 }
569}
570
571impl<P, D> From<std::collections::HashMap<P, D>> for InMemoryDir
572where
573 P: Into<PathBuf>,
574 D: IntoData,
575{
576 fn from(files: std::collections::HashMap<P, D>) -> Self {
577 let files = files
578 .into_iter()
579 .map(|(p, d)| (p.into(), d.into_data()))
580 .collect();
581 Self { files }
582 }
583}
584
585impl<P, D> From<std::collections::BTreeMap<P, D>> for InMemoryDir
586where
587 P: Into<PathBuf>,
588 D: IntoData,
589{
590 fn from(files: std::collections::BTreeMap<P, D>) -> Self {
591 let files = files
592 .into_iter()
593 .map(|(p, d)| (p.into(), d.into_data()))
594 .collect();
595 Self { files }
596 }
597}
598
599impl From<()> for InMemoryDir {
600 fn from(_files: ()) -> Self {
601 let files = Vec::new();
602 Self { files }
603 }
604}
605
606macro_rules! impl_from_tuple_for_inmemorydir {
613 ($($var:ident $path:ident $data:ident),+) => {
614 impl<$($path: Into<PathBuf>, $data: IntoData),+> From<($(($path, $data)),+ ,)> for InMemoryDir {
615 fn from(files: ($(($path, $data)),+,)) -> Self {
616 let ($($var),+ ,) = files;
617 let files = [$(($var.0.into(), $var.1.into_data())),+];
618 files.into()
619 }
620 }
621 };
622}
623
624macro_rules! impl_from_tuples_for_inmemorydir {
627 ($var1:ident $path1:ident $data1:ident, $($var:ident $path:ident $data:ident),+) => {
628 impl_from_tuples_for_inmemorydir!(__impl $var1 $path1 $data1; $($var $path $data),+);
629 };
630 (__impl $($var:ident $path:ident $data:ident),+; $var1:ident $path1:ident $data1:ident $(,$var2:ident $path2:ident $data2:ident)*) => {
631 impl_from_tuple_for_inmemorydir!($($var $path $data),+);
632 impl_from_tuples_for_inmemorydir!(__impl $($var $path $data),+, $var1 $path1 $data1; $($var2 $path2 $data2),*);
633 };
634 (__impl $($var:ident $path:ident $data:ident),+;) => {
635 impl_from_tuple_for_inmemorydir!($($var $path $data),+);
636 }
637}
638
639impl_from_tuples_for_inmemorydir!(
641 s1 P1 D1,
642 s2 P2 D2,
643 s3 P3 D3,
644 s4 P4 D4,
645 s5 P5 D5,
646 s6 P6 D6,
647 s7 P7 D7
648);
649
650#[cfg(test)]
651mod test {
652 use snapbox::assert_data_eq;
653 use snapbox::prelude::*;
654 use snapbox::str;
655
656 use super::*;
657
658 #[test]
659 fn wild_str_cmp() {
660 for (a, b) in &[
661 ("a b", "a b"),
662 ("a[..]b", "a b"),
663 ("a[..]", "a b"),
664 ("[..]", "a b"),
665 ("[..]b", "a b"),
666 ] {
667 assert_eq!(WildStr::new(a), b);
668 }
669 for (a, b) in &[("[..]b", "c"), ("b", "c"), ("b", "cb")] {
670 assert_ne!(WildStr::new(a), b);
671 }
672 }
673
674 #[test]
675 fn redact_elapsed_time() {
676 let mut subs = snapbox::Redactions::new();
677 add_regex_redactions(&mut subs);
678
679 assert_data_eq!(
680 subs.redact("[FINISHED] `release` profile [optimized] target(s) in 5.5s"),
681 str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
682 );
683 assert_data_eq!(
684 subs.redact("[FINISHED] `release` profile [optimized] target(s) in 1m 05s"),
685 str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
686 );
687 }
688}