use veridikt_derive::{DeriveConfig, DeriveResult, SourceUnit, StateSymbol, derive};
use veridikt_intent::QName;
mod common;
fn cfg() -> DeriveConfig {
DeriveConfig {
roots: vec!["src".into()],
cache_dir: None,
manifests: Vec::new(),
}
}
fn unit(path: &str, module: &str, text: &str) -> SourceUnit {
SourceUnit {
path: path.into(),
text: text.to_string(),
module: module.to_string(),
}
}
fn state(qname: &str, identifier: &str, file: &str, module: &str) -> StateSymbol {
StateSymbol {
qname: QName::from_dotted(qname),
identifier: identifier.to_string(),
file: file.into(),
module: module.to_string(),
}
}
fn edges(r: &DeriveResult) -> Vec<(String, String, String, String)> {
r.edges
.iter()
.map(|e| {
(
e.from.to_string(),
e.to.to_string(),
format!("{:?}", e.kind),
format!("{:?}", e.confidence),
)
})
.collect()
}
fn nodes(r: &DeriveResult) -> Vec<(String, &'static str)> {
r.nodes
.iter()
.map(|n| (n.qname.to_string(), n.kind.name()))
.collect()
}
fn e(from: &str, to: &str, kind: &str, conf: &str) -> (String, String, String, String) {
(from.into(), to.into(), kind.into(), conf.into())
}
#[test]
fn unresolvable_calls_are_dropped_and_counted_never_guessed() {
let files = [unit(
"src/m.py",
"App",
"def f():\n print(\"x\")\n helper.run()\n a.b.c()\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(edges(&r), []);
assert_eq!(r.unresolved_calls, 3);
}
#[test]
fn calls_without_an_honest_attribution_drop() {
let files = [unit(
"src/m.py",
"App",
"def g():\n pass\n\ng()\n\nclass C:\n x = g()\n\nh = lambda: g()\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(edges(&r), []);
assert_eq!(r.unresolved_calls, 3);
}
#[test]
fn colliding_derived_qnames_are_excluded_and_counted_not_guessed() {
let files = [unit(
"src/m.py",
"App",
"class A:\n def __init__(self):\n pass\n\nclass B:\n def __init__(self):\n pass\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(
nodes(&r),
[("App.A".to_string(), "type"), ("App.B".to_string(), "type")]
);
assert_eq!(r.ambiguous_names, 2);
}
#[test]
fn constructor_calls_resolve_to_a_type_and_drop() {
let files = [unit(
"src/m.py",
"App",
"class Foo:\n pass\n\ndef make():\n return Foo()\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(edges(&r), []);
assert_eq!(r.unresolved_calls, 1);
}
#[test]
fn imports_that_leave_derivation_scope_drop() {
let files = [unit(
"src/m.py",
"App",
"from requests import get\n\ndef fetch():\n get(\"u\")\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(edges(&r), []);
assert_eq!(r.unresolved_calls, 1);
}
#[test]
fn method_calls_without_a_local_construction_drop() {
let files = [unit(
"src/m.py",
"App",
"class Store:\n def save(self):\n pass\n\ndef other(s):\n s.save()\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(edges(&r), []);
assert_eq!(r.unresolved_calls, 1);
}
#[test]
fn declarations_become_function_and_type_nodes_assignments_do_not() {
let files = [unit(
"src/m.py",
"App",
"ledger = []\n\ndef f():\n pass\n\nclass C:\n def m(self):\n pass\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(
nodes(&r),
[
("App.f".to_string(), "function"),
("App.C".to_string(), "type"),
("App.m".to_string(), "function"),
]
);
assert_eq!(r.scope, [std::path::PathBuf::from("src/m.py")]);
let f = &r.nodes[0];
assert_eq!(f.loc.line, 3); assert_eq!(f.origin, veridikt_intent::Origin::Derived);
}
#[test]
fn same_file_calls_are_exact_including_recursion() {
let files = [unit(
"src/m.py",
"App",
"def helper():\n pass\n\ndef main():\n helper()\n main()\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(
edges(&r),
[
e("App.main", "App.helper", "Calls", "Exact"),
e("App.main", "App.main", "Calls", "Exact"),
]
);
assert_eq!(r.unresolved_calls, 0);
}
#[test]
fn nested_functions_derive_and_attribute_their_own_calls() {
let files = [unit(
"src/m.py",
"App",
"def outer():\n def inner():\n outer()\n inner()\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(
edges(&r),
[
e("App.inner", "App.outer", "Calls", "Exact"),
e("App.outer", "App.inner", "Calls", "Exact"),
]
);
}
#[test]
fn from_import_calls_resolve_across_files() {
let files = [
unit("src/pay/svc.py", "Payment", "def charge():\n pass\n"),
unit(
"src/user/u.py",
"User",
"from pay.svc import charge\n\ndef signup():\n charge()\n",
),
];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(
edges(&r),
[e("User.signup", "Payment.charge", "Calls", "Resolved")]
);
assert_eq!(r.unresolved_calls, 0);
}
#[test]
fn aliased_imports_resolve_one_level_deep_only() {
let files = [
unit("src/pay/svc.py", "Payment", "def charge():\n pass\n"),
unit(
"src/user/u.py",
"User",
"import pay.svc as svc\nimport pay.svc\nfrom pay.svc import charge as pay_up\n\ndef a():\n svc.charge()\n\ndef b():\n pay_up()\n\ndef c():\n pay.svc.charge()\n",
),
];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(
edges(&r),
[
e("User.a", "Payment.charge", "Calls", "Resolved"),
e("User.b", "Payment.charge", "Calls", "Resolved"),
]
);
assert_eq!(r.unresolved_calls, 1);
}
#[test]
fn method_call_on_a_locally_constructed_instance_is_exact() {
let files = [unit(
"src/m.py",
"App",
"class Store:\n def save(self):\n pass\n\ndef run():\n s = Store()\n s.save()\n",
)];
let r = derive(&cfg(), &common::packs(), &files, &[]);
assert_eq!(edges(&r), [e("App.run", "App.save", "Calls", "Exact")]);
assert_eq!(r.unresolved_calls, 1);
}
#[test]
fn touches_classify_writes_and_reads_and_dedupe_per_function() {
let files = [unit(
"src/m.py",
"App",
"ledger = []\nbalances = {}\n\ndef charge(user, amount):\n if balances.get(user, 0) < amount:\n return\n ledger.append(entry(user))\n ledger.append(entry(user))\n total = balances\n",
)];
let states = [
state("App.ledger", "ledger", "src/m.py", "App"),
state("App.balances", "balances", "src/m.py", "App"),
];
let r = derive(&cfg(), &common::packs(), &files, &states);
assert_eq!(
edges(&r),
[
e("App.charge", "App.balances", "Reads", "Heuristic"),
e("App.charge", "App.ledger", "Affects", "Heuristic"),
]
);
assert_eq!(r.edges[0].loc.line, 5);
assert_eq!(r.edges[1].loc.line, 7);
}
#[test]
fn assignment_augmented_assignment_and_mutators_are_writes_the_rest_reads() {
let files = [unit(
"src/m.py",
"App",
"count = 0\nitems = []\n\ndef reset():\n count = 0\n\ndef bump():\n count += 1\n\ndef peek():\n return items.copy()\n",
)];
let states = [
state("App.count", "count", "src/m.py", "App"),
state("App.items", "items", "src/m.py", "App"),
];
let r = derive(&cfg(), &common::packs(), &files, &states);
assert_eq!(
edges(&r),
[
e("App.reset", "App.count", "Affects", "Heuristic"),
e("App.bump", "App.count", "Affects", "Heuristic"),
e("App.peek", "App.items", "Reads", "Heuristic"),
]
);
}
#[test]
fn module_level_touches_produce_no_edges() {
let files = [unit("src/m.py", "App", "ledger = []\nledger.append(1)\n")];
let states = [state("App.ledger", "ledger", "src/m.py", "App")];
let r = derive(&cfg(), &common::packs(), &files, &states);
assert_eq!(edges(&r), []);
}
#[test]
fn cross_module_touches_need_an_import_that_resolves_to_the_defining_file() {
let files = [
unit("src/pay/svc.py", "Payment", "ledger = []\n"),
unit(
"src/user/u.py",
"User",
"from pay.svc import ledger\n\ndef audit():\n ledger.append(1)\n",
),
unit(
"src/other/o.py",
"Other",
"def peek():\n return ledger\n",
),
unit("src/user/fake.py", "User", "ledger = []\n"),
unit(
"src/user/u2.py",
"User",
"from user.fake import ledger\n\ndef shadow():\n ledger.append(2)\n",
),
];
let states = [state(
"Payment.ledger",
"ledger",
"src/pay/svc.py",
"Payment",
)];
let r = derive(&cfg(), &common::packs(), &files, &states);
assert_eq!(
edges(&r),
[e("User.audit", "Payment.ledger", "Affects", "Heuristic")]
);
}
#[test]
fn whole_import_alias_attribute_touches_resolve() {
let files = [
unit("src/pay/svc.py", "Payment", "ledger = []\n"),
unit(
"src/user/u.py",
"User",
"import pay.svc as svc\n\ndef audit():\n svc.ledger.append(1)\n n = svc.ledger\n",
),
];
let states = [state(
"Payment.ledger",
"ledger",
"src/pay/svc.py",
"Payment",
)];
let r = derive(&cfg(), &common::packs(), &files, &states);
assert_eq!(
edges(&r),
[
e("User.audit", "Payment.ledger", "Affects", "Heuristic"),
e("User.audit", "Payment.ledger", "Reads", "Heuristic"),
]
);
}
#[test]
fn import_statements_themselves_are_not_occurrences() {
let files = [
unit("src/pay/svc.py", "Payment", "ledger = []\n"),
unit("src/user/u.py", "User", "from pay.svc import ledger\n"),
];
let states = [state(
"Payment.ledger",
"ledger",
"src/pay/svc.py",
"Payment",
)];
let r = derive(&cfg(), &common::packs(), &files, &states);
assert_eq!(edges(&r), []);
}