1 | #[cfg (test)] |
2 | #[path = "tests.rs" ] |
3 | mod tests; |
4 | |
5 | use self::Normalization::*; |
6 | use crate::directory::Directory; |
7 | use crate::run::PathDependency; |
8 | use std::cmp; |
9 | use std::path::Path; |
10 | |
11 | #[derive(Copy, Clone)] |
12 | pub 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 | |
21 | macro_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 | |
42 | normalizations! { |
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. |
76 | pub 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 | |
87 | pub struct Variations { |
88 | variations: [String; Normalization::ALL.len()], |
89 | } |
90 | |
91 | impl 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 | |
110 | pub 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 | |
124 | fn 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 | |
148 | struct Filter<'a> { |
149 | all_lines: &'a [&'a str], |
150 | normalization: Normalization, |
151 | context: Context<'a>, |
152 | hide_numbers: usize, |
153 | } |
154 | |
155 | impl<'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 | |
415 | fn 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," |
420 | fn 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" |
428 | fn 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 | |
438 | fn 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)] |
476 | enum 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 | |
496 | fn 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 | |
559 | fn 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 | |