use std::collections::BTreeSet;
use pedant_graph::{
CodeGraph, GraphAnalysis, GraphEdgeId, GraphEdgeSelection, MisplacementCandidate,
};
use super::analysis_divergence_model::{self as model, DivergenceModel};
use super::analysis_fixture as given;
use super::analysis_oracle as oracle;
const CODE_RELATION_EDGES: &[&str] = &[
"alpha_call>alpha_leaf|Call|Resolved",
"alpha_leaf>root_call|Call|Resolved",
"src/lib.rs>alpha|Reference|Resolved",
"beta_call>beta_leaf|Call|Resolved",
"root_call>alpha_call|Call|Resolved",
"root_call>beta_call|Call|Resolved",
"root_call>root_leaf|Call|Resolved",
];
const EVERY_EDGE: &[&str] = &[
"app>helper|DependsOn|Resolved",
"alpha_call>alpha_leaf|Call|Resolved",
"alpha_leaf>root_call|Call|Resolved",
"src/lib.rs>alpha|Reference|Resolved",
"beta_call>beta_leaf|Call|Resolved",
"root_call>alpha_call|Call|Resolved",
"root_call>beta_call|Call|Resolved",
"root_call>root_leaf|Call|Resolved",
];
type CohesionRow = (&'static str, u32, u32, Option<f64>);
type CandidateRow = (&'static str, &'static str, &'static str, u32, u32, f64);
type BoundaryRow = (&'static str, &'static str);
struct DivergenceCase {
label: &'static str,
selection: GraphEdgeSelection,
edges: &'static [&'static str],
cohesion: &'static [CohesionRow],
modularity: f64,
candidates: &'static [CandidateRow],
boundary: &'static [BoundaryRow],
}
const CODE_RELATION_COHESION: &[CohesionRow] = &[
("app", 1, 3, Some(0.25)),
("alpha", 1, 1, Some(0.5)),
("beta", 1, 0, Some(1.0)),
("helper", 0, 0, None),
];
const EVERY_EDGE_COHESION: &[CohesionRow] = &[
("app", 1, 4, Some(0.2)),
("alpha", 1, 1, Some(0.5)),
("beta", 1, 0, Some(1.0)),
("helper", 0, 0, None),
];
const CODE_RELATION_MODULARITY: f64 = 5.0 / 49.0;
const EVERY_EDGE_MODULARITY: f64 = 3.0 / 32.0;
const EXPECTED_CANDIDATES: &[CandidateRow] = &[
("alpha_leaf", "alpha", "app", 1, 1, 1.0),
("root_call", "app", "alpha", 1, 3, 1.0 / 3.0),
("root_call", "app", "beta", 1, 3, 1.0 / 3.0),
];
const EXPECTED_BOUNDARY: &[BoundaryRow] = &[("alpha_call,alpha_leaf,root_call", "alpha,app")];
pub fn assert_divergence_matches_declared_partition_arithmetic() {
let graph = given::divergence_graph();
for case in hand_calculated_cases() {
let analysis = given::analysis(&graph, case.selection);
assert_eq!(
given::edge_texts(&graph, &selected_ids(&graph, case.selection)),
case.edges,
"{}: the arithmetic below is taken from this exact topology",
case.label
);
model::assert_matches(
&model::observed(&graph, &analysis),
&hand_written(&case),
case.label,
);
assert_cohesion_is_in_root_order(&graph, &analysis);
assert_candidates_are_sorted(&graph, analysis.divergence().candidates());
}
assert_no_threshold_is_applied(&graph);
}
fn hand_calculated_cases() -> [DivergenceCase; 2] {
[
DivergenceCase {
label: "code relations",
selection: GraphEdgeSelection::code_relations(),
edges: CODE_RELATION_EDGES,
cohesion: CODE_RELATION_COHESION,
modularity: CODE_RELATION_MODULARITY,
candidates: EXPECTED_CANDIDATES,
boundary: EXPECTED_BOUNDARY,
},
DivergenceCase {
label: "all",
selection: GraphEdgeSelection::all(),
edges: EVERY_EDGE,
cohesion: EVERY_EDGE_COHESION,
modularity: EVERY_EDGE_MODULARITY,
candidates: EXPECTED_CANDIDATES,
boundary: EXPECTED_BOUNDARY,
},
]
}
fn selected_ids(graph: &CodeGraph, selection: GraphEdgeSelection) -> Vec<GraphEdgeId> {
oracle::selected(graph, selection)
.iter()
.map(|edge| edge.id)
.collect()
}
fn hand_written(case: &DivergenceCase) -> DivergenceModel {
DivergenceModel {
cohesion: case
.cohesion
.iter()
.map(|(root, internal, boundary, score)| {
((*root).to_owned(), *internal, *boundary, *score)
})
.collect(),
modularity: Some(case.modularity),
candidates: case
.candidates
.iter()
.map(|(symbol, declared, candidate, foreign, total, ratio)| {
(
(*symbol).to_owned(),
(*declared).to_owned(),
(*candidate).to_owned(),
*foreign,
*total,
*ratio,
)
})
.collect(),
boundary: case
.boundary
.iter()
.map(|(members, partitions)| ((*members).to_owned(), (*partitions).to_owned()))
.collect(),
}
}
fn assert_cohesion_is_in_root_order(graph: &CodeGraph, analysis: &GraphAnalysis<'_>) {
let divergence = analysis.divergence();
let stated: Vec<u32> = divergence
.cohesion()
.iter()
.map(|record| record.root().index())
.collect();
let roots: Vec<u32> = analysis
.declared_partition()
.roots()
.iter()
.map(|root| root.index())
.collect();
assert_eq!(
stated,
roots,
"cohesion answers for every partition root once, in root-id order: {:?}",
given::cohesion_texts(graph, divergence.cohesion())
);
}
fn assert_candidates_are_sorted(graph: &CodeGraph, candidates: &[MisplacementCandidate]) {
let keys: Vec<(u32, u32)> = candidates
.iter()
.map(|record| {
(
record.symbol().index(),
record.candidate_partition().index(),
)
})
.collect();
assert!(
keys.windows(2).all(|pair| pair[0] < pair[1]),
"affinity records rise strictly by symbol then candidate: {:?}",
given::candidate_texts(graph, candidates)
);
}
fn assert_no_threshold_is_applied(graph: &CodeGraph) {
let analysis = given::analysis(graph, GraphEdgeSelection::code_relations());
let divergence = analysis.divergence();
let smallest = divergence
.candidates()
.iter()
.map(|record| record.affinity())
.fold(f64::INFINITY, f64::min);
assert!(
smallest < 0.5,
"a minority of foreign calls is still recorded: {smallest}"
);
let symbols: BTreeSet<String> = divergence
.candidates()
.iter()
.map(|record| given::name(graph, record.symbol()))
.collect();
assert!(
!symbols.contains("alpha_call"),
"a symbol whose every call stays at home states no candidate: {symbols:?}"
);
}
pub fn assert_divergence_selection_reuses_indexes() {
for graph in [given::divergence_graph(), given::selection_graph()] {
let mut ownership: BTreeSet<Vec<String>> = BTreeSet::new();
let mut answers: BTreeSet<Vec<String>> = BTreeSet::new();
for (label, selection) in given::metric_selections() {
let analysis = given::analysis(&graph, selection);
ownership.insert(owner_names(&graph, &analysis));
answers.insert(rendered(&graph, &analysis));
model::assert_matches(
&model::observed(&graph, &analysis),
&model::modelled(&graph, &analysis, selection),
&format!("{label}: the selected topology"),
);
}
assert_eq!(
ownership.len(),
1,
"which container owns a node is not a function of the selection"
);
assert!(
answers.len() > 1,
"the selections compared must not all answer the same"
);
}
}
fn owner_names(graph: &CodeGraph, analysis: &GraphAnalysis<'_>) -> Vec<String> {
let partition = analysis.declared_partition();
graph
.nodes()
.iter()
.map(|node| {
let owner = partition
.owner(node.id())
.unwrap_or_else(|| panic!("{} belongs to no partition", node.name()));
format!("{}|{}", node.name(), given::name(graph, owner))
})
.collect()
}
pub fn rendered(graph: &CodeGraph, analysis: &GraphAnalysis<'_>) -> Vec<String> {
let divergence = analysis.divergence();
let components = analysis.strongly_connected_components();
let mut answers = given::cohesion_texts(graph, divergence.cohesion());
answers.extend(given::candidate_texts(graph, divergence.candidates()));
answers.extend(given::boundary_texts(
graph,
&components,
divergence.boundary_components(),
));
let layout = analysis
.layout_assist()
.expect("the fixture graph is far below every ceiling");
answers.extend(given::layout_edge_texts(graph, layout.edges()));
answers.extend(given::layout_node_texts(graph, layout.nodes()));
answers.sort();
answers
}