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