1use serde::{Deserialize, Serialize};
15
16use crate::data::diff::LocsDiff;
17use crate::data::stats::Locs;
18use crate::query::options::Aggregation;
19use crate::query::queryset::{CountQuerySet, DiffQuerySet};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct TableRow {
24 pub label: String,
26 pub values: Vec<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct LOCTable {
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub title: Option<String>,
39 pub headers: Vec<String>,
41 pub rows: Vec<TableRow>,
43 pub footer: TableRow,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub summary: Option<TableRow>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub non_rust_summary: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub legend: Option<String>,
54}
55
56impl LOCTable {
57 pub fn from_count_queryset(qs: &CountQuerySet) -> Self {
62 let headers = build_headers(&qs.aggregation, &qs.line_types);
63 let rows: Vec<TableRow> = qs
64 .items
65 .iter()
66 .map(|item| TableRow {
67 label: item.label.clone(),
68 values: format_locs(&item.stats, &qs.line_types),
69 })
70 .collect();
71 let footer = TableRow {
72 label: build_footer_label(&qs.aggregation, rows.len(), qs.file_count),
73 values: format_locs(&qs.total, &qs.line_types),
74 };
75
76 LOCTable {
77 title: None,
78 headers,
79 rows,
80 footer,
81 summary: None,
82 non_rust_summary: None,
83 legend: None,
84 }
85 }
86
87 pub fn from_diff_queryset(qs: &DiffQuerySet) -> Self {
92 let headers = build_headers(&qs.aggregation, &qs.line_types);
93 let rows: Vec<TableRow> = qs
94 .items
95 .iter()
96 .map(|item| TableRow {
97 label: item.label.clone(),
98 values: format_locs_diff(&item.stats, &qs.line_types),
99 })
100 .collect();
101 let footer = TableRow {
102 label: build_footer_label(&qs.aggregation, rows.len(), qs.file_count),
103 values: format_locs_diff(&qs.total, &qs.line_types),
104 };
105 let title = Some(format!("Diff: {} → {}", qs.from_commit, qs.to_commit));
106
107 let lt = LineTypesView::from(&qs.line_types);
109 let total_added = filtered_sum(&qs.total.added, <);
110 let total_removed = filtered_sum(&qs.total.removed, <);
111 let net = total_added as i64 - total_removed as i64;
112 let summary = TableRow {
113 label: String::new(),
114 values: vec![format!(
115 "[additions]+{}[/additions] / [deletions]-{}[/deletions] / {} net",
116 total_added, total_removed, net
117 )],
118 };
119
120 let non_rust_summary = if qs.non_rust_added > 0 || qs.non_rust_removed > 0 {
121 let nr_net = qs.non_rust_added as i64 - qs.non_rust_removed as i64;
122 Some(format!(
123 "Non-Rust changes: [additions]+{}[/additions] / [deletions]-{}[/deletions] / {} net",
124 qs.non_rust_added, qs.non_rust_removed, nr_net
125 ))
126 } else {
127 None
128 };
129
130 LOCTable {
131 title,
132 headers,
133 rows,
134 footer,
135 summary: Some(summary),
136 non_rust_summary,
137 legend: Some("(+added / -removed / net)".to_string()),
138 }
139 }
140}
141
142struct LineTypesView {
145 code: bool,
146 tests: bool,
147 examples: bool,
148 docs: bool,
149 comments: bool,
150 blanks: bool,
151 total: bool,
152}
153
154impl From<&crate::query::options::LineTypes> for LineTypesView {
155 fn from(lt: &crate::query::options::LineTypes) -> Self {
156 LineTypesView {
157 code: lt.code,
158 tests: lt.tests,
159 examples: lt.examples,
160 docs: lt.docs,
161 comments: lt.comments,
162 blanks: lt.blanks,
163 total: lt.total,
164 }
165 }
166}
167
168fn build_footer_label(aggregation: &Aggregation, items_count: usize, file_count: usize) -> String {
173 match aggregation {
174 Aggregation::Total => format!("Total ({} files)", file_count),
175 Aggregation::ByCrate => format!("Total ({} crates)", items_count),
176 Aggregation::ByModule => format!("Total ({} modules)", items_count),
177 Aggregation::ByFile => format!("Total ({} files)", items_count),
178 }
179}
180
181fn build_headers(
183 aggregation: &Aggregation,
184 line_types: &crate::query::options::LineTypes,
185) -> Vec<String> {
186 let label_header = match aggregation {
187 Aggregation::Total => "Name".to_string(),
188 Aggregation::ByCrate => "Crate".to_string(),
189 Aggregation::ByModule => "Module".to_string(),
190 Aggregation::ByFile => "File".to_string(),
191 };
192
193 let lt = LineTypesView::from(line_types);
194 let mut headers = vec![label_header];
195
196 if lt.code {
197 headers.push("Code".to_string());
198 }
199 if lt.tests {
200 headers.push("Tests".to_string());
201 }
202 if lt.examples {
203 headers.push("Examples".to_string());
204 }
205 if lt.docs {
206 headers.push("Docs".to_string());
207 }
208 if lt.comments {
209 headers.push("Comments".to_string());
210 }
211 if lt.blanks {
212 headers.push("Blanks".to_string());
213 }
214 if lt.total {
215 headers.push("Total".to_string());
216 }
217
218 headers
219}
220
221fn filtered_sum(locs: &Locs, lt: &LineTypesView) -> u64 {
223 let mut sum = 0;
224 if lt.code {
225 sum += locs.code;
226 }
227 if lt.tests {
228 sum += locs.tests;
229 }
230 if lt.examples {
231 sum += locs.examples;
232 }
233 if lt.docs {
234 sum += locs.docs;
235 }
236 if lt.comments {
237 sum += locs.comments;
238 }
239 if lt.blanks {
240 sum += locs.blanks;
241 }
242 sum
243}
244
245fn format_locs(locs: &Locs, line_types: &crate::query::options::LineTypes) -> Vec<String> {
247 let lt = LineTypesView::from(line_types);
248 let mut values = Vec::new();
249
250 if lt.code {
251 values.push(locs.code.to_string());
252 }
253 if lt.tests {
254 values.push(locs.tests.to_string());
255 }
256 if lt.examples {
257 values.push(locs.examples.to_string());
258 }
259 if lt.docs {
260 values.push(locs.docs.to_string());
261 }
262 if lt.comments {
263 values.push(locs.comments.to_string());
264 }
265 if lt.blanks {
266 values.push(locs.blanks.to_string());
267 }
268 if lt.total {
269 values.push(locs.total.to_string());
271 }
272
273 values
274}
275
276fn format_diff_value(added: u64, removed: u64) -> String {
278 let net = added as i64 - removed as i64;
279 format!(
280 "[additions]+{}[/additions]/[deletions]-{}[/deletions]/{}",
281 added, removed, net
282 )
283}
284
285fn format_locs_diff(diff: &LocsDiff, line_types: &crate::query::options::LineTypes) -> Vec<String> {
287 let lt = LineTypesView::from(line_types);
288 let mut values = Vec::new();
289
290 if lt.code {
291 values.push(format_diff_value(diff.added.code, diff.removed.code));
292 }
293 if lt.tests {
294 values.push(format_diff_value(diff.added.tests, diff.removed.tests));
295 }
296 if lt.examples {
297 values.push(format_diff_value(
298 diff.added.examples,
299 diff.removed.examples,
300 ));
301 }
302 if lt.docs {
303 values.push(format_diff_value(diff.added.docs, diff.removed.docs));
304 }
305 if lt.comments {
306 values.push(format_diff_value(
307 diff.added.comments,
308 diff.removed.comments,
309 ));
310 }
311 if lt.blanks {
312 values.push(format_diff_value(diff.added.blanks, diff.removed.blanks));
313 }
314 if lt.total {
315 values.push(format_diff_value(diff.added.total, diff.removed.total));
317 }
318
319 values
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::data::counter::CountResult;
326 use crate::data::stats::CrateStats;
327 use crate::query::options::{LineTypes, Ordering};
328 use std::path::PathBuf;
329
330 fn sample_locs(code: u64, tests: u64) -> Locs {
331 Locs {
332 code,
333 tests,
334 examples: 0,
335 docs: 0,
336 comments: 0,
337 blanks: 0,
338 total: code + tests,
339 }
340 }
341
342 fn sample_count_result() -> CountResult {
343 CountResult {
344 root: PathBuf::from("/workspace"),
345 file_count: 4,
346 total: sample_locs(200, 100),
347 crates: vec![
348 CrateStats {
349 name: "alpha".to_string(),
350 path: PathBuf::from("/alpha"),
351 stats: sample_locs(50, 25),
352 files: vec![],
353 },
354 CrateStats {
355 name: "beta".to_string(),
356 path: PathBuf::from("/beta"),
357 stats: sample_locs(150, 75),
358 files: vec![],
359 },
360 ],
361 files: vec![],
362 modules: vec![],
363 }
364 }
365
366 #[test]
367 fn test_headers_by_crate() {
368 let headers = build_headers(&Aggregation::ByCrate, &LineTypes::everything());
369 assert_eq!(headers[0], "Crate");
370 assert_eq!(headers[1], "Code");
371 assert_eq!(headers[2], "Tests");
372 assert_eq!(headers[3], "Examples");
373 assert_eq!(headers[4], "Docs");
374 assert_eq!(headers[5], "Comments");
375 assert_eq!(headers[6], "Blanks");
376 assert_eq!(headers[7], "Total");
377 }
378
379 #[test]
380 fn test_headers_filtered_line_types() {
381 let line_types = LineTypes::new().with_code();
382 let headers = build_headers(&Aggregation::ByFile, &line_types);
383 assert_eq!(headers.len(), 3); assert_eq!(headers[0], "File");
385 assert_eq!(headers[1], "Code");
386 assert_eq!(headers[2], "Total");
387 }
388
389 #[test]
390 fn test_format_locs() {
391 let locs = sample_locs(100, 50);
392 let values = format_locs(&locs, &LineTypes::everything());
393 assert_eq!(values[0], "100"); assert_eq!(values[1], "50"); assert_eq!(values[2], "0"); assert_eq!(values[3], "0"); assert_eq!(values[4], "0"); assert_eq!(values[5], "0"); assert_eq!(values[6], "150"); }
401
402 #[test]
403 fn test_loc_table_from_queryset() {
404 let result = sample_count_result();
405 let qs = CountQuerySet::from_result(
406 &result,
407 Aggregation::ByCrate,
408 LineTypes::everything(),
409 Ordering::default(),
410 );
411 let table = LOCTable::from_count_queryset(&qs);
412
413 assert!(table.title.is_none());
414 assert_eq!(table.headers[0], "Crate");
415 assert_eq!(table.rows.len(), 2);
416 assert_eq!(table.rows[0].label, "alpha");
418 assert_eq!(table.rows[1].label, "beta");
419 assert_eq!(table.footer.label, "Total (2 crates)");
420 }
421
422 #[test]
423 fn test_ordering_by_label_ascending() {
424 let result = sample_count_result();
425 let qs = CountQuerySet::from_result(
426 &result,
427 Aggregation::ByCrate,
428 LineTypes::everything(),
429 Ordering::by_label(),
430 );
431 let table = LOCTable::from_count_queryset(&qs);
432
433 assert_eq!(table.rows[0].label, "alpha");
434 assert_eq!(table.rows[1].label, "beta");
435 }
436
437 #[test]
438 fn test_ordering_by_label_descending() {
439 let result = sample_count_result();
440 let qs = CountQuerySet::from_result(
441 &result,
442 Aggregation::ByCrate,
443 LineTypes::everything(),
444 Ordering::by_label().descending(),
445 );
446 let table = LOCTable::from_count_queryset(&qs);
447
448 assert_eq!(table.rows[0].label, "beta");
449 assert_eq!(table.rows[1].label, "alpha");
450 }
451
452 #[test]
453 fn test_ordering_by_code_descending() {
454 let result = sample_count_result();
455 let qs = CountQuerySet::from_result(
456 &result,
457 Aggregation::ByCrate,
458 LineTypes::everything(),
459 Ordering::by_code(), );
461 let table = LOCTable::from_count_queryset(&qs);
462
463 assert_eq!(table.rows[0].label, "beta");
465 assert_eq!(table.rows[0].values[0], "150");
466 assert_eq!(table.rows[1].label, "alpha");
467 assert_eq!(table.rows[1].values[0], "50");
468 }
469
470 #[test]
471 fn test_ordering_by_code_ascending() {
472 let result = sample_count_result();
473 let qs = CountQuerySet::from_result(
474 &result,
475 Aggregation::ByCrate,
476 LineTypes::everything(),
477 Ordering::by_code().ascending(),
478 );
479 let table = LOCTable::from_count_queryset(&qs);
480
481 assert_eq!(table.rows[0].label, "alpha");
483 assert_eq!(table.rows[1].label, "beta");
484 }
485
486 #[test]
487 fn test_ordering_by_total_descending() {
488 let result = sample_count_result();
489 let qs = CountQuerySet::from_result(
490 &result,
491 Aggregation::ByCrate,
492 LineTypes::everything(),
493 Ordering::by_total(),
494 );
495 let table = LOCTable::from_count_queryset(&qs);
496
497 assert_eq!(table.rows[0].label, "beta");
499 assert_eq!(table.rows[1].label, "alpha");
500 }
501
502 #[test]
503 fn test_format_diff_value() {
504 assert_eq!(
505 format_diff_value(10, 5),
506 "[additions]+10[/additions]/[deletions]-5[/deletions]/5"
507 );
508 assert_eq!(
509 format_diff_value(5, 10),
510 "[additions]+5[/additions]/[deletions]-10[/deletions]/-5"
511 );
512 assert_eq!(
513 format_diff_value(0, 0),
514 "[additions]+0[/additions]/[deletions]-0[/deletions]/0"
515 );
516 }
517}