use arrow::array::RecordBatch;
use crate::schema::{CodeNode, CodeNodeKind};
use crate::search::{CodeSearch, search_nodes};
pub fn discover_tests(batches: &[RecordBatch]) -> std::collections::HashMap<String, Vec<CodeNode>> {
let mut by_crate: std::collections::HashMap<String, Vec<CodeNode>> =
std::collections::HashMap::new();
let query = CodeSearch {
kind: Some(CodeNodeKind::RustTest),
..Default::default()
};
for batch in batches {
let result = search_nodes(batch, &query);
for node in result.nodes {
let crate_name = crate_from_path(&node);
by_crate.entry(crate_name).or_default().push(node);
}
}
by_crate
}
pub fn discover_tests_in_crate(batches: &[RecordBatch], crate_name: &str) -> Vec<CodeNode> {
let all = discover_tests(batches);
all.get(crate_name).cloned().unwrap_or_default()
}
pub fn discover_tests_for_function(batches: &[RecordBatch], function_name: &str) -> Vec<CodeNode> {
let query = CodeSearch {
kind: Some(CodeNodeKind::RustTest),
..Default::default()
};
let mut matching = Vec::new();
for batch in batches {
let result = search_nodes(batch, &query);
for node in result.nodes {
if let Some(ref body) = node.body
&& body.contains(function_name)
{
matching.push(node);
}
}
}
matching
}
fn crate_from_path(node: &CodeNode) -> String {
if let Some(ref path) = node.file_path
&& let Some(crate_name) = path
.strip_prefix("crates/")
.and_then(|rest| rest.split('/').next())
{
return crate_name.to_string();
}
if let Some(crate_name) = node
.id
.split("crates/")
.nth(1)
.and_then(|rest| rest.split('/').next())
{
return crate_name.to_string();
}
"unknown".to_string()
}
#[derive(Debug)]
pub struct DiscoverySummary {
pub total_tests: usize,
pub crates: Vec<(String, usize)>,
}
pub fn discovery_summary(
tests: &std::collections::HashMap<String, Vec<CodeNode>>,
) -> DiscoverySummary {
let mut crates: Vec<(String, usize)> =
tests.iter().map(|(k, v)| (k.clone(), v.len())).collect();
crates.sort_by(|a, b| a.0.cmp(&b.0));
let total = crates.iter().map(|(_, c)| c).sum();
DiscoverySummary {
total_tests: total,
crates,
}
}
impl std::fmt::Display for DiscoverySummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"Test Discovery: {} tests across {} crates",
self.total_tests,
self.crates.len()
)?;
for (crate_name, count) in &self.crates {
writeln!(f, " {crate_name}: {count} tests")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::CodeNodeKind;
fn make_test_node(id: &str, crate_path: &str, body: &str) -> CodeNode {
CodeNode {
id: format!("fn:crates/{crate_path}/src/lib.rs::{id}"),
kind: CodeNodeKind::RustTest,
name: id.into(),
body: Some(body.into()),
..CodeNode::default()
}
}
fn make_fn_node(id: &str, crate_path: &str, body: &str) -> CodeNode {
CodeNode {
id: format!("fn:crates/{crate_path}/src/lib.rs::{id}"),
kind: CodeNodeKind::RustFn,
name: id.into(),
body: Some(body.into()),
..CodeNode::default()
}
}
#[test]
fn test_discover_filters_by_kind() {
let nodes = vec![
make_test_node("test_add", "nusy-core", "fn test_add() { assert!(true); }"),
make_fn_node(
"add",
"nusy-core",
"pub fn add(a: i64, b: i64) -> i64 { a + b }",
),
make_test_node("test_mul", "nusy-core", "fn test_mul() { assert!(true); }"),
];
let batch = crate::schema::build_code_nodes_batch(&nodes).expect("batch");
let tests = discover_tests(&[batch]);
let core_tests = tests.get("nusy-core").expect("crate");
assert_eq!(
core_tests.len(),
2,
"should find 2 test nodes, not the fn node"
);
}
#[test]
fn test_discover_groups_by_crate() {
let nodes = vec![
make_test_node("t1", "crate-a", "fn t1() {}"),
make_test_node("t2", "crate-a", "fn t2() {}"),
make_test_node("t3", "crate-b", "fn t3() {}"),
];
let batch = crate::schema::build_code_nodes_batch(&nodes).expect("batch");
let tests = discover_tests(&[batch]);
assert_eq!(tests.get("crate-a").map(|v| v.len()), Some(2));
assert_eq!(tests.get("crate-b").map(|v| v.len()), Some(1));
}
#[test]
fn test_discover_tests_for_function() {
let nodes = vec![
make_test_node("test_add", "core", "fn test_add() { let r = add(1, 2); }"),
make_test_node("test_mul", "core", "fn test_mul() { let r = mul(2, 3); }"),
make_test_node("test_other", "core", "fn test_other() { }"),
];
let batch = crate::schema::build_code_nodes_batch(&nodes).expect("batch");
let add_tests = discover_tests_for_function(&[batch], "add");
assert!(
add_tests.len() <= 1,
"should find at most 1 test referencing 'add'"
);
}
#[test]
fn test_discovery_summary() {
let mut tests = std::collections::HashMap::new();
tests.insert(
"crate-a".to_string(),
vec![
make_test_node("t1", "crate-a", ""),
make_test_node("t2", "crate-a", ""),
],
);
tests.insert(
"crate-b".to_string(),
vec![make_test_node("t3", "crate-b", "")],
);
let summary = discovery_summary(&tests);
assert_eq!(summary.total_tests, 3);
assert_eq!(summary.crates.len(), 2);
}
#[test]
fn test_crate_from_path() {
let node = make_test_node("t", "nusy-arrow-core", "");
assert_eq!(crate_from_path(&node), "nusy-arrow-core");
}
#[test]
fn test_empty_batches() {
let tests = discover_tests(&[]);
assert!(tests.is_empty());
}
}