Skip to content

Commit f99c2f3

Browse files
committed
test: add comprehensive unit tests for core modules
Add unit tests covering stats, error handling, output formatters, and the main analyze function. Tests include edge cases for empty inputs, file traversal, gitignore handling, and output format validation.
1 parent adfe489 commit f99c2f3

7 files changed

Lines changed: 825 additions & 1 deletion

File tree

crates/codelens-core/src/analyzer/stats.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ mod duration_serde {
257257
duration.as_secs_f64().serialize(serializer)
258258
}
259259

260+
#[allow(dead_code)]
260261
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
261262
where
262263
D: Deserializer<'de>,
@@ -265,3 +266,213 @@ mod duration_serde {
265266
Ok(Duration::from_secs_f64(secs))
266267
}
267268
}
269+
270+
#[cfg(test)]
271+
mod tests {
272+
use super::*;
273+
274+
#[test]
275+
fn test_line_stats_default() {
276+
let stats = LineStats::default();
277+
assert_eq!(stats.total, 0);
278+
assert_eq!(stats.code, 0);
279+
assert_eq!(stats.comment, 0);
280+
assert_eq!(stats.blank, 0);
281+
}
282+
283+
#[test]
284+
fn test_line_stats_add() {
285+
let mut stats1 = LineStats {
286+
total: 100,
287+
code: 80,
288+
comment: 10,
289+
blank: 10,
290+
};
291+
let stats2 = LineStats {
292+
total: 50,
293+
code: 40,
294+
comment: 5,
295+
blank: 5,
296+
};
297+
298+
stats1.add(&stats2);
299+
300+
assert_eq!(stats1.total, 150);
301+
assert_eq!(stats1.code, 120);
302+
assert_eq!(stats1.comment, 15);
303+
assert_eq!(stats1.blank, 15);
304+
}
305+
306+
#[test]
307+
fn test_line_stats_add_trait() {
308+
let stats1 = LineStats {
309+
total: 100,
310+
code: 80,
311+
comment: 10,
312+
blank: 10,
313+
};
314+
let stats2 = LineStats {
315+
total: 50,
316+
code: 40,
317+
comment: 5,
318+
blank: 5,
319+
};
320+
321+
let result = stats1 + stats2;
322+
323+
assert_eq!(result.total, 150);
324+
assert_eq!(result.code, 120);
325+
}
326+
327+
#[test]
328+
fn test_line_stats_add_assign() {
329+
let mut stats1 = LineStats {
330+
total: 100,
331+
code: 80,
332+
comment: 10,
333+
blank: 10,
334+
};
335+
let stats2 = LineStats {
336+
total: 50,
337+
code: 40,
338+
comment: 5,
339+
blank: 5,
340+
};
341+
342+
stats1 += stats2;
343+
344+
assert_eq!(stats1.total, 150);
345+
assert_eq!(stats1.code, 120);
346+
}
347+
348+
#[test]
349+
fn test_complexity_add() {
350+
let mut c1 = Complexity {
351+
functions: 10,
352+
cyclomatic: 20,
353+
max_depth: 5,
354+
avg_func_lines: 0.0,
355+
};
356+
let c2 = Complexity {
357+
functions: 5,
358+
cyclomatic: 10,
359+
max_depth: 8,
360+
avg_func_lines: 0.0,
361+
};
362+
363+
c1.add(&c2);
364+
365+
assert_eq!(c1.functions, 15);
366+
assert_eq!(c1.cyclomatic, 30);
367+
assert_eq!(c1.max_depth, 8); // max of 5 and 8
368+
}
369+
370+
#[test]
371+
fn test_size_distribution() {
372+
let mut dist = SizeDistribution::default();
373+
374+
dist.add(500); // tiny: < 1KB
375+
dist.add(1024); // small: 1KB - 10KB
376+
dist.add(5000); // small
377+
dist.add(15000); // medium: 10KB - 100KB
378+
dist.add(500_000); // large: 100KB - 1MB
379+
dist.add(2_000_000); // huge: > 1MB
380+
381+
assert_eq!(dist.tiny, 1);
382+
assert_eq!(dist.small, 2);
383+
assert_eq!(dist.medium, 1);
384+
assert_eq!(dist.large, 1);
385+
assert_eq!(dist.huge, 1);
386+
}
387+
388+
#[test]
389+
fn test_summary_from_file_stats() {
390+
let files = vec![
391+
FileStats {
392+
path: PathBuf::from("src/main.rs"),
393+
language: "Rust".to_string(),
394+
lines: LineStats {
395+
total: 100,
396+
code: 80,
397+
comment: 10,
398+
blank: 10,
399+
},
400+
size: 2000,
401+
complexity: Complexity {
402+
functions: 5,
403+
cyclomatic: 10,
404+
max_depth: 3,
405+
avg_func_lines: 16.0,
406+
},
407+
},
408+
FileStats {
409+
path: PathBuf::from("src/lib.rs"),
410+
language: "Rust".to_string(),
411+
lines: LineStats {
412+
total: 50,
413+
code: 40,
414+
comment: 5,
415+
blank: 5,
416+
},
417+
size: 1000,
418+
complexity: Complexity {
419+
functions: 3,
420+
cyclomatic: 6,
421+
max_depth: 2,
422+
avg_func_lines: 13.3,
423+
},
424+
},
425+
FileStats {
426+
path: PathBuf::from("test.py"),
427+
language: "Python".to_string(),
428+
lines: LineStats {
429+
total: 30,
430+
code: 20,
431+
comment: 5,
432+
blank: 5,
433+
},
434+
size: 500,
435+
complexity: Complexity {
436+
functions: 2,
437+
cyclomatic: 4,
438+
max_depth: 2,
439+
avg_func_lines: 10.0,
440+
},
441+
},
442+
];
443+
444+
let summary = Summary::from_file_stats(&files);
445+
446+
assert_eq!(summary.total_files, 3);
447+
assert_eq!(summary.lines.total, 180);
448+
assert_eq!(summary.lines.code, 140);
449+
assert_eq!(summary.total_size, 3500);
450+
assert_eq!(summary.by_language.len(), 2);
451+
assert_eq!(summary.complexity.functions, 10);
452+
453+
// Rust should be first (more code lines)
454+
let first_lang = summary.by_language.keys().next().unwrap();
455+
assert_eq!(first_lang, "Rust");
456+
457+
let rust_stats = summary.by_language.get("Rust").unwrap();
458+
assert_eq!(rust_stats.files, 2);
459+
assert_eq!(rust_stats.lines.code, 120);
460+
}
461+
462+
#[test]
463+
fn test_summary_empty() {
464+
let summary = Summary::from_file_stats(&[]);
465+
466+
assert_eq!(summary.total_files, 0);
467+
assert_eq!(summary.lines.total, 0);
468+
assert!(summary.by_language.is_empty());
469+
}
470+
471+
#[test]
472+
fn test_file_stats_default() {
473+
let stats = FileStats::default();
474+
assert!(stats.path.as_os_str().is_empty());
475+
assert!(stats.language.is_empty());
476+
assert_eq!(stats.size, 0);
477+
}
478+
}

