use proptest::prelude::*;
use sinter_core::{
Confidence, Edge, Evidence, Graph, GraphError, Node, NodeId, Relation, Span, SymbolKind,
};
fn node(id: &str) -> Node {
Node {
id: NodeId::new(id),
kind: SymbolKind::Function,
name: id.to_string(),
file: "src/lib.rs".to_string(),
span: Span { start: 0, end: 10 },
signature: format!("fn {id}()"),
doc: None,
}
}
fn edge(src: &str, dst: &str, relation: Relation) -> Edge {
Edge {
src: NodeId::new(src),
dst: NodeId::new(dst),
relation,
evidence: Evidence::Structural,
confidence: Confidence::Certain,
site: None,
}
}
const RELATIONS: [Relation; 6] = [
Relation::Calls,
Relation::Uses,
Relation::Imports,
Relation::Contains,
Relation::Implements,
Relation::Extends,
];
fn arb_ids() -> impl Strategy<Value = Vec<String>> {
proptest::collection::btree_set("[A-Za-z][A-Za-z0-9_:]{0,15}", 1..30)
.prop_map(|s| s.into_iter().collect())
}
proptest! {
#[test]
fn endpoints_always_exist(ids in arb_ids(), pairs in proptest::collection::vec((any::<prop::sample::Index>(), any::<prop::sample::Index>(), 0usize..6), 0..60)) {
let mut g = Graph::new();
for id in &ids {
g.add_node(node(id)).unwrap();
}
for (a, b, r) in &pairs {
g.add_edge(edge(a.get(&ids), b.get(&ids), RELATIONS[*r])).unwrap();
}
for e in g.edges() {
prop_assert!(g.node(&e.src).is_some());
prop_assert!(g.node(&e.dst).is_some());
}
}
#[test]
fn missing_endpoint_rejected(ids in arb_ids(), ghost in "[A-Za-z][A-Za-z0-9_:]{0,15}") {
prop_assume!(!ids.contains(&ghost));
let mut g = Graph::new();
for id in &ids {
g.add_node(node(id)).unwrap();
}
let err = g.add_edge(edge(&ids[0], &ghost, Relation::Calls)).unwrap_err();
prop_assert_eq!(err, GraphError::MissingEndpoint(NodeId::new(ghost)));
}
#[test]
fn duplicate_id_rejected(ids in arb_ids(), pick in any::<prop::sample::Index>()) {
let mut g = Graph::new();
for id in &ids {
g.add_node(node(id)).unwrap();
}
let dup = pick.get(&ids);
let before = g.clone();
let err = g.add_node(node(dup)).unwrap_err();
prop_assert_eq!(err, GraphError::DuplicateNode(NodeId::new(dup)));
prop_assert_eq!(g, before);
}
#[test]
fn invalid_span_rejected(start in 0u64..1000, slack in 0u64..1000) {
let end = start.saturating_sub(slack); let mut n = node("f");
n.span = Span { start, end };
let mut g = Graph::new();
let rejected = matches!(g.add_node(n), Err(GraphError::InvalidSpan { .. }));
prop_assert!(rejected);
}
}
#[test]
fn ids_are_case_sensitive() {
let mut g = Graph::new();
g.add_node(node("Config")).unwrap();
g.add_node(node("config")).unwrap();
assert_eq!(g.node_count(), 2);
assert!(g.node(&NodeId::new("Config")).is_some());
assert!(g.node(&NodeId::new("config")).is_some());
}
#[test]
fn multigraph_parallel_edges() {
let mut g = Graph::new();
g.add_node(node("a")).unwrap();
g.add_node(node("b")).unwrap();
g.add_edge(edge("a", "b", Relation::Calls)).unwrap();
g.add_edge(edge("a", "b", Relation::Uses)).unwrap();
g.add_edge(edge("a", "b", Relation::Calls)).unwrap(); assert_eq!(g.edge_count(), 2);
let a = NodeId::new("a");
assert_eq!(g.edges_from(&a).count(), 2);
}
#[test]
fn empty_fields_rejected() {
let mut g = Graph::new();
let mut n = node("f");
n.name = String::new();
assert!(matches!(
g.add_node(n),
Err(GraphError::EmptyField { field: "name", .. })
));
let mut n = node("f");
n.file = String::new();
assert!(matches!(
g.add_node(n),
Err(GraphError::EmptyField { field: "file", .. })
));
}