use super::*;
use std::sync::Arc;
fn parse(source: &str) -> FileAnalysis {
let mut parser = crate::builder::create_parser();
let tree = parser.parse(source, None).unwrap();
crate::builder::build(&tree, source.as_bytes())
}
fn cache(idx: &crate::module_index::ModuleIndex, name: &str, src: &str) {
idx.insert_cache(
name,
Some(Arc::new(crate::file_analysis::CachedModule::new(
std::path::PathBuf::from(format!("/fake/g/{}.pm", name.replace("::", "/"))),
Arc::new(parse(src)),
))),
);
}
#[test]
fn walk_inherits_preserves_isa_order_and_caps_cycles() {
let fa = parse(
"package Top;\nuse parent -norequire, 'C';\n\
package A;\nuse parent -norequire, 'Top';\n\
package B;\nuse parent -norequire, 'Top';\n\
package C;\nuse parent -norequire, 'A', 'B';\n1;\n",
);
let g = GraphView::new(&fa, None);
let mut order: Vec<String> = Vec::new();
g.walk(Node::Class("C".into()), EdgeKindMask::INHERITS, &mut |n| {
if let Node::Class(c) = n {
order.push(c.clone());
}
std::ops::ControlFlow::Continue(())
});
assert_eq!(order, vec!["A", "Top", "B"]);
}
#[test]
fn walk_descendants_matches_index_fan_out() {
let idx = crate::module_index::ModuleIndex::new_for_test();
cache(&idx, "My::Role", "package My::Role;\nuse Moo::Role;\nrequires 'fetch';\n1;\n");
cache(&idx, "My::Composer", "package My::Composer;\nuse Moo;\nwith 'My::Role';\nsub fetch {1}\n1;\n");
cache(&idx, "My::SubRole", "package My::SubRole;\nuse Moo::Role;\nwith 'My::Role';\n1;\n");
cache(&idx, "My::Deep", "package My::Deep;\nuse Moo;\nwith 'My::SubRole';\nsub fetch {7}\n1;\n");
let fa = parse("package Probe;\n1;\n");
let g = GraphView::new(&fa, Some(&idx));
let mut got: Vec<String> = Vec::new();
g.walk(Node::Class("My::Role".into()), EdgeKindMask::INHERITS_INV, &mut |n| {
if let Node::Class(c) = n {
got.push(c.clone());
}
std::ops::ControlFlow::Continue(())
});
got.sort();
let mut index_bfs: Vec<String> = Vec::new();
idx.for_each_descendant_package("My::Role", &mut |pkg: &str, _cached: &Arc<crate::file_analysis::CachedModule>| {
index_bfs.push(pkg.to_string());
std::ops::ControlFlow::Continue(())
});
index_bfs.sort();
assert_eq!(got, index_bfs, "graph fan-out must match the index BFS");
assert_eq!(got, vec!["My::Composer", "My::Deep", "My::SubRole"]);
}
#[test]
fn walk_bridges_reaches_plugin_modules_terminally() {
let idx = crate::module_index::ModuleIndex::new_for_test();
let plugin_src = "package My::Plugin::W;\nuse Mojo::Base 'Mojolicious::Plugin';\n\
sub register {\n my ($self, $app) = @_;\n $app->helper(wcount => sub {1});\n}\n1;\n";
idx.register_workspace_module(
std::path::PathBuf::from("/fake/g/W.pm"),
Arc::new(parse(plugin_src)),
);
let fa = parse("package Probe;\n1;\n");
let g = GraphView::new(&fa, Some(&idx));
let mut mods: Vec<String> = Vec::new();
g.walk(
Node::Class("Mojolicious::Controller".into()),
EdgeKindMask::BRIDGES | EdgeKindMask::INHERITS,
&mut |n| {
if let Node::Module(m) = n {
mods.push(m.clone());
}
std::ops::ControlFlow::Continue(())
},
);
assert_eq!(mods, vec!["My::Plugin::W"]);
}
#[test]
fn class_isa_agrees_with_ancestor_walk() {
let fa = parse(
"package Base;\n1;\n\
package Mid;\nuse parent -norequire, 'Base';\n1;\n\
package Leaf;\nuse parent -norequire, 'Mid';\n1;\n\
package R;\nuse Moo::Role;\n1;\n\
package Composer;\nuse Moo;\nwith 'R';\nextends 'Leaf';\n1;\n\
package Unrelated;\n1;\n",
);
let cases = [
("Leaf", "Leaf", true), ("Leaf", "Mid", true), ("Leaf", "Base", true), ("Composer", "Base", true), ("Composer", "R", true), ("Leaf", "Unrelated", false),
("Base", "Leaf", false), ];
for (child, ancestor, want) in cases {
let got = fa.class_isa(child, ancestor, None);
let mut legacy = child == ancestor;
fa.for_each_ancestor_class_test(child, None, |c| {
if c == ancestor {
legacy = true;
}
std::ops::ControlFlow::Continue(())
});
assert_eq!(got, want, "class_isa({child}, {ancestor})");
assert_eq!(got, legacy, "class_isa vs ancestor walk disagree on ({child}, {ancestor})");
}
}
#[test]
fn edge_kind_all_covers_every_mask_bit() {
let union = EdgeKind::ALL
.iter()
.fold(EdgeKindMask::empty(), |acc, k| acc | k.flag());
assert_eq!(
union.bits(),
EdgeKindMask::all().bits(),
"an EdgeKind is missing from EdgeKind::ALL",
);
assert_eq!(
EdgeKind::ALL.len(),
EdgeKind::ALL.iter().map(|k| k.flag().bits()).collect::<std::collections::HashSet<_>>().len(),
);
}
#[test]
fn ancestor_funnel_includes_self_then_mro_order() {
let fa = parse(
"package Base;\n1;\n\
package Left;\nuse parent -norequire, 'Base';\n1;\n\
package Right;\nuse parent -norequire, 'Base';\n1;\n\
package A;\nuse parent -norequire, 'Left', 'Right';\n1;\n",
);
let mut order: Vec<String> = Vec::new();
fa.for_each_ancestor_class_test("A", None, |c| {
order.push(c.to_string());
std::ops::ControlFlow::Continue(())
});
assert_eq!(order, vec!["A", "Left", "Base", "Right"]);
}