1use crate::domain::summary::{FileSummary, compute_file_summaries, compute_summary};
73use crate::domain::types::{AnalysisResult, AnalysisSummary, FunctionVerdict};
74use serde::Serialize;
75
76#[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#[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 crap: CrapScore {
613 value: crap_value,
614 risk_level,
615 },
616 contributors: vec![],
617 },
618 threshold,
619 exceeds: crap_value > threshold,
620 diagnostic: None,
621 }
622 }
623
624 fn background_fixture() -> AnalysisResult {
626 let verdicts = vec![
627 mk_verdict("parse_lcov", "src/adapters/lcov.rs", 12, 100.0, 12.00, 25.0),
628 mk_verdict("walk_ast", "src/adapters/syn.rs", 18, 75.0, 23.06, 25.0),
629 mk_verdict(
630 "render_table",
631 "src/adapters/table.rs",
632 9,
633 60.0,
634 14.18,
635 25.0,
636 ),
637 mk_verdict(
638 "apply_threshold",
639 "src/domain/threshold.rs",
640 4,
641 100.0,
642 4.00,
643 25.0,
644 ),
645 mk_verdict(
646 "sort_verdicts",
647 "src/adapters/table.rs",
648 6,
649 0.0,
650 42.00,
651 25.0,
652 ),
653 mk_verdict("parse_args", "src/cli/mod.rs", 22, 50.0, 63.50, 25.0),
654 ];
655 let summary = compute_summary(&verdicts);
656 let passed = verdicts.iter().all(|v| !v.exceeds);
657 AnalysisResult {
658 functions: verdicts,
659 summary,
660 passed,
661 }
662 }
663
664 fn empty_result() -> AnalysisResult {
665 AnalysisResult {
666 functions: vec![],
667 summary: AnalysisSummary {
668 total_functions: 0,
669 total_files: 0,
670 exceeding_threshold: 0,
671 average_crap: 0.0,
672 median_crap: 0.0,
673 max_crap: None,
674 worst_function: None,
675 distribution: RiskDistribution {
676 low: 0,
677 acceptable: 0,
678 moderate: 0,
679 high: 0,
680 },
681 ..Default::default()
682 },
683 passed: true,
684 }
685 }
686
687 #[test]
690 fn default_spec_is_noop_on_fixture() {
691 let r = background_fixture();
695 let view = apply(&r, ViewSpec::default());
696 assert_eq!(view.shown.len(), r.functions.len());
697 assert_eq!(view.eligible_count, r.functions.len());
698 assert!(!view.truncated);
699 assert!(std::ptr::eq(view.full, &r));
701 for w in view.shown.windows(2) {
703 assert!(
704 w[0].scored.crap.value >= w[1].scored.crap.value,
705 "expected CRAP descending; got {} then {}",
706 w[0].scored.crap.value,
707 w[1].scored.crap.value
708 );
709 }
710 }
711
712 #[test]
713 fn default_spec_empty_input_is_empty_view() {
714 let r = empty_result();
716 let view = apply(&r, ViewSpec::default());
717 assert!(view.shown.is_empty());
718 assert_eq!(view.eligible_count, 0);
719 assert!(!view.truncated);
720 assert!(view.full.passed);
721 }
722
723 #[test]
724 fn view_full_immutability_after_apply() {
725 let r = background_fixture();
727 let crap_before: Vec<f64> = r.functions.iter().map(|v| v.scored.crap.value).collect();
728 let view = apply(&r, ViewSpec::default());
729 assert!(std::ptr::eq(view.full, &r));
731 let crap_after: Vec<f64> = r.functions.iter().map(|v| v.scored.crap.value).collect();
733 assert_eq!(crap_before, crap_after);
734 }
735
736 #[test]
737 fn default_spec_preserves_identity_set() {
738 let r = background_fixture();
740 let view = apply(&r, ViewSpec::default());
741 let shown_names: std::collections::HashSet<&String> = view
742 .shown
743 .iter()
744 .map(|v| &v.scored.identity.qualified_name)
745 .collect();
746 let original_names: std::collections::HashSet<&String> = r
747 .functions
748 .iter()
749 .map(|v| &v.scored.identity.qualified_name)
750 .collect();
751 assert_eq!(shown_names, original_names);
752 }
753
754 #[test]
757 fn coverage_range_new_validation_table() {
758 type Case = (f64, f64, Result<(f64, f64), ()>);
760 let cases: &[Case] = &[
761 (0.0, 100.0, Ok((0.0, 100.0))),
762 (50.0, 50.0, Ok((50.0, 50.0))),
763 (1.0, 90.0, Ok((1.0, 90.0))),
764 (-0.1, 50.0, Err(())),
765 (50.0, 100.1, Err(())),
766 (90.0, 50.0, Err(())),
767 (100.0, 0.0, Err(())),
768 ];
769 for (min, max, expect) in cases {
770 let got = CoverageRange::new(*min, *max);
771 match (got, expect) {
772 (Ok(r), Ok((emin, emax))) => {
773 assert!(
774 (r.min - emin).abs() < 1e-9 && (r.max - emax).abs() < 1e-9,
775 "min={min}, max={max}: got {r:?}, expected ({emin}, {emax})"
776 );
777 }
778 (Err(_), Err(())) => {} (got, expect) => panic!("min={min}, max={max}: got {got:?}, expected {expect:?}"),
780 }
781 }
782 }
783
784 #[test]
785 fn coverage_range_error_variants() {
786 let oor = CoverageRange::new(-1.0, 50.0).unwrap_err();
788 assert!(matches!(oor, CoverageRangeError::OutOfRange { .. }));
789 let mxm = CoverageRange::new(80.0, 20.0).unwrap_err();
790 assert!(matches!(mxm, CoverageRangeError::MinExceedsMax { .. }));
791 }
792
793 #[test]
796 fn sort_stability_on_tied_crap() {
797 let foo = mk_verdict("foo", "src/a.rs", 5, 80.0, 12.0, 25.0);
800 let bar = mk_verdict("bar", "src/a.rs", 5, 80.0, 12.0, 25.0);
801 let r = AnalysisResult {
802 functions: vec![foo, bar],
803 summary: empty_result().summary, passed: true,
805 };
806 let view = apply(&r, ViewSpec::default());
807 assert_eq!(
809 view.shown[0].scored.identity.qualified_name,
810 "foo",
811 "stable sort must preserve input order on ties; got {:?}",
812 view.shown
813 .iter()
814 .map(|v| &v.scored.identity.qualified_name)
815 .collect::<Vec<_>>()
816 );
817 assert_eq!(view.shown[1].scored.identity.qualified_name, "bar");
818 }
819
820 #[test]
823 fn only_failing_filter_retains_only_exceeds_true() {
824 let r = background_fixture();
826 let spec = ViewSpec {
827 filters: Filters {
828 only_failing: true,
829 ..Default::default()
830 },
831 ..Default::default()
832 };
833 let view = apply(&r, spec);
834 assert!(view.shown.iter().all(|v| v.exceeds));
835 for v in &view.shown {
837 assert!(v.scored.crap.value > v.threshold);
838 }
839 }
840
841 #[test]
842 fn coverage_range_filter_inclusive() {
843 let r = background_fixture();
845 let range = CoverageRange::new(50.0, 90.0).unwrap();
846 let spec = ViewSpec {
847 filters: Filters {
848 coverage_range: Some(range),
849 ..Default::default()
850 },
851 ..Default::default()
852 };
853 let view = apply(&r, spec);
854 assert!(view.shown.iter().all(|v| {
855 let cov = v.scored.coverage_percent;
856 cov.is_finite() && (50.0..=90.0).contains(&cov)
857 }));
858 let manual_count = r
859 .functions
860 .iter()
861 .filter(|v| v.scored.coverage_percent.is_finite())
862 .filter(|v| (50.0..=90.0).contains(&v.scored.coverage_percent))
863 .count();
864 assert_eq!(view.eligible_count, manual_count);
865 }
866
867 #[test]
868 fn coverage_range_boundary_inclusive_50_low() {
869 let v = mk_verdict("at50", "src/a.rs", 1, 50.0, 1.0, 25.0);
871 let r = AnalysisResult {
872 functions: vec![v],
873 summary: empty_result().summary,
874 passed: true,
875 };
876 let range = CoverageRange::new(50.0, 90.0).unwrap();
877 let spec = ViewSpec {
878 filters: Filters {
879 coverage_range: Some(range),
880 ..Default::default()
881 },
882 ..Default::default()
883 };
884 let view = apply(&r, spec);
885 assert_eq!(view.shown.len(), 1);
886 }
887
888 #[test]
889 fn coverage_range_boundary_inclusive_90_high() {
890 let v = mk_verdict("at90", "src/a.rs", 1, 90.0, 1.0, 25.0);
892 let r = AnalysisResult {
893 functions: vec![v],
894 summary: empty_result().summary,
895 passed: true,
896 };
897 let range = CoverageRange::new(50.0, 90.0).unwrap();
898 let spec = ViewSpec {
899 filters: Filters {
900 coverage_range: Some(range),
901 ..Default::default()
902 },
903 ..Default::default()
904 };
905 let view = apply(&r, spec);
906 assert_eq!(view.shown.len(), 1);
907 }
908
909 #[test]
910 fn coverage_range_boundary_inclusive_below_low() {
911 let v = mk_verdict("just_under", "src/a.rs", 1, 49.9, 1.0, 25.0);
913 let r = AnalysisResult {
914 functions: vec![v],
915 summary: empty_result().summary,
916 passed: true,
917 };
918 let range = CoverageRange::new(50.0, 90.0).unwrap();
919 let spec = ViewSpec {
920 filters: Filters {
921 coverage_range: Some(range),
922 ..Default::default()
923 },
924 ..Default::default()
925 };
926 let view = apply(&r, spec);
927 assert!(view.shown.is_empty());
928 }
929
930 #[test]
931 fn coverage_range_boundary_inclusive_above_high() {
932 let v = mk_verdict("just_over", "src/a.rs", 1, 90.1, 1.0, 25.0);
934 let r = AnalysisResult {
935 functions: vec![v],
936 summary: empty_result().summary,
937 passed: true,
938 };
939 let range = CoverageRange::new(50.0, 90.0).unwrap();
940 let spec = ViewSpec {
941 filters: Filters {
942 coverage_range: Some(range),
943 ..Default::default()
944 },
945 ..Default::default()
946 };
947 let view = apply(&r, spec);
948 assert!(view.shown.is_empty());
949 }
950
951 #[test]
952 fn coverage_range_boundary_inclusive_zero_singleton() {
953 let v = mk_verdict("zero", "src/a.rs", 1, 0.0, 1.0, 25.0);
955 let r = AnalysisResult {
956 functions: vec![v],
957 summary: empty_result().summary,
958 passed: true,
959 };
960 let range = CoverageRange::new(0.0, 0.0).unwrap();
961 let spec = ViewSpec {
962 filters: Filters {
963 coverage_range: Some(range),
964 ..Default::default()
965 },
966 ..Default::default()
967 };
968 let view = apply(&r, spec);
969 assert_eq!(view.shown.len(), 1);
970 }
971
972 #[test]
973 fn coverage_range_boundary_inclusive_hundred_singleton() {
974 let v = mk_verdict("full", "src/a.rs", 1, 100.0, 1.0, 25.0);
976 let r = AnalysisResult {
977 functions: vec![v],
978 summary: empty_result().summary,
979 passed: true,
980 };
981 let range = CoverageRange::new(100.0, 100.0).unwrap();
982 let spec = ViewSpec {
983 filters: Filters {
984 coverage_range: Some(range),
985 ..Default::default()
986 },
987 ..Default::default()
988 };
989 let view = apply(&r, spec);
990 assert_eq!(view.shown.len(), 1);
991 }
992
993 #[test]
994 fn filters_and_compose() {
995 let r = background_fixture();
998 let range = CoverageRange::new(50.0, 100.0).unwrap();
999 let spec = ViewSpec {
1000 filters: Filters {
1001 only_failing: true,
1002 coverage_range: Some(range),
1003 },
1004 ..Default::default()
1005 };
1006 let view = apply(&r, spec);
1007 for v in &view.shown {
1008 assert!(v.exceeds);
1009 let cov = v.scored.coverage_percent;
1010 assert!((50.0..=100.0).contains(&cov));
1011 }
1012 }
1013
1014 #[test]
1015 fn nan_coverage_excluded_from_range_filter() {
1016 let v = mk_verdict("zero_lines", "src/a.rs", 1, f64::NAN, 1.0, 25.0);
1018 let r = AnalysisResult {
1019 functions: vec![v],
1020 summary: empty_result().summary,
1021 passed: true,
1022 };
1023 let range = CoverageRange::new(0.0, 100.0).unwrap();
1024 let spec = ViewSpec {
1025 filters: Filters {
1026 coverage_range: Some(range),
1027 ..Default::default()
1028 },
1029 ..Default::default()
1030 };
1031 let view = apply(&r, spec);
1032 assert!(view.shown.is_empty());
1033 }
1034
1035 #[test]
1038 fn sort_by_crap_descending() {
1039 let r = background_fixture();
1041 let spec = ViewSpec {
1042 sort: SortKey::Crap,
1043 ..Default::default()
1044 };
1045 let view = apply(&r, spec);
1046 for w in view.shown.windows(2) {
1047 assert!(w[0].scored.crap.value >= w[1].scored.crap.value);
1048 }
1049 }
1050
1051 #[test]
1052 fn sort_by_coverage_ascending() {
1053 let r = background_fixture();
1055 let spec = ViewSpec {
1056 sort: SortKey::Coverage,
1057 ..Default::default()
1058 };
1059 let view = apply(&r, spec);
1060 for w in view.shown.windows(2) {
1061 assert!(w[0].scored.coverage_percent <= w[1].scored.coverage_percent);
1062 }
1063 }
1064
1065 #[test]
1066 fn sort_by_complexity_descending() {
1067 let r = background_fixture();
1069 let spec = ViewSpec {
1070 sort: SortKey::Complexity,
1071 ..Default::default()
1072 };
1073 let view = apply(&r, spec);
1074 for w in view.shown.windows(2) {
1075 assert!(w[0].scored.complexity >= w[1].scored.complexity);
1076 }
1077 }
1078
1079 #[test]
1080 fn sort_by_path_alphabetical_then_crap() {
1081 let r = background_fixture();
1083 let spec = ViewSpec {
1084 sort: SortKey::Path,
1085 ..Default::default()
1086 };
1087 let view = apply(&r, spec);
1088 for w in view.shown.windows(2) {
1090 let (a, b) = (
1091 &w[0].scored.identity.file_path,
1092 &w[1].scored.identity.file_path,
1093 );
1094 assert!(a <= b, "files not in ascending order: {a} then {b}");
1095 }
1096 for w in view.shown.windows(2) {
1098 if w[0].scored.identity.file_path == w[1].scored.identity.file_path {
1099 assert!(
1100 w[0].scored.crap.value >= w[1].scored.crap.value,
1101 "within file {}: CRAP not descending: {} then {}",
1102 w[0].scored.identity.file_path,
1103 w[0].scored.crap.value,
1104 w[1].scored.crap.value
1105 );
1106 }
1107 }
1108 }
1109
1110 #[test]
1111 fn sort_by_path_secondary_multi_file() {
1112 let verdicts = vec![
1116 mk_verdict("a_low", "src/a.rs", 1, 50.0, 5.0, 25.0),
1117 mk_verdict("a_high", "src/a.rs", 1, 50.0, 30.0, 25.0),
1118 mk_verdict("b_only", "src/b.rs", 1, 50.0, 10.0, 25.0),
1119 mk_verdict("c_low", "src/c.rs", 1, 50.0, 1.0, 25.0),
1120 mk_verdict("c_high", "src/c.rs", 1, 50.0, 50.0, 25.0),
1121 ];
1122 let r = AnalysisResult {
1123 functions: verdicts,
1124 summary: empty_result().summary,
1125 passed: true,
1126 };
1127 let spec = ViewSpec {
1128 sort: SortKey::Path,
1129 ..Default::default()
1130 };
1131 let view = apply(&r, spec);
1132 let names: Vec<&str> = view
1133 .shown
1134 .iter()
1135 .map(|v| v.scored.identity.qualified_name.as_str())
1136 .collect();
1137 assert_eq!(
1138 names,
1139 vec!["a_high", "a_low", "b_only", "c_high", "c_low"],
1140 "path sort with secondary CRAP-desc order wrong"
1141 );
1142 }
1143
1144 #[test]
1145 fn nan_coverage_sorts_last_under_coverage_ascending() {
1146 let verdicts = vec![
1148 mk_verdict("c10", "src/a.rs", 1, 10.0, 1.0, 25.0),
1149 mk_verdict("nan1", "src/a.rs", 1, f64::NAN, 1.0, 25.0),
1150 mk_verdict("c50", "src/a.rs", 1, 50.0, 1.0, 25.0),
1151 mk_verdict("nan2", "src/a.rs", 1, f64::NAN, 1.0, 25.0),
1152 mk_verdict("c90", "src/a.rs", 1, 90.0, 1.0, 25.0),
1153 ];
1154 let r = AnalysisResult {
1155 functions: verdicts,
1156 summary: empty_result().summary,
1157 passed: true,
1158 };
1159 let spec = ViewSpec {
1160 sort: SortKey::Coverage,
1161 ..Default::default()
1162 };
1163 let view = apply(&r, spec);
1164 let coverages: Vec<f64> = view
1166 .shown
1167 .iter()
1168 .map(|v| v.scored.coverage_percent)
1169 .collect();
1170 assert_eq!(coverages[0], 10.0);
1171 assert_eq!(coverages[1], 50.0);
1172 assert_eq!(coverages[2], 90.0);
1173 assert!(coverages[3].is_nan());
1174 assert!(coverages[4].is_nan());
1175 }
1176
1177 #[test]
1180 fn limit_truncates() {
1181 let r = background_fixture();
1183 let spec = ViewSpec {
1184 limit: Some(3),
1185 ..Default::default()
1186 };
1187 let view = apply(&r, spec);
1188 assert_eq!(view.shown.len(), 3);
1189 assert_eq!(view.eligible_count, 6);
1190 assert!(view.truncated);
1191 }
1192
1193 #[test]
1194 fn limit_greater_than_eligible() {
1195 let r = background_fixture();
1197 let spec = ViewSpec {
1198 limit: Some(100),
1199 ..Default::default()
1200 };
1201 let view = apply(&r, spec);
1202 assert_eq!(view.shown.len(), 6);
1203 assert_eq!(view.eligible_count, 6);
1204 assert!(!view.truncated);
1205 }
1206
1207 #[test]
1208 fn limit_none() {
1209 let r = background_fixture();
1211 let spec = ViewSpec {
1212 limit: None,
1213 ..Default::default()
1214 };
1215 let view = apply(&r, spec);
1216 assert_eq!(view.shown.len(), view.eligible_count);
1217 assert!(!view.truncated);
1218 }
1219
1220 #[test]
1221 fn limit_zero_treated_as_no_limit() {
1222 let r = background_fixture();
1225 let spec = ViewSpec {
1226 limit: Some(0),
1227 ..Default::default()
1228 };
1229 let view = apply(&r, spec);
1230 assert_eq!(view.shown.len(), view.eligible_count);
1231 assert!(!view.truncated);
1232 }
1233
1234 #[test]
1235 fn limit_equal_to_eligible_does_not_mark_truncated() {
1236 let r = background_fixture();
1240 assert_eq!(r.functions.len(), 6, "background fixture sanity");
1241 let spec = ViewSpec {
1242 limit: Some(6),
1243 ..Default::default()
1244 };
1245 let view = apply(&r, spec);
1246 assert_eq!(view.shown.len(), 6);
1247 assert_eq!(view.eligible_count, 6);
1248 assert!(!view.truncated, "limit == eligible must NOT mark truncated");
1249 }
1250
1251 #[test]
1254 fn order_filter_then_sort_then_truncate() {
1255 let r = background_fixture();
1258 let spec = ViewSpec {
1259 filters: Filters {
1260 only_failing: true,
1261 ..Default::default()
1262 },
1263 sort: SortKey::Coverage,
1264 limit: Some(2),
1265 ..Default::default()
1266 };
1267 let view = apply(&r, spec);
1268 assert_eq!(view.shown.len(), 2);
1269 for v in &view.shown {
1270 assert!(v.exceeds);
1271 }
1272 assert!(view.shown[0].scored.coverage_percent <= view.shown[1].scored.coverage_percent);
1274 let total_failing = r.functions.iter().filter(|v| v.exceeds).count();
1276 assert_eq!(view.eligible_count, total_failing);
1277 }
1278
1279 #[test]
1280 fn truncation_does_not_change_gate() {
1281 let verdicts = vec![
1284 mk_verdict("ok", "src/a.rs", 1, 100.0, 1.0, 25.0),
1285 mk_verdict("fail1", "src/a.rs", 1, 0.0, 50.0, 25.0),
1286 mk_verdict("fail2", "src/a.rs", 1, 0.0, 60.0, 25.0),
1287 mk_verdict("fail3", "src/a.rs", 1, 0.0, 70.0, 25.0),
1288 ];
1289 let summary = compute_summary(&verdicts);
1290 let passed = verdicts.iter().all(|v| !v.exceeds);
1291 let r = AnalysisResult {
1292 functions: verdicts,
1293 summary,
1294 passed,
1295 };
1296 let spec = ViewSpec {
1297 limit: Some(1),
1298 ..Default::default()
1299 };
1300 let view = apply(&r, spec);
1301 assert_eq!(view.shown.len(), 1);
1302 assert!(!view.full.passed);
1304 assert_eq!(view.full.summary.exceeding_threshold, 3);
1305 }
1306
1307 #[test]
1308 fn filtering_does_not_change_gate() {
1309 let verdicts = vec![
1312 mk_verdict("ok", "src/a.rs", 1, 100.0, 1.0, 25.0),
1313 mk_verdict("fail1", "src/a.rs", 1, 0.0, 50.0, 25.0),
1314 mk_verdict("fail2", "src/a.rs", 1, 0.0, 60.0, 25.0),
1315 mk_verdict("fail3", "src/a.rs", 1, 0.0, 70.0, 25.0),
1316 ];
1317 let summary = compute_summary(&verdicts);
1318 let r = AnalysisResult {
1319 functions: verdicts,
1320 summary,
1321 passed: false,
1322 };
1323 let range = CoverageRange::new(99.0, 100.0).unwrap();
1325 let spec = ViewSpec {
1326 filters: Filters {
1327 coverage_range: Some(range),
1328 ..Default::default()
1329 },
1330 ..Default::default()
1331 };
1332 let view = apply(&r, spec);
1333 assert!(view.shown.iter().all(|v| !v.exceeds));
1334 assert!(!view.full.passed);
1335 assert_eq!(view.full.summary.exceeding_threshold, 3);
1336 }
1337
1338 #[test]
1341 fn shown_summary_over_shown_subset() {
1342 let r = background_fixture();
1344 let spec = ViewSpec {
1345 filters: Filters {
1346 only_failing: true,
1347 ..Default::default()
1348 },
1349 ..Default::default()
1350 };
1351 let view = apply(&r, spec);
1352 assert_eq!(view.shown_summary.total_functions, view.shown.len());
1353 assert_eq!(
1354 view.shown_summary.exceeding_threshold,
1355 view.shown.len(),
1356 "every shown row exceeds, so shown_summary should report all"
1357 );
1358 let manual_avg: f64 =
1360 view.shown.iter().map(|v| v.scored.crap.value).sum::<f64>() / view.shown.len() as f64;
1361 assert!((view.shown_summary.average_crap - manual_avg).abs() < 1e-9);
1362 }
1363
1364 #[test]
1365 fn shown_summary_differs_from_full() {
1366 let verdicts = vec![
1368 mk_verdict("ok1", "src/a.rs", 1, 100.0, 1.0, 25.0),
1369 mk_verdict("ok2", "src/a.rs", 1, 100.0, 2.0, 25.0),
1370 mk_verdict("ok3", "src/a.rs", 1, 100.0, 3.0, 25.0),
1371 mk_verdict("fail1", "src/a.rs", 1, 0.0, 50.0, 25.0),
1372 mk_verdict("fail2", "src/a.rs", 1, 0.0, 60.0, 25.0),
1373 mk_verdict("fail3", "src/a.rs", 1, 0.0, 70.0, 25.0),
1374 ];
1375 let summary = compute_summary(&verdicts);
1376 let r = AnalysisResult {
1377 functions: verdicts,
1378 summary,
1379 passed: false,
1380 };
1381 let spec = ViewSpec {
1382 filters: Filters {
1383 only_failing: true,
1384 ..Default::default()
1385 },
1386 ..Default::default()
1387 };
1388 let view = apply(&r, spec);
1389 assert_eq!(view.full.summary.total_functions, 6);
1390 assert_eq!(view.shown_summary.total_functions, 3);
1391 }
1392
1393 #[test]
1396 fn all_filtered_out_produces_empty_shown() {
1397 let v = mk_verdict("low_cov", "src/a.rs", 1, 50.0, 1.0, 25.0);
1399 let r = AnalysisResult {
1400 functions: vec![v],
1401 summary: empty_result().summary,
1402 passed: true,
1403 };
1404 let range = CoverageRange::new(95.0, 100.0).unwrap();
1405 let spec = ViewSpec {
1406 filters: Filters {
1407 coverage_range: Some(range),
1408 ..Default::default()
1409 },
1410 ..Default::default()
1411 };
1412 let view = apply(&r, spec);
1413 assert!(view.shown.is_empty());
1414 assert_eq!(view.eligible_count, 0);
1415 assert!(!view.truncated);
1416 }
1417
1418 #[test]
1421 fn display_predicate_default_spec_is_false() {
1422 let r = background_fixture();
1424 let view = apply(&r, ViewSpec::default());
1425 assert!(!should_render_view_line(&view));
1426 }
1427
1428 #[test]
1429 fn display_predicate_sort_only_is_false() {
1430 let r = background_fixture();
1432 let spec = ViewSpec {
1433 sort: SortKey::Coverage,
1434 ..Default::default()
1435 };
1436 let view = apply(&r, spec);
1437 assert!(!should_render_view_line(&view));
1438 }
1439
1440 #[test]
1441 fn display_predicate_top_truncating_is_true() {
1442 let r = background_fixture();
1444 let spec = ViewSpec {
1445 limit: Some(2),
1446 ..Default::default()
1447 };
1448 let view = apply(&r, spec);
1449 assert!(should_render_view_line(&view));
1450 }
1451
1452 #[test]
1453 fn display_predicate_coverage_filter_excluding_is_true() {
1454 let r = background_fixture();
1456 let range = CoverageRange::new(99.0, 100.0).unwrap();
1457 let spec = ViewSpec {
1458 filters: Filters {
1459 coverage_range: Some(range),
1460 ..Default::default()
1461 },
1462 ..Default::default()
1463 };
1464 let view = apply(&r, spec);
1465 assert!(should_render_view_line(&view));
1466 }
1467
1468 #[test]
1469 fn display_predicate_only_failing_reducing_is_true() {
1470 let r = background_fixture();
1472 let spec = ViewSpec {
1473 filters: Filters {
1474 only_failing: true,
1475 ..Default::default()
1476 },
1477 ..Default::default()
1478 };
1479 let view = apply(&r, spec);
1480 assert!(should_render_view_line(&view));
1481 }
1482
1483 #[test]
1486 fn no_group_by_means_no_grouped_block() {
1487 let r = background_fixture();
1489 let view = apply(&r, ViewSpec::default());
1490 assert!(view.grouped.is_none());
1491 }
1492
1493 #[test]
1494 fn group_by_file_populates_grouped_block() {
1495 let r = background_fixture();
1497 let spec = ViewSpec {
1498 group_by: Some(GroupKey::File),
1499 ..Default::default()
1500 };
1501 let view = apply(&r, spec);
1502 let grouped = view.grouped.as_ref().expect("grouped block expected");
1503 assert_eq!(grouped.key, GroupKey::File);
1504 assert_eq!(grouped.files.len(), 5);
1507 assert_eq!(grouped.eligible_count, 5);
1508 assert!(!grouped.truncated);
1509 }
1510
1511 #[test]
1512 fn group_by_file_does_not_truncate_function_shown() {
1513 let r = background_fixture();
1515 let spec = ViewSpec {
1516 group_by: Some(GroupKey::File),
1517 limit: Some(2),
1518 ..Default::default()
1519 };
1520 let view = apply(&r, spec);
1521 assert_eq!(view.shown.len(), r.functions.len());
1523 assert!(!view.truncated);
1524 let grouped = view.grouped.as_ref().unwrap();
1525 assert_eq!(grouped.files.len(), 2);
1526 assert!(grouped.truncated);
1527 assert_eq!(grouped.eligible_count, 5);
1528 }
1529
1530 #[test]
1531 fn group_by_file_keeps_gate_unchanged() {
1532 let r = background_fixture();
1535 let baseline_passed = r.passed;
1536 let baseline_total = r.summary.total_functions;
1537 let baseline_exceeding = r.summary.exceeding_threshold;
1538 let spec = ViewSpec {
1539 group_by: Some(GroupKey::File),
1540 limit: Some(1),
1541 ..Default::default()
1542 };
1543 let view = apply(&r, spec);
1544 assert_eq!(view.full.passed, baseline_passed);
1545 assert_eq!(view.full.summary.total_functions, baseline_total);
1546 assert_eq!(view.full.summary.exceeding_threshold, baseline_exceeding);
1547 }
1548
1549 #[test]
1550 fn group_by_file_default_sort_is_avg_crap_desc() {
1551 let r = background_fixture();
1552 let spec = ViewSpec {
1553 group_by: Some(GroupKey::File),
1554 ..Default::default()
1555 };
1556 let view = apply(&r, spec);
1557 let files = &view.grouped.as_ref().unwrap().files;
1558 for w in files.windows(2) {
1559 assert!(
1560 w[0].average_crap >= w[1].average_crap,
1561 "files not in average_crap descending order"
1562 );
1563 }
1564 }
1565
1566 #[test]
1567 fn group_by_file_sort_by_coverage_ascending() {
1568 let r = background_fixture();
1569 let spec = ViewSpec {
1570 group_by: Some(GroupKey::File),
1571 sort: SortKey::Coverage,
1572 ..Default::default()
1573 };
1574 let view = apply(&r, spec);
1575 let files = &view.grouped.as_ref().unwrap().files;
1576 for w in files.windows(2) {
1577 assert!(w[0].average_coverage <= w[1].average_coverage);
1578 }
1579 }
1580
1581 #[test]
1582 fn group_by_file_sort_by_complexity_descending() {
1583 let r = background_fixture();
1584 let spec = ViewSpec {
1585 group_by: Some(GroupKey::File),
1586 sort: SortKey::Complexity,
1587 ..Default::default()
1588 };
1589 let view = apply(&r, spec);
1590 let files = &view.grouped.as_ref().unwrap().files;
1591 for w in files.windows(2) {
1592 assert!(w[0].max_complexity >= w[1].max_complexity);
1593 }
1594 }
1595
1596 #[test]
1597 fn group_by_file_sort_by_path_alphabetical() {
1598 let r = background_fixture();
1599 let spec = ViewSpec {
1600 group_by: Some(GroupKey::File),
1601 sort: SortKey::Path,
1602 ..Default::default()
1603 };
1604 let view = apply(&r, spec);
1605 let files = &view.grouped.as_ref().unwrap().files;
1606 for w in files.windows(2) {
1607 assert!(w[0].file_path <= w[1].file_path);
1608 }
1609 }
1610
1611 #[test]
1612 fn group_by_file_truncate_files() {
1613 let r = background_fixture();
1614 let spec = ViewSpec {
1615 group_by: Some(GroupKey::File),
1616 limit: Some(3),
1617 ..Default::default()
1618 };
1619 let view = apply(&r, spec);
1620 let grouped = view.grouped.as_ref().unwrap();
1621 assert_eq!(grouped.files.len(), 3);
1622 assert!(grouped.truncated);
1623 assert_eq!(grouped.eligible_count, 5);
1624 }
1625
1626 #[test]
1627 fn group_by_file_filters_compose_before_grouping() {
1628 let r = background_fixture();
1631 let spec = ViewSpec {
1632 filters: Filters {
1633 only_failing: true,
1634 ..Default::default()
1635 },
1636 group_by: Some(GroupKey::File),
1637 ..Default::default()
1638 };
1639 let view = apply(&r, spec);
1640 let grouped = view.grouped.as_ref().unwrap();
1641 assert_eq!(grouped.files.len(), 2);
1644 for f in &grouped.files {
1646 assert!(f.exceeding_count >= 1);
1647 }
1648 }
1649
1650 #[test]
1651 fn group_by_file_empty_input_produces_empty_files() {
1652 let r = empty_result();
1653 let spec = ViewSpec {
1654 group_by: Some(GroupKey::File),
1655 ..Default::default()
1656 };
1657 let view = apply(&r, spec);
1658 let grouped = view.grouped.as_ref().unwrap();
1659 assert!(grouped.files.is_empty());
1660 assert_eq!(grouped.eligible_count, 0);
1661 assert!(!grouped.truncated);
1662 }
1663
1664 #[test]
1665 fn display_predicate_group_by_only_default_input_is_false() {
1666 let r = background_fixture();
1670 let spec = ViewSpec {
1671 group_by: Some(GroupKey::File),
1672 ..Default::default()
1673 };
1674 let view = apply(&r, spec);
1675 assert!(!should_render_view_line(&view));
1676 }
1677
1678 #[test]
1679 fn display_predicate_group_by_truncating_files_is_true() {
1680 let r = background_fixture();
1681 let spec = ViewSpec {
1682 group_by: Some(GroupKey::File),
1683 limit: Some(2),
1684 ..Default::default()
1685 };
1686 let view = apply(&r, spec);
1687 assert!(should_render_view_line(&view));
1688 }
1689
1690 #[test]
1700 fn group_by_file_top_zero_is_no_limit() {
1701 let r = background_fixture();
1704 let spec = ViewSpec {
1705 group_by: Some(GroupKey::File),
1706 limit: Some(0),
1707 ..Default::default()
1708 };
1709 let view = apply(&r, spec);
1710 let grouped = view.grouped.as_ref().expect("grouping active");
1711 assert!(!grouped.truncated);
1712 assert_eq!(grouped.files.len(), 5);
1714 }
1715
1716 #[test]
1717 fn group_by_file_limit_equal_to_file_count_is_not_truncated() {
1718 let r = background_fixture();
1722 let spec = ViewSpec {
1723 group_by: Some(GroupKey::File),
1724 limit: Some(5),
1725 ..Default::default()
1726 };
1727 let view = apply(&r, spec);
1728 let grouped = view.grouped.as_ref().expect("grouping active");
1729 assert!(!grouped.truncated);
1730 assert_eq!(grouped.files.len(), 5);
1731 }
1732
1733 #[test]
1743 fn display_predicate_full_grouping_no_reduction_is_false() {
1744 let r = background_fixture();
1748 let spec = ViewSpec {
1749 group_by: Some(GroupKey::File),
1750 ..Default::default()
1751 };
1752 let view = apply(&r, spec);
1753 assert!(!should_render_view_line(&view));
1754 }
1755
1756 #[test]
1757 fn display_predicate_grouping_reduces_files_is_true() {
1758 let r = background_fixture();
1762 let spec = ViewSpec {
1763 group_by: Some(GroupKey::File),
1764 filters: Filters {
1765 only_failing: true,
1766 ..Default::default()
1767 },
1768 ..Default::default()
1769 };
1770 let view = apply(&r, spec);
1771 assert!(should_render_view_line(&view));
1772 }
1773}
1774
1775#[cfg(test)]
1776mod proptests {
1777 use super::*;
1778 use crate::test_strategies::{arb_analysis_result, arb_verdict_with_nan_coverage};
1779 use proptest::prelude::*;
1780
1781 fn legacy_sort_order(result: &AnalysisResult) -> Vec<&FunctionVerdict> {
1784 let mut sorted: Vec<&FunctionVerdict> = result.functions.iter().collect();
1785 sorted.sort_by(|a, b| {
1786 b.scored
1787 .crap
1788 .value
1789 .partial_cmp(&a.scored.crap.value)
1790 .unwrap_or(std::cmp::Ordering::Equal)
1791 });
1792 sorted
1793 }
1794
1795 proptest! {
1796 #![proptest_config(ProptestConfig::with_cases(256))]
1797
1798 #[test]
1806 fn prop_default_spec_order_matches_legacy_sort(result in arb_analysis_result()) {
1807 let view = apply(&result, ViewSpec::default());
1808 let legacy = legacy_sort_order(&result);
1809 prop_assert_eq!(view.shown.len(), legacy.len());
1810 for (a, b) in view.shown.iter().zip(legacy.iter()) {
1811 prop_assert!(std::ptr::eq(*a, *b));
1812 }
1813 }
1814
1815 #[test]
1817 fn prop_default_spec_preserves_identity(result in arb_analysis_result()) {
1818 let view = apply(&result, ViewSpec::default());
1819 let shown_identities: std::collections::HashSet<&crate::domain::types::FunctionIdentity> =
1820 view.shown.iter().map(|v| &v.scored.identity).collect();
1821 let original_identities: std::collections::HashSet<&crate::domain::types::FunctionIdentity> =
1822 result.functions.iter().map(|v| &v.scored.identity).collect();
1823 prop_assert_eq!(shown_identities, original_identities);
1824 }
1825
1826 #[test]
1829 fn prop_default_spec_preserves_summary(result in arb_analysis_result()) {
1830 let view = apply(&result, ViewSpec::default());
1831 prop_assert!(std::ptr::eq(view.full, &result));
1832 prop_assert_eq!(view.full.summary.total_functions, result.summary.total_functions);
1834 }
1835
1836 #[test]
1838 fn prop_display_predicate_biconditional(result in arb_analysis_result()) {
1839 let view = apply(&result, ViewSpec::default());
1841 let computed = should_render_view_line(&view);
1842 let expected = view.eligible_count < view.full.functions.len() || view.truncated;
1843 prop_assert_eq!(computed, expected);
1844 prop_assert!(!computed);
1846 }
1847
1848 #[test]
1850 fn prop_apply_never_panics_with_nan_coverage(
1851 verdicts in prop::collection::vec(arb_verdict_with_nan_coverage(), 0..50)
1852 ) {
1853 let result = AnalysisResult {
1854 functions: verdicts.clone(),
1855 summary: crate::domain::types::AnalysisSummary {
1856 total_functions: verdicts.len(),
1857 total_files: verdicts.len(),
1858 exceeding_threshold: 0,
1859 average_crap: 0.0,
1860 median_crap: 0.0,
1861 max_crap: None,
1862 worst_function: None,
1863 distribution: crate::domain::types::RiskDistribution {
1864 low: 0, acceptable: 0, moderate: 0, high: 0,
1865 },
1866 ..Default::default()
1867 },
1868 passed: true,
1869 };
1870 for sort in [SortKey::Crap, SortKey::Coverage, SortKey::Complexity, SortKey::Path] {
1872 let spec = ViewSpec {
1873 filters: Filters {
1874 coverage_range: Some(CoverageRange::new(0.0, 100.0).unwrap()),
1875 ..Default::default()
1876 },
1877 sort,
1878 limit: Some(10),
1879 ..Default::default()
1880 };
1881 let _ = apply(&result, spec);
1882 }
1883 }
1884 }
1885}