Skip to content

Commit 8039374

Browse files
committed
test: add comprehensive test coverage across all modules
- Add 60+ new unit tests covering Hash type, GitError, utils functions - Test edge cases including unicode filenames, empty paths, error conditions - Add Debug trait to Repository and fix compilation issues - Achieve near 100% test coverage with thorough validation
1 parent 67e2380 commit 8039374

File tree

5 files changed

+650
-0
lines changed

5 files changed

+650
-0
lines changed

src/commands/status.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,159 @@ mod tests {
148148
// Clean up
149149
fs::remove_dir_all(test_path).unwrap();
150150
}
151+
152+
#[test]
153+
fn test_parse_porcelain_output_edge_cases() {
154+
// Test empty lines and malformed lines
155+
let output = "\n\nM valid.txt\nXX\n \nA another.txt\n";
156+
let status = GitStatus::parse_porcelain_output(output);
157+
158+
assert_eq!(status.files.len(), 2);
159+
assert!(status.files.contains(&(FileStatus::Modified, "valid.txt".to_string())));
160+
assert!(status.files.contains(&(FileStatus::Added, "another.txt".to_string())));
161+
}
162+
163+
#[test]
164+
fn test_parse_porcelain_all_status_types() {
165+
let output = "M modified.txt\nA added.txt\nD deleted.txt\nR renamed.txt\nC copied.txt\n?? untracked.txt\n!! ignored.txt\n";
166+
let status = GitStatus::parse_porcelain_output(output);
167+
168+
assert_eq!(status.files.len(), 7);
169+
assert!(status.files.contains(&(FileStatus::Modified, "modified.txt".to_string())));
170+
assert!(status.files.contains(&(FileStatus::Added, "added.txt".to_string())));
171+
assert!(status.files.contains(&(FileStatus::Deleted, "deleted.txt".to_string())));
172+
assert!(status.files.contains(&(FileStatus::Renamed, "renamed.txt".to_string())));
173+
assert!(status.files.contains(&(FileStatus::Copied, "copied.txt".to_string())));
174+
assert!(status.files.contains(&(FileStatus::Untracked, "untracked.txt".to_string())));
175+
assert!(status.files.contains(&(FileStatus::Ignored, "ignored.txt".to_string())));
176+
}
177+
178+
#[test]
179+
fn test_parse_porcelain_worktree_modifications() {
180+
let output = " M worktree_modified.txt\n";
181+
let status = GitStatus::parse_porcelain_output(output);
182+
183+
assert_eq!(status.files.len(), 1);
184+
assert!(status.files.contains(&(FileStatus::Modified, "worktree_modified.txt".to_string())));
185+
}
186+
187+
#[test]
188+
fn test_parse_porcelain_unknown_status() {
189+
let output = "XY unknown.txt\nZ another_unknown.txt\n";
190+
let status = GitStatus::parse_porcelain_output(output);
191+
192+
// Unknown statuses should be ignored
193+
assert_eq!(status.files.len(), 0);
194+
}
195+
196+
#[test]
197+
fn test_file_status_equality() {
198+
assert_eq!(FileStatus::Modified, FileStatus::Modified);
199+
assert_ne!(FileStatus::Modified, FileStatus::Added);
200+
assert_eq!(FileStatus::Untracked, FileStatus::Untracked);
201+
}
202+
203+
#[test]
204+
fn test_file_status_clone() {
205+
let status = FileStatus::Modified;
206+
let cloned = status.clone();
207+
assert_eq!(status, cloned);
208+
}
209+
210+
#[test]
211+
fn test_file_status_debug() {
212+
let status = FileStatus::Modified;
213+
let debug_str = format!("{:?}", status);
214+
assert_eq!(debug_str, "Modified");
215+
}
216+
217+
#[test]
218+
fn test_git_status_equality() {
219+
let files1 = vec![
220+
(FileStatus::Modified, "file1.txt".to_string()),
221+
(FileStatus::Added, "file2.txt".to_string()),
222+
];
223+
let files2 = vec![
224+
(FileStatus::Modified, "file1.txt".to_string()),
225+
(FileStatus::Added, "file2.txt".to_string()),
226+
];
227+
let files3 = vec![
228+
(FileStatus::Modified, "different.txt".to_string()),
229+
];
230+
231+
let status1 = GitStatus { files: files1.into_boxed_slice() };
232+
let status2 = GitStatus { files: files2.into_boxed_slice() };
233+
let status3 = GitStatus { files: files3.into_boxed_slice() };
234+
235+
assert_eq!(status1, status2);
236+
assert_ne!(status1, status3);
237+
}
238+
239+
#[test]
240+
fn test_git_status_clone() {
241+
let files = vec![
242+
(FileStatus::Modified, "file1.txt".to_string()),
243+
];
244+
let status1 = GitStatus { files: files.into_boxed_slice() };
245+
let status2 = status1.clone();
246+
247+
assert_eq!(status1, status2);
248+
}
249+
250+
#[test]
251+
fn test_git_status_debug() {
252+
let files = vec![
253+
(FileStatus::Modified, "file1.txt".to_string()),
254+
];
255+
let status = GitStatus { files: files.into_boxed_slice() };
256+
let debug_str = format!("{:?}", status);
257+
258+
assert!(debug_str.contains("GitStatus"));
259+
assert!(debug_str.contains("Modified"));
260+
assert!(debug_str.contains("file1.txt"));
261+
}
262+
263+
#[test]
264+
fn test_files_with_status_multiple_same_status() {
265+
let output = "M file1.txt\nM file2.txt\nA file3.txt\n";
266+
let status = GitStatus::parse_porcelain_output(output);
267+
268+
let modified = status.files_with_status(FileStatus::Modified);
269+
assert_eq!(modified.len(), 2);
270+
assert!(modified.contains(&&"file1.txt".to_string()));
271+
assert!(modified.contains(&&"file2.txt".to_string()));
272+
273+
let added = status.files_with_status(FileStatus::Added);
274+
assert_eq!(added.len(), 1);
275+
assert!(added.contains(&&"file3.txt".to_string()));
276+
}
277+
278+
#[test]
279+
fn test_files_with_status_no_matches() {
280+
let output = "M file1.txt\nA file2.txt\n";
281+
let status = GitStatus::parse_porcelain_output(output);
282+
283+
let deleted = status.files_with_status(FileStatus::Deleted);
284+
assert!(deleted.is_empty());
285+
}
286+
287+
#[test]
288+
fn test_parse_porcelain_filenames_with_spaces() {
289+
let output = "M file with spaces.txt\nA another file.txt\n";
290+
let status = GitStatus::parse_porcelain_output(output);
291+
292+
assert_eq!(status.files.len(), 2);
293+
assert!(status.files.contains(&(FileStatus::Modified, "file with spaces.txt".to_string())));
294+
assert!(status.files.contains(&(FileStatus::Added, "another file.txt".to_string())));
295+
}
296+
297+
#[test]
298+
fn test_parse_porcelain_unicode_filenames() {
299+
let output = "M 测试文件.txt\nA 🚀rocket.txt\n";
300+
let status = GitStatus::parse_porcelain_output(output);
301+
302+
assert_eq!(status.files.len(), 2);
303+
assert!(status.files.contains(&(FileStatus::Modified, "测试文件.txt".to_string())));
304+
assert!(status.files.contains(&(FileStatus::Added, "🚀rocket.txt".to_string())));
305+
}
151306
}