crates/codelens-core/src/error.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,60 @@ pub enum Error {
6969

7070
/// Result type alias for codelens-core operations.
7171
pub type Result<T> = std::result::Result<T, Error>;
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::*;
76+
use std::io;
77+
78+
#[test]
79+
fn test_error_display_directory_not_found() {
80+
let err = Error::DirectoryNotFound {
81+
path: PathBuf::from("/nonexistent"),
82+
};
83+
assert_eq!(
84+
err.to_string(),
85+
"directory not found: /nonexistent"
86+
);
87+
}
88+
89+
#[test]
90+
fn test_error_display_invalid_language() {
91+
let err = Error::InvalidLanguage {
92+
name: "Test".to_string(),
93+
reason: "missing extensions".to_string(),
94+
};
95+
assert_eq!(
96+
err.to_string(),
97+
"invalid language definition 'Test': missing extensions"
98+
);
99+
}
100+
101+
#[test]
102+
fn test_error_display_invalid_regex() {
103+
let regex_err = regex::Regex::new("[invalid").unwrap_err();
104+
let err = Error::InvalidRegex {
105+
pattern: "[invalid".to_string(),
106+
source: regex_err,
107+
};
108+
assert!(err.to_string().contains("invalid regex pattern"));
109+
}
110+
111+
#[test]
112+
fn test_error_from_io_error() {
113+
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
114+
let err: Error = Error::OutputWrite(io_err);
115+
assert!(err.to_string().contains("failed to write output"));
116+
}
117+
118+
#[test]
119+
fn test_result_type() {
120+
let ok: Result<i32> = Ok(42);
121+
assert_eq!(ok.unwrap(), 42);
122+
123+
let err: Result<i32> = Err(Error::DirectoryNotFound {
124+
path: PathBuf::from("/test"),
125+
});
126+
assert!(err.is_err());
127+
}
128+
}

0 commit comments

Comments
 (0)