use std::collections::BTreeSet;
use pedant_graph::{
CodeGraph, GraphAnalysis, GraphAnalysisLimits, GraphCertainty, GraphDirection, GraphEdgeId,
GraphEdgeKind, GraphEdgeSelection,
};
use super::analysis_fixture as given;
const EVERY_KIND: &[GraphEdgeKind] = &[
GraphEdgeKind::Call,
GraphEdgeKind::Import,
GraphEdgeKind::Implementation,
GraphEdgeKind::Reference,
GraphEdgeKind::DependsOn,
];
const CODE_RELATION_KINDS: &[GraphEdgeKind] = &[
GraphEdgeKind::Call,
GraphEdgeKind::Import,
GraphEdgeKind::Implementation,
GraphEdgeKind::Reference,
];
const EVERY_CERTAINTY: &[GraphCertainty] = &[GraphCertainty::Resolved, GraphCertainty::Possible];
struct SelectionCase {
label: &'static str,
selection: GraphEdgeSelection,
kinds: &'static [GraphEdgeKind],
certainties: &'static [GraphCertainty],
}
pub fn assert_selection_is_explicit_and_shared() {
let graph = given::selection_graph();
let admitted = every_case(&graph);
for case in &admitted {
let analysis = given::analysis(&graph, case.selection);
assert_eq!(
given::edge_texts(&graph, &observed_edges(&analysis, &graph)),
given::edge_texts(
&graph,
&given::admitted_ids(&graph, case.kinds, case.certainties)
),
"{}: every query reads exactly the stated intersection",
case.label
);
assert_view_reads_back_its_own_terms(&graph, &analysis, case);
}
assert_cases_are_distinct(&graph, &admitted);
assert_rejected_edges_are_invisible(&graph);
}
fn assert_view_reads_back_its_own_terms(
graph: &CodeGraph,
analysis: &GraphAnalysis<'_>,
case: &SelectionCase,
) {
assert!(
std::ptr::eq(analysis.graph(), graph),
"{}: an analysis answers with the graph it borrowed",
case.label
);
let reported: Vec<GraphEdgeId> = graph
.edges()
.iter()
.filter(|edge| analysis.selection().allows(edge))
.map(|edge| edge.id())
.collect();
assert_eq!(
given::edge_texts(graph, &reported),
given::edge_texts(
graph,
&given::admitted_ids(graph, case.kinds, case.certainties)
),
"{}: the reported selection admits exactly what the indexes were built \
from",
case.label
);
assert_eq!(
analysis.limits(),
given::generous_limits(),
"{}: the reported ceilings are the ones construction was admitted \
under",
case.label
);
}
fn every_case(graph: &CodeGraph) -> Vec<SelectionCase> {
let cases = vec![
SelectionCase {
label: "no kind",
selection: GraphEdgeSelection::new(&[], EVERY_CERTAINTY),
kinds: &[],
certainties: EVERY_CERTAINTY,
},
SelectionCase {
label: "no certainty",
selection: GraphEdgeSelection::new(EVERY_KIND, &[]),
kinds: EVERY_KIND,
certainties: &[],
},
SelectionCase {
label: "all",
selection: GraphEdgeSelection::all(),
kinds: EVERY_KIND,
certainties: EVERY_CERTAINTY,
},
SelectionCase {
label: "code relations",
selection: GraphEdgeSelection::code_relations(),
kinds: CODE_RELATION_KINDS,
certainties: EVERY_CERTAINTY,
},
SelectionCase {
label: "duplicated calls",
selection: GraphEdgeSelection::new(
&[GraphEdgeKind::Call, GraphEdgeKind::Call],
&[GraphCertainty::Resolved, GraphCertainty::Resolved],
),
kinds: &[GraphEdgeKind::Call],
certainties: &[GraphCertainty::Resolved],
},
SelectionCase {
label: "resolved dependencies",
selection: GraphEdgeSelection::new(
&[GraphEdgeKind::DependsOn],
&[GraphCertainty::Resolved],
),
kinds: &[GraphEdgeKind::DependsOn],
certainties: &[GraphCertainty::Resolved],
},
];
for case in &cases {
let stated = !case.kinds.is_empty() && !case.certainties.is_empty();
let admitted = !given::admitted_ids(graph, case.kinds, case.certainties).is_empty();
assert_eq!(
stated, admitted,
"{}: a stated intersection must admit an edge and an empty one must not",
case.label
);
}
cases
}
fn assert_cases_are_distinct(graph: &CodeGraph, cases: &[SelectionCase]) {
let sets: Vec<Vec<String>> = cases
.iter()
.map(|case| {
given::edge_texts(
graph,
&given::admitted_ids(graph, case.kinds, case.certainties),
)
})
.collect();
let distinct: BTreeSet<&Vec<String>> = sets.iter().collect();
assert_eq!(
distinct.len(),
sets.len() - 1,
"only the two empty intersections may agree: {sets:?}"
);
assert_eq!(
given::edge_texts(
graph,
&given::admitted_ids(
graph,
&[GraphEdgeKind::DependsOn],
&[GraphCertainty::Resolved]
)
),
["app>helper|DependsOn|Resolved"],
"one kind at one certainty admits the one edge stating both"
);
}
fn assert_rejected_edges_are_invisible(graph: &CodeGraph) {
let root = given::unit_container(graph, "app");
let depth = u32::try_from(graph.nodes().len()).expect("the corpus is small");
let dependencies = given::analysis(
graph,
GraphEdgeSelection::new(&[GraphEdgeKind::DependsOn], &[GraphCertainty::Resolved]),
);
let reached = dependencies
.neighbors(root, depth, GraphDirection::Both)
.expect("the admitted depth is below the ceiling");
assert_eq!(
given::neighbor_texts(graph, &reached),
["helper:1"],
"a resolved-dependency selection reaches only the resolved dependency"
);
let none = given::analysis(graph, GraphEdgeSelection::new(&[], &[]));
let empty = none
.subgraph(root, depth, GraphDirection::Both)
.expect("the admitted depth is below the ceiling");
assert!(
empty.edges().is_empty(),
"an empty selection induces no edge: {:?}",
given::edge_texts(graph, empty.edges())
);
assert_eq!(
given::names(graph, empty.nodes()),
["app"],
"an empty selection leaves the seed alone"
);
}
fn observed_edges(analysis: &GraphAnalysis<'_>, graph: &CodeGraph) -> Vec<GraphEdgeId> {
let depth = u32::try_from(graph.nodes().len()).expect("the corpus is small");
let mut seen: BTreeSet<GraphEdgeId> = BTreeSet::new();
for node in graph.nodes() {
let induced = analysis
.subgraph(node.id(), depth, GraphDirection::Both)
.expect("the admitted depth is below the ceiling");
seen.extend(induced.edges().iter().copied());
}
seen.into_iter().collect()
}
pub fn assert_admission_limits_refuse() {
let graph = given::selection_graph();
let nodes = u32::try_from(graph.nodes().len()).expect("the corpus is small");
let selected = u32::try_from(given::admitted_ids(&graph, EVERY_KIND, EVERY_CERTAINTY).len())
.expect("the corpus is small");
let exact = GraphAnalysisLimits::new(nodes, selected, 0, 0);
let admitted = GraphAnalysis::new(&graph, GraphEdgeSelection::all(), exact)
.expect("ceilings equal to the produced counts admit the whole graph");
assert_eq!(
admitted.limits(),
exact,
"an admitted analysis reports the exact ceilings it was stated with"
);
for (label, limits, message) in admission_cases(nodes, selected) {
let refused = GraphAnalysis::new(&graph, GraphEdgeSelection::all(), limits)
.err()
.unwrap_or_else(|| panic!("{label}: the lowered ceiling was admitted"));
assert_eq!(
refused.to_string(),
message,
"{label}: the refusal must name its own count and ceiling"
);
}
assert!(
GraphAnalysis::new(
&graph,
GraphEdgeSelection::new(&[], &[]),
GraphAnalysisLimits::new(nodes, 0, u32::MAX, u64::MAX),
)
.is_ok(),
"the edge ceiling bounds the selected edges, not the graph's own"
);
assert!(
GraphAnalysis::new(&graph, GraphEdgeSelection::all(), given::generous_limits()).is_ok(),
"a refusal consumes nothing: the same graph still admits an analysis"
);
}
fn admission_cases(nodes: u32, selected: u32) -> [(&'static str, GraphAnalysisLimits, String); 3] {
let refused_nodes = format!(
"the graph states {nodes} nodes, above the ceiling of {}",
nodes - 1
);
let refused_edges = format!(
"the selection admits {selected} edges, above the ceiling of {}",
selected - 1
);
[
(
"nodes",
GraphAnalysisLimits::new(nodes - 1, u32::MAX, u32::MAX, u64::MAX),
refused_nodes.clone(),
),
(
"selected edges",
GraphAnalysisLimits::new(u32::MAX, selected - 1, u32::MAX, u64::MAX),
refused_edges,
),
(
"nodes before edges",
GraphAnalysisLimits::new(nodes - 1, selected - 1, u32::MAX, u64::MAX),
refused_nodes,
),
]
}