workspacer_analysis/
crate_analysis.rs

1// ---------------- [ File: workspacer-analysis/src/crate_analysis.rs ]
2crate::ix!();
3
4#[derive(Debug,Clone)]
5pub struct CrateAnalysis {
6
7    /// Total size of files in bytes
8    total_file_size:     u64,       
9
10    /// Total number of lines of code
11    total_lines_of_code: usize, 
12
13    /// Total number of source files
14    total_source_files:  usize,  
15
16    /// Total number of test files
17    total_test_files:    usize,    
18
19    /// Size of the largest file in bytes
20    largest_file_size:   u64,     
21
22    /// Size of the smallest file in bytes
23    smallest_file_size:  u64,    
24}
25
26impl CrateAnalysis {
27
28    /// Constructs a `CrateAnalysis` by analyzing the files in the given `CrateHandle`
29    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        // Analyze source files in `src/`
40        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        // Analyze test files if the `tests/` directory exists
56        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    // --- Getters ---
85    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    // We only implement the 3 traits below:
121    //   - HasTestsDirectory
122    //   - GetTestFiles
123    //   - GetSourceFilesWithExclusions
124    //
125    // That’s precisely what CrateAnalysis::new requires.
126    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        // Helper to create a file in `src/` and push to src_files
144        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            // Force flush
152            file.sync_all().await.unwrap();
153            drop(file); // ensure the handle is closed
154
155            // Double-check size
156            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        // Helper to create a file in `tests/` and push to test_files
168        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            // Force flush
176            file.sync_all().await.unwrap();
177            drop(file); // ensure the handle is closed
178
179            // Make sure the file truly has the expected size
180            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            // For mock purposes, we ignore the exclude list.
195            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    // ------------------------------------------------------------------------
213    //  Existing tests
214    // ------------------------------------------------------------------------
215
216    #[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        // largest_file_size() and smallest_file_size() are also tested implicitly.
225        // By default, smallest_file_size is u64::MAX if no files exist; but we only
226        // store that if at least one file was found. So:
227        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        // Ensure the file truly has 2 lines of code:
235        //   (Line 1) fn main() {
236        //   (Line 2) println!("hi");}
237        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        // Check sizes
245        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        // Double check from within the test:
258        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);//this is the assertion that failed. 17 is the expected_size in terms of length units in the input string
269        assert_eq!(analysis.largest_file_size(), expected_size);
270        assert_eq!(analysis.smallest_file_size(), expected_size);
271    }
272
273    // ------------------------------------------------------------------------
274    //  Additional tests for thorough coverage
275    // ------------------------------------------------------------------------
276
277    #[tokio::test]
278    async fn test_multiple_source_files() {
279        let mut handle = MockCrateHandle::new();
280        // Add 3 files with different line counts and sizes
281        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        // Lines
293        let lines_a = 1; // "let x = 1;"
294        let lines_b = 2; // 2 lines
295        let lines_c = 0; // empty
296        let total_lines = lines_a + lines_b + lines_c;
297        assert_eq!(analysis.total_lines_of_code(), total_lines);
298
299        // File sizes
300        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        // Largest & smallest
307        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        // Add 2 test files
317        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; // test_content_1 has 2 lines
327        let lines_2 = 1; // test_content_2 has 1 line
328        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        // Largest & smallest
336        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        // 2 source files
346        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        // 1 test file
352        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        // Check counts
358        assert_eq!(analysis.total_source_files(), 2);
359        assert_eq!(analysis.total_test_files(), 1);
360
361        // Lines
362        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        // File sizes
371        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        // Largest & smallest
380        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        // Even if we create an empty `tests/` directory, if there are no files in it,
389        // total_test_files remains 0.
390        let mut handle = MockCrateHandle::new();
391        // Force creation of an empty `tests` directory
392        let tests_dir = handle.root_dir.path().join("tests");
393        create_dir_all(&tests_dir).await.unwrap();
394        // Mark has_tests = true so it sees the directory
395        handle.has_tests = true;
396
397        // Add one src file for good measure
398        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        // Create an empty src file
410        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        // File size is 0
417        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    // This test won't actually show a difference in this mock because
423    // we're ignoring excludes. Provided here for completeness of the
424    // interface usage.
425    #[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        // In the real environment, the next call would skip "excluded.rs"
434        // if it is passed in `_exclude_files`.
435        // Our mock simply returns all files, so the result is the same.
436        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); // included=2 lines, excluded=1 line
445    }
446}