1crate::ix!();
3
4#[derive(Debug,Clone)]
5pub struct CrateAnalysis {
6
7 total_file_size: u64,
9
10 total_lines_of_code: usize,
12
13 total_source_files: usize,
15
16 total_test_files: usize,
18
19 largest_file_size: u64,
21
22 smallest_file_size: u64,
24}
25
26impl CrateAnalysis {
27
28 pub async fn new(crate_handle: &(impl HasTestsDirectory + GetTestFiles + GetSourceFilesWithExclusions)) -> Result<Self, WorkspaceError>
30 {
31
32 let mut total_file_size = 0;
33 let mut total_lines_of_code = 0;
34 let mut total_source_files = 0;
35 let mut total_test_files = 0;
36 let mut largest_file_size = 0;
37 let mut smallest_file_size = u64::MAX;
38
39 let source_files = crate_handle.source_files_excluding(&[]).await?;
41
42 for file in source_files {
43
44 let file_size = file.file_size().await?;
45 let lines_of_code = count_lines_in_file(&file).await?;
46
47 total_file_size += file_size;
48 total_lines_of_code += lines_of_code;
49 total_source_files += 1;
50
51 largest_file_size = largest_file_size.max(file_size);
52 smallest_file_size = smallest_file_size.min(file_size);
53 }
54
55 if crate_handle.has_tests_directory() {
57
58 let test_files = crate_handle.test_files().await?;
59
60 for file in test_files {
61
62 let file_size = file.file_size().await?;
63 let lines_of_code = count_lines_in_file(&file).await?;
64
65 total_file_size += file_size;
66 total_lines_of_code += lines_of_code;
67 total_test_files += 1;
68
69 largest_file_size = largest_file_size.max(file_size);
70 smallest_file_size = smallest_file_size.min(file_size);
71 }
72 }
73
74 Ok(CrateAnalysis {
75 total_file_size,
76 total_lines_of_code,
77 total_source_files,
78 total_test_files,
79 largest_file_size,
80 smallest_file_size,
81 })
82 }
83
84 pub fn total_file_size(&self) -> u64 {
86 self.total_file_size
87 }
88
89 pub fn total_lines_of_code(&self) -> usize {
90 self.total_lines_of_code
91 }
92
93 pub fn total_source_files(&self) -> usize {
94 self.total_source_files
95 }
96
97 pub fn total_test_files(&self) -> usize {
98 self.total_test_files
99 }
100
101 pub fn largest_file_size(&self) -> u64 {
102 self.largest_file_size
103 }
104
105 pub fn smallest_file_size(&self) -> u64 {
106 self.smallest_file_size
107 }
108}
109
110#[cfg(test)]
111mod test_crate_analysis {
112 use super::*;
113 use tempfile::TempDir;
114 use std::path::PathBuf;
115 use tokio::fs::{File, create_dir_all};
116 use tokio::io::AsyncWriteExt;
117 use workspacer_3p::tokio;
118 use crate::WorkspaceError;
119
120 struct MockCrateHandle {
127 root_dir: TempDir,
128 has_tests: bool,
129 src_files: Vec<PathBuf>,
130 test_files: Vec<PathBuf>,
131 }
132
133 impl MockCrateHandle {
134 fn new() -> Self {
135 Self {
136 root_dir: tempfile::tempdir().unwrap(),
137 has_tests: false,
138 src_files: vec![],
139 test_files: vec![],
140 }
141 }
142
143 async fn add_src_file(&mut self, name: &str, contents: &str) {
145 let src_dir = self.root_dir.path().join("src");
146 create_dir_all(&src_dir).await.unwrap();
147
148 let path = src_dir.join(name);
149 let mut file = File::create(&path).await.unwrap();
150 file.write_all(contents.as_bytes()).await.unwrap();
151 file.sync_all().await.unwrap();
153 drop(file); let meta = tokio::fs::metadata(&path).await.unwrap();
157 assert_eq!(
158 meta.len(),
159 contents.len() as u64,
160 "File size does not match expected!"
161 );
162
163 self.src_files.push(path);
164 }
165
166
167 async fn add_test_file(&mut self, name: &str, contents: &str) {
169 let tests_dir = self.root_dir.path().join("tests");
170 create_dir_all(&tests_dir).await.unwrap();
171
172 let path = tests_dir.join(name);
173 let mut file = File::create(&path).await.unwrap();
174 file.write_all(contents.as_bytes()).await.unwrap();
175 file.sync_all().await.unwrap();
177 drop(file); let meta = tokio::fs::metadata(&path).await.unwrap();
181 assert_eq!(meta.len(), contents.len() as u64, "File size does not match expected!");
182
183 self.test_files.push(path);
184 self.has_tests = true;
185 }
186 }
187
188 #[async_trait]
189 impl GetSourceFilesWithExclusions for MockCrateHandle {
190 async fn source_files_excluding(
191 &self,
192 _exclude_files: &[&str],
193 ) -> Result<Vec<PathBuf>, CrateError> {
194 Ok(self.src_files.clone())
196 }
197 }
198
199 #[async_trait]
200 impl GetTestFiles for MockCrateHandle {
201 async fn test_files(&self) -> Result<Vec<PathBuf>, CrateError> {
202 Ok(self.test_files.clone())
203 }
204 }
205
206 impl HasTestsDirectory for MockCrateHandle {
207 fn has_tests_directory(&self) -> bool {
208 self.has_tests
209 }
210 }
211
212 #[tokio::test]
217 async fn test_no_src_files_no_tests() {
218 let handle = MockCrateHandle::new();
219 let analysis = CrateAnalysis::new(&handle).await.unwrap();
220 assert_eq!(analysis.total_source_files(), 0);
221 assert_eq!(analysis.total_test_files(), 0);
222 assert_eq!(analysis.total_lines_of_code(), 0);
223 assert_eq!(analysis.total_file_size(), 0);
224 assert_eq!(analysis.largest_file_size(), 0);
228 assert_eq!(analysis.smallest_file_size(), u64::MAX);
229 }
230
231 #[tokio::test]
232 async fn test_single_src_file() {
233 let mut handle = MockCrateHandle::new();
234 let contents = "fn main(){\nprintln!(\"hi\");}";
238 handle.add_src_file("main.rs", contents).await;
239
240 let analysis = CrateAnalysis::new(&handle).await.unwrap();
241 assert_eq!(analysis.total_source_files(), 1);
242 assert_eq!(analysis.total_test_files(), 0);
243 assert_eq!(analysis.total_lines_of_code(), 2);
244 let expected_size = contents.len() as u64;
246 assert_eq!(analysis.total_file_size(), expected_size);
247 assert_eq!(analysis.largest_file_size(), expected_size);
248 assert_eq!(analysis.smallest_file_size(), expected_size);
249 }
250
251 #[tokio::test]
252 async fn test_has_tests() {
253 let mut handle = MockCrateHandle::new();
254 let contents = "testline\nanother\n";
255 handle.add_test_file("test_something.rs", contents).await;
256
257 let test_path = handle.test_files.last().unwrap();
259 let meta = tokio::fs::metadata(&test_path).await.unwrap();
260 assert_eq!(meta.len(), contents.len() as u64, "File not the size we expect!");
261
262 let analysis = CrateAnalysis::new(&handle).await.unwrap();
263
264 assert_eq!(analysis.total_source_files(), 0);
265 assert_eq!(analysis.total_test_files(), 1);
266 assert_eq!(analysis.total_lines_of_code(), 2);
267 let expected_size = contents.len() as u64;
268 assert_eq!(analysis.total_file_size(), expected_size);assert_eq!(analysis.largest_file_size(), expected_size);
270 assert_eq!(analysis.smallest_file_size(), expected_size);
271 }
272
273 #[tokio::test]
278 async fn test_multiple_source_files() {
279 let mut handle = MockCrateHandle::new();
280 let contents_a = "let x = 1;";
282 let contents_b = "fn foo() {}\nfn bar() {}";
283 let contents_c = "";
284 handle.add_src_file("file_a.rs", contents_a).await;
285 handle.add_src_file("file_b.rs", contents_b).await;
286 handle.add_src_file("file_c.rs", contents_c).await;
287
288 let analysis = CrateAnalysis::new(&handle).await.unwrap();
289 assert_eq!(analysis.total_source_files(), 3);
290 assert_eq!(analysis.total_test_files(), 0);
291
292 let lines_a = 1; let lines_b = 2; let lines_c = 0; let total_lines = lines_a + lines_b + lines_c;
297 assert_eq!(analysis.total_lines_of_code(), total_lines);
298
299 let size_a = contents_a.len() as u64;
301 let size_b = contents_b.len() as u64;
302 let size_c = contents_c.len() as u64;
303 let total_size = size_a + size_b + size_c;
304 assert_eq!(analysis.total_file_size(), total_size);
305
306 let largest = size_a.max(size_b).max(size_c);
308 let smallest = size_a.min(size_b).min(size_c);
309 assert_eq!(analysis.largest_file_size(), largest);
310 assert_eq!(analysis.smallest_file_size(), smallest);
311 }
312
313 #[tokio::test]
314 async fn test_multiple_test_files() {
315 let mut handle = MockCrateHandle::new();
316 let test_content_1 = "mod test1 {}\nmod test2 {}";
318 let test_content_2 = "mod test3 {}";
319 handle.add_test_file("test1.rs", test_content_1).await;
320 handle.add_test_file("test2.rs", test_content_2).await;
321
322 let analysis = CrateAnalysis::new(&handle).await.unwrap();
323 assert_eq!(analysis.total_source_files(), 0);
324 assert_eq!(analysis.total_test_files(), 2);
325
326 let lines_1 = 2; let lines_2 = 1; assert_eq!(analysis.total_lines_of_code(), lines_1 + lines_2);
329
330 let size_1 = test_content_1.len() as u64;
331 let size_2 = test_content_2.len() as u64;
332 let total_size = size_1 + size_2;
333 assert_eq!(analysis.total_file_size(), total_size);
334
335 let largest = size_1.max(size_2);
337 let smallest = size_1.min(size_2);
338 assert_eq!(analysis.largest_file_size(), largest);
339 assert_eq!(analysis.smallest_file_size(), smallest);
340 }
341
342 #[tokio::test]
343 async fn test_mixed_source_and_test_files() {
344 let mut handle = MockCrateHandle::new();
345 let src1 = "src1 line1\nsrc1 line2";
347 let src2 = "src2 line1";
348 handle.add_src_file("file1.rs", src1).await;
349 handle.add_src_file("file2.rs", src2).await;
350
351 let test1 = "test line1\ntest line2\ntest line3";
353 handle.add_test_file("test_stuff.rs", test1).await;
354
355 let analysis = CrateAnalysis::new(&handle).await.unwrap();
356
357 assert_eq!(analysis.total_source_files(), 2);
359 assert_eq!(analysis.total_test_files(), 1);
360
361 let lines_src1 = 2;
363 let lines_src2 = 1;
364 let lines_test1 = 3;
365 assert_eq!(
366 analysis.total_lines_of_code(),
367 lines_src1 + lines_src2 + lines_test1
368 );
369
370 let size_src1 = src1.len() as u64;
372 let size_src2 = src2.len() as u64;
373 let size_test1 = test1.len() as u64;
374 assert_eq!(
375 analysis.total_file_size(),
376 size_src1 + size_src2 + size_test1
377 );
378
379 let largest = size_src1.max(size_src2).max(size_test1);
381 let smallest = size_src1.min(size_src2).min(size_test1);
382 assert_eq!(analysis.largest_file_size(), largest);
383 assert_eq!(analysis.smallest_file_size(), smallest);
384 }
385
386 #[tokio::test]
387 async fn test_tests_dir_exists_but_empty() {
388 let mut handle = MockCrateHandle::new();
391 let tests_dir = handle.root_dir.path().join("tests");
393 create_dir_all(&tests_dir).await.unwrap();
394 handle.has_tests = true;
396
397 let contents = "fn main() {}";
399 handle.add_src_file("main.rs", contents).await;
400
401 let analysis = CrateAnalysis::new(&handle).await.unwrap();
402 assert_eq!(analysis.total_source_files(), 1);
403 assert_eq!(analysis.total_test_files(), 0);
404 }
405
406 #[tokio::test]
407 async fn test_zero_sized_file() {
408 let mut handle = MockCrateHandle::new();
409 handle.add_src_file("empty.rs", "").await;
411
412 let analysis = CrateAnalysis::new(&handle).await.unwrap();
413 assert_eq!(analysis.total_source_files(), 1);
414 assert_eq!(analysis.total_test_files(), 0);
415 assert_eq!(analysis.total_lines_of_code(), 0);
416 assert_eq!(analysis.total_file_size(), 0);
418 assert_eq!(analysis.largest_file_size(), 0);
419 assert_eq!(analysis.smallest_file_size(), 0);
420 }
421
422 #[tokio::test]
426 async fn test_excluded_files_are_ignored() {
427 let mut handle = MockCrateHandle::new();
428 let included = "line1\nline2";
429 let excluded = "some content";
430 handle.add_src_file("included.rs", included).await;
431 handle.add_src_file("excluded.rs", excluded).await;
432
433 let files = handle
437 .source_files_excluding(&["excluded.rs"])
438 .await
439 .expect("should get source files");
440 assert_eq!(files.len(), 2, "Mock returns all files, ignoring excludes");
441
442 let analysis = CrateAnalysis::new(&handle).await.unwrap();
443 assert_eq!(analysis.total_source_files(), 2);
444 assert_eq!(analysis.total_lines_of_code(), 3); }
446}