#![allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
use super::*;
fn parse(source: &str) -> PreprocParser {
PreprocParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.h"), None)
}
#[test]
fn preprocess_empty_include_does_not_panic() {
let parser = parse("#include \"\"\n");
let mut results = PreprocResults::default();
preprocess_with_parser(&parser, &PathBuf::from("test.h"), &mut results);
let pf = results
.files
.get(&PathBuf::from("test.h"))
.expect("file entry must be inserted");
assert!(pf.direct_includes.is_empty());
}
#[test]
fn preprocess_whitespace_only_include_does_not_panic() {
let parser = parse("#include \" \"\n");
let mut results = PreprocResults::default();
preprocess_with_parser(&parser, &PathBuf::from("test.h"), &mut results);
let pf = results
.files
.get(&PathBuf::from("test.h"))
.expect("file entry must be inserted");
assert!(pf.direct_includes.is_empty());
}
#[test]
fn preprocess_valid_include_is_recorded() {
let parser = parse("#include \" foo.h \"\n");
let mut results = PreprocResults::default();
preprocess_with_parser(&parser, &PathBuf::from("test.h"), &mut results);
let pf = results
.files
.get(&PathBuf::from("test.h"))
.expect("file entry must be inserted");
assert!(pf.direct_includes.contains("foo.h"));
}
#[test]
fn preprocess_define_records_macro() {
let parser = parse("#define FOO 1\n");
let mut results = PreprocResults::default();
preprocess_with_parser(&parser, &PathBuf::from("test.h"), &mut results);
let pf = results
.files
.get(&PathBuf::from("test.h"))
.expect("file entry must be inserted");
assert!(pf.macros.contains("FOO"));
}
fn macros_of(source: &str) -> HashSet<String> {
let parser = parse(source);
let mut results = PreprocResults::default();
preprocess_with_parser(&parser, &PathBuf::from("test.h"), &mut results);
results
.files
.get(&PathBuf::from("test.h"))
.expect("file entry must be inserted")
.macros
.clone()
}
#[test]
fn preprocess_undef_removes_defined_macro() {
let macros = macros_of("#define FOO 1\n#undef FOO\n");
assert!(
!macros.contains("FOO"),
"#undef FOO must un-define FOO; got {macros:?}"
);
}
#[test]
fn preprocess_undef_of_never_defined_is_noop() {
let macros = macros_of("#undef NEVER_DEFINED\n");
assert!(!macros.contains("NEVER_DEFINED"));
}
#[test]
fn preprocess_define_after_undef_reintroduces_in_source_order() {
let macros = macros_of("#undef FOO\n#define FOO 1\n");
assert!(
macros.contains("FOO"),
"the trailing source-order #define must win; got {macros:?}"
);
}
#[test]
fn preprocess_undef_leaves_other_macros() {
let macros = macros_of("#define FOO 1\n#define BAR 2\n#undef FOO\n");
assert!(!macros.contains("FOO"));
assert!(macros.contains("BAR"));
}
#[test]
fn preprocess_define_of_special_token_is_skipped() {
let macros = macros_of("#define size_t unsigned\n#define APP_FLAG 1\n");
assert!(
!macros.contains("size_t"),
"special token `size_t` must be filtered out; got {macros:?}"
);
assert!(
macros.contains("APP_FLAG"),
"an ordinary adjacent macro must still be recorded; got {macros:?}"
);
}
#[test]
fn ambiguous_include_resolves_to_single_deterministic_candidate() {
let includer = PathBuf::from("proj/src/main.c");
let cfg_a = PathBuf::from("proj/aaa/config.h");
let cfg_b = PathBuf::from("proj/zzz/config.h");
let mut files: HashMap<PathBuf, PreprocFile> = HashMap::new();
let mut main = PreprocFile::default();
main.direct_includes.insert("config.h".to_string());
files.insert(includer.clone(), main);
files.insert(cfg_a.clone(), PreprocFile::new_macros(&["FROM_A"]));
files.insert(cfg_b.clone(), PreprocFile::new_macros(&["FROM_B"]));
let mut all_files: HashMap<String, Vec<PathBuf>> = HashMap::new();
all_files.insert("config.h".to_string(), vec![cfg_b.clone(), cfg_a.clone()]);
all_files.insert("main.c".to_string(), vec![includer.clone()]);
let diagnostics = fix_includes(&mut files, &all_files);
assert!(
diagnostics.is_empty(),
"no diagnostics expected for a clean ambiguous resolve; got {diagnostics:?}"
);
let main = files.get(&includer).expect("main.c retained");
assert!(main.indirect_includes.contains("proj/aaa/config.h"));
assert!(!main.indirect_includes.contains("proj/zzz/config.h"));
let macros = get_macros(&includer, &files);
assert!(macros.contains("FROM_A"));
assert!(
!macros.contains("FROM_B"),
"macros from the unselected candidate must not leak; got {macros:?}"
);
}
#[test]
fn self_inclusion_is_reported_as_diagnostic() {
let self_path = PathBuf::from("a.h");
let mut files: HashMap<PathBuf, PreprocFile> = HashMap::new();
let mut a = PreprocFile::default();
a.direct_includes.insert("a.h".to_string());
files.insert(self_path.clone(), a);
let mut all_files: HashMap<String, Vec<PathBuf>> = HashMap::new();
all_files.insert("a.h".to_string(), vec![self_path.clone()]);
let diagnostics = fix_includes(&mut files, &all_files);
assert_eq!(
diagnostics,
vec![PreprocDiagnostic::SelfInclusion {
file: self_path.clone(),
}]
);
}
#[test]
fn fix_includes_handles_simple_cycle() {
let mut files: HashMap<PathBuf, PreprocFile> = HashMap::new();
let mut a = PreprocFile::default();
a.direct_includes.insert("b.h".to_string());
let mut b = PreprocFile::default();
b.direct_includes.insert("a.h".to_string());
files.insert(PathBuf::from("a.h"), a);
files.insert(PathBuf::from("b.h"), b);
let mut all_files: HashMap<String, Vec<PathBuf>> = HashMap::new();
all_files.insert("a.h".to_string(), vec![PathBuf::from("a.h")]);
all_files.insert("b.h".to_string(), vec![PathBuf::from("b.h")]);
let diagnostics = fix_includes(&mut files, &all_files);
assert_eq!(
diagnostics,
vec![PreprocDiagnostic::IncludeCycle {
members: vec!["a.h".to_string(), "b.h".to_string()],
}]
);
let a = files
.get(&PathBuf::from("a.h"))
.expect("a.h must be retained");
assert!(a.indirect_includes.contains("a.h"));
assert!(a.indirect_includes.contains("b.h"));
let b = files
.get(&PathBuf::from("b.h"))
.expect("b.h must be retained");
assert!(b.indirect_includes.contains("a.h"));
assert!(b.indirect_includes.contains("b.h"));
}
#[test]
fn ensure_node_returns_stable_index_on_repeat() {
let mut g: IncludeGraph = StableGraph::new();
let mut nodes: HashMap<PathBuf, NodeIndex> = HashMap::new();
let p = PathBuf::from("a.h");
let first = ensure_node(&mut g, &mut nodes, &p);
let second = ensure_node(&mut g, &mut nodes, &p);
assert_eq!(first, second);
assert_eq!(g.node_count(), 1);
assert_eq!(nodes.len(), 1);
}
#[test]
fn scc_external_neighbors_dedups_and_excludes_intra_component() {
let mut graph: IncludeGraph = StableGraph::new();
let member_a = graph.add_node(PathBuf::from("a.h"));
let member_b = graph.add_node(PathBuf::from("b.h"));
let pred = graph.add_node(PathBuf::from("x.h"));
let succ = graph.add_node(PathBuf::from("y.h"));
graph.add_edge(member_a, member_b, 0);
graph.add_edge(member_b, member_a, 0);
graph.add_edge(pred, member_a, 0);
graph.add_edge(pred, member_b, 0);
graph.add_edge(member_a, succ, 0);
graph.add_edge(member_b, succ, 0);
let component = vec![member_a, member_b];
let incoming = scc_external_neighbors(&graph, &component, Direction::Incoming);
let outgoing = scc_external_neighbors(&graph, &component, Direction::Outgoing);
assert_eq!(incoming, vec![pred]);
assert_eq!(outgoing, vec![succ]);
}
#[test]
fn strip_include_quotes_rejects_too_short_spans() {
let code = b"#include \"\"";
assert_eq!(strip_include_quotes(code, 9, 9), None);
assert_eq!(strip_include_quotes(code, 9, 10), None);
}
#[test]
fn strip_include_quotes_handles_valid_and_empty_payloads() {
let code = b"#include \" foo.h \"";
assert_eq!(strip_include_quotes(code, 9, code.len()), Some("foo.h"));
let code = b"#include \"\"";
assert_eq!(strip_include_quotes(code, 9, 11), None);
let code = b"#include \" \"";
assert_eq!(strip_include_quotes(code, 9, 14), None);
}
fn closures_both_ways(spec: &[(&str, &[&str])]) -> Vec<(String, HashSet<String>, HashSet<String>)> {
let mut files: HashMap<PathBuf, PreprocFile> = HashMap::new();
let mut all_files: HashMap<String, Vec<PathBuf>> = HashMap::new();
for (name, includes) in spec {
let mut pf = PreprocFile::default();
pf.direct_includes
.extend(includes.iter().map(|i| (*i).to_string()));
files.insert(PathBuf::from(name), pf);
all_files
.entry((*name).to_string())
.or_default()
.push(PathBuf::from(name));
}
let mut diagnostics = Vec::new();
let (mut g, mut nodes) = build_include_graph(&files, &all_files, &mut diagnostics);
let scc_map = collapse_scc(&mut g, &mut nodes, &mut diagnostics);
let closures =
compute_include_closures(&g, &scc_map).expect("collapse_scc leaves an acyclic graph");
let mut both: Vec<(String, HashSet<String>, HashSet<String>)> = nodes
.iter()
.map(|(path, start)| {
let mut merged = HashSet::new();
closures.materialize(*start, &mut merged, &mut Vec::new());
let mut walked = HashSet::new();
accumulate_reachable_includes(&g, *start, &scc_map, &mut walked, &mut Vec::new());
(path.display().to_string(), merged, walked)
})
.collect();
both.sort_by(|a, b| a.0.cmp(&b.0));
both
}
fn assert_closures(shape: &str, spec: &[(&str, &[&str])], expected: &[(&str, &[&str])]) {
let both = closures_both_ways(spec);
for (path, merged, walked) in &both {
assert_eq!(
merged, walked,
"{shape}: closure of {path} diverges from the walk it replaced"
);
}
let want: Vec<(String, HashSet<String>)> = expected
.iter()
.map(|(name, reachable)| {
(
(*name).to_string(),
reachable.iter().map(|r| (*r).to_string()).collect(),
)
})
.collect();
let have: Vec<(String, HashSet<String>)> = both
.into_iter()
.map(|(path, merged, _)| (path, merged))
.collect();
assert_eq!(have, want, "{shape}: unexpected closure");
}
#[test]
fn merge_sorted_ids_yields_one_sorted_copy_of_each_id() {
let mut out = Vec::new();
merge_sorted_ids(&[1, 3, 5], &[3, 4, 5, 9], &mut out);
assert_eq!(out, vec![1, 3, 4, 5, 9]);
merge_sorted_ids(&[], &[2, 7], &mut out);
assert_eq!(out, vec![2, 7]);
merge_sorted_ids(&[2, 7], &[], &mut out);
assert_eq!(out, vec![2, 7]);
merge_sorted_ids(&[], &[], &mut out);
assert!(out.is_empty());
}
#[test]
fn merge_sorted_ids_replaces_the_scratch_buffer() {
let mut out = vec![100, 200, 300];
merge_sorted_ids(&[1, 4], &[2, 4], &mut out);
assert_eq!(out, vec![1, 2, 4]);
}
#[test]
fn closure_matches_the_walk_on_a_chain() {
assert_closures(
"chain",
&[
("a.h", &["b.h"]),
("b.h", &["c.h"]),
("c.h", &["d.h"]),
("d.h", &[]),
],
&[
("a.h", &["a.h", "b.h", "c.h", "d.h"]),
("b.h", &["b.h", "c.h", "d.h"]),
("c.h", &["c.h", "d.h"]),
("d.h", &["d.h"]),
],
);
}
#[test]
fn closure_matches_the_walk_on_a_diamond() {
assert_closures(
"diamond",
&[
("a.h", &["b.h", "c.h"]),
("b.h", &["d.h"]),
("c.h", &["d.h"]),
("d.h", &[]),
],
&[
("a.h", &["a.h", "b.h", "c.h", "d.h"]),
("b.h", &["b.h", "d.h"]),
("c.h", &["c.h", "d.h"]),
("d.h", &["d.h"]),
],
);
}
#[test]
fn closure_matches_the_walk_on_a_self_cycle() {
assert_closures(
"self-cycle",
&[("a.h", &["a.h"]), ("b.h", &[])],
&[("a.h", &["a.h"]), ("b.h", &["b.h"])],
);
}
#[test]
fn closure_matches_the_walk_on_a_mutual_cycle() {
assert_closures(
"mutual cycle",
&[("a.h", &["b.h"]), ("b.h", &["a.h"])],
&[("a.h", &["a.h", "b.h"]), ("b.h", &["a.h", "b.h"])],
);
}
#[test]
fn closure_matches_the_walk_on_a_three_member_scc() {
assert_closures(
"SCC of 3",
&[
("a.h", &["b.h"]),
("b.h", &["c.h"]),
("c.h", &["a.h", "y.h"]),
("x.h", &["a.h"]),
("y.h", &[]),
],
&[
("a.h", &["a.h", "b.h", "c.h", "y.h"]),
("b.h", &["a.h", "b.h", "c.h", "y.h"]),
("c.h", &["a.h", "b.h", "c.h", "y.h"]),
("x.h", &["a.h", "b.h", "c.h", "x.h", "y.h"]),
("y.h", &["y.h"]),
],
);
}
#[test]
fn closure_matches_the_walk_on_disconnected_components() {
assert_closures(
"disconnected",
&[
("a.h", &["b.h"]),
("b.h", &[]),
("c.h", &["d.h"]),
("d.h", &[]),
],
&[
("a.h", &["a.h", "b.h"]),
("b.h", &["b.h"]),
("c.h", &["c.h", "d.h"]),
("d.h", &["d.h"]),
],
);
}
#[test]
fn closure_matches_the_walk_on_a_missing_header() {
assert_closures(
"missing header",
&[("a.h", &["nowhere.h"]), ("b.h", &[])],
&[("a.h", &["a.h"]), ("b.h", &["b.h"])],
);
}
#[test]
fn closure_matches_the_walk_on_a_deep_chain() {
const DEPTH: usize = 200;
let names: Vec<String> = (0..DEPTH).map(|i| format!("h{i:03}.h")).collect();
let includes: Vec<Vec<&str>> = (0..DEPTH)
.map(|i| {
names
.get(i + 1)
.map(|n| vec![n.as_str()])
.unwrap_or_default()
})
.collect();
let spec: Vec<(&str, &[&str])> = names
.iter()
.zip(&includes)
.map(|(name, inc)| (name.as_str(), inc.as_slice()))
.collect();
for (depth, (path, merged, walked)) in closures_both_ways(&spec).iter().enumerate() {
assert_eq!(
merged, walked,
"deep chain: {path} diverges at depth {depth}"
);
assert_eq!(
merged.len(),
DEPTH - depth,
"deep chain: {path} must reach every file below it"
);
}
}
#[test]
fn duplicate_edges_contribute_one_closure_entry() {
let mut g: IncludeGraph = StableGraph::new();
let a = g.add_node(PathBuf::from("a.h"));
let b = g.add_node(PathBuf::from("b.h"));
g.add_edge(a, b, 0);
g.add_edge(a, b, 0);
let scc_map = HashMap::new();
let closures = compute_include_closures(&g, &scc_map).expect("no cycle");
let mut merged = HashSet::new();
closures.materialize(a, &mut merged, &mut Vec::new());
let mut walked = HashSet::new();
accumulate_reachable_includes(&g, a, &scc_map, &mut walked, &mut Vec::new());
assert_eq!(
merged,
HashSet::from(["a.h".to_string(), "b.h".to_string()])
);
assert_eq!(merged, walked);
}
#[cfg(unix)]
#[test]
fn non_utf8_nodes_report_once_and_do_not_break_the_closure() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let mut g: IncludeGraph = StableGraph::new();
let a = g.add_node(PathBuf::from("a.h"));
let undecodable = g.add_node(PathBuf::from(OsStr::from_bytes(b"b\xff.h")));
let c = g.add_node(PathBuf::from("c.h"));
g.add_edge(a, undecodable, 0);
g.add_edge(undecodable, c, 0);
let scc_map = HashMap::new();
let closures = compute_include_closures(&g, &scc_map).expect("no cycle");
let (mut merged, mut merged_diagnostics) = (HashSet::new(), Vec::new());
closures.materialize(a, &mut merged, &mut merged_diagnostics);
let (mut walked, mut walked_diagnostics) = (HashSet::new(), Vec::new());
accumulate_reachable_includes(&g, a, &scc_map, &mut walked, &mut walked_diagnostics);
assert_eq!(
merged,
HashSet::from(["a.h".to_string(), "c.h".to_string()])
);
assert_eq!(merged, walked);
assert_eq!(merged_diagnostics, walked_diagnostics);
assert_eq!(merged_diagnostics.len(), 1);
}
#[test]
fn the_closure_is_computed_once_for_the_whole_graph() {
std::thread::spawn(|| {
assert_eq!(
include_graph_walks::observed(),
0,
"a fresh thread must not have walked yet"
);
let mut files: HashMap<PathBuf, PreprocFile> = HashMap::new();
let mut all_files: HashMap<String, Vec<PathBuf>> = HashMap::new();
for (name, includes) in [
("a.h", &["b.h"][..]),
("b.h", &["c.h"]),
("c.h", &["d.h"]),
("d.h", &[]),
] {
let mut pf = PreprocFile::default();
pf.direct_includes
.extend(includes.iter().map(|i| (*i).to_string()));
files.insert(PathBuf::from(name), pf);
all_files.insert(name.to_string(), vec![PathBuf::from(name)]);
}
assert!(fix_includes(&mut files, &all_files).is_empty());
assert_eq!(
files
.get(&PathBuf::from("a.h"))
.expect("a.h is retained")
.indirect_includes
.len(),
4
);
assert_eq!(
include_graph_walks::observed(),
1,
"four files must share one reverse-topological pass, \
not walk the graph once each"
);
})
.join()
.expect("closure-count thread must not panic");
}
#[test]
fn visible_macros_borrows_exactly_what_get_macros_owns() {
std::thread::spawn(|| {
let root = PathBuf::from("a.h");
let mut a = PreprocFile::new_macros(&["FROM_A"]);
a.direct_includes.insert("b.h".to_string());
let mut b = PreprocFile::new_macros(&["FROM_B"]);
b.direct_includes.insert("c.h".to_string());
let mut files: HashMap<PathBuf, PreprocFile> = HashMap::new();
files.insert(root.clone(), a);
files.insert(PathBuf::from("b.h"), b);
files.insert(PathBuf::from("c.h"), PreprocFile::new_macros(&["FROM_C"]));
let mut all_files: HashMap<String, Vec<PathBuf>> = HashMap::new();
for name in ["a.h", "b.h", "c.h"] {
all_files.insert(name.to_string(), vec![PathBuf::from(name)]);
}
assert!(fix_includes(&mut files, &all_files).is_empty());
let borrowed = visible_macros(&root, &files);
assert_eq!(
borrowed,
HashSet::from(["FROM_A", "FROM_B", "FROM_C"]),
"every transitively visible macro must be borrowed"
);
let owned_before = owned_macro_sets::observed();
let owned = get_macros(&root, &files);
assert_eq!(
owned,
borrowed.iter().map(|m| (*m).to_string()).collect(),
"the borrowing form must not change what a caller sees"
);
assert_eq!(
owned_macro_sets::observed(),
owned_before + 1,
"only the published owning form allocates a copy"
);
})
.join()
.expect("macro-view thread must not panic");
}
#[cfg(feature = "cpp")]
#[test]
fn parsing_a_cpp_file_never_owns_the_macro_set() {
std::thread::spawn(|| {
let path = PathBuf::from("foo.cpp");
let mut unit = PreprocFile::default();
unit.indirect_includes.insert("dep.h".to_string());
let files = HashMap::from([
(path.clone(), unit),
(
PathBuf::from("dep.h"),
PreprocFile::new_macros(&["DBG", "FOO"]),
),
]);
let pr = std::sync::Arc::new(PreprocResults { files });
assert_eq!(
owned_macro_sets::observed(),
0,
"a fresh thread must not have owned a set yet"
);
let space = crate::Ast::parse(
crate::Source::new(
crate::LANG::Cpp,
b"int f(int x) { return DBG ? FOO : x; }".as_slice(),
)
.with_preproc_path(Some(&path))
.with_preproc(Some(pr)),
)
.expect("cpp feature enabled")
.metrics(crate::MetricsOptions::default())
.expect("walker succeeds");
assert_eq!(space.metrics.halstead.unique_operands(), 3);
assert_eq!(
owned_macro_sets::observed(),
0,
"the parse must borrow the macro names out of the \
preprocessor results, not clone them"
);
})
.join()
.expect("parse thread must not panic");
}
#[test]
fn preprocess_truncated_include_does_not_panic() {
let parser = parse("#include \"\n");
let mut results = PreprocResults::default();
preprocess_with_parser(&parser, &PathBuf::from("test.h"), &mut results);
let pf = results
.files
.get(&PathBuf::from("test.h"))
.expect("file entry must be inserted");
assert!(pf.direct_includes.is_empty());
}
#[test]
fn preproc_diagnostic_display_renders_each_single_line_variant() {
assert_eq!(
PreprocDiagnostic::SelfInclusion {
file: PathBuf::from("inc/self ref.h"),
}
.to_string(),
"possible self inclusion inc/self ref.h",
);
assert_eq!(
PreprocDiagnostic::NonUtf8CyclePath {
path: "bad/\u{fffd}.h".to_owned(),
}
.to_string(),
"skipping non-UTF-8 path in include cycle: bad/\u{fffd}.h",
);
assert_eq!(
PreprocDiagnostic::NonUtf8IndirectInclude {
path: "bad/\u{fffd}.h".to_owned(),
}
.to_string(),
"skipping non-UTF-8 indirect include path: bad/\u{fffd}.h",
);
assert_eq!(
PreprocDiagnostic::NotPreprocessed {
file: PathBuf::from("vendor/unseen.h"),
}
.to_string(),
"included file which has not been preprocessed: vendor/unseen.h",
);
}
#[test]
fn preproc_diagnostic_display_lists_every_cycle_member() {
let rendered = PreprocDiagnostic::IncludeCycle {
members: vec!["z.h".to_owned(), "a b.h".to_owned(), "m.h".to_owned()],
}
.to_string();
assert_eq!(
rendered,
"possible include cycle:\n - \"z.h\"\n - \"a b.h\"\n - \"m.h\"",
);
let empty = PreprocDiagnostic::IncludeCycle {
members: Vec::new(),
}
.to_string();
assert_eq!(
empty, "possible include cycle:",
"an empty member list still renders the header and nothing else"
);
}
struct FailingSink {
budget: usize,
writes: usize,
}
impl std::fmt::Write for FailingSink {
fn write_str(&mut self, _s: &str) -> std::fmt::Result {
if self.budget == 0 {
return Err(std::fmt::Error);
}
self.budget -= 1;
self.writes += 1;
Ok(())
}
}
#[test]
fn preproc_diagnostic_display_propagates_every_formatter_error() {
use std::fmt::Write as _;
let cycle = PreprocDiagnostic::IncludeCycle {
members: vec!["a.h".to_owned(), "b.h".to_owned()],
};
let mut counter = FailingSink {
budget: usize::MAX,
writes: 0,
};
write!(counter, "{cycle}").expect("an unlimited sink never fails");
let total = counter.writes;
assert!(
total > 1,
"a single-write rendering could not test propagation"
);
for budget in 0..total {
let mut sink = FailingSink { budget, writes: 0 };
assert!(
write!(sink, "{cycle}").is_err(),
"a sink failing on write {budget} must surface as an error"
);
}
}