use tempfile::tempdir;
use tokensave::db::NodeFilter;
use tokensave::tokensave::TokenSave;
use tokensave::types::{Node, NodeKind, Visibility};
async fn indexed() -> (tempfile::TempDir, TokenSave) {
let tmp = tempdir().unwrap();
let root = tmp.path();
std::fs::create_dir_all(root.join("src/inner")).unwrap();
std::fs::create_dir_all(root.join("other")).unwrap();
std::fs::write(
root.join("src/lib.rs"),
"pub mod inner;\npub fn exported() -> i32 { 1 }\nfn private_one() -> i32 { 2 }\n",
)
.unwrap();
std::fs::write(
root.join("src/inner/deep.rs"),
"use std::collections::HashMap;\npub fn deep_fn() -> i32 {\n let mut m = HashMap::new();\n m.insert(1, 2);\n m.len() as i32\n}\n",
)
.unwrap();
std::fs::write(
root.join("other/elsewhere.rs"),
"pub fn elsewhere() -> i32 { 3 }\npub struct Marker;\n",
)
.unwrap();
let cg = TokenSave::init(root).await.unwrap();
cg.sync().await.unwrap();
(tmp, cg)
}
fn ids(mut nodes: Vec<Node>) -> Vec<String> {
nodes.sort_by(|a, b| a.id.cmp(&b.id));
nodes.into_iter().map(|n| n.id).collect()
}
fn ids_ref(nodes: &[Node]) -> Vec<String> {
let mut v: Vec<String> = nodes.iter().map(|n| n.id.clone()).collect();
v.sort();
v
}
fn path_matches(file_path: &str, prefix: &str) -> bool {
let with_slash = if prefix.ends_with('/') {
prefix.to_string()
} else {
format!("{prefix}/")
};
file_path == prefix || file_path.starts_with(&with_slash)
}
#[tokio::test]
async fn filter_matches_module_api_predicate() {
let (_tmp, cg) = indexed().await;
let all = cg.get_all_nodes().await.unwrap();
let expected: Vec<Node> = all
.iter()
.filter(|n| n.visibility == Visibility::Pub && path_matches(&n.file_path, "src"))
.cloned()
.collect();
let actual = cg
.db()
.get_nodes_filtered(&NodeFilter::new().path_prefix("src").public_only())
.await
.unwrap();
assert!(
!expected.is_empty(),
"fixture must have public nodes in src"
);
assert_eq!(ids_ref(&expected), ids(actual));
}
#[tokio::test]
async fn filter_matches_redundancy_candidate_predicate() {
let (_tmp, cg) = indexed().await;
let all = cg.get_all_nodes().await.unwrap();
let kinds = [
NodeKind::Function,
NodeKind::Method,
NodeKind::SingletonMethod,
];
let expected: Vec<Node> = all
.iter()
.filter(|n| kinds.contains(&n.kind))
.filter(|n| n.end_line.saturating_sub(n.start_line) + 1 >= 3)
.cloned()
.collect();
let actual = cg
.db()
.get_nodes_filtered(&NodeFilter::new().kinds(&kinds).min_lines(3))
.await
.unwrap();
assert!(
!expected.is_empty(),
"fixture must have a function of at least 3 lines"
);
assert_eq!(ids_ref(&expected), ids(actual));
}
#[tokio::test]
async fn filter_matches_unused_imports_predicate() {
let (_tmp, cg) = indexed().await;
let all = cg.get_all_nodes().await.unwrap();
let expected: Vec<Node> = all
.iter()
.filter(|n| n.kind == NodeKind::Use && path_matches(&n.file_path, "src"))
.cloned()
.collect();
let actual = cg
.db()
.get_nodes_filtered(&NodeFilter::new().path_prefix("src").kinds(&[NodeKind::Use]))
.await
.unwrap();
assert!(!expected.is_empty(), "fixture must have a `use` in src");
assert_eq!(ids_ref(&expected), ids(actual));
}
#[tokio::test]
async fn filter_matches_literal_search_substring_predicate() {
let (_tmp, cg) = indexed().await;
let all = cg.get_all_nodes().await.unwrap();
let needle = "deep";
let expected: Vec<Node> = all
.iter()
.filter(|n| {
n.name.to_ascii_lowercase().contains(needle)
|| n.qualified_name.to_ascii_lowercase().contains(needle)
})
.cloned()
.collect();
let actual = cg
.db()
.get_nodes_filtered(&NodeFilter::new().name_contains(needle))
.await
.unwrap();
assert!(!expected.is_empty(), "fixture must contain `deep`");
assert_eq!(ids_ref(&expected), ids(actual));
}
#[tokio::test]
async fn path_prefix_does_not_match_a_sibling_with_the_same_stem() {
let tmp = tempdir().unwrap();
let root = tmp.path();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::create_dir_all(root.join("srcfoo")).unwrap();
std::fs::write(root.join("src/a.rs"), "pub fn a() -> i32 { 1 }\n").unwrap();
std::fs::write(root.join("srcfoo/b.rs"), "pub fn b() -> i32 { 2 }\n").unwrap();
let cg = TokenSave::init(root).await.unwrap();
cg.sync().await.unwrap();
let scoped = cg
.db()
.get_nodes_filtered(&NodeFilter::new().path_prefix("src"))
.await
.unwrap();
assert!(
scoped.iter().any(|n| n.file_path.starts_with("src/")),
"src/ must be included"
);
assert!(
!scoped.iter().any(|n| n.file_path.starts_with("srcfoo")),
"srcfoo must not be swept in by a prefix match: {:?}",
scoped.iter().map(|n| &n.file_path).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn path_prefix_treats_like_wildcards_literally() {
let tmp = tempdir().unwrap();
let root = tmp.path();
std::fs::create_dir_all(root.join("a_b")).unwrap();
std::fs::create_dir_all(root.join("axb")).unwrap();
std::fs::write(root.join("a_b/one.rs"), "pub fn one() -> i32 { 1 }\n").unwrap();
std::fs::write(root.join("axb/two.rs"), "pub fn two() -> i32 { 2 }\n").unwrap();
let cg = TokenSave::init(root).await.unwrap();
cg.sync().await.unwrap();
let scoped = cg
.db()
.get_nodes_filtered(&NodeFilter::new().path_prefix("a_b"))
.await
.unwrap();
assert!(
scoped.iter().any(|n| n.file_path.starts_with("a_b/")),
"the literal directory must match"
);
assert!(
!scoped.iter().any(|n| n.file_path.starts_with("axb")),
"`_` must not act as a single-character wildcard: {:?}",
scoped.iter().map(|n| &n.file_path).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn an_empty_filter_returns_every_node() {
let (_tmp, cg) = indexed().await;
let all = cg.get_all_nodes().await.unwrap();
let filtered = cg
.db()
.get_nodes_filtered(&NodeFilter::new())
.await
.unwrap();
assert_eq!(ids_ref(&all), ids(filtered));
}
#[tokio::test]
async fn node_paths_matches_the_map_built_from_full_nodes() {
let (_tmp, cg) = indexed().await;
let expected: std::collections::HashMap<String, String> = cg
.get_all_nodes()
.await
.unwrap()
.into_iter()
.map(|n| (n.id, n.file_path))
.collect();
let actual = cg.db().get_node_paths().await.unwrap();
assert!(!expected.is_empty(), "fixture must have nodes");
assert_eq!(
expected, actual,
"the projection must agree with the map built from full nodes"
);
}