1use crate::domain::summary::{FileSummary, compute_file_summaries, compute_summary};
73use crate::domain::types::{AnalysisResult, AnalysisSummary, FunctionVerdict};
74use serde::Serialize;
75
76#[non_exhaustive]
97#[derive(Debug, Clone, Default, Serialize)]
98pub struct ViewSpec {
99 pub filters: Filters,
101 pub sort: SortKey,
103 pub limit: Option<usize>,
107 pub group_by: Option<GroupKey>,
114}
115
116#[non_exhaustive]
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
124#[serde(rename_all = "lowercase")]
125pub enum GroupKey {
126 File,
128}
129
130impl GroupKey {
131 pub fn as_wire_str(&self) -> &'static str {
137 match self {
138 Self::File => "file",
139 }
140 }
141}
142
143#[non_exhaustive]
151#[derive(Debug, Clone, Default, Serialize)]
152pub struct Filters {
153 pub only_failing: bool,
156 pub coverage_range: Option<CoverageRange>,
160}
161
162#[non_exhaustive]
170#[derive(Debug, Clone, Copy, Serialize)]
171pub struct CoverageRange {
172 pub min: f64,
174 pub max: f64,
176}
177
178impl CoverageRange {
179 pub fn new(min: f64, max: f64) -> Result<Self, CoverageRangeError> {
183 if !is_in_unit_percent(min) {
184 return Err(CoverageRangeError::OutOfRange { value: min });
185 }
186 if !is_in_unit_percent(max) {
187 return Err(CoverageRangeError::OutOfRange { value: max });
188 }
189 if min > max {
190 return Err(CoverageRangeError::MinExceedsMax { min, max });
191 }
192 Ok(Self { min, max })
193 }
194}
195
196fn is_in_unit_percent(v: f64) -> bool {
197 v.is_finite() && (0.0..=100.0).contains(&v)
198}
199
200#[non_exhaustive]
204#[derive(Debug, Clone, Copy, thiserror::Error, PartialEq)]
205pub enum CoverageRangeError {
206 #[error("coverage value out of range: {value}")]
207 OutOfRange { value: f64 },
208 #[error("min ({min}) exceeds max ({max})")]
209 MinExceedsMax { min: f64, max: f64 },
210}
211
212#[non_exhaustive]
232#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
233#[serde(rename_all = "lowercase")]
234pub enum SortKey {
235 #[default]
237 Crap,
238 Coverage,
240 Complexity,
242 Path,
244}
245
246impl SortKey {
247 pub fn as_wire_str(&self) -> &'static str {
249 match self {
250 Self::Crap => "crap",
251 Self::Coverage => "coverage",
252 Self::Complexity => "complexity",
253 Self::Path => "path",
254 }
255 }
256}
257
258#[non_exhaustive]
276#[derive(Debug, Serialize)]
277pub struct AnalysisView<'a> {
278 #[serde(skip)]
282 pub full: &'a AnalysisResult,
283 pub spec: ViewSpec,
285 pub eligible_count: usize,
289 pub truncated: bool,
293 pub shown: Vec<&'a FunctionVerdict>,
296 pub shown_summary: AnalysisSummary,
300 pub grouped: Option<GroupedView>,
305}
306
307#[non_exhaustive]
317#[derive(Debug, Clone, Serialize)]
318pub struct GroupedView {
319 pub key: GroupKey,
321 pub eligible_count: usize,
324 pub truncated: bool,
326 pub files: Vec<FileSummary>,
329}
330
331pub fn apply<'a>(result: &'a AnalysisResult, spec: ViewSpec) -> AnalysisView<'a> {
353 let eligible: Vec<&'a FunctionVerdict> = apply_filters(&result.functions, &spec.filters);
354 let eligible_count = eligible.len();
355
356 let grouped = apply_grouping(&eligible, &spec);
366
367 let (shown, truncated) = if grouped.is_some() {
368 (eligible, false)
369 } else {
370 let mut shown = eligible;
371 sort_in_place(&mut shown, spec.sort);
372 let truncated = truncate_to(&mut shown, spec.limit);
373 (shown, truncated)
374 };
375
376 let shown_summary = compute_summary(shown.iter().copied());
379
380 AnalysisView {
381 full: result,
382 spec,
383 eligible_count,
384 truncated,
385 shown,
386 shown_summary,
387 grouped,
388 }
389}
390
391fn apply_grouping(eligible: &[&FunctionVerdict], spec: &ViewSpec) -> Option<GroupedView> {
400 let key = spec.group_by?;
401 let mut files = compute_file_summaries(eligible.iter().copied());
402 let eligible_count = files.len();
403 sort_files_in_place(&mut files, spec.sort);
404 let truncated = truncate_files_to(&mut files, spec.limit);
405 Some(GroupedView {
406 key,
407 eligible_count,
408 truncated,
409 files,
410 })
411}
412
413fn sort_files_in_place(files: &mut [FileSummary], key: SortKey) {
423 match key {
424 SortKey::Crap => files.sort_by(cmp_files_by_avg_crap),
425 SortKey::Coverage => files.sort_by(cmp_files_by_avg_coverage),
426 SortKey::Complexity => files.sort_by_key(|f| std::cmp::Reverse(f.max_complexity)),
427 SortKey::Path => files.sort_by(|a, b| a.file_path.cmp(&b.file_path)),
428 }
429}
430
431fn cmp_files_by_avg_crap(a: &FileSummary, b: &FileSummary) -> std::cmp::Ordering {
432 let (ax, bx) = (a.average_crap, b.average_crap);
433 match (ax.is_nan(), bx.is_nan()) {
434 (true, true) => std::cmp::Ordering::Equal,
435 (true, false) => std::cmp::Ordering::Greater,
436 (false, true) => std::cmp::Ordering::Less,
437 (false, false) => bx.partial_cmp(&ax).expect("non-NaN partial_cmp infallible"),
438 }
439}
440
441fn cmp_files_by_avg_coverage(a: &FileSummary, b: &FileSummary) -> std::cmp::Ordering {
442 let (ax, bx) = (a.average_coverage, b.average_coverage);
443 match (ax.is_nan(), bx.is_nan()) {
444 (true, true) => std::cmp::Ordering::Equal,
445 (true, false) => std::cmp::Ordering::Greater,
446 (false, true) => std::cmp::Ordering::Less,
447 (false, false) => ax.partial_cmp(&bx).expect("non-NaN partial_cmp infallible"),
448 }
449}
450
451fn truncate_files_to(files: &mut Vec<FileSummary>, limit: Option<usize>) -> bool {
452 match limit {
453 Some(n) if n > 0 && files.len() > n => {
454 files.truncate(n);
455 true
456 }
457 _ => false,
458 }
459}
460
461fn apply_filters<'a>(
469 verdicts: &'a [FunctionVerdict],
470 filters: &Filters,
471) -> Vec<&'a FunctionVerdict> {
472 verdicts
473 .iter()
474 .filter(|v| !filters.only_failing || v.exceeds)
475 .filter(|v| match &filters.coverage_range {
476 Some(range) => matches_coverage_range(v.scored.coverage_percent, range),
477 None => true,
478 })
479 .collect()
480}
481
482fn matches_coverage_range(cov: f64, range: &CoverageRange) -> bool {
483 cov.is_finite() && cov >= range.min && cov <= range.max
484}
485
486fn sort_in_place(shown: &mut [&FunctionVerdict], key: SortKey) {
489 match key {
492 SortKey::Crap => shown.sort_by(cmp_by_crap),
493 SortKey::Coverage => shown.sort_by(cmp_by_coverage),
494 SortKey::Complexity => shown.sort_by(cmp_by_complexity),
495 SortKey::Path => shown.sort_by(cmp_by_path),
496 }
497}
498
499fn cmp_by_crap(a: &&FunctionVerdict, b: &&FunctionVerdict) -> std::cmp::Ordering {
503 let (ax, bx) = (a.scored.crap.value, b.scored.crap.value);
504 match (ax.is_nan(), bx.is_nan()) {
505 (true, true) => std::cmp::Ordering::Equal,
506 (true, false) => std::cmp::Ordering::Greater,
507 (false, true) => std::cmp::Ordering::Less,
508 (false, false) => bx.partial_cmp(&ax).expect("non-NaN partial_cmp infallible"),
509 }
510}
511
512fn cmp_by_coverage(a: &&FunctionVerdict, b: &&FunctionVerdict) -> std::cmp::Ordering {
514 let (ax, bx) = (a.scored.coverage_percent, b.scored.coverage_percent);
515 match (ax.is_nan(), bx.is_nan()) {
516 (true, true) => std::cmp::Ordering::Equal,
517 (true, false) => std::cmp::Ordering::Greater,
518 (false, true) => std::cmp::Ordering::Less,
519 (false, false) => ax.partial_cmp(&bx).expect("non-NaN partial_cmp infallible"),
520 }
521}
522
523fn cmp_by_complexity(a: &&FunctionVerdict, b: &&FunctionVerdict) -> std::cmp::Ordering {
525 b.scored.complexity.cmp(&a.scored.complexity)
526}
527
528fn cmp_by_path(a: &&FunctionVerdict, b: &&FunctionVerdict) -> std::cmp::Ordering {
530 match a
531 .scored
532 .identity
533 .file_path
534 .cmp(&b.scored.identity.file_path)
535 {
536 std::cmp::Ordering::Equal => cmp_by_crap(a, b),
537 ord => ord,
538 }
539}
540
541fn truncate_to(shown: &mut Vec<&FunctionVerdict>, limit: Option<usize>) -> bool {
547 match limit {
548 Some(n) if n > 0 && shown.len() > n => {
549 shown.truncate(n);
550 true
551 }
552 _ => false,
553 }
554}
555
556pub fn should_render_view_line(view: &AnalysisView<'_>) -> bool {
571 view.eligible_count < view.full.functions.len()
572 || view.truncated
573 || view.grouped.as_ref().is_some_and(|g| g.truncated)
574}
575
576#[cfg(test)]
579mod tests {
580 use super::*;
581 use crate::domain::types::{
582 AnalysisSummary, ComplexityMetric, CrapScore, FunctionIdentity, FunctionVerdict,
583 RiskDistribution, ScoredFunction, SourceSpan,
584 };
585
586 fn mk_verdict(
589 name: &str,
590 file: &str,
591 complexity: u32,
592 coverage: f64,
593 crap_value: f64,
594 threshold: f64,
595 ) -> FunctionVerdict {
596 let risk_level = crate::domain::crap::classify_risk(crap_value);
597 FunctionVerdict {
598 scored: ScoredFunction {
599 identity: FunctionIdentity {
600 file_path: file.to_string(),
601 qualified_name: name.to_string(),
602 span: SourceSpan {
603 start_line: 1,
604 end_line: 10,
605 start_column: 0,
606 end_column: 0,
607 },
608 },
609 complexity,
610 complexity_metric: ComplexityMetric::Cognitive,
611 coverage_percent: coverage,
612 branch_coverage_percent: None,
617 crap: CrapScore {
618 value: crap_value,
619 risk_level,
620 },
621 contributors: vec![],
622 },
623 threshold,
624 exceeds: crap_value > threshold,
625 diagnostic: None,
626 }
627 }
628
629 fn background_fixture() -> AnalysisResult {
631 let verdicts = vec![
632 mk_verdict("parse_lcov", "src/adapters/lcov.rs", 12, 100.0, 12.00, 25.0),
633 mk_verdict("walk_ast", "src/adapters/syn.rs", 18, 75.0, 23.06, 25.0),
634 mk_verdict(
635 "render_table",
636 "src/adapters/table.rs",
637 9,
638 60.0,
639 14.18,
640 25.0,
641 ),
642 mk_verdict(
643 "apply_threshold",
644 "src/domain/threshold.rs",
645 4,
646 100.0,
647 4.00,
648 25.0,
649 ),
650 mk_verdict(
651 "sort_verdicts",
652 "src/adapters/table.rs",
653 6,
654 0.0,
655 42.00,
656 25.0,
657 ),
658 mk_verdict("parse_args", "src/cli/mod.rs", 22, 50.0, 63.50, 25.0),
659 ];
660 let summary = compute_summary(&verdicts);
661 let passed = verdicts.iter().all(|v| !v.exceeds);
662 AnalysisResult {
663 functions: verdicts,
664 summary,
665 passed,
666 }
667 }
668
669 fn empty_result() -> AnalysisResult {
670 AnalysisResult {
671 functions: vec![],
672 summary: AnalysisSummary {
673 total_functions: 0,
674 total_files: 0,
675 exceeding_threshold: 0,
676 average_crap: 0.0,
677 median_crap: 0.0,
678 max_crap: None,
679 worst_function: None,
680 distribution: RiskDistribution {
681 low: 0,
682 acceptable: 0,
683 moderate: 0,
684 high: 0,
685 },
686 ..Default::default()
687 },
688 passed: true,
689 }
690 }
691
692 #[test]
695 fn default_spec_is_noop_on_fixture() {
696 let r = background_fixture();
700 let view = apply(&r, ViewSpec::default());
701 assert_eq!(view.shown.len(), r.functions.len());
702 assert_eq!(view.eligible_count, r.functions.len());
703 assert!(!view.truncated);
704 assert!(std::ptr::eq(view.full, &r));
706 for w in view.shown.windows(2) {
708 assert!(
709 w[0].scored.crap.value >= w[1].scored.crap.value,
710 "expected CRAP descending; got {} then {}",
711 w[0].scored.crap.value,
712 w[1].scored.crap.value
713 );
714 }
715 }
716
717 #[test]
718 fn default_spec_empty_input_is_empty_view() {
719 let r = empty_result();
721 let view = apply(&r, ViewSpec::default());
722 assert!(view.shown.is_empty());
723 assert_eq!(view.eligible_count, 0);
724 assert!(!view.truncated);
725 assert!(view.full.passed);
726 }
727
728 #[test]
729 fn view_full_immutability_after_apply() {
730 let r = background_fixture();
732 let crap_before: Vec<f64> = r.functions.iter().map(|v| v.scored.crap.value).collect();
733 let view = apply(&r, ViewSpec::default());
734 assert!(std::ptr::eq(view.full, &r));
736 let crap_after: Vec<f64> = r.functions.iter().map(|v| v.scored.crap.value).collect();
738 assert_eq!(crap_before, crap_after);
739 }
740
741 #[test]
742 fn default_spec_preserves_identity_set() {
743 let r = background_fixture();
745 let view = apply(&r, ViewSpec::default());
746 let shown_names: std::collections::HashSet<&String> = view
747 .shown
748 .iter()
749 .map(|v| &v.scored.identity.qualified_name)
750 .collect();
751 let original_names: std::collections::HashSet<&String> = r
752 .functions
753 .iter()
754 .map(|v| &v.scored.identity.qualified_name)
755 .collect();
756 assert_eq!(shown_names, original_names);
757 }
758
759 #[test]
762 fn coverage_range_new_validation_table() {
763 type Case = (f64, f64, Result<(f64, f64), ()>);
765 let cases: &[Case] = &[
766 (0.0, 100.0, Ok((0.0, 100.0))),
767 (50.0, 50.0, Ok((50.0, 50.0))),
768 (1.0, 90.0, Ok((1.0, 90.0))),
769 (-0.1, 50.0, Err(())),
770 (50.0, 100.1, Err(())),
771 (90.0, 50.0, Err(())),
772 (100.0, 0.0, Err(())),
773 ];
774 for (min, max, expect) in cases {
775 let got = CoverageRange::new(*min, *max);
776 match (got, expect) {
777 (Ok(r), Ok((emin, emax))) => {
778 assert!(
779 (r.min - emin).abs() < 1e-9 && (r.max - emax).abs() < 1e-9,
780 "min={min}, max={max}: got {r:?}, expected ({emin}, {emax})"
781 );
782 }
783 (Err(_), Err(())) => {} (got, expect) => panic!("min={min}, max={max}: got {got:?}, expected {expect:?}"),
785 }
786 }
787 }
788
789 #[test]
790 fn coverage_range_error_variants() {
791 let oor = CoverageRange::new(-1.0, 50.0).unwrap_err();
793 assert!(matches!(oor, CoverageRangeError::OutOfRange { .. }));
794 let mxm = CoverageRange::new(80.0, 20.0).unwrap_err();
795 assert!(matches!(mxm, CoverageRangeError::MinExceedsMax { .. }));
796 }
797
798 #[test]
801 fn sort_stability_on_tied_crap() {
802 let foo = mk_verdict("foo", "src/a.rs", 5, 80.0, 12.0, 25.0);
805 let bar = mk_verdict("bar", "src/a.rs", 5, 80.0, 12.0, 25.0);
806 let r = AnalysisResult {
807 functions: vec![foo, bar],
808 summary: empty_result().summary, passed: true,
810 };
811 let view = apply(&r, ViewSpec::default());
812 assert_eq!(
814 view.shown[0].scored.identity.qualified_name,
815 "foo",
816 "stable sort must preserve input order on ties; got {:?}",
817 view.shown
818 .iter()
819 .map(|v| &v.scored.identity.qualified_name)
820 .collect::<Vec<_>>()
821 );
822 assert_eq!(view.shown[1].scored.identity.qualified_name, "bar");
823 }
824
825 #[test]
828 fn only_failing_filter_retains_only_exceeds_true() {
829 let r = background_fixture();
831 let spec = ViewSpec {
832 filters: Filters {
833 only_failing: true,
834 ..Default::default()
835 },
836 ..Default::default()
837 };
838 let view = apply(&r, spec);
839 assert!(view.shown.iter().all(|v| v.exceeds));
840 for v in &view.shown {
842 assert!(v.scored.crap.value > v.threshold);
843 }
844 }
845
846 #[test]
847 fn coverage_range_filter_inclusive() {
848 let r = background_fixture();
850 let range = CoverageRange::new(50.0, 90.0).unwrap();
851 let spec = ViewSpec {
852 filters: Filters {
853 coverage_range: Some(range),
854 ..Default::default()
855 },
856 ..Default::default()
857 };
858 let view = apply(&r, spec);
859 assert!(view.shown.iter().all(|v| {
860 let cov = v.scored.coverage_percent;
861 cov.is_finite() && (50.0..=90.0).contains(&cov)
862 }));
863 let manual_count = r
864 .functions
865 .iter()
866 .filter(|v| v.scored.coverage_percent.is_finite())
867 .filter(|v| (50.0..=90.0).contains(&v.scored.coverage_percent))
868 .count();
869 assert_eq!(view.eligible_count, manual_count);
870 }
871
872 #[test]
873 fn coverage_range_boundary_inclusive_50_low() {
874 let v = mk_verdict("at50", "src/a.rs", 1, 50.0, 1.0, 25.0);
876 let r = AnalysisResult {
877 functions: vec![v],
878 summary: empty_result().summary,
879 passed: true,
880 };
881 let range = CoverageRange::new(50.0, 90.0).unwrap();
882 let spec = ViewSpec {
883 filters: Filters {
884 coverage_range: Some(range),
885 ..Default::default()
886 },
887 ..Default::default()
888 };
889 let view = apply(&r, spec);
890 assert_eq!(view.shown.len(), 1);
891 }
892
893 #[test]
894 fn coverage_range_boundary_inclusive_90_high() {
895 let v = mk_verdict("at90", "src/a.rs", 1, 90.0, 1.0, 25.0);
897 let r = AnalysisResult {
898 functions: vec![v],
899 summary: empty_result().summary,
900 passed: true,
901 };
902 let range = CoverageRange::new(50.0, 90.0).unwrap();
903 let spec = ViewSpec {
904 filters: Filters {
905 coverage_range: Some(range),
906 ..Default::default()
907 },
908 ..Default::default()
909 };
910 let view = apply(&r, spec);
911 assert_eq!(view.shown.len(), 1);
912 }
913
914 #[test]
915 fn coverage_range_boundary_inclusive_below_low() {
916 let v = mk_verdict("just_under", "src/a.rs", 1, 49.9, 1.0, 25.0);
918 let r = AnalysisResult {
919 functions: vec![v],
920 summary: empty_result().summary,
921 passed: true,
922 };
923 let range = CoverageRange::new(50.0, 90.0).unwrap();
924 let spec = ViewSpec {
925 filters: Filters {
926 coverage_range: Some(range),
927 ..Default::default()
928 },
929 ..Default::default()
930 };
931 let view = apply(&r, spec);
932 assert!(view.shown.is_empty());
933 }
934
935 #[test]
936 fn coverage_range_boundary_inclusive_above_high() {
937 let v = mk_verdict("just_over", "src/a.rs", 1, 90.1, 1.0, 25.0);
939 let r = AnalysisResult {
940 functions: vec![v],
941 summary: empty_result().summary,
942 passed: true,
943 };
944 let range = CoverageRange::new(50.0, 90.0).unwrap();
945 let spec = ViewSpec {
946 filters: Filters {
947 coverage_range: Some(range),
948 ..Default::default()
949 },
950 ..Default::default()
951 };
952 let view = apply(&r, spec);
953 assert!(view.shown.is_empty());
954 }
955
956 #[test]
957 fn coverage_range_boundary_inclusive_zero_singleton() {
958 let v = mk_verdict("zero", "src/a.rs", 1, 0.0, 1.0, 25.0);
960 let r = AnalysisResult {
961 functions: vec![v],
962 summary: empty_result().summary,
963 passed: true,
964 };
965 let range = CoverageRange::new(0.0, 0.0).unwrap();
966 let spec = ViewSpec {
967 filters: Filters {
968 coverage_range: Some(range),
969 ..Default::default()
970 },
971 ..Default::default()
972 };
973 let view = apply(&r, spec);
974 assert_eq!(view.shown.len(), 1);
975 }
976
977 #[test]
978 fn coverage_range_boundary_inclusive_hundred_singleton() {
979 let v = mk_verdict("full", "src/a.rs", 1, 100.0, 1.0, 25.0);
981 let r = AnalysisResult {
982 functions: vec![v],
983 summary: empty_result().summary,
984 passed: true,
985 };
986 let range = CoverageRange::new(100.0, 100.0).unwrap();
987 let spec = ViewSpec {
988 filters: Filters {
989 coverage_range: Some(range),
990 ..Default::default()
991 },
992 ..Default::default()
993 };
994 let view = apply(&r, spec);
995 assert_eq!(view.shown.len(), 1);
996 }
997
998 #[test]
999 fn filters_and_compose() {
1000 let r = background_fixture();
1003 let range = CoverageRange::new(50.0, 100.0).unwrap();
1004 let spec = ViewSpec {
1005 filters: Filters {
1006 only_failing: true,
1007 coverage_range: Some(range),
1008 },
1009 ..Default::default()
1010 };
1011 let view = apply(&r, spec);
1012 for v in &view.shown {
1013 assert!(v.exceeds);
1014 let cov = v.scored.coverage_percent;
1015 assert!((50.0..=100.0).contains(&cov));
1016 }
1017 }
1018
1019 #[test]
1020 fn nan_coverage_excluded_from_range_filter() {
1021 let v = mk_verdict("zero_lines", "src/a.rs", 1, f64::NAN, 1.0, 25.0);
1023 let r = AnalysisResult {
1024 functions: vec![v],
1025 summary: empty_result().summary,
1026 passed: true,
1027 };
1028 let range = CoverageRange::new(0.0, 100.0).unwrap();
1029 let spec = ViewSpec {
1030 filters: Filters {
1031 coverage_range: Some(range),
1032 ..Default::default()
1033 },
1034 ..Default::default()
1035 };
1036 let view = apply(&r, spec);
1037 assert!(view.shown.is_empty());
1038 }
1039
1040 #[test]
1043 fn sort_by_crap_descending() {
1044 let r = background_fixture();
1046 let spec = ViewSpec {
1047 sort: SortKey::Crap,
1048 ..Default::default()
1049 };
1050 let view = apply(&r, spec);
1051 for w in view.shown.windows(2) {
1052 assert!(w[0].scored.crap.value >= w[1].scored.crap.value);
1053 }
1054 }
1055
1056 #[test]
1057 fn sort_by_coverage_ascending() {
1058 let r = background_fixture();
1060 let spec = ViewSpec {
1061 sort: SortKey::Coverage,
1062 ..Default::default()
1063 };
1064 let view = apply(&r, spec);
1065 for w in view.shown.windows(2) {
1066 assert!(w[0].scored.coverage_percent <= w[1].scored.coverage_percent);
1067 }
1068 }
1069
1070 #[test]
1071 fn sort_by_complexity_descending() {
1072 let r = background_fixture();
1074 let spec = ViewSpec {
1075 sort: SortKey::Complexity,
1076 ..Default::default()
1077 };
1078 let view = apply(&r, spec);
1079 for w in view.shown.windows(2) {
1080 assert!(w[0].scored.complexity >= w[1].scored.complexity);
1081 }
1082 }
1083
1084 #[test]
1085 fn sort_by_path_alphabetical_then_crap() {
1086 let r = background_fixture();
1088 let spec = ViewSpec {
1089 sort: SortKey::Path,
1090 ..Default::default()
1091 };
1092 let view = apply(&r, spec);
1093 for w in view.shown.windows(2) {
1095 let (a, b) = (
1096 &w[0].scored.identity.file_path,
1097 &w[1].scored.identity.file_path,
1098 );
1099 assert!(a <= b, "files not in ascending order: {a} then {b}");
1100 }
1101 for w in view.shown.windows(2) {
1103 if w[0].scored.identity.file_path == w[1].scored.identity.file_path {
1104 assert!(
1105 w[0].scored.crap.value >= w[1].scored.crap.value,
1106 "within file {}: CRAP not descending: {} then {}",
1107 w[0].scored.identity.file_path,
1108 w[0].scored.crap.value,
1109 w[1].scored.crap.value
1110 );
1111 }
1112 }
1113 }
1114
1115 #[test]
1116 fn sort_by_path_secondary_multi_file() {
1117 let verdicts = vec![
1121 mk_verdict("a_low", "src/a.rs", 1, 50.0, 5.0, 25.0),
1122 mk_verdict("a_high", "src/a.rs", 1, 50.0, 30.0, 25.0),
1123 mk_verdict("b_only", "src/b.rs", 1, 50.0, 10.0, 25.0),
1124 mk_verdict("c_low", "src/c.rs", 1, 50.0, 1.0, 25.0),
1125 mk_verdict("c_high", "src/c.rs", 1, 50.0, 50.0, 25.0),
1126 ];
1127 let r = AnalysisResult {
1128 functions: verdicts,
1129 summary: empty_result().summary,
1130 passed: true,
1131 };
1132 let spec = ViewSpec {
1133 sort: SortKey::Path,
1134 ..Default::default()
1135 };
1136 let view = apply(&r, spec);
1137 let names: Vec<&str> = view
1138 .shown
1139 .iter()
1140 .map(|v| v.scored.identity.qualified_name.as_str())
1141 .collect();
1142 assert_eq!(
1143 names,
1144 vec!["a_high", "a_low", "b_only", "c_high", "c_low"],
1145 "path sort with secondary CRAP-desc order wrong"
1146 );
1147 }
1148
1149 #[test]
1150 fn nan_coverage_sorts_last_under_coverage_ascending() {
1151 let verdicts = vec![
1153 mk_verdict("c10", "src/a.rs", 1, 10.0, 1.0, 25.0),
1154 mk_verdict("nan1", "src/a.rs", 1, f64::NAN, 1.0, 25.0),
1155 mk_verdict("c50", "src/a.rs", 1, 50.0, 1.0, 25.0),
1156 mk_verdict("nan2", "src/a.rs", 1, f64::NAN, 1.0, 25.0),
1157 mk_verdict("c90", "src/a.rs", 1, 90.0, 1.0, 25.0),
1158 ];
1159 let r = AnalysisResult {
1160 functions: verdicts,
1161 summary: empty_result().summary,
1162 passed: true,
1163 };
1164 let spec = ViewSpec {
1165 sort: SortKey::Coverage,
1166 ..Default::default()
1167 };
1168 let view = apply(&r, spec);
1169 let coverages: Vec<f64> = view
1171 .shown
1172 .iter()
1173 .map(|v| v.scored.coverage_percent)
1174 .collect();
1175 assert_eq!(coverages[0], 10.0);
1176 assert_eq!(coverages[1], 50.0);
1177 assert_eq!(coverages[2], 90.0);
1178 assert!(coverages[3].is_nan());
1179 assert!(coverages[4].is_nan());
1180 }
1181
1182 #[test]
1185 fn limit_truncates() {
1186 let r = background_fixture();
1188 let spec = ViewSpec {
1189 limit: Some(3),
1190 ..Default::default()
1191 };
1192 let view = apply(&r, spec);
1193 assert_eq!(view.shown.len(), 3);
1194 assert_eq!(view.eligible_count, 6);
1195 assert!(view.truncated);
1196 }
1197
1198 #[test]
1199 fn limit_greater_than_eligible() {
1200 let r = background_fixture();
1202 let spec = ViewSpec {
1203 limit: Some(100),
1204 ..Default::default()
1205 };
1206 let view = apply(&r, spec);
1207 assert_eq!(view.shown.len(), 6);
1208 assert_eq!(view.eligible_count, 6);
1209 assert!(!view.truncated);
1210 }
1211
1212 #[test]
1213 fn limit_none() {
1214 let r = background_fixture();
1216 let spec = ViewSpec {
1217 limit: None,
1218 ..Default::default()
1219 };
1220 let view = apply(&r, spec);
1221 assert_eq!(view.shown.len(), view.eligible_count);
1222 assert!(!view.truncated);
1223 }
1224
1225 #[test]
1226 fn limit_zero_treated_as_no_limit() {
1227 let r = background_fixture();
1230 let spec = ViewSpec {
1231 limit: Some(0),
1232 ..Default::default()
1233 };
1234 let view = apply(&r, spec);
1235 assert_eq!(view.shown.len(), view.eligible_count);
1236 assert!(!view.truncated);
1237 }
1238
1239 #[test]
1240 fn limit_equal_to_eligible_does_not_mark_truncated() {
1241 let r = background_fixture();
1245 assert_eq!(r.functions.len(), 6, "background fixture sanity");
1246 let spec = ViewSpec {
1247 limit: Some(6),
1248 ..Default::default()
1249 };
1250 let view = apply(&r, spec);
1251 assert_eq!(view.shown.len(), 6);
1252 assert_eq!(view.eligible_count, 6);
1253 assert!(!view.truncated, "limit == eligible must NOT mark truncated");
1254 }
1255
1256 #[test]
1259 fn order_filter_then_sort_then_truncate() {
1260 let r = background_fixture();
1263 let spec = ViewSpec {
1264 filters: Filters {
1265 only_failing: true,
1266 ..Default::default()
1267 },
1268 sort: SortKey::Coverage,
1269 limit: Some(2),
1270 ..Default::default()
1271 };
1272 let view = apply(&r, spec);
1273 assert_eq!(view.shown.len(), 2);
1274 for v in &view.shown {
1275 assert!(v.exceeds);
1276 }
1277 assert!(view.shown[0].scored.coverage_percent <= view.shown[1].scored.coverage_percent);
1279 let total_failing = r.functions.iter().filter(|v| v.exceeds).count();
1281 assert_eq!(view.eligible_count, total_failing);
1282 }
1283
1284 #[test]
1285 fn truncation_does_not_change_gate() {
1286 let verdicts = vec![
1289 mk_verdict("ok", "src/a.rs", 1, 100.0, 1.0, 25.0),
1290 mk_verdict("fail1", "src/a.rs", 1, 0.0, 50.0, 25.0),
1291 mk_verdict("fail2", "src/a.rs", 1, 0.0, 60.0, 25.0),
1292 mk_verdict("fail3", "src/a.rs", 1, 0.0, 70.0, 25.0),
1293 ];
1294 let summary = compute_summary(&verdicts);
1295 let passed = verdicts.iter().all(|v| !v.exceeds);
1296 let r = AnalysisResult {
1297 functions: verdicts,
1298 summary,
1299 passed,
1300 };
1301 let spec = ViewSpec {
1302 limit: Some(1),
1303 ..Default::default()
1304 };
1305 let view = apply(&r, spec);
1306 assert_eq!(view.shown.len(), 1);
1307 assert!(!view.full.passed);
1309 assert_eq!(view.full.summary.exceeding_threshold, 3);
1310 }
1311
1312 #[test]
1313 fn filtering_does_not_change_gate() {
1314 let verdicts = vec![
1317 mk_verdict("ok", "src/a.rs", 1, 100.0, 1.0, 25.0),
1318 mk_verdict("fail1", "src/a.rs", 1, 0.0, 50.0, 25.0),
1319 mk_verdict("fail2", "src/a.rs", 1, 0.0, 60.0, 25.0),
1320 mk_verdict("fail3", "src/a.rs", 1, 0.0, 70.0, 25.0),
1321 ];
1322 let summary = compute_summary(&verdicts);
1323 let r = AnalysisResult {
1324 functions: verdicts,
1325 summary,
1326 passed: false,
1327 };
1328 let range = CoverageRange::new(99.0, 100.0).unwrap();
1330 let spec = ViewSpec {
1331 filters: Filters {
1332 coverage_range: Some(range),
1333 ..Default::default()
1334 },
1335 ..Default::default()
1336 };
1337 let view = apply(&r, spec);
1338 assert!(view.shown.iter().all(|v| !v.exceeds));
1339 assert!(!view.full.passed);
1340 assert_eq!(view.full.summary.exceeding_threshold, 3);
1341 }
1342
1343 #[test]
1346 fn shown_summary_over_shown_subset() {
1347 let r = background_fixture();
1349 let spec = ViewSpec {
1350 filters: Filters {
1351 only_failing: true,
1352 ..Default::default()
1353 },
1354 ..Default::default()
1355 };
1356 let view = apply(&r, spec);
1357 assert_eq!(view.shown_summary.total_functions, view.shown.len());
1358 assert_eq!(
1359 view.shown_summary.exceeding_threshold,
1360 view.shown.len(),
1361 "every shown row exceeds, so shown_summary should report all"
1362 );
1363 let manual_avg: f64 =
1365 view.shown.iter().map(|v| v.scored.crap.value).sum::<f64>() / view.shown.len() as f64;
1366 assert!((view.shown_summary.average_crap - manual_avg).abs() < 1e-9);
1367 }
1368
1369 #[test]
1370 fn shown_summary_differs_from_full() {
1371 let verdicts = vec![
1373 mk_verdict("ok1", "src/a.rs", 1, 100.0, 1.0, 25.0),
1374 mk_verdict("ok2", "src/a.rs", 1, 100.0, 2.0, 25.0),
1375 mk_verdict("ok3", "src/a.rs", 1, 100.0, 3.0, 25.0),
1376 mk_verdict("fail1", "src/a.rs", 1, 0.0, 50.0, 25.0),
1377 mk_verdict("fail2", "src/a.rs", 1, 0.0, 60.0, 25.0),
1378 mk_verdict("fail3", "src/a.rs", 1, 0.0, 70.0, 25.0),
1379 ];
1380 let summary = compute_summary(&verdicts);
1381 let r = AnalysisResult {
1382 functions: verdicts,
1383 summary,
1384 passed: false,
1385 };
1386 let spec = ViewSpec {
1387 filters: Filters {
1388 only_failing: true,
1389 ..Default::default()
1390 },
1391 ..Default::default()
1392 };
1393 let view = apply(&r, spec);
1394 assert_eq!(view.full.summary.total_functions, 6);
1395 assert_eq!(view.shown_summary.total_functions, 3);
1396 }
1397
1398 #[test]
1401 fn all_filtered_out_produces_empty_shown() {
1402 let v = mk_verdict("low_cov", "src/a.rs", 1, 50.0, 1.0, 25.0);
1404 let r = AnalysisResult {
1405 functions: vec![v],
1406 summary: empty_result().summary,
1407 passed: true,
1408 };
1409 let range = CoverageRange::new(95.0, 100.0).unwrap();
1410 let spec = ViewSpec {
1411 filters: Filters {
1412 coverage_range: Some(range),
1413 ..Default::default()
1414 },
1415 ..Default::default()
1416 };
1417 let view = apply(&r, spec);
1418 assert!(view.shown.is_empty());
1419 assert_eq!(view.eligible_count, 0);
1420 assert!(!view.truncated);
1421 }
1422
1423 #[test]
1426 fn display_predicate_default_spec_is_false() {
1427 let r = background_fixture();
1429 let view = apply(&r, ViewSpec::default());
1430 assert!(!should_render_view_line(&view));
1431 }
1432
1433 #[test]
1434 fn display_predicate_sort_only_is_false() {
1435 let r = background_fixture();
1437 let spec = ViewSpec {
1438 sort: SortKey::Coverage,
1439 ..Default::default()
1440 };
1441 let view = apply(&r, spec);
1442 assert!(!should_render_view_line(&view));
1443 }
1444
1445 #[test]
1446 fn display_predicate_top_truncating_is_true() {
1447 let r = background_fixture();
1449 let spec = ViewSpec {
1450 limit: Some(2),
1451 ..Default::default()
1452 };
1453 let view = apply(&r, spec);
1454 assert!(should_render_view_line(&view));
1455 }
1456
1457 #[test]
1458 fn display_predicate_coverage_filter_excluding_is_true() {
1459 let r = background_fixture();
1461 let range = CoverageRange::new(99.0, 100.0).unwrap();
1462 let spec = ViewSpec {
1463 filters: Filters {
1464 coverage_range: Some(range),
1465 ..Default::default()
1466 },
1467 ..Default::default()
1468 };
1469 let view = apply(&r, spec);
1470 assert!(should_render_view_line(&view));
1471 }
1472
1473 #[test]
1474 fn display_predicate_only_failing_reducing_is_true() {
1475 let r = background_fixture();
1477 let spec = ViewSpec {
1478 filters: Filters {
1479 only_failing: true,
1480 ..Default::default()
1481 },
1482 ..Default::default()
1483 };
1484 let view = apply(&r, spec);
1485 assert!(should_render_view_line(&view));
1486 }
1487
1488 #[test]
1491 fn no_group_by_means_no_grouped_block() {
1492 let r = background_fixture();
1494 let view = apply(&r, ViewSpec::default());
1495 assert!(view.grouped.is_none());
1496 }
1497
1498 #[test]
1499 fn group_by_file_populates_grouped_block() {
1500 let r = background_fixture();
1502 let spec = ViewSpec {
1503 group_by: Some(GroupKey::File),
1504 ..Default::default()
1505 };
1506 let view = apply(&r, spec);
1507 let grouped = view.grouped.as_ref().expect("grouped block expected");
1508 assert_eq!(grouped.key, GroupKey::File);
1509 assert_eq!(grouped.files.len(), 5);
1512 assert_eq!(grouped.eligible_count, 5);
1513 assert!(!grouped.truncated);
1514 }
1515
1516 #[test]
1517 fn group_by_file_does_not_truncate_function_shown() {
1518 let r = background_fixture();
1520 let spec = ViewSpec {
1521 group_by: Some(GroupKey::File),
1522 limit: Some(2),
1523 ..Default::default()
1524 };
1525 let view = apply(&r, spec);
1526 assert_eq!(view.shown.len(), r.functions.len());
1528 assert!(!view.truncated);
1529 let grouped = view.grouped.as_ref().unwrap();
1530 assert_eq!(grouped.files.len(), 2);
1531 assert!(grouped.truncated);
1532 assert_eq!(grouped.eligible_count, 5);
1533 }
1534
1535 #[test]
1536 fn group_by_file_keeps_gate_unchanged() {
1537 let r = background_fixture();
1540 let baseline_passed = r.passed;
1541 let baseline_total = r.summary.total_functions;
1542 let baseline_exceeding = r.summary.exceeding_threshold;
1543 let spec = ViewSpec {
1544 group_by: Some(GroupKey::File),
1545 limit: Some(1),
1546 ..Default::default()
1547 };
1548 let view = apply(&r, spec);
1549 assert_eq!(view.full.passed, baseline_passed);
1550 assert_eq!(view.full.summary.total_functions, baseline_total);
1551 assert_eq!(view.full.summary.exceeding_threshold, baseline_exceeding);
1552 }
1553
1554 #[test]
1555 fn group_by_file_default_sort_is_avg_crap_desc() {
1556 let r = background_fixture();
1557 let spec = ViewSpec {
1558 group_by: Some(GroupKey::File),
1559 ..Default::default()
1560 };
1561 let view = apply(&r, spec);
1562 let files = &view.grouped.as_ref().unwrap().files;
1563 for w in files.windows(2) {
1564 assert!(
1565 w[0].average_crap >= w[1].average_crap,
1566 "files not in average_crap descending order"
1567 );
1568 }
1569 }
1570
1571 #[test]
1572 fn group_by_file_sort_by_coverage_ascending() {
1573 let r = background_fixture();
1574 let spec = ViewSpec {
1575 group_by: Some(GroupKey::File),
1576 sort: SortKey::Coverage,
1577 ..Default::default()
1578 };
1579 let view = apply(&r, spec);
1580 let files = &view.grouped.as_ref().unwrap().files;
1581 for w in files.windows(2) {
1582 assert!(w[0].average_coverage <= w[1].average_coverage);
1583 }
1584 }
1585
1586 #[test]
1587 fn group_by_file_sort_by_complexity_descending() {
1588 let r = background_fixture();
1589 let spec = ViewSpec {
1590 group_by: Some(GroupKey::File),
1591 sort: SortKey::Complexity,
1592 ..Default::default()
1593 };
1594 let view = apply(&r, spec);
1595 let files = &view.grouped.as_ref().unwrap().files;
1596 for w in files.windows(2) {
1597 assert!(w[0].max_complexity >= w[1].max_complexity);
1598 }
1599 }
1600
1601 #[test]
1602 fn group_by_file_sort_by_path_alphabetical() {
1603 let r = background_fixture();
1604 let spec = ViewSpec {
1605 group_by: Some(GroupKey::File),
1606 sort: SortKey::Path,
1607 ..Default::default()
1608 };
1609 let view = apply(&r, spec);
1610 let files = &view.grouped.as_ref().unwrap().files;
1611 for w in files.windows(2) {
1612 assert!(w[0].file_path <= w[1].file_path);
1613 }
1614 }
1615
1616 #[test]
1617 fn group_by_file_truncate_files() {
1618 let r = background_fixture();
1619 let spec = ViewSpec {
1620 group_by: Some(GroupKey::File),
1621 limit: Some(3),
1622 ..Default::default()
1623 };
1624 let view = apply(&r, spec);
1625 let grouped = view.grouped.as_ref().unwrap();
1626 assert_eq!(grouped.files.len(), 3);
1627 assert!(grouped.truncated);
1628 assert_eq!(grouped.eligible_count, 5);
1629 }
1630
1631 #[test]
1632 fn group_by_file_filters_compose_before_grouping() {
1633 let r = background_fixture();
1636 let spec = ViewSpec {
1637 filters: Filters {
1638 only_failing: true,
1639 ..Default::default()
1640 },
1641 group_by: Some(GroupKey::File),
1642 ..Default::default()
1643 };
1644 let view = apply(&r, spec);
1645 let grouped = view.grouped.as_ref().unwrap();
1646 assert_eq!(grouped.files.len(), 2);
1649 for f in &grouped.files {
1651 assert!(f.exceeding_count >= 1);
1652 }
1653 }
1654
1655 #[test]
1656 fn group_by_file_empty_input_produces_empty_files() {
1657 let r = empty_result();
1658 let spec = ViewSpec {
1659 group_by: Some(GroupKey::File),
1660 ..Default::default()
1661 };
1662 let view = apply(&r, spec);
1663 let grouped = view.grouped.as_ref().unwrap();
1664 assert!(grouped.files.is_empty());
1665 assert_eq!(grouped.eligible_count, 0);
1666 assert!(!grouped.truncated);
1667 }
1668
1669 #[test]
1670 fn display_predicate_group_by_only_default_input_is_false() {
1671 let r = background_fixture();
1675 let spec = ViewSpec {
1676 group_by: Some(GroupKey::File),
1677 ..Default::default()
1678 };
1679 let view = apply(&r, spec);
1680 assert!(!should_render_view_line(&view));
1681 }
1682
1683 #[test]
1684 fn display_predicate_group_by_truncating_files_is_true() {
1685 let r = background_fixture();
1686 let spec = ViewSpec {
1687 group_by: Some(GroupKey::File),
1688 limit: Some(2),
1689 ..Default::default()
1690 };
1691 let view = apply(&r, spec);
1692 assert!(should_render_view_line(&view));
1693 }
1694
1695 #[test]
1705 fn group_by_file_top_zero_is_no_limit() {
1706 let r = background_fixture();
1709 let spec = ViewSpec {
1710 group_by: Some(GroupKey::File),
1711 limit: Some(0),
1712 ..Default::default()
1713 };
1714 let view = apply(&r, spec);
1715 let grouped = view.grouped.as_ref().expect("grouping active");
1716 assert!(!grouped.truncated);
1717 assert_eq!(grouped.files.len(), 5);
1719 }
1720
1721 #[test]
1722 fn group_by_file_limit_equal_to_file_count_is_not_truncated() {
1723 let r = background_fixture();
1727 let spec = ViewSpec {
1728 group_by: Some(GroupKey::File),
1729 limit: Some(5),
1730 ..Default::default()
1731 };
1732 let view = apply(&r, spec);
1733 let grouped = view.grouped.as_ref().expect("grouping active");
1734 assert!(!grouped.truncated);
1735 assert_eq!(grouped.files.len(), 5);
1736 }
1737
1738 #[test]
1748 fn display_predicate_full_grouping_no_reduction_is_false() {
1749 let r = background_fixture();
1753 let spec = ViewSpec {
1754 group_by: Some(GroupKey::File),
1755 ..Default::default()
1756 };
1757 let view = apply(&r, spec);
1758 assert!(!should_render_view_line(&view));
1759 }
1760
1761 #[test]
1762 fn display_predicate_grouping_reduces_files_is_true() {
1763 let r = background_fixture();
1767 let spec = ViewSpec {
1768 group_by: Some(GroupKey::File),
1769 filters: Filters {
1770 only_failing: true,
1771 ..Default::default()
1772 },
1773 ..Default::default()
1774 };
1775 let view = apply(&r, spec);
1776 assert!(should_render_view_line(&view));
1777 }
1778}
1779
1780#[cfg(test)]
1781mod proptests {
1782 use super::*;
1783 use crate::test_strategies::{arb_analysis_result, arb_verdict_with_nan_coverage};
1784 use proptest::prelude::*;
1785
1786 fn legacy_sort_order(result: &AnalysisResult) -> Vec<&FunctionVerdict> {
1789 let mut sorted: Vec<&FunctionVerdict> = result.functions.iter().collect();
1790 sorted.sort_by(|a, b| {
1791 b.scored
1792 .crap
1793 .value
1794 .partial_cmp(&a.scored.crap.value)
1795 .unwrap_or(std::cmp::Ordering::Equal)
1796 });
1797 sorted
1798 }
1799
1800 proptest! {
1801 #![proptest_config(ProptestConfig::with_cases(256))]
1802
1803 #[test]
1811 fn prop_default_spec_order_matches_legacy_sort(result in arb_analysis_result()) {
1812 let view = apply(&result, ViewSpec::default());
1813 let legacy = legacy_sort_order(&result);
1814 prop_assert_eq!(view.shown.len(), legacy.len());
1815 for (a, b) in view.shown.iter().zip(legacy.iter()) {
1816 prop_assert!(std::ptr::eq(*a, *b));
1817 }
1818 }
1819
1820 #[test]
1822 fn prop_default_spec_preserves_identity(result in arb_analysis_result()) {
1823 let view = apply(&result, ViewSpec::default());
1824 let shown_identities: std::collections::HashSet<&crate::domain::types::FunctionIdentity> =
1825 view.shown.iter().map(|v| &v.scored.identity).collect();
1826 let original_identities: std::collections::HashSet<&crate::domain::types::FunctionIdentity> =
1827 result.functions.iter().map(|v| &v.scored.identity).collect();
1828 prop_assert_eq!(shown_identities, original_identities);
1829 }
1830
1831 #[test]
1834 fn prop_default_spec_preserves_summary(result in arb_analysis_result()) {
1835 let view = apply(&result, ViewSpec::default());
1836 prop_assert!(std::ptr::eq(view.full, &result));
1837 prop_assert_eq!(view.full.summary.total_functions, result.summary.total_functions);
1839 }
1840
1841 #[test]
1843 fn prop_display_predicate_biconditional(result in arb_analysis_result()) {
1844 let view = apply(&result, ViewSpec::default());
1846 let computed = should_render_view_line(&view);
1847 let expected = view.eligible_count < view.full.functions.len() || view.truncated;
1848 prop_assert_eq!(computed, expected);
1849 prop_assert!(!computed);
1851 }
1852
1853 #[test]
1855 fn prop_apply_never_panics_with_nan_coverage(
1856 verdicts in prop::collection::vec(arb_verdict_with_nan_coverage(), 0..50)
1857 ) {
1858 let result = AnalysisResult {
1859 functions: verdicts.clone(),
1860 summary: crate::domain::types::AnalysisSummary {
1861 total_functions: verdicts.len(),
1862 total_files: verdicts.len(),
1863 exceeding_threshold: 0,
1864 average_crap: 0.0,
1865 median_crap: 0.0,
1866 max_crap: None,
1867 worst_function: None,
1868 distribution: crate::domain::types::RiskDistribution {
1869 low: 0, acceptable: 0, moderate: 0, high: 0,
1870 },
1871 ..Default::default()
1872 },
1873 passed: true,
1874 };
1875 for sort in [SortKey::Crap, SortKey::Coverage, SortKey::Complexity, SortKey::Path] {
1877 let spec = ViewSpec {
1878 filters: Filters {
1879 coverage_range: Some(CoverageRange::new(0.0, 100.0).unwrap()),
1880 ..Default::default()
1881 },
1882 sort,
1883 limit: Some(10),
1884 ..Default::default()
1885 };
1886 let _ = apply(&result, spec);
1887 }
1888 }
1889 }
1890}