use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::config::RawSubstitution;
use crate::util;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "RawSubstitution")]
pub struct Substitution {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone)]
pub struct NormalizationContext {
pub workspace_root: PathBuf,
pub sysroot: PathBuf,
pub cargo_registry: Option<PathBuf>,
pub compat_short_cargo: bool,
pub extra_substitutions: Vec<Substitution>,
pub strip_lines: Vec<String>,
pub strip_line_prefixes: Vec<String>,
}
impl NormalizationContext {
pub fn new(workspace_root: PathBuf, sysroot: PathBuf) -> Self {
let cargo_registry = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cargo")))
.map(|p| p.join("registry"));
Self {
workspace_root,
sysroot,
cargo_registry,
compat_short_cargo: false,
extra_substitutions: Vec::new(),
strip_lines: Vec::new(),
strip_line_prefixes: Vec::new(),
}
}
pub fn with_compat_short_cargo(mut self, enabled: bool) -> Self {
self.compat_short_cargo = enabled;
self
}
pub fn with_extra_substitutions(mut self, subs: Vec<Substitution>) -> Self {
self.extra_substitutions = subs;
self
}
pub fn with_strip_lines(mut self, lines: Vec<String>) -> Self {
self.strip_lines = lines;
self
}
pub fn with_strip_line_prefixes(mut self, prefixes: Vec<String>) -> Self {
self.strip_line_prefixes = prefixes;
self
}
}
pub fn normalize(input: &str, ctx: &NormalizationContext, fixture_dir: &Path) -> String {
let mut substitutions: Vec<(String, &'static str)> = Vec::new();
push_path(&mut substitutions, fixture_dir, "$DIR");
push_path(&mut substitutions, &ctx.workspace_root, "$WORKSPACE");
push_path(&mut substitutions, &ctx.sysroot, "$RUST");
if let Some(reg) = &ctx.cargo_registry
&& !ctx.compat_short_cargo
{
push_path(&mut substitutions, reg, "$CARGO/registry");
}
substitutions.sort_by_key(|(needle, _)| std::cmp::Reverse(needle.len()));
let unified_le = unify_line_endings(input);
let mut intermediate: Vec<String> = Vec::with_capacity(unified_le.lines().count() + 1);
for line in unified_le.lines() {
let mut s = line.to_string();
if has_path_marker(&s) {
s = rewrite_path_separators_in_path_lines(&s);
}
s = rewrite_long_type_note_path(&s);
for (needle, repl) in &substitutions {
s = replace_advancing(&s, needle, repl);
}
if ctx.compat_short_cargo
&& let Some(reg) = &ctx.cargo_registry
{
s = rewrite_cargo_short(&s, reg);
}
for sub in &ctx.extra_substitutions {
s = replace_advancing(&s, &sub.from, &sub.to);
}
s = rewrite_type_ids(&s);
let trimmed = s.trim_end_matches([' ', '\t']);
intermediate.push(trimmed.to_string());
}
let mut after_strip: Vec<String> = Vec::with_capacity(intermediate.len());
for line in intermediate {
if should_strip_line(&line, &ctx.strip_lines, &ctx.strip_line_prefixes) {
continue;
}
after_strip.push(line);
}
let mut out = String::with_capacity(input.len());
let mut prev_blank = false;
for line in after_strip {
let is_blank = line.is_empty();
if is_blank && prev_blank {
continue;
}
out.push_str(&line);
out.push('\n');
prev_blank = is_blank;
}
while out.ends_with('\n') {
out.pop();
}
out
}
fn should_strip_line(line: &str, exact: &[String], prefixes: &[String]) -> bool {
if exact.iter().any(|e| e == line) {
return true;
}
if prefixes.iter().any(|p| line.starts_with(p.as_str())) {
return true;
}
false
}
fn push_path(out: &mut Vec<(String, &'static str)>, p: &Path, placeholder: &'static str) {
let s = util::to_forward_slash(&p.to_string_lossy());
if s.is_empty() {
return;
}
out.push((s, placeholder));
}
fn replace_advancing(s: &str, needle: &str, repl: &str) -> String {
if needle.is_empty() {
return s.to_string();
}
if !s.contains(needle) {
return s.to_string();
}
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(idx) = rest.find(needle) {
out.push_str(&rest[..idx]);
out.push_str(repl);
rest = &rest[idx + needle.len()..];
}
out.push_str(rest);
out
}
fn rewrite_cargo_short(s: &str, cargo_registry: &Path) -> String {
let registry = util::to_forward_slash(&cargo_registry.to_string_lossy());
if registry.is_empty() {
return s.to_string();
}
let middles: [String; 2] = [
format!("{registry}/src/github.com-"),
format!("{registry}/src/index.crates.io-"),
];
const HEX_LEN: usize = 16;
if !middles.iter().any(|m| s.contains(m)) {
return s.to_string();
}
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
let mut consumed = 0usize;
for middle in &middles {
let m = middle.as_bytes();
if i + m.len() + HEX_LEN + 1 > bytes.len() {
continue;
}
if &bytes[i..i + m.len()] != m {
continue;
}
let hex_start = i + m.len();
let hex_end = hex_start + HEX_LEN;
let hex = &bytes[hex_start..hex_end];
if !hex.iter().all(|&b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
continue;
}
if bytes[hex_end] != b'/' {
continue;
}
out.push_str("$CARGO");
consumed = m.len() + HEX_LEN;
break;
}
if consumed > 0 {
i += consumed;
continue;
}
let mut j = i + 1;
while j < bytes.len() && (bytes[j] & 0xC0) == 0x80 {
j += 1;
}
out.push_str(&s[i..j]);
i = j;
}
out
}
fn rewrite_type_ids(s: &str) -> String {
if !s.contains('#') {
return s.to_string();
}
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'#' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
let mut j = i + 1;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
out.push_str("$TYPEID");
i = j;
} else {
let mut j = i + 1;
while j < bytes.len() && (bytes[j] & 0xC0) == 0x80 {
j += 1;
}
out.push_str(&s[i..j]);
i = j;
}
}
out
}
fn unify_line_endings(s: &str) -> String {
if !s.contains('\r') {
return s.to_string();
}
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'\r' {
out.push('\n');
if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
i += 2;
} else {
i += 1;
}
} else {
let mut j = i + 1;
while j < bytes.len() && (bytes[j] & 0xC0) == 0x80 {
j += 1;
}
out.push_str(&s[i..j]);
i = j;
}
}
out
}
fn has_path_marker(line: &str) -> bool {
line.contains("--> ") || line.contains("::: ")
}
fn rewrite_long_type_note_path(line: &str) -> String {
const MARKERS: &[&str] = &[
"the full name for the type has been written to '",
"the full type name has been written to '",
];
for marker in MARKERS {
let Some(prefix_idx) = line.find(marker) else {
continue;
};
let after_quote = prefix_idx + marker.len();
let Some(close_rel) = line[after_quote..].find('\'') else {
return line.to_string();
};
let close_abs = after_quote + close_rel;
let mut out = String::with_capacity(line.len());
out.push_str(&line[..after_quote]);
out.push_str("$LONGTYPE_FILE");
out.push_str(&line[close_abs..]);
return out;
}
line.to_string()
}
fn rewrite_path_separators_in_path_lines(line: &str) -> String {
for marker in ["--> ", "::: "] {
if let Some(idx) = line.find(marker) {
let head_end = idx + marker.len();
let head = &line[..head_end];
let tail = &line[head_end..];
return format!("{head}{}", util::to_forward_slash(tail));
}
}
line.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx(workspace: &str, sysroot: &str) -> NormalizationContext {
NormalizationContext {
workspace_root: PathBuf::from(workspace),
sysroot: PathBuf::from(sysroot),
cargo_registry: Some(PathBuf::from("/home/u/.cargo/registry")),
compat_short_cargo: false,
extra_substitutions: Vec::new(),
strip_lines: Vec::new(),
strip_line_prefixes: Vec::new(),
}
}
fn assert_normalizes(input: &str, expected: &str) {
let c = ctx("/p", "/r");
let dir = PathBuf::from("/p/x");
let out = normalize(input, &c, &dir);
assert_eq!(out, expected);
}
#[test]
fn rewrites_dir_prefix_then_workspace_prefix() {
let input = " --> /p/tests/lihaaf/compile_fail/foo.rs:3:1\n";
let c = ctx("/p", "/home/u/.rustup/x");
let dir = PathBuf::from("/p/tests/lihaaf/compile_fail");
let out = normalize(input, &c, &dir);
assert_eq!(out, " --> $DIR/foo.rs:3:1");
}
#[test]
fn longest_prefix_wins() {
let input = " --> /p/tests/lihaaf/compile_fail/foo.rs:3:1\n ::: /p/src/lib.rs:1:1\n";
let c = ctx("/p", "/home/u/.rustup/x");
let dir = PathBuf::from("/p/tests/lihaaf/compile_fail");
let out = normalize(input, &c, &dir);
let expected = " --> $DIR/foo.rs:3:1\n ::: $WORKSPACE/src/lib.rs:1:1";
assert_eq!(out, expected);
}
#[test]
fn rewrites_sysroot_prefix() {
let input = " ::: /home/u/.rustup/x/lib/core/src/option.rs:1:1\n";
let c = ctx("/p", "/home/u/.rustup/x");
let dir = PathBuf::from("/p/tests/lihaaf/compile_fail");
let out = normalize(input, &c, &dir);
assert_eq!(out, " ::: $RUST/lib/core/src/option.rs:1:1");
}
#[test]
fn type_id_rewrite_replaces_hash_digits() {
assert_normalizes(
"expected `Foo#0`, found `Bar#42`\n",
"expected `Foo$TYPEID`, found `Bar$TYPEID`",
);
}
#[test]
fn type_id_does_not_touch_hash_without_digits() {
assert_normalizes(
"see issue #[123] (a TODO comment)\n",
"see issue #[123] (a TODO comment)",
);
}
#[test]
fn collapses_blank_line_runs() {
assert_normalizes("alpha\n\n\n\nomega\n", "alpha\n\nomega");
}
#[test]
fn strips_trailing_whitespace() {
assert_normalizes("alpha \nbeta\t\t\n", "alpha\nbeta");
}
#[test]
fn unifies_crlf_and_lone_cr_to_lf() {
assert_normalizes("a\r\nb\rc\nd\n", "a\nb\nc\nd");
}
#[test]
fn does_not_touch_diagnostic_text() {
assert_normalizes(
"error: unknown on_delete value `bogus`; expected one of: cascade\n",
"error: unknown on_delete value `bogus`; expected one of: cascade",
);
}
#[test]
fn preserves_rustc_aborting_summary() {
assert_normalizes(
"error: bad\nerror: aborting due to 1 previous error\n",
"error: bad\nerror: aborting due to 1 previous error",
);
}
#[test]
fn preserves_rustc_aborting_plural() {
assert_normalizes(
"error: a\nerror: b\nerror: aborting due to 42 previous errors\n",
"error: a\nerror: b\nerror: aborting due to 42 previous errors",
);
}
#[test]
fn preserves_unrelated_aborting_text() {
assert_normalizes(
"error: aborting due to user request\n",
"error: aborting due to user request",
);
}
#[test]
fn preserves_rustc_explain_pointer() {
assert_normalizes(
"error: bad\n\nFor more information about this error, try `rustc --explain E0463`.\n",
"error: bad\n\nFor more information about this error, try `rustc --explain E0463`.",
);
}
#[test]
fn determinism_same_inputs_produce_same_bytes() {
let input = "\
--> /p/tests/lihaaf/compile_fail/foo.rs:3:1
/nix/store/abc123-rust-1.95.0/lib/rustlib/x.rs:1:1
/build/sandbox/internal/wrappers/cc-wrapper-1.0
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
$WORKSPACE/.cargo-cache/dropped
";
let mut c = ctx("/p", "/r");
c.extra_substitutions = vec![Substitution {
from: "/nix/store/abc123-rust-1.95.0/lib/rustlib".into(),
to: "$RUST/lib/rustlib".into(),
}];
c.strip_lines = vec![
"/build/sandbox/internal/wrappers/cc-wrapper-1.0".into(),
"error: aborting due to 1 previous error".into(),
];
c.strip_line_prefixes = vec![
"$WORKSPACE/.cargo-cache/".into(),
"For more information about this error".into(),
];
let dir = PathBuf::from("/p/tests/lihaaf/compile_fail");
let a = normalize(input, &c, &dir);
let b = normalize(input, &c, &dir);
assert_eq!(a, b);
}
#[test]
fn long_type_note_two_sessions_normalize_to_same_bytes() {
let session_a = " = note: the full name for the type has been written to '/tmp/phase85-orchestration/lihaaf-djogi-validation/target/lihaaf-session-NqO1Du/tests_lihaaf_compile_fail_sealed_into_distinct_columns.rs/sealed_into_distinct_columns.long-type-13784649802967031202.txt'\n";
let session_b = " = note: the full name for the type has been written to '/tmp/phase85-targets/djogi-lihaaf/lihaaf-session-b8ldWS/tests_lihaaf_compile_fail_sealed_into_distinct_columns.rs/sealed_into_distinct_columns.long-type-3815226114102655174.txt'\n";
let c = ctx("/p", "/r");
let dir = PathBuf::from("/p/tests/lihaaf/compile_fail");
let out_a = normalize(session_a, &c, &dir);
let out_b = normalize(session_b, &c, &dir);
assert_eq!(
out_a, out_b,
"two sessions' long-type notes must normalize identically:\n a = {out_a:?}\n b = {out_b:?}",
);
assert!(
out_a.contains("$LONGTYPE_FILE"),
"expected $LONGTYPE_FILE placeholder, got: {out_a:?}",
);
assert!(
!out_a.contains("lihaaf-session-"),
"session-dir suffix must be normalized away, got: {out_a:?}",
);
assert!(
!out_a.contains("13784649802967031202"),
"type-hash digits from session a must be normalized away: {out_a:?}",
);
assert!(
!out_b.contains("3815226114102655174"),
"type-hash digits from session b must be normalized away: {out_b:?}",
);
assert!(
out_a.contains("the full name for the type has been written to"),
"primary note text must be preserved, got: {out_a:?}",
);
}
#[test]
fn long_type_note_normalizes_alternative_phrasing() {
let line = " = note: the full type name has been written to '/var/folders/abc/T/lihaaf-session-xyz/foo.long-type-9999.txt'\n";
let c = ctx("/p", "/r");
let dir = PathBuf::from("/p/x");
let out = normalize(line, &c, &dir);
assert!(
out.contains("$LONGTYPE_FILE"),
"expected $LONGTYPE_FILE placeholder, got: {out:?}",
);
assert!(
!out.contains("lihaaf-session-xyz"),
"session-dir suffix must be normalized away: {out:?}",
);
assert!(
!out.contains("9999"),
"type-hash digits must be normalized away: {out:?}",
);
assert!(
out.contains("the full type name has been written to"),
"alt-phrasing note text must be preserved: {out:?}",
);
}
#[test]
fn long_type_note_preserves_surrounding_diagnostic() {
let input = "\
error[E0277]: the trait bound is not satisfied
--> /p/tests/foo.rs:1:1
|
1 | bad code here
| ^^^
= note: the full name for the type has been written to '/tmp/x/lihaaf-session-AbCdEf/foo.long-type-12345.txt'
= note: consider using `--verbose` to print the full type name to the console
error: aborting due to 1 previous error
";
let c = ctx("/p", "/r");
let dir = PathBuf::from("/p/tests");
let out = normalize(input, &c, &dir);
assert!(
out.contains("error[E0277]: the trait bound is not satisfied"),
"primary error code+message must be preserved: {out:?}",
);
assert!(
out.contains("consider using `--verbose`"),
"secondary `--verbose` hint must be preserved: {out:?}",
);
assert!(
out.contains("error: aborting due to 1 previous error"),
"rustc summary line must be preserved: {out:?}",
);
assert!(
out.contains("$LONGTYPE_FILE"),
"long-type path must be normalized to placeholder: {out:?}",
);
assert!(
!out.contains("lihaaf-session-AbCdEf"),
"volatile session dir must be normalized away: {out:?}",
);
assert!(
!out.contains("long-type-12345"),
"type-hash digits must be normalized away: {out:?}",
);
}
#[test]
fn long_type_note_left_intact_when_no_match() {
assert_normalizes(
" = note: consider using `--verbose` to print the full type name to the console\n",
" = note: consider using `--verbose` to print the full type name to the console",
);
}
fn ctx_compat(workspace: &str, sysroot: &str) -> NormalizationContext {
NormalizationContext {
workspace_root: PathBuf::from(workspace),
sysroot: PathBuf::from(sysroot),
cargo_registry: Some(PathBuf::from("/home/u/.cargo/registry")),
compat_short_cargo: true,
extra_substitutions: Vec::new(),
strip_lines: Vec::new(),
strip_line_prefixes: Vec::new(),
}
}
fn ctx_non_compat_no_collision(workspace: &str, sysroot: &str) -> NormalizationContext {
NormalizationContext {
workspace_root: PathBuf::from(workspace),
sysroot: PathBuf::from(sysroot),
cargo_registry: Some(PathBuf::from("/home/u/.cargo/registry")),
compat_short_cargo: false,
extra_substitutions: Vec::new(),
strip_lines: Vec::new(),
strip_line_prefixes: Vec::new(),
}
}
#[test]
fn compat_a_index_crates_io_rewrites_to_short_form() {
let input = " --> /home/u/.cargo/registry/src/index.crates.io-1234567890abcdef/foo-1.0.0/src/lib.rs:3:1\n";
let c = ctx_compat("/p", "/sysroot");
let dir = PathBuf::from("/p/x");
let out = normalize(input, &c, &dir);
assert_eq!(out, " --> $CARGO/foo-1.0.0/src/lib.rs:3:1");
}
#[test]
fn compat_b_github_com_handled_identically() {
let input = " --> /home/u/.cargo/registry/src/github.com-1234567890abcdef/foo-1.0.0/src/lib.rs:3:1\n";
let c = ctx_compat("/p", "/sysroot");
let dir = PathBuf::from("/p/x");
let out = normalize(input, &c, &dir);
assert_eq!(out, " --> $CARGO/foo-1.0.0/src/lib.rs:3:1");
}
#[test]
fn compat_c_line_without_registry_segment_unchanged() {
let input = " --> /p/tests/foo.rs:3:1\n";
let c = ctx_compat("/p", "/sysroot");
let dir = PathBuf::from("/p/tests");
let out = normalize(input, &c, &dir);
assert_eq!(out, " --> $DIR/foo.rs:3:1");
}
#[test]
fn compat_d_flag_off_byte_identical_to_v0_1() {
let input = " --> /home/u/.cargo/registry/src/index.crates.io-1234567890abcdef/foo-1.0.0/src/lib.rs:3:1\n";
let c = ctx_non_compat_no_collision("/p", "/sysroot");
let dir = PathBuf::from("/p/x");
let out = normalize(input, &c, &dir);
assert_eq!(
out,
" --> $CARGO/registry/src/index.crates.io-1234567890abcdef/foo-1.0.0/src/lib.rs:3:1"
);
}
fn ctx_with_extras(
extras: Vec<Substitution>,
strip_lines: Vec<String>,
strip_line_prefixes: Vec<String>,
) -> NormalizationContext {
NormalizationContext {
workspace_root: PathBuf::from("/lihaaf_test_ws_root"),
sysroot: PathBuf::from("/lihaaf_test_sysroot"),
cargo_registry: Some(PathBuf::from("/lihaaf_test_cargo/registry")),
compat_short_cargo: false,
extra_substitutions: extras,
strip_lines,
strip_line_prefixes,
}
}
fn test_fixture_dir() -> PathBuf {
PathBuf::from("/lihaaf_test_fixture_dir")
}
#[test]
fn extra_substitutions_apply_after_builtins() {
let input = " ::: /nix/store/abc123-rust-1.95.0/lib/rustlib/std/src/option.rs:1:1\n";
let extras = vec![Substitution {
from: "/nix/store/abc123-rust-1.95.0/lib/rustlib".into(),
to: "$RUST/lib/rustlib".into(),
}];
let c = ctx_with_extras(extras, vec![], vec![]);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, " ::: $RUST/lib/rustlib/std/src/option.rs:1:1");
}
#[test]
fn extra_substitutions_apply_in_declared_order() {
let input = " ::: /opt/vendored/rust-1.95.0/lib/rust-1.95.0/std/src/option.rs:1:1\n";
let extras = vec![
Substitution {
from: "/opt/vendored/rust-1.95.0/lib".into(),
to: "$RUST/lib".into(),
},
Substitution {
from: "$RUST/lib/rust-1.95.0".into(),
to: "$RUST".into(),
},
];
let c = ctx_with_extras(extras, vec![], vec![]);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, " ::: $RUST/std/src/option.rs:1:1");
}
#[test]
fn extra_substitutions_empty_default_byte_identical() {
let input = " --> /p/tests/lihaaf/compile_fail/foo.rs:3:1\n";
let c = ctx("/p", "/r");
let dir = PathBuf::from("/p/tests/lihaaf/compile_fail");
let out = normalize(input, &c, &dir);
assert_eq!(out, " --> $DIR/foo.rs:3:1");
}
#[test]
fn strip_lines_drops_full_line_match() {
let input = "alpha\n/build/sandbox/internal/wrappers/cc-wrapper-1.0\nomega\n";
let c = ctx_with_extras(
vec![],
vec!["/build/sandbox/internal/wrappers/cc-wrapper-1.0".into()],
vec![],
);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, "alpha\nomega");
}
#[test]
fn strip_lines_drops_banner_line_match() {
let input = "error: bad\nerror: aborting due to 1 previous error\n";
let c = ctx_with_extras(
vec![],
vec!["error: aborting due to 1 previous error".into()],
vec![],
);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, "error: bad");
}
#[test]
fn strip_lines_no_partial_match() {
let input = "/build/sandbox/internal/wrappers/cc-wrapper-1.0 plus more\nbeta\n";
let c = ctx_with_extras(
vec![],
vec!["/build/sandbox/internal/wrappers/cc-wrapper-1.0".into()],
vec![],
);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(
out,
"/build/sandbox/internal/wrappers/cc-wrapper-1.0 plus more\nbeta",
);
}
#[test]
fn strip_line_prefixes_matches_prefix_only() {
let input =
"$WORKSPACE/.cargo-cache/aaa-001\n$WORKSPACE/.cargo-cache/bbb-002\nother line\n";
let c = ctx_with_extras(vec![], vec![], vec!["$WORKSPACE/.cargo-cache/".into()]);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, "other line");
}
#[test]
fn strip_line_prefixes_drops_explain_footer_family() {
let input = "error: bad\n\nFor more information about this error, try `rustc --explain E0277`.\nFor more information about this error, try `rustc --explain E0463`.\n";
let c = ctx_with_extras(
vec![],
vec![],
vec!["For more information about this error".into()],
);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, "error: bad");
}
#[test]
fn strip_line_prefixes_drops_macro_origin_trailer_family() {
let input = "error: bad\nnote: this error originates from the macro `m` in the crate `c`\nnote: this error originates from the attribute macro `derive_more::Display`\nfinal\n";
let c = ctx_with_extras(
vec![],
vec![],
vec!["note: this error originates from ".into()],
);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, "error: bad\nfinal");
}
#[test]
fn strip_patterns_apply_after_trim_trailing_whitespace() {
let input = "alpha\n/build/sandbox/internal/wrappers/cc-wrapper-1.0 \t\nomega\n";
let c = ctx_with_extras(
vec![],
vec!["/build/sandbox/internal/wrappers/cc-wrapper-1.0".into()],
vec![],
);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, "alpha\nomega");
}
#[test]
fn strip_patterns_do_not_affect_diagnostic_text() {
let input = "error: unknown on_delete value `bogus`; expected one of: cascade\n";
let c = ctx_with_extras(vec![], vec![], vec!["$WORKSPACE/.cargo-cache/".into()]);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(
out,
"error: unknown on_delete value `bogus`; expected one of: cascade",
);
}
#[test]
fn extra_substitutions_run_before_type_id_collapse() {
let input = " ::: /vendored/cache/path:1:1\n";
let extras = vec![Substitution {
from: "/vendored/cache/path".into(),
to: "/x/#0/y".into(),
}];
let c = ctx_with_extras(extras, vec![], vec![]);
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, " ::: /x/$TYPEID/y:1:1");
}
#[test]
fn compose_with_compat_short_cargo() {
let input = " --> /home/u/.cargo/registry/src/index.crates.io-1234567890abcdef/foo-1.0.0/src/lib.rs:3:1\n";
let extras = vec![Substitution {
from: "$CARGO/foo-1.0.0".into(),
to: "$CARGO/foo".into(),
}];
let c = NormalizationContext {
workspace_root: PathBuf::from("/lihaaf_test_ws_root"),
sysroot: PathBuf::from("/lihaaf_test_sysroot"),
cargo_registry: Some(PathBuf::from("/home/u/.cargo/registry")),
compat_short_cargo: true,
extra_substitutions: extras,
strip_lines: vec![],
strip_line_prefixes: vec![],
};
let out = normalize(input, &c, &test_fixture_dir());
assert_eq!(out, " --> $CARGO/foo/src/lib.rs:3:1");
}
#[test]
fn extra_substitutions_no_newline_in_to_debug_assertion() {
let input = " ::: /a/b/c:1:1\n";
let extras = vec![Substitution {
from: "/a/b/c".into(),
to: "$WORKSPACE/inserted\nbad".into(),
}];
let c = ctx_with_extras(extras, vec![], vec![]);
let out = normalize(input, &c, &test_fixture_dir());
assert!(out.contains("$WORKSPACE/inserted"));
assert!(out.contains("bad:1:1"));
}
}