use std::path::Path;
use crate::core::NormalizedPath;
use crate::depgraph::args::{ParsedArgs, UserDepFlags};
use crate::depgraph::search_paths::IncludeSearchPaths;
use super::super::{
compute_artifact_key, compute_artifact_key_normalized_inplace,
compute_artifact_key_normalized_with_root, compute_artifact_key_with, compute_context_key,
CompileContext,
};
use super::make_context;
#[test]
fn context_key_deterministic() {
let ctx = make_context("/src/foo.c", &["/inc"], &["DEBUG"]);
let k1 = ctx.context_key();
let k2 = ctx.context_key();
assert_eq!(k1, k2);
}
#[test]
fn different_source_different_key() {
let k1 = make_context("/src/a.c", &["/inc"], &[]).context_key();
let k2 = make_context("/src/b.c", &["/inc"], &[]).context_key();
assert_ne!(k1, k2);
}
#[test]
fn different_defines_different_key() {
let k1 = make_context("/src/a.c", &["/inc"], &["DEBUG"]).context_key();
let k2 = make_context("/src/a.c", &["/inc"], &["RELEASE"]).context_key();
assert_ne!(k1, k2);
}
#[test]
fn define_order_irrelevant() {
let k1 = make_context("/src/a.c", &[], &["AAA", "BBB"]).context_key();
let k2 = make_context("/src/a.c", &[], &["BBB", "AAA"]).context_key();
assert_eq!(k1, k2, "define order should not affect context key");
}
#[test]
fn include_dir_order_matters() {
let k1 = make_context("/src/a.c", &["/first", "/second"], &[]).context_key();
let k2 = make_context("/src/a.c", &["/second", "/first"], &[]).context_key();
assert_ne!(k1, k2, "include dir order should affect context key");
}
#[cfg(windows)]
#[test]
fn windows_context_key_normalizes_equivalent_path_spellings() {
let ctx1 = CompileContext {
source_file: NormalizedPath::from(r"C:\work\src\main.cpp"),
include_search: IncludeSearchPaths {
user: vec![NormalizedPath::from(r"C:\work\include")],
..Default::default()
},
defines: Vec::new(),
flags: Vec::new(),
force_includes: vec![NormalizedPath::from(r"C:\work\pch\base.h")],
unknown_flags: Vec::new(),
};
let ctx2 = CompileContext {
source_file: NormalizedPath::from("c:/work/src/main.cpp"),
include_search: IncludeSearchPaths {
user: vec![NormalizedPath::from("c:/work/include")],
..Default::default()
},
defines: Vec::new(),
flags: Vec::new(),
force_includes: vec![NormalizedPath::from("c:/work/pch/base.h")],
unknown_flags: Vec::new(),
};
assert_eq!(ctx1.context_key(), ctx2.context_key());
}
#[cfg(windows)]
#[test]
fn windows_artifact_key_normalizes_equivalent_path_spellings() {
let ctx = CompileContext {
source_file: NormalizedPath::from(r"C:\work\src\main.cpp"),
include_search: IncludeSearchPaths::default(),
defines: Vec::new(),
flags: Vec::new(),
force_includes: Vec::new(),
unknown_flags: Vec::new(),
};
let key = ctx.context_key();
let mut file_hashes_a = vec![
(
NormalizedPath::from(r"C:\work\include\foo.h"),
crate::hash::hash_bytes(b"header"),
),
(
NormalizedPath::from(r"C:\work\src\main.cpp"),
crate::hash::hash_bytes(b"source"),
),
];
let mut file_hashes_b = vec![
(
NormalizedPath::from("c:/work/include/foo.h"),
crate::hash::hash_bytes(b"header"),
),
(
NormalizedPath::from("c:/work/src/main.cpp"),
crate::hash::hash_bytes(b"source"),
),
];
assert_eq!(
compute_artifact_key(&key, &mut file_hashes_a, None),
compute_artifact_key(&key, &mut file_hashes_b, None)
);
}
#[test]
fn artifact_key_changes_with_content() {
let ctx = make_context("/src/a.c", &[], &[]);
let ck = ctx.context_key();
let hash_a = crate::hash::hash_bytes(b"content A");
let hash_b = crate::hash::hash_bytes(b"content B");
let ak1 = compute_artifact_key(&ck, &mut [(NormalizedPath::from("/src/a.c"), hash_a)], None);
let ak2 = compute_artifact_key(&ck, &mut [(NormalizedPath::from("/src/a.c"), hash_b)], None);
assert_ne!(ak1, ak2);
}
#[test]
fn artifact_key_stable_same_content() {
let ctx = make_context("/src/a.c", &[], &[]);
let ck = ctx.context_key();
let hash = crate::hash::hash_bytes(b"content");
let ak1 = compute_artifact_key(&ck, &mut [(NormalizedPath::from("/src/a.c"), hash)], None);
let ak2 = compute_artifact_key(&ck, &mut [(NormalizedPath::from("/src/a.c"), hash)], None);
assert_eq!(ak1, ak2);
}
#[test]
fn artifact_key_file_order_irrelevant() {
let ctx = make_context("/src/a.c", &[], &[]);
let ck = ctx.context_key();
let h1 = crate::hash::hash_bytes(b"content 1");
let h2 = crate::hash::hash_bytes(b"content 2");
let ak1 = compute_artifact_key(
&ck,
&mut [
(NormalizedPath::from("/a.h"), h1),
(NormalizedPath::from("/b.h"), h2),
],
None,
);
let ak2 = compute_artifact_key(
&ck,
&mut [
(NormalizedPath::from("/b.h"), h2),
(NormalizedPath::from("/a.h"), h1),
],
None,
);
assert_eq!(ak1, ak2, "file order should not affect artifact key");
}
#[test]
fn context_key_ignores_workspace_root_when_key_root_is_stable() {
let ctx_a = make_context(
"/workspace-a/src/main.cpp",
&["/workspace-a/include"],
&["DEBUG"],
);
let ctx_b = make_context(
"/workspace-b/src/main.cpp",
&["/workspace-b/include"],
&["DEBUG"],
);
let key_a = compute_context_key(&ctx_a, Some(Path::new("/workspace-a")), None);
let key_b = compute_context_key(&ctx_b, Some(Path::new("/workspace-b")), None);
assert_eq!(key_a, key_b);
}
#[test]
fn cxx_context_key_with_root_normalizes_file_prefix_map_roots() {
let mut ctx_a = make_context("/workspace-a/src/main.cpp", &[], &[]);
ctx_a.flags = vec!["-ffile-prefix-map=/workspace-a=.".to_string()];
let mut ctx_b = make_context("/workspace-b/src/main.cpp", &[], &[]);
ctx_b.flags = vec!["-ffile-prefix-map=/workspace-b=.".to_string()];
assert_eq!(
compute_context_key(&ctx_a, Some(Path::new("/workspace-a")), None),
compute_context_key(&ctx_b, Some(Path::new("/workspace-b")), None),
"equivalent file-prefix-map old prefixes under the key root should match"
);
}
#[test]
fn cxx_context_key_with_root_preserves_file_prefix_map_new_prefixes() {
let mut ctx_a = make_context("/workspace-a/src/main.cpp", &[], &[]);
ctx_a.flags = vec!["-ffile-prefix-map=/workspace-a=.".to_string()];
let mut ctx_b = make_context("/workspace-b/src/main.cpp", &[], &[]);
ctx_b.flags = vec!["-ffile-prefix-map=/workspace-b=/src".to_string()];
assert_ne!(
compute_context_key(&ctx_a, Some(Path::new("/workspace-a")), None),
compute_context_key(&ctx_b, Some(Path::new("/workspace-b")), None),
"different file-prefix-map new prefixes should remain key-significant"
);
}
#[test]
fn cxx_context_key_with_root_keeps_external_file_prefix_map_old_prefixes_distinct() {
let mut ctx_a = make_context("/workspace-a/src/main.cpp", &[], &[]);
ctx_a.flags = vec!["-ffile-prefix-map=/external-a=.".to_string()];
let mut ctx_b = make_context("/workspace-b/src/main.cpp", &[], &[]);
ctx_b.flags = vec!["-ffile-prefix-map=/external-b=.".to_string()];
assert_ne!(
compute_context_key(&ctx_a, Some(Path::new("/workspace-a")), None),
compute_context_key(&ctx_b, Some(Path::new("/workspace-b")), None),
"file-prefix-map old prefixes outside the key root should keep absolute identity"
);
}
#[test]
fn cxx_context_key_with_root_normalizes_prefix_maps_in_unknown_flags() {
let mut ctx_a = make_context("/workspace-a/src/main.cpp", &[], &[]);
ctx_a.unknown_flags = vec![
"-fcoverage-prefix-map=/workspace-a=/coverage".to_string(),
"-fdebug-prefix-map=/workspace-a=/debug".to_string(),
"-fmacro-prefix-map=/workspace-a=/macro".to_string(),
"-fprofile-prefix-map=/workspace-a=/profile".to_string(),
];
let mut ctx_b = make_context("/workspace-b/src/main.cpp", &[], &[]);
ctx_b.unknown_flags = vec![
"-fcoverage-prefix-map=/workspace-b=/coverage".to_string(),
"-fdebug-prefix-map=/workspace-b=/debug".to_string(),
"-fmacro-prefix-map=/workspace-b=/macro".to_string(),
"-fprofile-prefix-map=/workspace-b=/profile".to_string(),
];
assert_eq!(
compute_context_key(&ctx_a, Some(Path::new("/workspace-a")), None),
compute_context_key(&ctx_b, Some(Path::new("/workspace-b")), None),
"C/C++ prefix-map flags should normalize under unknown_flags"
);
}
#[test]
fn artifact_key_ignores_workspace_root_when_key_root_is_stable() {
let ctx = make_context("/workspace-a/src/main.cpp", &["/workspace-a/include"], &[]);
let key = compute_context_key(&ctx, Some(Path::new("/workspace-a")), None);
let mut hashes_a = vec![
(
NormalizedPath::from("/workspace-a/include/foo.h"),
crate::hash::hash_bytes(b"header"),
),
(
NormalizedPath::from("/workspace-a/src/main.cpp"),
crate::hash::hash_bytes(b"source"),
),
];
let mut hashes_b = vec![
(
NormalizedPath::from("/workspace-b/include/foo.h"),
crate::hash::hash_bytes(b"header"),
),
(
NormalizedPath::from("/workspace-b/src/main.cpp"),
crate::hash::hash_bytes(b"source"),
),
];
assert_eq!(
compute_artifact_key(&key, &mut hashes_a, Some(Path::new("/workspace-a"))),
compute_artifact_key(&key, &mut hashes_b, Some(Path::new("/workspace-b")))
);
}
#[test]
fn context_key_display() {
let ctx = make_context("/src/a.c", &[], &[]);
let key = ctx.context_key();
let display = format!("{key}");
assert!(display.starts_with("ctx:"));
assert_eq!(display.len(), 4 + 64); }
#[test]
fn from_parsed_args_sorts() {
let args = ParsedArgs {
source_file: NormalizedPath::from("/src/a.c"),
output_file: None,
include_search: IncludeSearchPaths::default(),
defines: vec!["ZZZ".into(), "AAA".into()],
undefines: Vec::new(),
flags: vec!["-Wall".into(), "-O2".into()],
force_includes: Vec::new(),
compiler: None,
dep_flags: UserDepFlags::default(),
unknown_flags: vec!["--zzz".into(), "--aaa".into()],
};
let ctx = CompileContext::from_parsed_args(args);
assert_eq!(ctx.defines, vec!["AAA", "ZZZ"]);
assert_eq!(ctx.flags, vec!["-O2", "-Wall"]);
assert_eq!(ctx.unknown_flags, vec!["--aaa", "--zzz"]);
}
#[test]
fn different_flags_different_key() {
let mut ctx1 = make_context("/src/a.c", &[], &[]);
ctx1.flags = vec!["-std=c++17".into()];
let mut ctx2 = make_context("/src/a.c", &[], &[]);
ctx2.flags = vec!["-std=c++20".into()];
assert_ne!(ctx1.context_key(), ctx2.context_key());
}
#[test]
fn force_include_affects_key() {
let ctx1 = make_context("/src/a.c", &[], &[]);
let mut ctx2 = make_context("/src/a.c", &[], &[]);
ctx2.force_includes = vec![NormalizedPath::from("/pch.h")];
assert_ne!(ctx1.context_key(), ctx2.context_key());
}
#[test]
fn unknown_flags_affect_key() {
let ctx1 = make_context("/src/a.c", &[], &[]);
let mut ctx2 = make_context("/src/a.c", &[], &[]);
ctx2.unknown_flags = vec!["--deploy-dependencies".into()];
assert_ne!(
ctx1.context_key(),
ctx2.context_key(),
"unknown flags should affect context key"
);
}
#[test]
fn unknown_flags_order_irrelevant() {
let mut ctx1 = make_context("/src/a.c", &[], &[]);
ctx1.unknown_flags = vec!["--aaa".into(), "--bbb".into()];
let mut ctx2 = make_context("/src/a.c", &[], &[]);
ctx2.unknown_flags = vec!["--bbb".into(), "--aaa".into()];
ctx1.unknown_flags.sort();
ctx2.unknown_flags.sort();
assert_eq!(
ctx1.context_key(),
ctx2.context_key(),
"unknown flag order should not affect context key"
);
}
#[test]
fn compute_artifact_key_with_does_not_call_p_cmp() {
use std::cell::Cell;
use std::cmp::Ordering;
use std::sync::Arc;
#[derive(Eq, PartialEq)]
struct CountingPath {
inner: NormalizedPath,
cmp_calls: &'static Cell<usize>,
}
impl PartialOrd for CountingPath {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CountingPath {
fn cmp(&self, other: &Self) -> Ordering {
self.cmp_calls.set(self.cmp_calls.get() + 1);
self.inner.cmp(&other.inner)
}
}
impl AsRef<std::path::Path> for CountingPath {
fn as_ref(&self) -> &std::path::Path {
self.inner.as_path()
}
}
thread_local! {
static CALLS: Cell<usize> = const { Cell::new(0) };
}
let cmp_calls: &'static Cell<usize> = Box::leak(Box::new(Cell::new(0)));
let ctx = make_context("/src/main.cpp", &[], &[]);
let ck = ctx.context_key();
let mut file_hashes: Vec<(CountingPath, crate::hash::ContentHash)> = (0..16)
.map(|i| {
(
CountingPath {
inner: NormalizedPath::from(format!("/inc/h{i:02}.h")),
cmp_calls,
},
crate::hash::hash_bytes(format!("header-{i}").as_bytes()),
)
})
.collect();
let _key = compute_artifact_key_with(&ck, &mut file_hashes, None, |path, _| {
Arc::<str>::from(path.to_string_lossy().into_owned())
});
let count = cmp_calls.get();
assert_eq!(
count, 0,
"issue #571: sort must NOT call P::cmp (which on NormalizedPath \
invokes normalize_for_key twice and bypasses the #553 cache). \
Observed {count} comparisons — the sort regressed to the prior \
O(n log n)-normalize shape.",
);
let _ = CALLS.with(|c| c.get()); }
#[test]
fn compute_artifact_key_with_byte_identical_to_prior_shape() {
let ctx = make_context("/src/main.cpp", &["/inc"], &["DEBUG"]);
let ck = ctx.context_key();
let mut file_hashes: Vec<(NormalizedPath, crate::hash::ContentHash)> = vec![
(
NormalizedPath::from("/inc/zlast.h"),
crate::hash::hash_bytes(b"zlast content"),
),
(
NormalizedPath::from("/inc/amid.h"),
crate::hash::hash_bytes(b"amid content"),
),
(
NormalizedPath::from("/inc/mfirst.h"),
crate::hash::hash_bytes(b"mfirst content"),
),
(
NormalizedPath::from("/src/main.cpp"),
crate::hash::hash_bytes(b"source"),
),
];
let key1 = compute_artifact_key(&ck, &mut file_hashes, None);
file_hashes.reverse();
let key2 = compute_artifact_key(&ck, &mut file_hashes, None);
assert_eq!(
key1, key2,
"input order must not perturb the artifact key (sort is the determinizer)"
);
}
#[test]
fn compute_artifact_key_normalized_inplace_matches_closure_path() {
let ctx = make_context("/src/main.cpp", &["/inc"], &["DEBUG"]);
let ck = ctx.context_key();
let inputs: Vec<(NormalizedPath, crate::hash::ContentHash)> = vec![
(
NormalizedPath::from("/inc/zlast.h"),
crate::hash::hash_bytes(b"zlast content"),
),
(
NormalizedPath::from("/inc/amid.h"),
crate::hash::hash_bytes(b"amid content"),
),
(
NormalizedPath::from("/inc/mfirst.h"),
crate::hash::hash_bytes(b"mfirst content"),
),
(
NormalizedPath::from("/src/main.cpp"),
crate::hash::hash_bytes(b"source"),
),
];
let mut slow_inputs = inputs.clone();
let slow_key = compute_artifact_key(&ck, &mut slow_inputs, None);
let mut fast_inputs = inputs;
let fast_key = compute_artifact_key_normalized_inplace(&ck, &mut fast_inputs);
assert_eq!(
slow_key, fast_key,
"fast-path and slow-path must produce bit-identical ArtifactKey \
— any divergence invalidates every cache entry written by the \
cc/cpp pipeline post-#585",
);
}
#[test]
fn compute_artifact_key_normalized_with_root_matches_closure() {
let ctx = make_context("/proj/src/main.cpp", &["/inc"], &[]);
let ck = ctx.context_key();
let inputs: Vec<(NormalizedPath, crate::hash::ContentHash)> = vec![
(
NormalizedPath::from("/usr/include/c++/13/iostream"),
crate::hash::hash_bytes(b"iostream-content"),
),
(
NormalizedPath::from("/proj/src/main.cpp"),
crate::hash::hash_bytes(b"main-content"),
),
(
NormalizedPath::from("/proj/include/local.h"),
crate::hash::hash_bytes(b"local-content"),
),
(
NormalizedPath::from("/usr/include/stdio.h"),
crate::hash::hash_bytes(b"stdio-content"),
),
];
let mut slow1 = inputs.clone();
let slow_key_none = compute_artifact_key(&ck, &mut slow1, None);
let fast_key_none = compute_artifact_key_normalized_with_root(&ck, &inputs, None);
assert_eq!(
slow_key_none, fast_key_none,
"key_root: None — fast-path must match closure path",
);
let mut slow2 = inputs.clone();
let slow_key_root = compute_artifact_key(&ck, &mut slow2, Some(std::path::Path::new("/proj")));
let fast_key_root = compute_artifact_key_normalized_with_root(
&ck,
&inputs,
Some(std::path::Path::new("/proj")),
);
assert_eq!(
slow_key_root, fast_key_root,
"key_root: Some — fast-path must match closure path for mixed \
project-local + system-header inputs",
);
}