termesh_test_support/
fake_git.rs1use 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}