1use arrow::array::RecordBatch;
6
7use crate::schema::{CodeNode, CodeNodeKind};
8use crate::search::{CodeSearch, search_nodes};
9
10pub fn discover_tests(batches: &[RecordBatch]) -> std::collections::HashMap<String, Vec<CodeNode>> {
15 let mut by_crate: std::collections::HashMap<String, Vec<CodeNode>> =
16 std::collections::HashMap::new();
17
18 let query = CodeSearch {
19 kind: Some(CodeNodeKind::RustTest),
20 ..Default::default()
21 };
22
23 for batch in batches {
24 let result = search_nodes(batch, &query);
25 for node in result.nodes {
26 let crate_name = crate_from_path(&node);
27 by_crate.entry(crate_name).or_default().push(node);
28 }
29 }
30
31 by_crate
32}
33
34pub fn discover_tests_in_crate(batches: &[RecordBatch], crate_name: &str) -> Vec<CodeNode> {
36 let all = discover_tests(batches);
37 all.get(crate_name).cloned().unwrap_or_default()
38}
39
40pub fn discover_tests_for_function(batches: &[RecordBatch], function_name: &str) -> Vec<CodeNode> {
45 let query = CodeSearch {
46 kind: Some(CodeNodeKind::RustTest),
47 ..Default::default()
48 };
49
50 let mut matching = Vec::new();
51 for batch in batches {
52 let result = search_nodes(batch, &query);
53 for node in result.nodes {
54 if let Some(ref body) = node.body
55 && body.contains(function_name)
56 {
57 matching.push(node);
58 }
59 }
60 }
61 matching
62}
63
64fn crate_from_path(node: &CodeNode) -> String {
66 if let Some(ref path) = node.file_path
68 && let Some(crate_name) = path
69 .strip_prefix("crates/")
70 .and_then(|rest| rest.split('/').next())
71 {
72 return crate_name.to_string();
73 }
74 if let Some(crate_name) = node
76 .id
77 .split("crates/")
78 .nth(1)
79 .and_then(|rest| rest.split('/').next())
80 {
81 return crate_name.to_string();
82 }
83 "unknown".to_string()
84}
85
86#[derive(Debug)]
88pub struct DiscoverySummary {
89 pub total_tests: usize,
90 pub crates: Vec<(String, usize)>,
91}
92
93pub fn discovery_summary(
95 tests: &std::collections::HashMap<String, Vec<CodeNode>>,
96) -> DiscoverySummary {
97 let mut crates: Vec<(String, usize)> =
98 tests.iter().map(|(k, v)| (k.clone(), v.len())).collect();
99 crates.sort_by(|a, b| a.0.cmp(&b.0));
100
101 let total = crates.iter().map(|(_, c)| c).sum();
102
103 DiscoverySummary {
104 total_tests: total,
105 crates,
106 }
107}
108
109impl std::fmt::Display for DiscoverySummary {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 writeln!(
112 f,
113 "Test Discovery: {} tests across {} crates",
114 self.total_tests,
115 self.crates.len()
116 )?;
117 for (crate_name, count) in &self.crates {
118 writeln!(f, " {crate_name}: {count} tests")?;
119 }
120 Ok(())
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::schema::CodeNodeKind;
128
129 fn make_test_node(id: &str, crate_path: &str, body: &str) -> CodeNode {
130 CodeNode {
132 id: format!("fn:crates/{crate_path}/src/lib.rs::{id}"),
133 kind: CodeNodeKind::RustTest,
134 name: id.into(),
135 body: Some(body.into()),
136 ..CodeNode::default()
137 }
138 }
139
140 fn make_fn_node(id: &str, crate_path: &str, body: &str) -> CodeNode {
141 CodeNode {
142 id: format!("fn:crates/{crate_path}/src/lib.rs::{id}"),
143 kind: CodeNodeKind::RustFn,
144 name: id.into(),
145 body: Some(body.into()),
146 ..CodeNode::default()
147 }
148 }
149
150 #[test]
151 fn test_discover_filters_by_kind() {
152 let nodes = vec![
153 make_test_node("test_add", "nusy-core", "fn test_add() { assert!(true); }"),
154 make_fn_node(
155 "add",
156 "nusy-core",
157 "pub fn add(a: i64, b: i64) -> i64 { a + b }",
158 ),
159 make_test_node("test_mul", "nusy-core", "fn test_mul() { assert!(true); }"),
160 ];
161 let batch = crate::schema::build_code_nodes_batch(&nodes).expect("batch");
162
163 let tests = discover_tests(&[batch]);
164 let core_tests = tests.get("nusy-core").expect("crate");
165 assert_eq!(
166 core_tests.len(),
167 2,
168 "should find 2 test nodes, not the fn node"
169 );
170 }
171
172 #[test]
173 fn test_discover_groups_by_crate() {
174 let nodes = vec![
175 make_test_node("t1", "crate-a", "fn t1() {}"),
176 make_test_node("t2", "crate-a", "fn t2() {}"),
177 make_test_node("t3", "crate-b", "fn t3() {}"),
178 ];
179 let batch = crate::schema::build_code_nodes_batch(&nodes).expect("batch");
180
181 let tests = discover_tests(&[batch]);
182 assert_eq!(tests.get("crate-a").map(|v| v.len()), Some(2));
183 assert_eq!(tests.get("crate-b").map(|v| v.len()), Some(1));
184 }
185
186 #[test]
187 fn test_discover_tests_for_function() {
188 let nodes = vec![
191 make_test_node("test_add", "core", "fn test_add() { let r = add(1, 2); }"),
192 make_test_node("test_mul", "core", "fn test_mul() { let r = mul(2, 3); }"),
193 make_test_node("test_other", "core", "fn test_other() { }"),
194 ];
195 let batch = crate::schema::build_code_nodes_batch(&nodes).expect("batch");
196
197 let add_tests = discover_tests_for_function(&[batch], "add");
198 assert!(
201 add_tests.len() <= 1,
202 "should find at most 1 test referencing 'add'"
203 );
204 }
205
206 #[test]
207 fn test_discovery_summary() {
208 let mut tests = std::collections::HashMap::new();
209 tests.insert(
210 "crate-a".to_string(),
211 vec![
212 make_test_node("t1", "crate-a", ""),
213 make_test_node("t2", "crate-a", ""),
214 ],
215 );
216 tests.insert(
217 "crate-b".to_string(),
218 vec![make_test_node("t3", "crate-b", "")],
219 );
220
221 let summary = discovery_summary(&tests);
222 assert_eq!(summary.total_tests, 3);
223 assert_eq!(summary.crates.len(), 2);
224 }
225
226 #[test]
227 fn test_crate_from_path() {
228 let node = make_test_node("t", "nusy-arrow-core", "");
229 assert_eq!(crate_from_path(&node), "nusy-arrow-core");
230 }
231
232 #[test]
233 fn test_empty_batches() {
234 let tests = discover_tests(&[]);
235 assert!(tests.is_empty());
236 }
237}