1crate::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#[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 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>, 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 pub fn builder() -> WorkspaceAnalysisBuilder {
66 WorkspaceAnalysisBuilder::new()
67 }
68
69 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>, }
111
112impl WorkspaceAnalysisBuilder {
113 pub fn new() -> Self {
114 Self {
115 crate_analyses: Vec::new(),
116 }
117 }
118
119 pub fn add_crate_analysis(&mut self, analysis: CrateAnalysis) -> &mut Self {
121 self.crate_analyses.push(analysis);
122 self
123 }
124
125 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 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 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 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 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 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 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 #[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 #[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 #[tokio::test]
315 async fn test_single_crate_no_files() {
316 let mut workspace = MockWorkspace::new();
317 let crate_empty = MockCrateHandle::new(); 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 assert_eq!(analysis.average_file_size(), 0.0);
331 assert_eq!(analysis.average_lines_per_file(), 0.0);
332 }
333
334 #[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 let mut workspace = MockWorkspace::new();
348 workspace.add_crate_handle(crate_handle);
349
350 let analysis = workspace.analyze().await.unwrap();
352 assert_eq!(analysis.crate_analyses().len(), 1);
353
354 assert_eq!(analysis.total_lines_of_code(), 7);
357 assert_eq!(analysis.total_source_files(), 2);
359 assert_eq!(analysis.total_test_files(), 1);
360
361 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 let avg_file_size = total_size as f64 / 2.0;
373 let avg_lines = 7.0 / 2.0;
374 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 #[tokio::test]
381 async fn test_multiple_crates() {
382 let mut crate1 = MockCrateHandle::new();
384 crate1.add_src_file("file1.rs", "line1\nline2").await; crate1.add_test_file("test1.rs", "test\nstuff").await; let mut crate2 = MockCrateHandle::new();
389 crate2.add_src_file("file2.rs", "foo\nbar\nbaz").await; crate2.add_src_file("file3.rs", "").await; let crate3 = MockCrateHandle::new(); let mut workspace = MockWorkspace::new();
398 workspace
399 .add_crate_handle(crate1)
400 .add_crate_handle(crate2)
401 .add_crate_handle(crate3);
402
403 let analysis = workspace.analyze().await.unwrap();
405 assert_eq!(analysis.crate_analyses().len(), 3);
406
407 assert_eq!(analysis.total_lines_of_code(), 7);
410
411 assert_eq!(analysis.total_source_files(), 3);
413
414 assert_eq!(analysis.total_test_files(), 1);
416
417 let size_c1_f1 = "line1\nline2".len() as u64; let size_c1_t1 = "test\nstuff".len() as u64; let size_c2_f1 = "foo\nbar\nbaz".len() as u64; let size_c2_f2 = "".len() as u64; 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 ]
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 ]
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 let expected_avg_size = 32.0 / 3.0;
454 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 #[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; let mut workspace = MockWorkspace::new();
467 workspace.add_crate_handle(crate_with_tests);
468
469 let analysis = workspace.analyze().await.unwrap();
470
471 assert_eq!(analysis.total_source_files(), 0);
473 assert_eq!(analysis.total_test_files(), 1);
474 assert_eq!(analysis.total_lines_of_code(), 3);
476 assert_eq!(analysis.average_file_size(), 0.0);
478 assert_eq!(analysis.average_lines_per_file(), 0.0);
479 }
480}