workspacer_analysis/
workspace_analysis.rs

1// ---------------- [ File: workspacer-analysis/src/workspace_analysis.rs ]
2crate::ix!();
3
4#[async_trait]
5pub trait Analyze {
6    type Analysis;
7    type Error;
8
9    async fn analyze(&self) -> Result<Self::Analysis, Self::Error>;
10}
11
12/// Here we implement `Analyze` for `Workspace<P, H>`.
13/// Notice that each crate is `Arc<Mutex<H>>`. Now that we have "passthrough" impls
14/// (see below), `Arc<Mutex<H>>` will satisfy `HasTestsDirectory + GetTestFiles + GetSourceFilesWithExclusions`.
15#[async_trait]
16impl<P, H> Analyze for Workspace<P, H>
17where
18    for<'async_trait> P: From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
19    for<'async_trait> H: CrateHandleInterface<P>
20        + HasTestsDirectory
21        + GetTestFiles
22        + GetSourceFilesWithExclusions
23        + Send
24        + Sync
25        + 'async_trait,
26{
27    type Analysis = WorkspaceSizeAnalysis;
28    type Error    = WorkspaceError;
29
30    async fn analyze(&self) -> Result<Self::Analysis, Self::Error> {
31        info!("Analyzing entire workspace for size metrics...");
32
33        let mut builder = WorkspaceAnalysisBuilder::new();
34
35        for crate_handle in self.crates() {
36            // Now that we've given Arc<Mutex<H>> passthrough impls (below),
37            // we can call `CrateAnalysis::new(&*crate_handle)`.
38            // The `&*crate_handle` turns the `Arc<Mutex<H>>` reference into something
39            // that implements `HasTestsDirectory + GetTestFiles + GetSourceFilesWithExclusions`.
40            let crate_analysis = CrateAnalysis::new(&*crate_handle).await?;
41            builder.add_crate_analysis(crate_analysis);
42        }
43
44        Ok(builder.build())
45    }
46}
47
48#[derive(Debug,Clone)]
49pub struct WorkspaceSizeAnalysis {
50    crate_analyses: Vec<CrateAnalysis>, // Collection of crate analyses
51
52    // Workspace-level metrics
53    total_file_size:        u64,
54    total_lines_of_code:    usize,
55    total_source_files:     usize,
56    total_test_files:       usize,
57    largest_file_size:      u64,
58    smallest_file_size:     u64,
59    average_file_size:      f64,
60    average_lines_per_file: f64,
61}
62
63impl WorkspaceSizeAnalysis {
64    /// Starts the builder for `WorkspaceSizeAnalysis`
65    pub fn builder() -> WorkspaceAnalysisBuilder {
66        WorkspaceAnalysisBuilder::new()
67    }
68
69    // --- Accessors ---
70    
71    pub fn crate_analyses(&self) -> &Vec<CrateAnalysis> {
72        &self.crate_analyses
73    }
74
75    pub fn total_file_size(&self) -> u64 {
76        self.total_file_size
77    }
78
79    pub fn total_lines_of_code(&self) -> usize {
80        self.total_lines_of_code
81    }
82
83    pub fn total_source_files(&self) -> usize {
84        self.total_source_files
85    }
86
87    pub fn total_test_files(&self) -> usize {
88        self.total_test_files
89    }
90
91    pub fn largest_file_size(&self) -> u64 {
92        self.largest_file_size
93    }
94
95    pub fn smallest_file_size(&self) -> u64 {
96        self.smallest_file_size
97    }
98
99    pub fn average_file_size(&self) -> f64 {
100        self.average_file_size
101    }
102
103    pub fn average_lines_per_file(&self) -> f64 {
104        self.average_lines_per_file
105    }
106}
107
108pub struct WorkspaceAnalysisBuilder {
109    crate_analyses: Vec<CrateAnalysis>, // Collection of crate analyses
110}
111
112impl WorkspaceAnalysisBuilder {
113    pub fn new() -> Self {
114        Self {
115            crate_analyses: Vec::new(),
116        }
117    }
118
119    /// Adds a crate analysis to the builder
120    pub fn add_crate_analysis(&mut self, analysis: CrateAnalysis) -> &mut Self {
121        self.crate_analyses.push(analysis);
122        self
123    }
124
125    /// Builds and returns the `WorkspaceSizeAnalysis` by calculating workspace-level metrics
126    pub fn build(&self) -> WorkspaceSizeAnalysis {
127        let mut total_file_size     = 0;
128        let mut total_lines_of_code = 0;
129        let mut total_source_files  = 0;
130        let mut total_test_files    = 0;
131        let mut largest_file_size   = 0;
132        let mut smallest_file_size  = u64::MAX;
133
134        // Aggregate data from each crate analysis
135        for crate_analysis in &self.crate_analyses {
136            total_file_size     += crate_analysis.total_file_size();
137            total_lines_of_code += crate_analysis.total_lines_of_code();
138            total_source_files  += crate_analysis.total_source_files();
139            total_test_files    += crate_analysis.total_test_files();
140            largest_file_size   = largest_file_size.max(crate_analysis.largest_file_size());
141            smallest_file_size  = smallest_file_size.min(crate_analysis.smallest_file_size());
142        }
143
144        let average_file_size = if total_source_files > 0 {
145            total_file_size as f64 / total_source_files as f64
146        } else {
147            0.0
148        };
149        let average_lines_per_file = if total_source_files > 0 {
150            total_lines_of_code as f64 / total_source_files as f64
151        } else {
152            0.0
153        };
154
155        WorkspaceSizeAnalysis {
156            crate_analyses: self.crate_analyses.clone(),
157            total_file_size,
158            total_lines_of_code,
159            total_source_files,
160            total_test_files,
161            largest_file_size,
162            smallest_file_size,
163            average_file_size,
164            average_lines_per_file,
165        }
166    }
167}
168
169#[cfg(test)]
170mod test_workspace_analysis {
171    use super::*;
172
173    // -------------------------------------------------------------------------
174    // A mock crate handle that we can analyze with CrateAnalysis::new(...)
175    // -------------------------------------------------------------------------
176    struct MockCrateHandle {
177        root_dir:      TempDir,
178        has_tests:     bool,
179        src_files:     Vec<PathBuf>,
180        test_files:    Vec<PathBuf>,
181    }
182
183    impl MockCrateHandle {
184        fn new() -> Self {
185            Self {
186                root_dir: tempfile::tempdir().unwrap(),
187                has_tests: false,
188                src_files: vec![],
189                test_files: vec![],
190            }
191        }
192
193        async fn add_src_file(&mut self, file_name: &str, contents: &str) {
194            let src_dir = self.root_dir.path().join("src");
195            tokio::fs::create_dir_all(&src_dir).await.unwrap();
196
197            let path = src_dir.join(file_name);
198            let mut file = File::create(&path).await.unwrap();
199            file.write_all(contents.as_bytes()).await.unwrap();
200            // Ensure the file is flushed
201            file.sync_all().await.unwrap();
202            drop(file);
203
204            self.src_files.push(path);
205        }
206
207        async fn add_test_file(&mut self, file_name: &str, contents: &str) {
208            let test_dir = self.root_dir.path().join("tests");
209            tokio::fs::create_dir_all(&test_dir).await.unwrap();
210
211            let path = test_dir.join(file_name);
212            let mut file = File::create(&path).await.unwrap();
213            file.write_all(contents.as_bytes()).await.unwrap();
214            file.sync_all().await.unwrap();
215            drop(file);
216
217            self.test_files.push(path);
218            self.has_tests = true;
219        }
220    }
221
222    #[async_trait]
223    impl GetSourceFilesWithExclusions for MockCrateHandle {
224        async fn source_files_excluding(&self, _exclude: &[&str]) -> Result<Vec<PathBuf>, CrateError> {
225            // For simplicity in this mock, we ignore the exclusion list
226            Ok(self.src_files.clone())
227        }
228    }
229
230    #[async_trait]
231    impl GetTestFiles for MockCrateHandle {
232        async fn test_files(&self) -> Result<Vec<PathBuf>, CrateError> {
233            Ok(self.test_files.clone())
234        }
235    }
236
237    impl HasTestsDirectory for MockCrateHandle {
238        fn has_tests_directory(&self) -> bool {
239            self.has_tests
240        }
241    }
242
243    // -------------------------------------------------------------------------
244    // MockWorkspace that contains multiple MockCrateHandles
245    // -------------------------------------------------------------------------
246    struct MockWorkspace {
247        crates: Vec<MockCrateHandle>,
248    }
249
250    impl MockWorkspace {
251        fn new() -> Self {
252            Self { crates: vec![] }
253        }
254
255        fn add_crate_handle(&mut self, crate_handle: MockCrateHandle) -> &mut Self {
256            self.crates.push(crate_handle);
257            self
258        }
259    }
260
261    // This lets: `for crate_handle in &mock_workspace { ... }`
262    impl<'a> IntoIterator for &'a MockWorkspace {
263        type Item     = &'a MockCrateHandle;
264        type IntoIter = std::slice::Iter<'a, MockCrateHandle>;
265
266        fn into_iter(self) -> Self::IntoIter {
267            self.crates.iter()
268        }
269    }
270
271    // -------------------------------------------------------------------------
272    // Implement the Analyze trait for our mock workspace:
273    // this matches the real logic in your code.
274    // -------------------------------------------------------------------------
275    #[async_trait]
276    impl Analyze for MockWorkspace {
277        type Analysis = WorkspaceSizeAnalysis;
278        type Error    = WorkspaceError;
279
280        async fn analyze(&self) -> Result<Self::Analysis, Self::Error> {
281            let mut builder = WorkspaceSizeAnalysis::builder();
282
283            for crate_handle in self {
284                let crate_analysis = CrateAnalysis::new(crate_handle).await?;
285                builder.add_crate_analysis(crate_analysis);
286            }
287
288            Ok(builder.build())
289        }
290    }
291
292    // -------------------------------------------------------------------------
293    // Now we can test workspace-level analysis with multiple crates.
294    // -------------------------------------------------------------------------
295
296    // 1) No crates at all
297    #[tokio::test]
298    async fn test_no_crates_in_workspace() {
299        let workspace = MockWorkspace::new();
300        let analysis = workspace.analyze().await.unwrap();
301
302        assert_eq!(analysis.crate_analyses().len(), 0);
303        assert_eq!(analysis.total_file_size(), 0);
304        assert_eq!(analysis.total_lines_of_code(), 0);
305        assert_eq!(analysis.total_source_files(), 0);
306        assert_eq!(analysis.total_test_files(), 0);
307        assert_eq!(analysis.largest_file_size(), 0);
308        assert_eq!(analysis.smallest_file_size(), u64::MAX);
309        assert_eq!(analysis.average_file_size(), 0.0);
310        assert_eq!(analysis.average_lines_per_file(), 0.0);
311    }
312
313    // 2) Single crate with no files
314    #[tokio::test]
315    async fn test_single_crate_no_files() {
316        let mut workspace = MockWorkspace::new();
317        let crate_empty = MockCrateHandle::new(); // No src or test files
318
319        workspace.add_crate_handle(crate_empty);
320        let analysis = workspace.analyze().await.unwrap();
321
322        assert_eq!(analysis.crate_analyses().len(), 1);
323        assert_eq!(analysis.total_file_size(), 0);
324        assert_eq!(analysis.total_lines_of_code(), 0);
325        assert_eq!(analysis.total_source_files(), 0);
326        assert_eq!(analysis.total_test_files(), 0);
327        assert_eq!(analysis.largest_file_size(), 0);
328        assert_eq!(analysis.smallest_file_size(), u64::MAX);
329        // Because total_source_files is 0, average is 0.0
330        assert_eq!(analysis.average_file_size(), 0.0);
331        assert_eq!(analysis.average_lines_per_file(), 0.0);
332    }
333
334    // 3) Single crate with multiple files (src and tests)
335    #[tokio::test]
336    async fn test_single_crate_multiple_files() {
337        let mut crate_handle = MockCrateHandle::new();
338        let src_a = "fn main() {}\nprintln!(\"hello\");";
339        let src_b = "mod sub;\nmod sub2;";
340        let test_a = "test fn1\ntest fn2\ntest fn3";
341
342        crate_handle.add_src_file("main.rs", src_a).await;
343        crate_handle.add_src_file("lib.rs", src_b).await;
344        crate_handle.add_test_file("test_something.rs", test_a).await;
345
346        // Put into workspace
347        let mut workspace = MockWorkspace::new();
348        workspace.add_crate_handle(crate_handle);
349
350        // Analyze
351        let analysis = workspace.analyze().await.unwrap();
352        assert_eq!(analysis.crate_analyses().len(), 1);
353
354        // Summaries
355        // src_a has 2 lines, src_b has 2, test_a has 3 => 7 total
356        assert_eq!(analysis.total_lines_of_code(), 7);
357        // total source files = 2, total test files = 1
358        assert_eq!(analysis.total_source_files(), 2);
359        assert_eq!(analysis.total_test_files(), 1);
360
361        // File sizes
362        let size_a = src_a.len() as u64;
363        let size_b = src_b.len() as u64;
364        let size_test = test_a.len() as u64;
365        let total_size = size_a + size_b + size_test;
366        assert_eq!(analysis.total_file_size(), total_size);
367        assert_eq!(analysis.largest_file_size(), size_a.max(size_b).max(size_test));
368        assert_eq!(analysis.smallest_file_size(), size_a.min(size_b).min(size_test));
369
370        // Averages are computed over the total source files (not including test files).
371        // So we have 2 source files => average_file_size and average_lines_per_file
372        let avg_file_size = total_size as f64 / 2.0;
373        let avg_lines     = 7.0 / 2.0;
374        // floating comparisons
375        assert!((analysis.average_file_size() - avg_file_size).abs() < f64::EPSILON);
376        assert!((analysis.average_lines_per_file() - avg_lines).abs() < f64::EPSILON);
377    }
378
379    // 4) Multiple crates in the same workspace
380    #[tokio::test]
381    async fn test_multiple_crates() {
382        // First crate
383        let mut crate1 = MockCrateHandle::new();
384        crate1.add_src_file("file1.rs", "line1\nline2").await; // 2 lines
385        crate1.add_test_file("test1.rs", "test\nstuff").await; // 2 lines
386
387        // Second crate
388        let mut crate2 = MockCrateHandle::new();
389        // no test files
390        crate2.add_src_file("file2.rs", "foo\nbar\nbaz").await; // 3 lines
391        crate2.add_src_file("file3.rs", "").await;              // 0 lines
392
393        // Third crate
394        let crate3 = MockCrateHandle::new(); // empty
395
396        // Build the workspace
397        let mut workspace = MockWorkspace::new();
398        workspace
399            .add_crate_handle(crate1)
400            .add_crate_handle(crate2)
401            .add_crate_handle(crate3);
402
403        // Analyze
404        let analysis = workspace.analyze().await.unwrap();
405        assert_eq!(analysis.crate_analyses().len(), 3);
406
407        // Summations across all crates
408        // lines: crate1 => 2+2=4, crate2 => 3+0=3, crate3 => 0 => total=7
409        assert_eq!(analysis.total_lines_of_code(), 7);
410
411        // source files: crate1 => 1, crate2 => 2, crate3 => 0 => total=3
412        assert_eq!(analysis.total_source_files(), 3);
413
414        // test files: crate1 => 1, crate2 => 0, crate3 => 0 => total=1
415        assert_eq!(analysis.total_test_files(), 1);
416
417        // file sizes
418        let size_c1_f1 = "line1\nline2".len() as u64; // 11
419        let size_c1_t1 = "test\nstuff".len() as u64;  // 10
420        let size_c2_f1 = "foo\nbar\nbaz".len() as u64; // 11
421        let size_c2_f2 = "".len() as u64;              // 0
422        // crate3 => no files => size=0
423        let total_file_size = size_c1_f1 + size_c1_t1 + size_c2_f1 + size_c2_f2;
424        assert_eq!(analysis.total_file_size(), total_file_size);
425
426        let largest_file_size = *[
427            size_c1_f1,
428            size_c1_t1,
429            size_c2_f1,
430            size_c2_f2,
431            0 // crate3
432        ]
433        .iter()
434        .max()
435        .unwrap();
436        let smallest_file_size = *[
437            size_c1_f1,
438            size_c1_t1,
439            size_c2_f1,
440            size_c2_f2,
441            u64::MAX // if none, that is the default, but we do have files
442        ]
443        .iter()
444        .min()
445        .unwrap();
446        assert_eq!(analysis.largest_file_size(), largest_file_size);
447        assert_eq!(analysis.smallest_file_size(), smallest_file_size);
448
449        // average_file_size and average_lines_per_file are per "source file" (3 total).
450        // total_file_size = 11 + 10 + 11 + 0 = 32
451        // total_source_files = 3
452        // => average_file_size = 32 / 3
453        let expected_avg_size = 32.0 / 3.0;
454        // total_lines_of_code=7 => 7/3=2.3333...
455        let expected_avg_lines = 7.0 / 3.0;
456        assert!((analysis.average_file_size() - expected_avg_size).abs() < f64::EPSILON);
457        assert!((analysis.average_lines_per_file() - expected_avg_lines).abs() < f64::EPSILON);
458    }
459
460    // 5) Confirm average is zero if no source files exist
461    #[tokio::test]
462    async fn test_all_test_files_no_source_files() {
463        let mut crate_with_tests = MockCrateHandle::new();
464        crate_with_tests.add_test_file("test_only.rs", "one\ntwo\nthree").await; // 3 lines
465
466        let mut workspace = MockWorkspace::new();
467        workspace.add_crate_handle(crate_with_tests);
468
469        let analysis = workspace.analyze().await.unwrap();
470
471        // 0 source files => 1 test file
472        assert_eq!(analysis.total_source_files(), 0);
473        assert_eq!(analysis.total_test_files(), 1);
474        // lines => 3
475        assert_eq!(analysis.total_lines_of_code(), 3);
476        // Because total_source_files is 0, average_file_size & average_lines_per_file = 0.0
477        assert_eq!(analysis.average_file_size(), 0.0);
478        assert_eq!(analysis.average_lines_per_file(), 0.0);
479    }
480}