Skip to main content

termesh_test_support/
lib.rs

1//! Fixtures, fake services, recorded streams, render snapshots. Phase 00+.
2//!
3//! Every service trait ships with a fake here so logic is testable without the OS
4//! (CONTRIBUTING.md invariants). Keeping them in one crate — rather than behind `cfg(test)`
5//! in each service — is what lets `app`-level tests and render snapshots use them too.
6#![forbid(unsafe_code)]
7
8pub mod fake_clipboard;
9pub mod fake_fs;
10pub mod fake_git;
11pub mod fake_permission_store;
12pub mod fake_tasks;
13pub mod scripted_agent;
14pub mod scripted_lsp;
15pub mod scripted_pty;
16pub mod scripted_search;
17
18pub use fake_clipboard::FakeClipboard;
19pub use fake_fs::FakeFileSystem;
20pub use fake_git::{FakeGitCall, FakeGitControl, FakeGitService};
21pub use fake_permission_store::FakePermissionStore;
22pub use fake_tasks::FakeTaskService;
23pub use scripted_agent::{ScriptedAgent, ScriptedUpdate};
24pub use scripted_lsp::{FakeLspCall, FakeLspControl, ScriptedLanguageServer};
25pub use scripted_pty::{ScriptedPty, ScriptedPtyControl};
26pub use scripted_search::{ScriptedSearch, ScriptedSearchControl};
27
28use std::path::{Path, PathBuf};
29use std::sync::atomic::{AtomicUsize, Ordering};
30use termesh_core::{DirEntryInfo, FsResult};
31use termesh_filesystem::FileSystemService;
32
33/// Decorates the in-memory filesystem with algorithmic I/O counters. Tests assert call
34/// counts rather than elapsed time, so a loaded CI runner cannot turn a regression into
35/// a flaky budget failure.
36pub struct CountingFileSystem {
37    inner: FakeFileSystem,
38    read_dir_calls: AtomicUsize,
39    read_file_calls: AtomicUsize,
40}
41
42impl CountingFileSystem {
43    pub fn new(inner: FakeFileSystem) -> Self {
44        Self { inner, read_dir_calls: AtomicUsize::new(0), read_file_calls: AtomicUsize::new(0) }
45    }
46
47    pub fn read_dir_calls(&self) -> usize {
48        self.read_dir_calls.load(Ordering::Relaxed)
49    }
50
51    pub fn read_file_calls(&self) -> usize {
52        self.read_file_calls.load(Ordering::Relaxed)
53    }
54}
55
56impl FileSystemService for CountingFileSystem {
57    fn read_dir(&self, path: &Path) -> FsResult<Vec<DirEntryInfo>> {
58        self.read_dir_calls.fetch_add(1, Ordering::Relaxed);
59        self.inner.read_dir(path)
60    }
61
62    fn read_file(&self, path: &Path) -> FsResult<Vec<u8>> {
63        self.read_file_calls.fetch_add(1, Ordering::Relaxed);
64        self.inner.read_file(path)
65    }
66
67    fn create_file(&self, path: &Path) -> FsResult<()> {
68        self.inner.create_file(path)
69    }
70
71    fn write_file(&self, path: &Path, contents: &[u8]) -> FsResult<()> {
72        self.inner.write_file(path, contents)
73    }
74
75    fn create_dir(&self, path: &Path) -> FsResult<()> {
76        self.inner.create_dir(path)
77    }
78
79    fn rename(&self, from: &Path, to: &Path) -> FsResult<()> {
80        self.inner.rename(from, to)
81    }
82
83    fn remove_file(&self, path: &Path) -> FsResult<()> {
84        self.inner.remove_file(path)
85    }
86
87    fn remove_dir_all(&self, path: &Path) -> FsResult<()> {
88        self.inner.remove_dir_all(path)
89    }
90
91    fn canonicalize(&self, path: &Path) -> FsResult<PathBuf> {
92        self.inner.canonicalize(path)
93    }
94}
95
96/// A deep, wide shape whose unexplored branches stand in for an arbitrarily large
97/// repository. The laziness tests count directory calls, so fully materialising
98/// `width.pow(depth)` nodes would consume memory without strengthening the assertion.
99pub fn synthetic_tree(depth: usize, width: usize) -> FakeFileSystem {
100    let fs = FakeFileSystem::new();
101    fs.add_file("/big/Cargo.toml", b"[package]\nname = \"big\"\nversion = \"0.0.0\"\n");
102    let mut trunk = PathBuf::from("/big");
103    for level in 0..depth {
104        for branch in 0..width {
105            let directory = trunk.join(format!("level-{level}-branch-{branch}"));
106            fs.add_dir(&directory);
107            fs.add_file(directory.join("source.rs"), b"fn synthetic() {}\n");
108        }
109        trunk.push(format!("level-{level}-branch-0"));
110    }
111    fs
112}
113
114#[cfg(test)]
115mod phase_05_search_tests {
116    use std::path::PathBuf;
117    use std::sync::atomic::AtomicBool;
118    use std::sync::{Arc, Mutex};
119
120    use termesh_core::{SearchEvent, SearchMode, SearchRequest, SearchRequestId};
121    use termesh_search::{SearchEventSink, SearchService};
122
123    use crate::ScriptedSearch;
124
125    #[test]
126    fn scripted_search_records_requests_and_replays_the_matching_script() {
127        let request = SearchRequest {
128            id: SearchRequestId::new(8),
129            root: PathBuf::from("/repo"),
130            mode: SearchMode::Files,
131            query: "main".into(),
132            limit: 20,
133        };
134        let event = SearchEvent::Finished { id: request.id, truncated: false };
135        let mut search =
136            ScriptedSearch::new().with_script(SearchMode::Files, "main", vec![event.clone()]);
137        let control = search.control();
138        let received = Arc::new(Mutex::new(Vec::new()));
139        let received_for_sink = received.clone();
140        let sink: SearchEventSink = Arc::new(move |event| {
141            received_for_sink.lock().unwrap().push(event);
142        });
143        search.search(&request, &AtomicBool::new(false), &sink).unwrap();
144        assert_eq!(control.requests(), vec![request]);
145        assert_eq!(*received.lock().unwrap(), vec![event]);
146    }
147}