1#[cfg(test)]
2#[path = "tests.rs"]
3mod tests;
4
5use self::Normalization::*;
6use crate::directory::Directory;
7use crate::run::PathDependency;
8use std::cmp;
9use std::path::Path;
10
11#[derive(Copy, Clone)]
12pub struct Context<'a> {
13 pub krate: &'a str,
14 pub source_dir: &'a Directory,
15 pub workspace: &'a Directory,
16 pub input_file: &'a Path,
17 pub target_dir: &'a Directory,
18 pub path_dependencies: &'a [PathDependency],
19}
20
21macro_rules! normalizations {
22 ($($name:ident,)*) => {
23 #[derive(PartialOrd, PartialEq, Copy, Clone)]
24 enum Normalization {
25 $($name,)*
26 }
27
28 impl Normalization {
29 const ALL: &'static [Self] = &[$($name),*];
30 }
31
32 impl Default for Variations {
33 fn default() -> Self {
34 Variations {
35 variations: [$(($name, String::new()).1),*],
36 }
37 }
38 }
39 };
40}
41
42normalizations! {
43 Basic,
44 StripCouldNotCompile,
45 StripCouldNotCompile2,
46 StripForMoreInformation,
47 StripForMoreInformation2,
48 TrimEnd,
49 RustLib,
50 TypeDirBackslash,
51 WorkspaceLines,
52 PathDependencies,
53 CargoRegistry,
54 ArrowOtherCrate,
55 RelativeToDir,
56 LinesOutsideInputFile,
57 Unindent,
58 AndOthers,
59 StripLongTypeNameFiles,
60 UnindentAfterHelp,
61 // New normalization steps are to be inserted here at the end so that any
62 // snapshots saved before your normalization change remain passing.
63}
64
65/// For a given compiler output, produces the set of saved outputs against which
66/// the compiler's output would be considered correct. If the test's saved
67/// stderr file is identical to any one of these variations, the test will pass.
68///
69/// This is a set rather than just one normalized output in order to avoid
70/// breaking existing tests when introducing new normalization steps. Someone
71/// may have saved stderr snapshots with an older version of trybuild, and those
72/// tests need to continue to pass with newer versions of trybuild.
73///
74/// There is one "preferred" variation which is what we print when the stderr
75/// file is absent or not a match.
76pub fn diagnostics(output: &str, context: Context) -> Variations {
77 let output = output.replace("\r\n", "\n");
78
79 let mut result = Variations::default();
80 for (i, normalization) in Normalization::ALL.iter().enumerate() {
81 result.variations[i] = apply(&output, *normalization, context);
82 }
83
84 result
85}
86
87pub struct Variations {
88 variations: [String; Normalization::ALL.len()],
89}
90
91impl Variations {
92 pub fn preferred(&self) -> &str {
93 self.variations.last().unwrap()
94 }
95
96 pub fn any<F: FnMut(&str) -> bool>(&self, mut f: F) -> bool {
97 self.variations.iter().any(|stderr| f(stderr))
98 }
99
100 pub fn concat(&mut self, other: &Self) {
101 for (this, other) in self.variations.iter_mut().zip(&other.variations) {
102 if !this.is_empty() && !other.is_empty() {
103 this.push('\n');
104 }
105 this.push_str(other);
106 }
107 }
108}
109
110pub fn trim<S: AsRef<[u8]>>(output: S) -> String {
111 let bytes = output.as_ref();
112 let mut normalized = String::from_utf8_lossy(bytes).into_owned();
113
114 let len = normalized.trim_end().len();
115 normalized.truncate(len);
116
117 if !normalized.is_empty() {
118 normalized.push('\n');
119 }
120
121 normalized
122}
123
124fn apply(original: &str, normalization: Normalization, context: Context) -> String {
125 let mut normalized = String::new();
126
127 let lines: Vec<&str> = original.lines().collect();
128 let mut filter = Filter {
129 all_lines: &lines,
130 normalization,
131 context,
132 hide_numbers: 0,
133 };
134 for i in 0..lines.len() {
135 if let Some(line) = filter.apply(i) {
136 normalized += &line;
137 if !normalized.ends_with("\n\n") {
138 normalized.push('\n');
139 }
140 }
141 }
142
143 normalized = unindent(normalized, normalization);
144
145 trim(normalized)
146}
147
148struct Filter<'a> {
149 all_lines: &'a [&'a str],
150 normalization: Normalization,
151 context: Context<'a>,
152 hide_numbers: usize,
153}
154
155impl<'a> Filter<'a> {
156 fn apply(&mut self, index: usize) -> Option<String> {
157 let mut line = self.all_lines[index].to_owned();
158
159 if self.hide_numbers > 0 {
160 hide_leading_numbers(&mut line);
161 self.hide_numbers -= 1;
162 }
163
164 let trim_start = line.trim_start();
165 let indent = line.len() - trim_start.len();
166 let prefix = if trim_start.starts_with("--> ") {
167 Some("--> ")
168 } else if trim_start.starts_with("::: ") {
169 Some("::: ")
170 } else {
171 None
172 };
173
174 if prefix == Some("--> ") && self.normalization < ArrowOtherCrate {
175 if let Some(cut_end) = line.rfind(&['/', '\\'][..]) {
176 let cut_start = indent + 4;
177 line.replace_range(cut_start..cut_end + 1, "$DIR/");
178 return Some(line);
179 }
180 }
181
182 if prefix.is_some() {
183 line = line.replace('\\', "/");
184 let line_lower = line.to_ascii_lowercase();
185 let target_dir_pat = self
186 .context
187 .target_dir
188 .to_string_lossy()
189 .to_ascii_lowercase()
190 .replace('\\', "/");
191 let source_dir_pat = self
192 .context
193 .source_dir
194 .to_string_lossy()
195 .to_ascii_lowercase()
196 .replace('\\', "/");
197 let mut other_crate = false;
198 if line_lower.find(&target_dir_pat) == Some(indent + 4) {
199 let mut offset = indent + 4 + target_dir_pat.len();
200 let mut out_dir_crate_name = None;
201 while let Some(slash) = line[offset..].find('/') {
202 let component = &line[offset..offset + slash];
203 if component == "out" {
204 if let Some(out_dir_crate_name) = out_dir_crate_name {
205 let replacement = format!("$OUT_DIR[{}]", out_dir_crate_name);
206 line.replace_range(indent + 4..offset + 3, &replacement);
207 other_crate = true;
208 break;
209 }
210 } else if component.len() > 17
211 && component.rfind('-') == Some(component.len() - 17)
212 && is_ascii_lowercase_hex(&component[component.len() - 16..])
213 {
214 out_dir_crate_name = Some(&component[..component.len() - 17]);
215 } else {
216 out_dir_crate_name = None;
217 }
218 offset += slash + 1;
219 }
220 } else if let Some(i) = line_lower.find(&source_dir_pat) {
221 if self.normalization >= RelativeToDir && i == indent + 4 {
222 line.replace_range(i..i + source_dir_pat.len(), "");
223 if self.normalization < LinesOutsideInputFile {
224 return Some(line);
225 }
226 let input_file_pat = self
227 .context
228 .input_file
229 .to_string_lossy()
230 .to_ascii_lowercase()
231 .replace('\\', "/");
232 if line_lower[i + source_dir_pat.len()..].starts_with(&input_file_pat) {
233 // Keep line numbers only within the input file (the
234 // path passed to our `fn compile_fail`. All other
235 // source files get line numbers erased below.
236 return Some(line);
237 }
238 } else {
239 line.replace_range(i..i + source_dir_pat.len() - 1, "$DIR");
240 if self.normalization < LinesOutsideInputFile {
241 return Some(line);
242 }
243 }
244 other_crate = true;
245 } else {
246 let workspace_pat = self
247 .context
248 .workspace
249 .to_string_lossy()
250 .to_ascii_lowercase()
251 .replace('\\', "/");
252 if let Some(i) = line_lower.find(&workspace_pat) {
253 line.replace_range(i..i + workspace_pat.len() - 1, "$WORKSPACE");
254 other_crate = true;
255 }
256 }
257 if self.normalization >= PathDependencies && !other_crate {
258 for path_dep in self.context.path_dependencies {
259 let path_dep_pat = path_dep
260 .normalized_path
261 .to_string_lossy()
262 .to_ascii_lowercase()
263 .replace('\\', "/");
264 if let Some(i) = line_lower.find(&path_dep_pat) {
265 let var = format!("${}", path_dep.name.to_uppercase().replace('-', "_"));
266 line.replace_range(i..i + path_dep_pat.len() - 1, &var);
267 other_crate = true;
268 break;
269 }
270 }
271 }
272 if self.normalization >= RustLib && !other_crate {
273 if let Some(pos) = line.find("/rustlib/src/rust/src/") {
274 // --> /home/.rustup/toolchains/nightly/lib/rustlib/src/rust/src/libstd/net/ip.rs:83:1
275 // --> $RUST/src/libstd/net/ip.rs:83:1
276 line.replace_range(indent + 4..pos + 17, "$RUST");
277 other_crate = true;
278 } else if let Some(pos) = line.find("/rustlib/src/rust/library/") {
279 // --> /home/.rustup/toolchains/nightly/lib/rustlib/src/rust/library/std/src/net/ip.rs:83:1
280 // --> $RUST/std/src/net/ip.rs:83:1
281 line.replace_range(indent + 4..pos + 25, "$RUST");
282 other_crate = true;
283 } else if line[indent + 4..].starts_with("/rustc/")
284 && line
285 .get(indent + 11..indent + 51)
286 .map_or(false, is_ascii_lowercase_hex)
287 && line[indent + 51..].starts_with("/library/")
288 {
289 // --> /rustc/c5c7d2b37780dac1092e75f12ab97dd56c30861e/library/std/src/net/ip.rs:83:1
290 // --> $RUST/std/src/net/ip.rs:83:1
291 line.replace_range(indent + 4..indent + 59, "$RUST");
292 other_crate = true;
293 }
294 }
295 if self.normalization >= CargoRegistry && !other_crate {
296 if let Some(pos) = line
297 .find("/registry/src/github.com-")
298 .or_else(|| line.find("/registry/src/index.crates.io-"))
299 {
300 let hash_start = pos + line[pos..].find('-').unwrap() + 1;
301 let hash_end = hash_start + 16;
302 if line
303 .get(hash_start..hash_end)
304 .map_or(false, is_ascii_lowercase_hex)
305 && line[hash_end..].starts_with('/')
306 {
307 // --> /home/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.64/src/de.rs:2584:8
308 // --> $CARGO/serde_json-1.0.64/src/de.rs:2584:8
309 line.replace_range(indent + 4..hash_end, "$CARGO");
310 other_crate = true;
311 }
312 }
313 }
314 if other_crate && self.normalization >= WorkspaceLines {
315 // Blank out line numbers for this particular error since rustc
316 // tends to reach into code from outside of the test case. The
317 // test stderr shouldn't need to be updated every time we touch
318 // those files.
319 hide_trailing_numbers(&mut line);
320 self.hide_numbers = 1;
321 while let Some(next_line) = self.all_lines.get(index + self.hide_numbers) {
322 match next_line.trim_start().chars().next().unwrap_or_default() {
323 '0'..='9' | '|' | '.' => self.hide_numbers += 1,
324 _ => break,
325 }
326 }
327 }
328 return Some(line);
329 }
330
331 if line.starts_with("error: aborting due to ") {
332 return None;
333 }
334
335 if line == "To learn more, run the command again with --verbose." {
336 return None;
337 }
338
339 if self.normalization >= StripCouldNotCompile {
340 if line.starts_with("error: Could not compile `") {
341 return None;
342 }
343 }
344
345 if self.normalization >= StripCouldNotCompile2 {
346 if line.starts_with("error: could not compile `") {
347 return None;
348 }
349 }
350
351 if self.normalization >= StripForMoreInformation {
352 if line.starts_with("For more information about this error, try `rustc --explain") {
353 return None;
354 }
355 }
356
357 if self.normalization >= StripForMoreInformation2 {
358 if line.starts_with("Some errors have detailed explanations:") {
359 return None;
360 }
361 if line.starts_with("For more information about an error, try `rustc --explain") {
362 return None;
363 }
364 }
365
366 if self.normalization >= TrimEnd {
367 line.truncate(line.trim_end().len());
368 }
369
370 if self.normalization >= TypeDirBackslash {
371 if line
372 .trim_start()
373 .starts_with("= note: required because it appears within the type")
374 {
375 line = line.replace('\\', "/");
376 }
377 }
378
379 if self.normalization >= AndOthers {
380 let trim_start = line.trim_start();
381 if trim_start.starts_with("and ") && line.ends_with(" others") {
382 let indent = line.len() - trim_start.len();
383 let num_start = indent + "and ".len();
384 let num_end = line.len() - " others".len();
385 if num_start < num_end
386 && line[num_start..num_end].bytes().all(|b| b.is_ascii_digit())
387 {
388 line.replace_range(num_start..num_end, "$N");
389 }
390 }
391 }
392
393 if self.normalization >= StripLongTypeNameFiles {
394 let trimmed_line = line.trim_start();
395 let trimmed_line = trimmed_line
396 .strip_prefix("= note: ")
397 .unwrap_or(trimmed_line);
398 if trimmed_line.starts_with("the full type name has been written to") {
399 return None;
400 }
401 }
402
403 line = line.replace(self.context.krate, "$CRATE");
404 line = replace_case_insensitive(&line, &self.context.source_dir.to_string_lossy(), "$DIR/");
405 line = replace_case_insensitive(
406 &line,
407 &self.context.workspace.to_string_lossy(),
408 "$WORKSPACE/",
409 );
410
411 Some(line)
412 }
413}
414
415fn is_ascii_lowercase_hex(s: &str) -> bool {
416 s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
417}
418
419// "10 | T: Send," -> " | T: Send,"
420fn hide_leading_numbers(line: &mut String) {
421 let n = line.bytes().take_while(u8::is_ascii_digit).count();
422 for i in 0..n {
423 line.replace_range(i..i + 1, " ");
424 }
425}
426
427// "main.rs:22:29" -> "main.rs"
428fn hide_trailing_numbers(line: &mut String) {
429 for _ in 0..2 {
430 let digits = line.bytes().rev().take_while(u8::is_ascii_digit).count();
431 if digits == 0 || !line[..line.len() - digits].ends_with(':') {
432 return;
433 }
434 line.truncate(line.len() - digits - 1);
435 }
436}
437
438fn replace_case_insensitive(line: &str, pattern: &str, replacement: &str) -> String {
439 let line_lower = line.to_ascii_lowercase().replace('\\', "/");
440 let pattern_lower = pattern.to_ascii_lowercase().replace('\\', "/");
441 let mut replaced = String::with_capacity(line.len());
442
443 let line_lower = line_lower.as_str();
444 let mut split = line_lower.split(&pattern_lower);
445 let mut pos = 0;
446 let mut insert_replacement = false;
447 while let Some(keep) = split.next() {
448 if insert_replacement {
449 replaced.push_str(replacement);
450 pos += pattern.len();
451 }
452 let mut keep = &line[pos..pos + keep.len()];
453 if insert_replacement {
454 let end_of_maybe_path = keep.find(&[' ', ':'][..]).unwrap_or(keep.len());
455 replaced.push_str(&keep[..end_of_maybe_path].replace('\\', "/"));
456 pos += end_of_maybe_path;
457 keep = &keep[end_of_maybe_path..];
458 }
459 replaced.push_str(keep);
460 pos += keep.len();
461 insert_replacement = true;
462 if replaced.ends_with(|ch: char| ch.is_ascii_alphanumeric()) {
463 if let Some(ch) = line[pos..].chars().next() {
464 replaced.push(ch);
465 pos += ch.len_utf8();
466 split = line_lower[pos..].split(&pattern_lower);
467 insert_replacement = false;
468 }
469 }
470 }
471
472 replaced
473}
474
475#[derive(PartialEq)]
476enum IndentedLineKind {
477 // `error`
478 // `warning`
479 Heading,
480
481 // Contains max number of spaces that can be cut based on this line.
482 // ` --> foo` = 2
483 // ` | foo` = 3
484 // ` ::: foo` = 2
485 // `10 | foo` = 1
486 Code(usize),
487
488 // `note:`
489 // `...`
490 Note,
491
492 // Contains number of leading spaces.
493 Other(usize),
494}
495
496fn unindent(diag: String, normalization: Normalization) -> String {
497 if normalization < Unindent {
498 return diag;
499 }
500
501 let mut normalized = String::new();
502 let mut lines = diag.lines();
503
504 while let Some(line) = lines.next() {
505 normalized.push_str(line);
506 normalized.push('\n');
507
508 if indented_line_kind(line, normalization) != IndentedLineKind::Heading {
509 continue;
510 }
511
512 let mut ahead = lines.clone();
513 let next_line = match ahead.next() {
514 Some(line) => line,
515 None => continue,
516 };
517
518 if let IndentedLineKind::Code(indent) = indented_line_kind(next_line, normalization) {
519 if next_line[indent + 1..].starts_with("--> ") {
520 let mut lines_in_block = 1;
521 let mut least_indent = indent;
522 while let Some(line) = ahead.next() {
523 match indented_line_kind(line, normalization) {
524 IndentedLineKind::Heading => break,
525 IndentedLineKind::Code(indent) => {
526 lines_in_block += 1;
527 least_indent = cmp::min(least_indent, indent);
528 }
529 IndentedLineKind::Note => lines_in_block += 1,
530 IndentedLineKind::Other(spaces) => {
531 if spaces > 10 {
532 lines_in_block += 1;
533 } else {
534 break;
535 }
536 }
537 }
538 }
539 for _ in 0..lines_in_block {
540 let line = lines.next().unwrap();
541 if let IndentedLineKind::Code(_) | IndentedLineKind::Other(_) =
542 indented_line_kind(line, normalization)
543 {
544 let space = line.find(' ').unwrap();
545 normalized.push_str(&line[..space]);
546 normalized.push_str(&line[space + least_indent..]);
547 } else {
548 normalized.push_str(line);
549 }
550 normalized.push('\n');
551 }
552 }
553 }
554 }
555
556 normalized
557}
558
559fn indented_line_kind(line: &str, normalization: Normalization) -> IndentedLineKind {
560 if let Some(heading_len) = if line.starts_with("error") {
561 Some("error".len())
562 } else if line.starts_with("warning") {
563 Some("warning".len())
564 } else {
565 None
566 } {
567 if line[heading_len..].starts_with(&[':', '['][..]) {
568 return IndentedLineKind::Heading;
569 }
570 }
571
572 if line.starts_with("note:")
573 || line == "..."
574 || normalization >= UnindentAfterHelp && line.starts_with("help:")
575 {
576 return IndentedLineKind::Note;
577 }
578
579 let is_space = |b: &u8| *b == b' ';
580 if let Some(rest) = line.strip_prefix("... ") {
581 let spaces = rest.bytes().take_while(is_space).count();
582 return IndentedLineKind::Code(spaces);
583 }
584
585 let digits = line.bytes().take_while(u8::is_ascii_digit).count();
586 let spaces = line[digits..].bytes().take_while(|b| *b == b' ').count();
587 let rest = &line[digits + spaces..];
588 if spaces > 0
589 && (rest == "|"
590 || rest.starts_with("| ")
591 || digits == 0
592 && (rest.starts_with("--> ") || rest.starts_with("::: ") || rest.starts_with("= ")))
593 {
594 return IndentedLineKind::Code(spaces - 1);
595 }
596
597 IndentedLineKind::Other(if digits == 0 { spaces } else { 0 })
598}
599