Skip to main content

the_code_graph_domain/
test_support.rs

1use crate::error::Result;
2use crate::model::*;
3use crate::ports::{
4    EmbeddingProvider, FileData, GraphStore, ParseProvider, SearchIndex, VectorStore,
5};
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicUsize, Ordering};
8
9/// In-memory implementation of GraphStore + SearchIndex for testing.
10pub struct InMemoryGraphStore {
11    pub files: Vec<FileNode>,
12    pub symbols: Vec<SymbolNode>,
13    pub edges: Vec<Edge>,
14    pub symbols_for_files_calls: AtomicUsize,
15    pub edges_streaming_calls: AtomicUsize,
16}
17
18impl Clone for InMemoryGraphStore {
19    fn clone(&self) -> Self {
20        Self {
21            files: self.files.clone(),
22            symbols: self.symbols.clone(),
23            edges: self.edges.clone(),
24            symbols_for_files_calls: AtomicUsize::new(
25                self.symbols_for_files_calls.load(Ordering::Relaxed),
26            ),
27            edges_streaming_calls: AtomicUsize::new(
28                self.edges_streaming_calls.load(Ordering::Relaxed),
29            ),
30        }
31    }
32}
33
34impl Default for InMemoryGraphStore {
35    fn default() -> Self {
36        Self {
37            files: Vec::new(),
38            symbols: Vec::new(),
39            edges: Vec::new(),
40            symbols_for_files_calls: AtomicUsize::new(0),
41            edges_streaming_calls: AtomicUsize::new(0),
42        }
43    }
44}
45
46impl InMemoryGraphStore {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    pub fn insert_file(&mut self, file: FileNode) {
52        self.files.push(file);
53    }
54
55    pub fn insert_symbol(&mut self, symbol: SymbolNode) {
56        self.symbols.push(symbol);
57    }
58
59    pub fn insert_edge(&mut self, edge: Edge) {
60        self.edges.push(edge);
61    }
62}
63
64impl GraphStore for InMemoryGraphStore {
65    fn upsert_file(&self, _file: &FileNode) -> Result<()> {
66        Ok(())
67    }
68    fn upsert_symbol(&self, _symbol: &SymbolNode) -> Result<()> {
69        Ok(())
70    }
71    fn upsert_edge(&self, _edge: &Edge) -> Result<()> {
72        Ok(())
73    }
74    fn get_file(&self, path: &Path) -> Result<Option<FileNode>> {
75        Ok(self.files.iter().find(|f| f.path == path).cloned())
76    }
77    fn get_symbol(&self, qualified_name: &str) -> Result<Option<SymbolNode>> {
78        Ok(self
79            .symbols
80            .iter()
81            .find(|s| s.qualified_name == qualified_name)
82            .cloned())
83    }
84    fn get_edges_from(&self, source: &str) -> Result<Vec<Edge>> {
85        Ok(self
86            .edges
87            .iter()
88            .filter(|e| e.source == source)
89            .cloned()
90            .collect())
91    }
92    fn get_edges_to(&self, target: &str) -> Result<Vec<Edge>> {
93        Ok(self
94            .edges
95            .iter()
96            .filter(|e| e.target == target)
97            .cloned()
98            .collect())
99    }
100    fn all_files(&self) -> Result<Vec<FileNode>> {
101        Ok(self.files.clone())
102    }
103    fn all_symbols(&self) -> Result<Vec<SymbolNode>> {
104        Ok(self.symbols.clone())
105    }
106    fn all_edges(&self) -> Result<Vec<Edge>> {
107        Ok(self.edges.clone())
108    }
109    fn remove_file(&self, _path: &Path) -> Result<()> {
110        Ok(())
111    }
112    fn remove_symbols_in_file(&self, _path: &Path) -> Result<()> {
113        Ok(())
114    }
115    fn stats(&self) -> Result<GraphStats> {
116        Ok(GraphStats {
117            files: self.files.len(),
118            symbols: self.symbols.len(),
119            edges: self.edges.len(),
120            entry_point_count: None,
121            avg_criticality: None,
122            clone_clusters: None,
123            duplication_pct: None,
124            most_duplicated: None,
125            avg_risk: None,
126            p90_risk: None,
127            community_count: None,
128            modularity: None,
129        })
130    }
131    fn find_by_name(&self, pattern: &str) -> Result<Vec<SymbolNode>> {
132        let exact: Vec<SymbolNode> = self
133            .symbols
134            .iter()
135            .filter(|s| s.name == pattern)
136            .cloned()
137            .collect();
138        if !exact.is_empty() {
139            return Ok(exact);
140        }
141        Ok(self
142            .symbols
143            .iter()
144            .filter(|s| s.name.starts_with(pattern))
145            .cloned()
146            .collect())
147    }
148
149    fn symbols_for_files(&self, paths: &[&Path]) -> Result<Vec<SymbolNode>> {
150        self.symbols_for_files_calls.fetch_add(1, Ordering::Relaxed);
151        Ok(self
152            .symbols
153            .iter()
154            .filter(|s| paths.contains(&&*s.location.file))
155            .cloned()
156            .collect())
157    }
158
159    fn edges_streaming(&self, callback: &mut dyn FnMut(Edge) -> Result<()>) -> Result<()> {
160        self.edges_streaming_calls.fetch_add(1, Ordering::Relaxed);
161        for edge in &self.edges {
162            callback(edge.clone())?;
163        }
164        Ok(())
165    }
166
167    fn store_file_data(
168        &self,
169        _file: &FileNode,
170        _symbols: &[SymbolNode],
171        _edges: &[Edge],
172    ) -> Result<()> {
173        Ok(())
174    }
175
176    fn remove_file_data(&self, _path: &Path) -> Result<()> {
177        Ok(())
178    }
179}
180
181impl SearchIndex for InMemoryGraphStore {
182    fn index_symbol(&self, _symbol: &SymbolNode) -> Result<()> {
183        Ok(())
184    }
185    fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchResult>> {
186        let results: Vec<SearchResult> = self
187            .symbols
188            .iter()
189            .filter(|s| s.name.contains(query) || s.qualified_name.contains(query))
190            .take(limit)
191            .map(|s| SearchResult {
192                qualified_name: s.qualified_name.clone(),
193                name: s.name.clone(),
194                kind: s.kind,
195                file_path: s.location.file.clone(),
196                score: 1.0,
197                score_source: None,
198            })
199            .collect();
200        Ok(results)
201    }
202    fn rebuild(&self) -> Result<()> {
203        Ok(())
204    }
205}
206
207/// Mock ParseProvider that returns canned FileData for testing.
208pub struct MockParseProvider {
209    pub results: Vec<FileData>,
210}
211
212impl MockParseProvider {
213    pub fn new(results: Vec<FileData>) -> Self {
214        Self { results }
215    }
216}
217
218impl ParseProvider for MockParseProvider {
219    fn parse_and_resolve(
220        &self,
221        _files: &[(PathBuf, Vec<u8>)],
222        _project_root: &Path,
223    ) -> crate::error::Result<Vec<FileData>> {
224        Ok(self.results.clone())
225    }
226}
227
228/// Mock FileSystem for testing.
229pub struct MockFileSystem {
230    pub files: Vec<(PathBuf, String)>,
231    pub hashes: Vec<(PathBuf, String)>,
232}
233
234impl MockFileSystem {
235    pub fn new(files: Vec<(PathBuf, String)>) -> Self {
236        Self {
237            files,
238            hashes: vec![],
239        }
240    }
241
242    pub fn with_hashes(mut self, hashes: Vec<(PathBuf, String)>) -> Self {
243        self.hashes = hashes;
244        self
245    }
246}
247
248impl crate::ports::FileSystem for MockFileSystem {
249    fn list_files(&self, _root: &Path, extensions: &[&str]) -> Result<Vec<PathBuf>> {
250        Ok(self
251            .files
252            .iter()
253            .filter(|(p, _)| {
254                p.extension()
255                    .and_then(|e| e.to_str())
256                    .is_some_and(|e| extensions.contains(&e))
257            })
258            .map(|(p, _)| p.clone())
259            .collect())
260    }
261
262    fn read_file(&self, path: &Path) -> Result<String> {
263        self.files
264            .iter()
265            .find(|(p, _)| p == path)
266            .map(|(_, content)| content.clone())
267            .ok_or_else(|| {
268                crate::error::CodeGraphError::Other(format!("file not found: {}", path.display()))
269            })
270    }
271
272    fn file_hash(&self, path: &Path) -> Result<String> {
273        if !self.hashes.is_empty() {
274            return self
275                .hashes
276                .iter()
277                .find(|(p, _)| p == path)
278                .map(|(_, h)| h.clone())
279                .ok_or_else(|| {
280                    crate::error::CodeGraphError::Other(format!(
281                        "file not found: {}",
282                        path.display()
283                    ))
284                });
285        }
286        Ok("mock_hash".to_string())
287    }
288}
289
290/// Mock GitProvider for testing.
291pub struct MockGitProvider {
292    pub modified: Vec<PathBuf>,
293}
294
295impl Default for MockGitProvider {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301impl MockGitProvider {
302    pub fn new() -> Self {
303        Self { modified: vec![] }
304    }
305
306    pub fn with_modified(files: Vec<PathBuf>) -> Self {
307        Self { modified: files }
308    }
309}
310
311impl crate::ports::GitProvider for MockGitProvider {
312    fn current_head(&self) -> Result<String> {
313        Ok("abcd1234".to_string())
314    }
315
316    fn changed_files(&self, _from: &str, _to: &str) -> Result<Vec<PathBuf>> {
317        Ok(vec![])
318    }
319
320    fn diff_hunks(&self, _from: &str, _to: Option<&str>) -> Result<Vec<DiffHunk>> {
321        Ok(vec![])
322    }
323
324    fn modified_files(&self) -> Result<Vec<PathBuf>> {
325        Ok(self.modified.clone())
326    }
327}
328
329// ---------------------------------------------------------------------------
330// InMemoryVectorStore
331// ---------------------------------------------------------------------------
332
333/// In-memory VectorStore implementation for testing.
334/// Uses cosine similarity for nearest-neighbour search.
335pub struct InMemoryVectorStore {
336    pub entries: std::sync::Mutex<Vec<EmbeddingEntry>>,
337}
338
339impl Default for InMemoryVectorStore {
340    fn default() -> Self {
341        Self::new()
342    }
343}
344
345impl InMemoryVectorStore {
346    pub fn new() -> Self {
347        Self {
348            entries: std::sync::Mutex::new(Vec::new()),
349        }
350    }
351}
352
353fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
354    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
355    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
356    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
357    if norm_a == 0.0 || norm_b == 0.0 {
358        return 0.0;
359    }
360    (dot / (norm_a * norm_b)) as f64
361}
362
363impl VectorStore for InMemoryVectorStore {
364    fn store_embeddings(&self, entries: &[EmbeddingEntry]) -> Result<()> {
365        let mut store = self.entries.lock().unwrap();
366        for entry in entries {
367            store.retain(|e| e.qualified_name != entry.qualified_name);
368            store.push(entry.clone());
369        }
370        Ok(())
371    }
372
373    fn search_nearest(&self, query_vec: &[f32], limit: usize) -> Result<Vec<(String, f64)>> {
374        let store = self.entries.lock().unwrap();
375        let mut scored: Vec<(String, f64)> = store
376            .iter()
377            .map(|e| {
378                let score = cosine_similarity(query_vec, &e.vector);
379                (e.qualified_name.clone(), score)
380            })
381            .collect();
382        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
383        scored.truncate(limit);
384        Ok(scored)
385    }
386
387    fn has_embeddings(&self) -> bool {
388        !self.entries.lock().unwrap().is_empty()
389    }
390
391    fn count(&self) -> Result<usize> {
392        Ok(self.entries.lock().unwrap().len())
393    }
394
395    fn remove_embeddings(&self, qualified_names: &[&str]) -> Result<()> {
396        let mut store = self.entries.lock().unwrap();
397        store.retain(|e| !qualified_names.contains(&e.qualified_name.as_str()));
398        Ok(())
399    }
400
401    fn get_stored_hashes(&self) -> Result<Vec<(String, String)>> {
402        let store = self.entries.lock().unwrap();
403        Ok(store
404            .iter()
405            .map(|e| (e.qualified_name.clone(), e.text_hash.clone()))
406            .collect())
407    }
408}
409
410// ---------------------------------------------------------------------------
411// InMemoryEmbeddingProvider
412// ---------------------------------------------------------------------------
413
414/// Deterministic mock EmbeddingProvider for testing.
415/// Returns a fixed-dimension vector derived from the text hash.
416#[derive(Clone)]
417pub struct InMemoryEmbeddingProvider {
418    pub dimension: usize,
419}
420
421impl Default for InMemoryEmbeddingProvider {
422    fn default() -> Self {
423        Self::new(4)
424    }
425}
426
427impl InMemoryEmbeddingProvider {
428    pub fn new(dimension: usize) -> Self {
429        Self { dimension }
430    }
431
432    fn text_to_vec(&self, text: &str) -> Vec<f32> {
433        let mut v = vec![0.0f32; self.dimension];
434        for (i, b) in text.bytes().enumerate() {
435            v[i % self.dimension] += b as f32;
436        }
437        // Normalise so cosine similarity is meaningful
438        let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
439        if norm > 0.0 {
440            for x in &mut v {
441                *x /= norm;
442            }
443        }
444        v
445    }
446}
447
448impl EmbeddingProvider for InMemoryEmbeddingProvider {
449    fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>> {
450        Ok(texts.iter().map(|t| self.text_to_vec(t)).collect())
451    }
452
453    fn embed_query(&self, text: &str) -> Result<Vec<f32>> {
454        Ok(self.text_to_vec(text))
455    }
456
457    fn dimension(&self) -> usize {
458        self.dimension
459    }
460}