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 ("[MERGING]", " Merging"),
342];
343
344pub(crate) fn match_contains(
349 expected: &str,
350 actual: &str,
351 redactions: &snapbox::Redactions,
352) -> Result<()> {
353 let expected = normalize_expected(expected, redactions);
354 let actual = normalize_actual(actual, redactions);
355 let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
356 let a: Vec<_> = actual.lines().collect();
357 if e.len() == 0 {
358 bail!("expected length must not be zero");
359 }
360 for window in a.windows(e.len()) {
361 if e == window {
362 return Ok(());
363 }
364 }
365 bail!(
366 "expected to find:\n\
367 {}\n\n\
368 did not find in output:\n\
369 {}",
370 expected,
371 actual
372 );
373}
374
375pub(crate) fn match_does_not_contain(
380 expected: &str,
381 actual: &str,
382 redactions: &snapbox::Redactions,
383) -> Result<()> {
384 if match_contains(expected, actual, redactions).is_ok() {
385 bail!(
386 "expected not to find:\n\
387 {}\n\n\
388 but found in output:\n\
389 {}",
390 expected,
391 actual
392 );
393 } else {
394 Ok(())
395 }
396}
397
398pub(crate) fn match_with_without(
406 actual: &str,
407 with: &[String],
408 without: &[String],
409 redactions: &snapbox::Redactions,
410) -> Result<()> {
411 let actual = normalize_actual(actual, redactions);
412 let norm = |s: &String| format!("[..]{}[..]", normalize_expected(s, redactions));
413 let with: Vec<_> = with.iter().map(norm).collect();
414 let without: Vec<_> = without.iter().map(norm).collect();
415 let with_wild: Vec<_> = with.iter().map(|w| WildStr::new(w)).collect();
416 let without_wild: Vec<_> = without.iter().map(|w| WildStr::new(w)).collect();
417
418 let matches: Vec<_> = actual
419 .lines()
420 .filter(|line| with_wild.iter().all(|with| with == line))
421 .filter(|line| !without_wild.iter().any(|without| without == line))
422 .collect();
423 match matches.len() {
424 0 => bail!(
425 "Could not find expected line in output.\n\
426 With contents: {:?}\n\
427 Without contents: {:?}\n\
428 Actual stderr:\n\
429 {}\n",
430 with,
431 without,
432 actual
433 ),
434 1 => Ok(()),
435 _ => bail!(
436 "Found multiple matching lines, but only expected one.\n\
437 With contents: {:?}\n\
438 Without contents: {:?}\n\
439 Matching lines:\n\
440 {}\n",
441 with,
442 without,
443 itertools::join(matches, "\n")
444 ),
445 }
446}
447
448fn normalize_actual(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");
454 let content = redactions.redact(&content);
455 content
456}
457
458fn normalize_expected(content: &str, redactions: &snapbox::Redactions) -> String {
460 use snapbox::filter::Filter as _;
461 let content = snapbox::filter::FilterPaths.filter(content.into_data());
462 let content = snapbox::filter::FilterNewlines.filter(content);
463 let content = content.render().expect("came in as a String");
465 let content = redactions.clear_unused(&content);
466 content.into_owned()
467}
468
469struct WildStr<'a> {
471 has_meta: bool,
472 line: &'a str,
473}
474
475impl<'a> WildStr<'a> {
476 fn new(line: &'a str) -> WildStr<'a> {
477 WildStr {
478 has_meta: line.contains("[..]"),
479 line,
480 }
481 }
482}
483
484impl PartialEq<&str> for WildStr<'_> {
485 fn eq(&self, other: &&str) -> bool {
486 if self.has_meta {
487 meta_cmp(self.line, other)
488 } else {
489 self.line == *other
490 }
491 }
492}
493
494fn meta_cmp(a: &str, mut b: &str) -> bool {
495 for (i, part) in a.split("[..]").enumerate() {
496 match b.find(part) {
497 Some(j) => {
498 if i == 0 && j != 0 {
499 return false;
500 }
501 b = &b[j + part.len()..];
502 }
503 None => return false,
504 }
505 }
506 b.is_empty() || a.ends_with("[..]")
507}
508
509impl fmt::Display for WildStr<'_> {
510 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511 f.write_str(&self.line)
512 }
513}
514
515impl fmt::Debug for WildStr<'_> {
516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517 write!(f, "{:?}", self.line)
518 }
519}
520
521pub struct InMemoryDir {
522 files: Vec<(PathBuf, Data)>,
523}
524
525impl InMemoryDir {
526 pub fn paths(&self) -> impl Iterator<Item = &Path> {
527 self.files.iter().map(|(p, _)| p.as_path())
528 }
529
530 #[track_caller]
531 pub fn assert_contains(&self, expected: &Self) {
532 use std::fmt::Write as _;
533 let assert = assert_e2e();
534 let mut errs = String::new();
535 for (path, expected_data) in &expected.files {
536 let actual_data = self
537 .files
538 .iter()
539 .find_map(|(p, d)| (path == p).then(|| d.clone()))
540 .unwrap_or_else(|| Data::new());
541 if let Err(err) =
542 assert.try_eq(Some(&path.display()), actual_data, expected_data.clone())
543 {
544 let _ = write!(&mut errs, "{err}");
545 }
546 }
547 if !errs.is_empty() {
548 panic!("{errs}")
549 }
550 }
551}
552
553impl<P, D> FromIterator<(P, D)> for InMemoryDir
554where
555 P: Into<std::path::PathBuf>,
556 D: IntoData,
557{
558 fn from_iter<I: IntoIterator<Item = (P, D)>>(files: I) -> Self {
559 let files = files
560 .into_iter()
561 .map(|(p, d)| (p.into(), d.into_data()))
562 .collect();
563 Self { files }
564 }
565}
566
567impl<const N: usize, P, D> From<[(P, D); N]> for InMemoryDir
568where
569 P: Into<PathBuf>,
570 D: IntoData,
571{
572 fn from(files: [(P, D); N]) -> Self {
573 let files = files
574 .into_iter()
575 .map(|(p, d)| (p.into(), d.into_data()))
576 .collect();
577 Self { files }
578 }
579}
580
581impl<P, D> From<std::collections::HashMap<P, D>> for InMemoryDir
582where
583 P: Into<PathBuf>,
584 D: IntoData,
585{
586 fn from(files: std::collections::HashMap<P, D>) -> Self {
587 let files = files
588 .into_iter()
589 .map(|(p, d)| (p.into(), d.into_data()))
590 .collect();
591 Self { files }
592 }
593}
594
595impl<P, D> From<std::collections::BTreeMap<P, D>> for InMemoryDir
596where
597 P: Into<PathBuf>,
598 D: IntoData,
599{
600 fn from(files: std::collections::BTreeMap<P, D>) -> Self {
601 let files = files
602 .into_iter()
603 .map(|(p, d)| (p.into(), d.into_data()))
604 .collect();
605 Self { files }
606 }
607}
608
609impl From<()> for InMemoryDir {
610 fn from(_files: ()) -> Self {
611 let files = Vec::new();
612 Self { files }
613 }
614}
615
616macro_rules! impl_from_tuple_for_inmemorydir {
623 ($($var:ident $path:ident $data:ident),+) => {
624 impl<$($path: Into<PathBuf>, $data: IntoData),+> From<($(($path, $data)),+ ,)> for InMemoryDir {
625 fn from(files: ($(($path, $data)),+,)) -> Self {
626 let ($($var),+ ,) = files;
627 let files = [$(($var.0.into(), $var.1.into_data())),+];
628 files.into()
629 }
630 }
631 };
632}
633
634macro_rules! impl_from_tuples_for_inmemorydir {
637 ($var1:ident $path1:ident $data1:ident, $($var:ident $path:ident $data:ident),+) => {
638 impl_from_tuples_for_inmemorydir!(__impl $var1 $path1 $data1; $($var $path $data),+);
639 };
640 (__impl $($var:ident $path:ident $data:ident),+; $var1:ident $path1:ident $data1:ident $(,$var2:ident $path2:ident $data2:ident)*) => {
641 impl_from_tuple_for_inmemorydir!($($var $path $data),+);
642 impl_from_tuples_for_inmemorydir!(__impl $($var $path $data),+, $var1 $path1 $data1; $($var2 $path2 $data2),*);
643 };
644 (__impl $($var:ident $path:ident $data:ident),+;) => {
645 impl_from_tuple_for_inmemorydir!($($var $path $data),+);
646 }
647}
648
649impl_from_tuples_for_inmemorydir!(
651 s1 P1 D1,
652 s2 P2 D2,
653 s3 P3 D3,
654 s4 P4 D4,
655 s5 P5 D5,
656 s6 P6 D6,
657 s7 P7 D7
658);
659
660#[cfg(test)]
661mod test {
662 use snapbox::assert_data_eq;
663 use snapbox::prelude::*;
664 use snapbox::str;
665
666 use super::*;
667
668 #[test]
669 fn wild_str_cmp() {
670 for (a, b) in &[
671 ("a b", "a b"),
672 ("a[..]b", "a b"),
673 ("a[..]", "a b"),
674 ("[..]", "a b"),
675 ("[..]b", "a b"),
676 ] {
677 assert_eq!(WildStr::new(a), b);
678 }
679 for (a, b) in &[("[..]b", "c"), ("b", "c"), ("b", "cb")] {
680 assert_ne!(WildStr::new(a), b);
681 }
682 }
683
684 #[test]
685 fn redact_elapsed_time() {
686 let mut subs = snapbox::Redactions::new();
687 add_regex_redactions(&mut subs);
688
689 assert_data_eq!(
690 subs.redact("[FINISHED] `release` profile [optimized] target(s) in 5.5s"),
691 str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
692 );
693 assert_data_eq!(
694 subs.redact("[FINISHED] `release` profile [optimized] target(s) in 1m 05s"),
695 str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
696 );
697 }
698}