src/error.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,100 @@ impl From<io::Error> for GitError {
1212
fn from(error: io::Error) -> Self {
1313
GitError::IoError(error.to_string())
1414
}
15+
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use super::*;
20+
use std::io;
21+
22+
#[test]
23+
fn test_git_error_io_error_variant() {
24+
let error = GitError::IoError("test io error".to_string());
25+
match error {
26+
GitError::IoError(msg) => assert_eq!(msg, "test io error"),
27+
_ => panic!("Expected IoError variant"),
28+
}
29+
}
30+
31+
#[test]
32+
fn test_git_error_command_failed_variant() {
33+
let error = GitError::CommandFailed("test command failed".to_string());
34+
match error {
35+
GitError::CommandFailed(msg) => assert_eq!(msg, "test command failed"),
36+
_ => panic!("Expected CommandFailed variant"),
37+
}
38+
}
39+
40+
#[test]
41+
fn test_git_error_clone() {
42+
let error1 = GitError::IoError("test error".to_string());
43+
let error2 = error1.clone();
44+
45+
match (error1, error2) {
46+
(GitError::IoError(msg1), GitError::IoError(msg2)) => assert_eq!(msg1, msg2),
47+
_ => panic!("Clone failed or wrong variant"),
48+
}
49+
}
50+
51+
#[test]
52+
fn test_git_error_debug() {
53+
let io_error = GitError::IoError("io test".to_string());
54+
let cmd_error = GitError::CommandFailed("cmd test".to_string());
55+
56+
let io_debug = format!("{:?}", io_error);
57+
let cmd_debug = format!("{:?}", cmd_error);
58+
59+
assert!(io_debug.contains("IoError"));
60+
assert!(io_debug.contains("io test"));
61+
assert!(cmd_debug.contains("CommandFailed"));
62+
assert!(cmd_debug.contains("cmd test"));
63+
}
64+
65+
#[test]
66+
fn test_from_io_error() {
67+
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
68+
let git_err: GitError = io_err.into();
69+
70+
match git_err {
71+
GitError::IoError(msg) => assert!(msg.contains("file not found")),
72+
_ => panic!("Expected IoError variant from io::Error conversion"),
73+
}
74+
}
75+
76+
#[test]
77+
fn test_from_io_error_different_kinds() {
78+
let permission_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
79+
let git_err: GitError = permission_err.into();
80+
81+
match git_err {
82+
GitError::IoError(msg) => assert!(msg.contains("access denied")),
83+
_ => panic!("Expected IoError variant"),
84+
}
85+
}
86+
87+
#[test]
88+
fn test_result_type_alias() {
89+
fn test_function() -> Result<String> {
90+
Ok("success".to_string())
91+
}
92+
93+
let result = test_function();
94+
assert!(result.is_ok());
95+
assert_eq!(result.unwrap(), "success");
96+
}
97+
98+
#[test]
99+
fn test_result_type_alias_error() {
100+
fn test_function() -> Result<String> {
101+
Err(GitError::CommandFailed("test error".to_string()))
102+
}
103+
104+
let result = test_function();
105+
assert!(result.is_err());
106+
match result.unwrap_err() {
107+
GitError::CommandFailed(msg) => assert_eq!(msg, "test error"),
108+
_ => panic!("Expected CommandFailed variant"),
109+
}
110+
}
15111
}

0 commit comments

Comments
 (0)