use std::collections::BTreeSet;
use pedant_graph::{
CodeGraph, GraphAnalysis, GraphAnalysisLimits, GraphEdgeSelection, GraphNodeId,
};
use super::analysis_fixture as given;
use super::analysis_oracle as oracle;
use super::corpus_analysis::ANALYSIS_PAIR_CORPUS;
use super::corpus_generated::{
TOPOLOGY_MODULES, TOPOLOGY_WIDTH, diamond_corpus, topology_corpus, topology_name,
topology_states,
};
const TOLERANCE: f64 = 1e-12;
const MULTIPLICITIES: [usize; 3] = [1, 2, 3];
pub fn assert_degree_and_betweenness_match_oracles() {
let exhaustive = given::generated_graph(&topology_corpus());
assert_topology_states_every_subset(&exhaustive);
assert_metrics_match(
&exhaustive,
given::calls_only(),
"every three-node topology",
);
for multiplicity in MULTIPLICITIES {
let diamond = given::generated_graph(&diamond_corpus(multiplicity));
assert_metrics_match(
&diamond,
given::calls_only(),
&format!("a diamond of multiplicity {multiplicity}"),
);
}
assert_parallel_and_self_loop_degrees();
assert_small_graphs_normalize_to_zero();
}
fn assert_topology_states_every_subset(graph: &CodeGraph) {
let selection = given::calls_only();
assert_eq!(
graph.nodes().len(),
2 + TOPOLOGY_MODULES * (1 + TOPOLOGY_WIDTH),
"one unit root, one source, and one container and three functions per \
stated subset"
);
let mut realized: Vec<(String, String)> = graph
.edges()
.iter()
.filter(|edge| selection.allows(edge))
.map(|edge| {
(
given::name(graph, edge.source()),
given::name(graph, edge.target()),
)
})
.collect();
realized.sort();
let mut stated: Vec<(String, String)> = (0..TOPOLOGY_MODULES).flat_map(stated_calls).collect();
stated.sort();
assert_eq!(
realized.len(),
TOPOLOGY_MODULES * TOPOLOGY_WIDTH * TOPOLOGY_WIDTH / 2,
"each ordered pair is stated by exactly half of the subsets"
);
assert_eq!(
realized, stated,
"the projected call topology is exactly the subsets the corpus states"
);
}
fn stated_calls(subset: usize) -> Vec<(String, String)> {
(0..TOPOLOGY_WIDTH)
.flat_map(|from| (0..TOPOLOGY_WIDTH).map(move |to| (from, to)))
.filter(|(from, to)| topology_states(subset, *from, *to))
.map(|(from, to)| (topology_name(subset, from), topology_name(subset, to)))
.collect()
}
fn assert_metrics_match(graph: &CodeGraph, selection: GraphEdgeSelection, subject: &str) {
let analysis = given::analysis(graph, selection);
let edges = oracle::selected(graph, selection);
assert_degrees_match(graph, &analysis, &edges, subject);
assert_betweenness_matches(graph, &analysis, &edges, subject);
assert_some_route_has_an_interior(&analysis, subject);
}
fn assert_some_route_has_an_interior(analysis: &GraphAnalysis<'_>, subject: &str) {
let scores = analysis
.betweenness_centrality()
.unwrap_or_else(|error| panic!("{subject}: {error}"));
assert!(
scores.iter().any(|record| record.raw() > 0.0),
"{subject}: a compared corpus must route through something"
);
}
fn assert_degrees_match(
graph: &CodeGraph,
analysis: &GraphAnalysis<'_>,
edges: &[oracle::SelectedEdge],
subject: &str,
) {
let counted = analysis.degree_centrality();
assert_records_are_dense(counted.iter().map(|record| record.node()), subject);
assert_eq!(
counted
.iter()
.map(|record| (record.incoming(), record.outgoing()))
.collect::<Vec<(u32, u32)>>(),
oracle::degrees(graph.nodes().len(), edges),
"{subject}: degree reads exactly the selected edges"
);
}
fn assert_betweenness_matches(
graph: &CodeGraph,
analysis: &GraphAnalysis<'_>,
edges: &[oracle::SelectedEdge],
subject: &str,
) {
let nodes = graph.nodes().len();
let scores = analysis
.betweenness_centrality()
.unwrap_or_else(|error| panic!("{subject}: {error}"));
assert_records_are_dense(scores.iter().map(|record| record.node()), subject);
let modelled = oracle::raw_betweenness(nodes, edges);
assert_eq!(
scores.len(),
modelled.len(),
"{subject}: one score per raw node"
);
for (record, expected) in scores.iter().zip(&modelled) {
assert_close(record.raw(), *expected, subject, "raw");
assert_close(
record.normalized(),
oracle::normalized(nodes, *expected),
subject,
"normalized",
);
}
}
fn assert_records_are_dense(nodes: impl Iterator<Item = GraphNodeId>, subject: &str) {
let listed: Vec<u32> = nodes.map(|node| node.index()).collect();
assert_eq!(
listed,
(0..listed.len() as u32).collect::<Vec<u32>>(),
"{subject}: one record per raw node, in raw node-id order"
);
}
fn assert_close(measured: f64, modelled: f64, subject: &str, label: &str) {
assert!(
measured.is_finite(),
"{subject}: the {label} score {measured} is not finite"
);
assert!(
(measured - modelled).abs() <= TOLERANCE,
"{subject}: the {label} score {measured} differs from the model {modelled}"
);
}
fn assert_parallel_and_self_loop_degrees() {
let graph = given::traversal_graph();
let analysis = given::analysis(&graph, given::calls_only());
let degrees = analysis.degree_centrality();
let counted = |name: &str| {
let node = given::definition(&graph, name);
let record = degrees[node.index() as usize];
(record.incoming(), record.outgoing())
};
assert_eq!(
counted("seed"),
(1, 3),
"two parallel calls to left and one to right count as three"
);
assert_eq!(
counted("left"),
(2, 1),
"both parallel calls arrive, and the one call out leaves"
);
assert_eq!(
counted("wheel"),
(1, 1),
"a self-loop counts once incoming and once outgoing"
);
assert_eq!(counted("stranded"), (0, 0), "an isolate counts nothing");
}
fn assert_small_graphs_normalize_to_zero() {
let graph = given::graph_of(ANALYSIS_PAIR_CORPUS);
assert_eq!(
graph.nodes().len(),
2,
"the smallest real repository states one unit root and one source"
);
let analysis = given::analysis(&graph, GraphEdgeSelection::all());
let scores = analysis
.betweenness_centrality()
.expect("the smallest graph is far below every ceiling");
assert_eq!(
scores
.iter()
.map(|record| (record.raw(), record.normalized()))
.collect::<Vec<(f64, f64)>>(),
vec![(0.0, 0.0); 2],
"below three nodes no ordered pair has an interior, so every score is zero"
);
}
pub fn assert_metric_selection_reuses_indexes() {
let graph = given::selection_graph();
let mut rendered: BTreeSet<Vec<String>> = BTreeSet::new();
for (label, selection) in given::metric_selections() {
let analysis = given::analysis(&graph, selection);
let edges = oracle::selected(&graph, selection);
assert_degrees_match(&graph, &analysis, &edges, label);
assert_betweenness_matches(&graph, &analysis, &edges, label);
rendered.insert(given::degree_texts(&graph, &analysis.degree_centrality()));
}
assert_eq!(
rendered.len(),
given::metric_selections().len(),
"each stated selection answers with degrees of its own"
);
}
pub fn assert_betweenness_refuses_work() {
let graph = given::traversal_graph();
let selection = given::calls_only();
let nodes = graph.nodes().len() as u64;
let selected = oracle::selected(&graph, selection).len() as u64;
let required = nodes * (nodes + selected);
let admitted = bounded(&graph, selection, required);
let before = admitted_scores(&graph, &admitted);
assert_eq!(
before.len() as u64,
nodes,
"a ceiling equal to the required work admits every score"
);
for lowered in [required - 1, required / 2, 0] {
assert_refusal_repeats(&graph, selection, lowered, required);
}
assert_eq!(
admitted_scores(&graph, &admitted),
before,
"a refusal consumes nothing: the analysis that already answered answers \
the same again"
);
}
fn admitted_scores(graph: &CodeGraph, analysis: &GraphAnalysis<'_>) -> Vec<String> {
let scores = analysis
.betweenness_centrality()
.unwrap_or_else(|error| panic!("this ceiling admits every score: {error}"));
given::betweenness_texts(graph, &scores)
}
fn assert_refusal_repeats(
graph: &CodeGraph,
selection: GraphEdgeSelection,
lowered: u64,
required: u64,
) {
let refusing = bounded(graph, selection, lowered);
let expected = format!("betweenness needs {required} steps, above the ceiling of {lowered}");
for attempt in ["asked once", "asked again"] {
let refused = refusing
.betweenness_centrality()
.err()
.unwrap_or_else(|| panic!("{attempt}: a ceiling of {lowered} was admitted"));
assert_eq!(
refused.to_string(),
expected,
"{attempt}: the refusal names the work it needed and the ceiling it \
was given"
);
}
}
fn bounded(graph: &CodeGraph, selection: GraphEdgeSelection, work: u64) -> GraphAnalysis<'_> {
GraphAnalysis::new(
graph,
selection,
GraphAnalysisLimits::new(u32::MAX, u32::MAX, u32::MAX, work),
)
.expect("the corpus is far below every other ceiling")
}