Skip to main content

termesh_test_support/
fake_git.rs

1use std::collections::VecDeque;
2use std::path::{Path, PathBuf};
3use std::sync::{Arc, Mutex};
4
5use termesh_core::{
6    GitBranch, GitDiffTarget, GitFailure, GitFailureKind, GitFileDiff, GitOperation,
7    GitRepositorySnapshot, GitResult,
8};
9use termesh_git::GitService;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum FakeGitCall {
13    Snapshot { root: PathBuf },
14    Diff { root: PathBuf, path: PathBuf, target: GitDiffTarget },
15    Branches { root: PathBuf },
16    Execute { root: PathBuf, operation: GitOperation },
17}
18
19#[derive(Clone)]
20pub struct FakeGitControl {
21    calls: Arc<Mutex<Vec<FakeGitCall>>>,
22}
23
24impl FakeGitControl {
25    pub fn calls(&self) -> Vec<FakeGitCall> {
26        self.calls.lock().expect("fake Git call log poisoned").clone()
27    }
28}
29
30#[derive(Default)]
31pub struct FakeGitService {
32    snapshots: VecDeque<GitResult<GitRepositorySnapshot>>,
33    diffs: VecDeque<GitResult<GitFileDiff>>,
34    branches: VecDeque<GitResult<Vec<GitBranch>>>,
35    executions: VecDeque<GitResult<String>>,
36    calls: Arc<Mutex<Vec<FakeGitCall>>>,
37}
38
39impl FakeGitService {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    pub fn control(&self) -> FakeGitControl {
45        FakeGitControl { calls: self.calls.clone() }
46    }
47
48    pub fn with_snapshot_result(mut self, result: GitResult<GitRepositorySnapshot>) -> Self {
49        self.snapshots.push_back(result);
50        self
51    }
52
53    pub fn with_diff_result(mut self, result: GitResult<GitFileDiff>) -> Self {
54        self.diffs.push_back(result);
55        self
56    }
57
58    pub fn with_branches_result(mut self, result: GitResult<Vec<GitBranch>>) -> Self {
59        self.branches.push_back(result);
60        self
61    }
62
63    pub fn with_execute_result(mut self, result: GitResult<String>) -> Self {
64        self.executions.push_back(result);
65        self
66    }
67
68    fn record(&self, call: FakeGitCall) {
69        self.calls.lock().expect("fake Git call log poisoned").push(call);
70    }
71}
72
73impl GitService for FakeGitService {
74    fn snapshot(&mut self, root: &Path) -> GitResult<GitRepositorySnapshot> {
75        self.record(FakeGitCall::Snapshot { root: root.to_path_buf() });
76        self.snapshots.pop_front().unwrap_or_else(|| Err(missing_result("snapshot")))
77    }
78
79    fn diff(&mut self, root: &Path, path: &Path, target: GitDiffTarget) -> GitResult<GitFileDiff> {
80        self.record(FakeGitCall::Diff {
81            root: root.to_path_buf(),
82            path: path.to_path_buf(),
83            target,
84        });
85        self.diffs.pop_front().unwrap_or_else(|| Err(missing_result("diff")))
86    }
87
88    fn branches(&mut self, root: &Path) -> GitResult<Vec<GitBranch>> {
89        self.record(FakeGitCall::Branches { root: root.to_path_buf() });
90        self.branches.pop_front().unwrap_or_else(|| Err(missing_result("branches")))
91    }
92
93    fn execute(&mut self, root: &Path, operation: &GitOperation) -> GitResult<String> {
94        self.record(FakeGitCall::Execute {
95            root: root.to_path_buf(),
96            operation: operation.clone(),
97        });
98        self.executions.pop_front().unwrap_or_else(|| Err(missing_result("execute")))
99    }
100}
101
102fn missing_result(method: &str) -> GitFailure {
103    GitFailure {
104        kind: GitFailureKind::Command,
105        message: format!("no scripted Git {method} result"),
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use termesh_core::{
112        GitBranch, GitBranchStatus, GitContextDiff, GitDiffTarget, GitOperation,
113        GitRepositorySnapshot,
114    };
115    use termesh_git::GitService;
116
117    use super::{FakeGitCall, FakeGitService};
118
119    fn snapshot() -> GitRepositorySnapshot {
120        GitRepositorySnapshot {
121            repository_root: "/repo".into(),
122            workspace_root: "/repo/workspace".into(),
123            branch: GitBranchStatus::default(),
124            files: Vec::new(),
125            context_diff: GitContextDiff::default(),
126        }
127    }
128
129    #[test]
130    fn queued_results_are_returned_and_full_calls_are_recorded_in_order() {
131        let mut service = FakeGitService::new()
132            .with_snapshot_result(Ok(snapshot()))
133            .with_branches_result(Ok(vec![GitBranch { name: "main".into(), current: true }]))
134            .with_execute_result(Ok("fetched".into()));
135        let control = service.control();
136
137        service.snapshot("/repo/workspace".as_ref()).unwrap();
138        service.branches("/repo/workspace".as_ref()).unwrap();
139        service.execute("/repo/workspace".as_ref(), &GitOperation::Fetch).unwrap();
140
141        assert_eq!(
142            control.calls(),
143            vec![
144                FakeGitCall::Snapshot { root: "/repo/workspace".into() },
145                FakeGitCall::Branches { root: "/repo/workspace".into() },
146                FakeGitCall::Execute {
147                    root: "/repo/workspace".into(),
148                    operation: GitOperation::Fetch,
149                },
150            ]
151        );
152    }
153
154    #[test]
155    fn records_diff_paths_and_targets() {
156        let mut service = FakeGitService::new().with_diff_result(Err(termesh_core::GitFailure {
157            kind: termesh_core::GitFailureKind::Command,
158            message: "scripted".into(),
159        }));
160        let control = service.control();
161        let _ = service.diff("/repo".as_ref(), "src/lib.rs".as_ref(), GitDiffTarget::Index);
162        assert_eq!(
163            control.calls(),
164            vec![FakeGitCall::Diff {
165                root: "/repo".into(),
166                path: "src/lib.rs".into(),
167                target: GitDiffTarget::Index,
168            }]
169        );
170    }
171}