1use crate::cross_compile::try_alternate;
45use crate::paths;
46use crate::rustc_host;
47use anyhow::{Result, bail};
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(
233 "[HASH]",
234 regex!(r"build/[a-z0-9]{2}/(?<redacted>[a-f0-9]{16})"),
235 )
236 .unwrap();
237 subs.insert("[HASH]", regex!(r"/[a-z0-9\-_]+-(?<redacted>[0-9a-f]{16})"))
238 .unwrap();
239 subs.insert("[HASH]", regex!(r"/(?<redacted>[a-f0-9]{2}\/[0-9a-f]{14})"))
241 .unwrap();
242 subs.insert("[HASH]", regex!(r"[a-z0-9]+-(?<redacted>[a-f0-9]{16})"))
244 .unwrap();
245 subs.insert("[HASH]", regex!(r"\/(?<redacted>[0-9a-f]{16})\/"))
247 .unwrap();
248 subs.insert(
249 "[AVG_ELAPSED]",
250 regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?) ns/iter"),
251 )
252 .unwrap();
253 subs.insert(
254 "[JITTER]",
255 regex!(r"ns/iter \(\+/- (?<redacted>[0-9]+(\.[0-9]+)?)\)"),
256 )
257 .unwrap();
258
259 subs.insert(
264 "[TIME_DIFF_AFTER_LAST_BUILD]",
265 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)"),
266 )
267 .unwrap();
268}
269
270static MIN_LITERAL_REDACTIONS: &[(&str, &str)] = &[
271 ("[EXE]", std::env::consts::EXE_SUFFIX),
272 ("[BROKEN_PIPE]", "Broken pipe (os error 32)"),
273 ("[BROKEN_PIPE]", "The pipe is being closed. (os error 232)"),
274 ("[NOT_FOUND]", "No such file or directory (os error 2)"),
276 (
278 "[NOT_FOUND]",
279 "The system cannot find the file specified. (os error 2)",
280 ),
281 (
282 "[NOT_FOUND]",
283 "The system cannot find the path specified. (os error 3)",
284 ),
285 ("[NOT_FOUND]", "Access is denied. (os error 5)"),
286 ("[NOT_FOUND]", "program not found"),
287 ("[EXIT_STATUS]", "exit status"),
289 ("[EXIT_STATUS]", "exit code"),
291];
292static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[
293 ("[RUNNING]", " Running"),
294 ("[COMPILING]", " Compiling"),
295 ("[CHECKING]", " Checking"),
296 ("[COMPLETED]", " Completed"),
297 ("[CREATED]", " Created"),
298 ("[CREATING]", " Creating"),
299 ("[CREDENTIAL]", " Credential"),
300 ("[DOWNGRADING]", " Downgrading"),
301 ("[FINISHED]", " Finished"),
302 ("[ERROR]", "error:"),
303 ("[WARNING]", "warning:"),
304 ("[NOTE]", "note:"),
305 ("[HELP]", "help:"),
306 ("[DOCUMENTING]", " Documenting"),
307 ("[SCRAPING]", " Scraping"),
308 ("[FRESH]", " Fresh"),
309 ("[DIRTY]", " Dirty"),
310 ("[LOCKING]", " Locking"),
311 ("[UPDATING]", " Updating"),
312 ("[UPGRADING]", " Upgrading"),
313 ("[ADDING]", " Adding"),
314 ("[REMOVING]", " Removing"),
315 ("[REMOVED]", " Removed"),
316 ("[UNCHANGED]", " Unchanged"),
317 ("[DOCTEST]", " Doc-tests"),
318 ("[PACKAGING]", " Packaging"),
319 ("[PACKAGED]", " Packaged"),
320 ("[DOWNLOADING]", " Downloading"),
321 ("[DOWNLOADED]", " Downloaded"),
322 ("[UPLOADING]", " Uploading"),
323 ("[UPLOADED]", " Uploaded"),
324 ("[VERIFYING]", " Verifying"),
325 ("[ARCHIVING]", " Archiving"),
326 ("[INSTALLING]", " Installing"),
327 ("[REPLACING]", " Replacing"),
328 ("[UNPACKING]", " Unpacking"),
329 ("[SUMMARY]", " Summary"),
330 ("[FIXED]", " Fixed"),
331 ("[FIXING]", " Fixing"),
332 ("[IGNORED]", " Ignored"),
333 ("[INSTALLED]", " Installed"),
334 ("[REPLACED]", " Replaced"),
335 ("[BUILDING]", " Building"),
336 ("[LOGIN]", " Login"),
337 ("[LOGOUT]", " Logout"),
338 ("[YANK]", " Yank"),
339 ("[OWNER]", " Owner"),
340 ("[MIGRATING]", " Migrating"),
341 ("[EXECUTABLE]", " Executable"),
342 ("[SKIPPING]", " Skipping"),
343 ("[WAITING]", " Waiting"),
344 ("[PUBLISHED]", " Published"),
345 ("[BLOCKING]", " Blocking"),
346 ("[GENERATED]", " Generated"),
347 ("[OPENING]", " Opening"),
348 ("[MERGING]", " Merging"),
349];
350
351pub(crate) fn match_contains(
356 expected: &str,
357 actual: &str,
358 redactions: &snapbox::Redactions,
359) -> Result<()> {
360 let expected = normalize_expected(expected, redactions);
361 let actual = normalize_actual(actual, redactions);
362 let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
363 let a: Vec<_> = actual.lines().collect();
364 if e.len() == 0 {
365 bail!("expected length must not be zero");
366 }
367 for window in a.windows(e.len()) {
368 if e == window {
369 return Ok(());
370 }
371 }
372 bail!(
373 "expected to find:\n\
374 {}\n\n\
375 did not find in output:\n\
376 {}",
377 expected,
378 actual
379 );
380}
381
382pub(crate) fn match_does_not_contain(
387 expected: &str,
388 actual: &str,
389 redactions: &snapbox::Redactions,
390) -> Result<()> {
391 if match_contains(expected, actual, redactions).is_ok() {
392 bail!(
393 "expected not to find:\n\
394 {}\n\n\
395 but found in output:\n\
396 {}",
397 expected,
398 actual
399 );
400 } else {
401 Ok(())
402 }
403}
404
405pub(crate) fn match_with_without(
413 actual: &str,
414 with: &[String],
415 without: &[String],
416 redactions: &snapbox::Redactions,
417) -> Result<()> {
418 let actual = normalize_actual(actual, redactions);
419 let norm = |s: &String| format!("[..]{}[..]", normalize_expected(s, redactions));
420 let with: Vec<_> = with.iter().map(norm).collect();
421 let without: Vec<_> = without.iter().map(norm).collect();
422 let with_wild: Vec<_> = with.iter().map(|w| WildStr::new(w)).collect();
423 let without_wild: Vec<_> = without.iter().map(|w| WildStr::new(w)).collect();
424
425 let matches: Vec<_> = actual
426 .lines()
427 .filter(|line| with_wild.iter().all(|with| with == line))
428 .filter(|line| !without_wild.iter().any(|without| without == line))
429 .collect();
430 match matches.len() {
431 0 => bail!(
432 "Could not find expected line in output.\n\
433 With contents: {:?}\n\
434 Without contents: {:?}\n\
435 Actual stderr:\n\
436 {}\n",
437 with,
438 without,
439 actual
440 ),
441 1 => Ok(()),
442 _ => bail!(
443 "Found multiple matching lines, but only expected one.\n\
444 With contents: {:?}\n\
445 Without contents: {:?}\n\
446 Matching lines:\n\
447 {}\n",
448 with,
449 without,
450 itertools::join(matches, "\n")
451 ),
452 }
453}
454
455fn normalize_actual(content: &str, redactions: &snapbox::Redactions) -> String {
457 use snapbox::filter::Filter as _;
458 let content = snapbox::filter::FilterPaths.filter(content.into_data());
459 let content = snapbox::filter::FilterNewlines.filter(content);
460 let content = content.render().expect("came in as a String");
461 let content = redactions.redact(&content);
462 content
463}
464
465fn normalize_expected(content: &str, redactions: &snapbox::Redactions) -> String {
467 use snapbox::filter::Filter as _;
468 let content = snapbox::filter::FilterPaths.filter(content.into_data());
469 let content = snapbox::filter::FilterNewlines.filter(content);
470 let content = content.render().expect("came in as a String");
472 let content = redactions.clear_unused(&content);
473 content.into_owned()
474}
475
476struct WildStr<'a> {
478 has_meta: bool,
479 line: &'a str,
480}
481
482impl<'a> WildStr<'a> {
483 fn new(line: &'a str) -> WildStr<'a> {
484 WildStr {
485 has_meta: line.contains("[..]"),
486 line,
487 }
488 }
489}
490
491impl PartialEq<&str> for WildStr<'_> {
492 fn eq(&self, other: &&str) -> bool {
493 if self.has_meta {
494 meta_cmp(self.line, other)
495 } else {
496 self.line == *other
497 }
498 }
499}
500
501fn meta_cmp(a: &str, mut b: &str) -> bool {
502 for (i, part) in a.split("[..]").enumerate() {
503 match b.find(part) {
504 Some(j) => {
505 if i == 0 && j != 0 {
506 return false;
507 }
508 b = &b[j + part.len()..];
509 }
510 None => return false,
511 }
512 }
513 b.is_empty() || a.ends_with("[..]")
514}
515
516impl fmt::Display for WildStr<'_> {
517 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518 f.write_str(&self.line)
519 }
520}
521
522impl fmt::Debug for WildStr<'_> {
523 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524 write!(f, "{:?}", self.line)
525 }
526}
527
528pub struct InMemoryDir {
529 files: Vec<(PathBuf, Data)>,
530}
531
532impl InMemoryDir {
533 pub fn paths(&self) -> impl Iterator<Item = &Path> {
534 self.files.iter().map(|(p, _)| p.as_path())
535 }
536
537 #[track_caller]
538 pub fn assert_contains(&self, expected: &Self) {
539 use std::fmt::Write as _;
540 let assert = assert_e2e();
541 let mut errs = String::new();
542 for (path, expected_data) in &expected.files {
543 let actual_data = self
544 .files
545 .iter()
546 .find_map(|(p, d)| (path == p).then(|| d.clone()))
547 .unwrap_or_else(|| Data::new());
548 if let Err(err) =
549 assert.try_eq(Some(&path.display()), actual_data, expected_data.clone())
550 {
551 let _ = write!(&mut errs, "{err}");
552 }
553 }
554 if !errs.is_empty() {
555 panic!("{errs}")
556 }
557 }
558}
559
560impl<P, D> FromIterator<(P, D)> for InMemoryDir
561where
562 P: Into<std::path::PathBuf>,
563 D: IntoData,
564{
565 fn from_iter<I: IntoIterator<Item = (P, D)>>(files: I) -> Self {
566 let files = files
567 .into_iter()
568 .map(|(p, d)| (p.into(), d.into_data()))
569 .collect();
570 Self { files }
571 }
572}
573
574impl<const N: usize, P, D> From<[(P, D); N]> for InMemoryDir
575where
576 P: Into<PathBuf>,
577 D: IntoData,
578{
579 fn from(files: [(P, D); N]) -> Self {
580 let files = files
581 .into_iter()
582 .map(|(p, d)| (p.into(), d.into_data()))
583 .collect();
584 Self { files }
585 }
586}
587
588impl<P, D> From<std::collections::HashMap<P, D>> for InMemoryDir
589where
590 P: Into<PathBuf>,
591 D: IntoData,
592{
593 fn from(files: std::collections::HashMap<P, D>) -> Self {
594 let files = files
595 .into_iter()
596 .map(|(p, d)| (p.into(), d.into_data()))
597 .collect();
598 Self { files }
599 }
600}
601
602impl<P, D> From<std::collections::BTreeMap<P, D>> for InMemoryDir
603where
604 P: Into<PathBuf>,
605 D: IntoData,
606{
607 fn from(files: std::collections::BTreeMap<P, D>) -> Self {
608 let files = files
609 .into_iter()
610 .map(|(p, d)| (p.into(), d.into_data()))
611 .collect();
612 Self { files }
613 }
614}
615
616impl From<()> for InMemoryDir {
617 fn from(_files: ()) -> Self {
618 let files = Vec::new();
619 Self { files }
620 }
621}
622
623macro_rules! impl_from_tuple_for_inmemorydir {
630 ($($var:ident $path:ident $data:ident),+) => {
631 impl<$($path: Into<PathBuf>, $data: IntoData),+> From<($(($path, $data)),+ ,)> for InMemoryDir {
632 fn from(files: ($(($path, $data)),+,)) -> Self {
633 let ($($var),+ ,) = files;
634 let files = [$(($var.0.into(), $var.1.into_data())),+];
635 files.into()
636 }
637 }
638 };
639}
640
641macro_rules! impl_from_tuples_for_inmemorydir {
644 ($var1:ident $path1:ident $data1:ident, $($var:ident $path:ident $data:ident),+) => {
645 impl_from_tuples_for_inmemorydir!(__impl $var1 $path1 $data1; $($var $path $data),+);
646 };
647 (__impl $($var:ident $path:ident $data:ident),+; $var1:ident $path1:ident $data1:ident $(,$var2:ident $path2:ident $data2:ident)*) => {
648 impl_from_tuple_for_inmemorydir!($($var $path $data),+);
649 impl_from_tuples_for_inmemorydir!(__impl $($var $path $data),+, $var1 $path1 $data1; $($var2 $path2 $data2),*);
650 };
651 (__impl $($var:ident $path:ident $data:ident),+;) => {
652 impl_from_tuple_for_inmemorydir!($($var $path $data),+);
653 }
654}
655
656impl_from_tuples_for_inmemorydir!(
658 s1 P1 D1,
659 s2 P2 D2,
660 s3 P3 D3,
661 s4 P4 D4,
662 s5 P5 D5,
663 s6 P6 D6,
664 s7 P7 D7
665);
666
667#[cfg(test)]
668mod test {
669 use snapbox::assert_data_eq;
670 use snapbox::prelude::*;
671 use snapbox::str;
672
673 use super::*;
674
675 #[test]
676 fn wild_str_cmp() {
677 for (a, b) in &[
678 ("a b", "a b"),
679 ("a[..]b", "a b"),
680 ("a[..]", "a b"),
681 ("[..]", "a b"),
682 ("[..]b", "a b"),
683 ] {
684 assert_eq!(WildStr::new(a), b);
685 }
686 for (a, b) in &[("[..]b", "c"), ("b", "c"), ("b", "cb")] {
687 assert_ne!(WildStr::new(a), b);
688 }
689 }
690
691 #[test]
692 fn redact_elapsed_time() {
693 let mut subs = snapbox::Redactions::new();
694 add_regex_redactions(&mut subs);
695
696 assert_data_eq!(
697 subs.redact("[FINISHED] `release` profile [optimized] target(s) in 5.5s"),
698 str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
699 );
700 assert_data_eq!(
701 subs.redact("[FINISHED] `release` profile [optimized] target(s) in 1m 05s"),
702 str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
703 );
704 }
705}