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