use super::{coverage_gap_summary, CoverageCounts, CoverageGapKind, CoverageSeverity};
use keyhog_sources::SkipCounts;
fn count_for(summary: &[(String, usize)], needle: &str) -> Option<usize> {
summary
.iter()
.find(|(reason, _)| reason.contains(needle))
.map(|(_, count)| *count)
}
fn with_skip(skip: SkipCounts) -> CoverageCounts {
CoverageCounts {
skip,
..Default::default()
}
}
#[test]
fn empty_snapshot_yields_no_entries() {
let summary = coverage_gap_summary(&CoverageCounts::default());
assert!(
summary.is_empty(),
"a scan with zero coverage gaps must produce no notifications, got {summary:?}"
);
}
#[test]
fn zero_count_categories_are_filtered_out() {
let counts = with_skip(SkipCounts {
over_max_size: 4,
..Default::default()
});
let summary = coverage_gap_summary(&counts);
assert_eq!(
summary.len(),
1,
"only the single non-zero category may appear, got {summary:?}"
);
assert_eq!(
count_for(
&summary,
"exceeded a configured size cap (--max-file-size or the matching --limit-*-bytes)"
),
Some(4)
);
}
#[test]
fn over_max_size_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
over_max_size: 7,
..Default::default()
}));
assert_eq!(
count_for(
&s,
"exceeded a configured size cap (--max-file-size or the matching --limit-*-bytes)"
),
Some(7)
);
}
#[test]
fn binary_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
binary: 3,
..Default::default()
}));
assert_eq!(
count_for(&s, "binary (extension or content sniff)"),
Some(3)
);
}
#[test]
fn excluded_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
excluded: 9,
..Default::default()
}));
assert_eq!(count_for(&s, "exclusion policy"), Some(9));
}
#[test]
fn unreadable_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
unreadable: 5,
..Default::default()
}));
assert_eq!(
count_for(&s, "unreadable (permission denied or I/O error)"),
Some(5)
);
}
#[test]
fn git_object_unreadable_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
git_object_unreadable: 2,
..Default::default()
}));
assert_eq!(count_for(&s, "Git object unreadable"), Some(2));
}
#[test]
fn archive_truncated_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
archive_truncated: 6,
..Default::default()
}));
assert_eq!(
count_for(&s, "extraction truncated by an unpack budget"),
Some(6)
);
}
#[test]
fn binary_section_name_unresolved_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
binary_section_name_unresolved: 1,
..Default::default()
}));
assert_eq!(count_for(&s, "binary section name unresolved"), Some(1));
}
#[test]
fn source_truncated_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
source_truncated: 8,
..Default::default()
}));
assert_eq!(count_for(&s, "source scan truncated by aggregate"), Some(8));
}
#[test]
fn structured_source_parse_failures_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
structured_source_parse_failures: 4,
..Default::default()
}));
assert_eq!(count_for(&s, "structured source parse failed"), Some(4));
}
#[test]
fn archive_duplicate_scan_unavailable_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
archive_duplicate_scan_unavailable: 3,
..Default::default()
}));
assert_eq!(count_for(&s, "archive duplicate-entry detection"), Some(3));
}
#[test]
fn git_lfs_pointer_surfaces() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
git_lfs_pointer: 2,
..Default::default()
}));
assert_eq!(count_for(&s, "Git-LFS pointer"), Some(2));
}
#[test]
fn vendored_path_suppressions_surface_with_their_count() {
let s = coverage_gap_summary(&CoverageCounts {
vendored_path_suppressions: 3,
..Default::default()
});
assert_eq!(count_for(&s, "vendored/minified path policy"), Some(3));
}
#[test]
fn vendored_path_suppression_is_warn_not_fail() {
assert_eq!(
CoverageGapKind::VendoredPathSuppressed.severity(),
CoverageSeverity::Warn
);
assert_eq!(
CoverageCounts {
vendored_path_suppressions: 9,
..Default::default()
}
.fail_class_total(),
0,
"a recoverable precision drop must not flip a scan to the incomplete exit"
);
}
#[test]
fn nothing_scanned_no_input_surfaces_and_fails_closed() {
let counts = CoverageCounts {
nothing_scanned_no_input: true,
..Default::default()
};
let s = coverage_gap_summary(&counts);
assert_eq!(count_for(&s, "no skip was counted"), Some(1));
assert_eq!(
CoverageGapKind::NothingScannedNoInput.severity(),
CoverageSeverity::Fail
);
assert_eq!(
counts.fail_class_total(),
1,
"a scan that examined nothing must reach the incomplete exit"
);
}
#[test]
fn nothing_scanned_all_skipped_is_a_distinct_row() {
let counts = CoverageCounts {
nothing_scanned_all_skipped: true,
..Default::default()
};
let s = coverage_gap_summary(&counts);
assert_eq!(count_for(&s, "every candidate was skipped"), Some(1));
assert_eq!(
count_for(&s, "no skip was counted"),
None,
"the two nothing-scanned causes must never be reported together"
);
assert_eq!(counts.fail_class_total(), 1);
}
#[test]
fn excluded_files_alone_do_not_reach_the_incomplete_exit() {
let counts = with_skip(SkipCounts {
excluded: 18,
..Default::default()
});
assert_eq!(
counts.fail_class_total(),
0,
"policy exclusions on a scan that read bytes must stay exit 0"
);
assert!(
!counts.covered_nothing(),
"a default snapshot has not proved the scan covered nothing"
);
}
#[test]
fn source_errors_surface() {
let s = coverage_gap_summary(&CoverageCounts {
source_errors: 5,
..Default::default()
});
assert_eq!(count_for(&s, "source emitted error rows"), Some(5));
}
#[test]
fn scanner_structured_parse_failures_surface() {
let s = coverage_gap_summary(&CoverageCounts {
scanner_structured_parse_failures: 3,
..Default::default()
});
assert_eq!(count_for(&s, "scanner structured parse failed"), Some(3));
}
#[test]
fn scanner_structured_oversize_skip_surfaces() {
let s = coverage_gap_summary(&CoverageCounts {
scanner_structured_oversize_skips: 4,
..Default::default()
});
assert_eq!(
count_for(&s, "scanner structured decode-through skipped by size cap"),
Some(4),
"structured decode-through oversize skips must reach the structured report"
);
}
#[test]
fn scanner_decode_truncations_surface() {
let s = coverage_gap_summary(&CoverageCounts {
scanner_decode_truncations: 6,
..Default::default()
});
assert_eq!(count_for(&s, "scanner decode-through truncated"), Some(6));
}
#[test]
fn scanner_invalid_pattern_index_skips_surface() {
let s = coverage_gap_summary(&CoverageCounts {
scanner_invalid_pattern_index_skips: 2,
..Default::default()
});
assert_eq!(
count_for(
&s,
"scanner pattern expansion skipped by invalid pattern index"
),
Some(2)
);
}
#[test]
fn scanner_boundary_cardinality_mismatches_surface() {
let s = coverage_gap_summary(&CoverageCounts {
scanner_boundary_cardinality_mismatches: 1,
..Default::default()
});
assert_eq!(
count_for(&s, "scanner boundary reassembly skipped"),
Some(1)
);
}
#[test]
fn scanner_line_offset_mismatches_surface() {
let s = coverage_gap_summary(&CoverageCounts {
scanner_line_offset_mismatches: 3,
..Default::default()
});
assert_eq!(
count_for(
&s,
"scanner multiline attribution used fallback source offsets"
),
Some(3)
);
}
#[test]
fn binary_degraded_surfaces() {
let s = coverage_gap_summary(&CoverageCounts {
binary_degraded: 2,
..Default::default()
});
assert_eq!(
count_for(&s, "binary deep analysis degraded to strings-only"),
Some(2)
);
}
#[test]
fn binary_unreadable_surfaces() {
let s = coverage_gap_summary(&CoverageCounts {
binary_unreadable: 4,
skip: SkipCounts {
unreadable: 4,
..Default::default()
},
..Default::default()
});
assert_eq!(
count_for(
&s,
"binary unreadable (permission denied or I/O error; binary NOT scanned)"
),
Some(4),
"unreadable binaries must reach the structured report, not just the human summary"
);
}
#[test]
fn unreadable_count_excludes_unreadable_binaries() {
let s = coverage_gap_summary(&CoverageCounts {
binary_unreadable: 2,
skip: SkipCounts {
unreadable: 5,
..Default::default()
},
..Default::default()
});
assert_eq!(
count_for(&s, "unreadable (permission denied or I/O error)"),
Some(3),
"generic unreadable must exclude the 2 unreadable binaries (5 - 2 = 3)"
);
assert_eq!(
count_for(&s, "binary unreadable (permission denied"),
Some(2)
);
}
#[test]
fn all_unreadable_being_binaries_drops_the_generic_line() {
let s = coverage_gap_summary(&CoverageCounts {
binary_unreadable: 3,
skip: SkipCounts {
unreadable: 3,
..Default::default()
},
..Default::default()
});
assert_eq!(
count_for(&s, "unreadable (permission denied or I/O error)"),
None,
"no non-binary unreadable files remain, so the generic line must be filtered out"
);
assert_eq!(
count_for(&s, "binary unreadable (permission denied"),
Some(3)
);
}
fn all_ones() -> CoverageCounts {
CoverageCounts {
skip: SkipCounts {
over_max_size: 1,
binary: 1,
excluded: 1,
unreadable: 2, git_object_unreadable: 1,
archive_truncated: 1,
binary_section_name_unresolved: 1,
source_truncated: 1,
structured_source_parse_failures: 1,
archive_duplicate_scan_unavailable: 1,
git_lfs_pointer: 1,
},
source_errors: 1,
batches_not_routed: 1,
scanner_structured_parse_failures: 1,
scanner_structured_oversize_skips: 1,
scanner_decode_truncations: 1,
scanner_decode_oversize_skips: 1,
scanner_invalid_pattern_index_skips: 1,
scanner_boundary_cardinality_mismatches: 1,
scanner_line_offset_mismatches: 1,
scanner_chunk_deadline_aborts: 1,
scanner_binary_strings_named_exclusions: 1,
binary_degraded: 1,
binary_unreadable: 1,
vendored_path_suppressions: 1,
nothing_scanned_no_input: true,
nothing_scanned_all_skipped: true,
}
}
#[test]
fn every_category_surfaces_when_all_counters_are_nonzero() {
let s = coverage_gap_summary(&all_ones());
assert_eq!(
s.len(),
27,
"every one of the 27 coverage-gap categories must surface, got {} ({s:?})",
s.len()
);
}
#[test]
fn all_reasons_are_unique() {
let s = coverage_gap_summary(&all_ones());
let mut reasons: Vec<&str> = s.iter().map(|(r, _)| r.as_str()).collect();
reasons.sort_unstable();
let unique = {
let mut u = reasons.clone();
u.dedup();
u.len()
};
assert_eq!(
unique,
reasons.len(),
"two categories share a reason string (a drift/copy bug): {reasons:?}"
);
}
#[test]
fn all_reasons_are_nonempty() {
let s = coverage_gap_summary(&all_ones());
assert!(
s.iter().all(|(r, _)| !r.trim().is_empty()),
"every coverage-gap reason must be a non-empty sentence"
);
}
#[test]
fn surfaced_count_equals_input_count() {
let s = coverage_gap_summary(&with_skip(SkipCounts {
over_max_size: 42,
..Default::default()
}));
assert_eq!(
count_for(
&s,
"exceeded a configured size cap (--max-file-size or the matching --limit-*-bytes)"
),
Some(42),
"the surfaced count must be the exact input count, not a boolean/clamp"
);
}
#[test]
fn all_has_twenty_seven_kinds() {
assert_eq!(
CoverageGapKind::ALL.len(),
27,
"the canonical coverage-gap set must have exactly 27 categories"
);
}
#[test]
fn all_kinds_are_distinct() {
let mut seen: Vec<CoverageGapKind> = Vec::new();
for kind in CoverageGapKind::ALL {
assert!(
!seen.contains(&kind),
"{kind:?} appears more than once in CoverageGapKind::ALL"
);
seen.push(kind);
}
}
#[test]
fn every_kind_is_nonzero_on_all_ones() {
let counts = all_ones();
for kind in CoverageGapKind::ALL {
assert!(
kind.count(&counts) > 0,
"{kind:?} has a zero count when every counter is set: its field is unwired"
);
}
}
#[test]
fn every_kind_is_zero_on_empty() {
let counts = CoverageCounts::default();
for kind in CoverageGapKind::ALL {
assert_eq!(
kind.count(&counts),
0,
"{kind:?} must be zero for an all-zero snapshot"
);
}
}
#[test]
fn fail_severity_set_is_exact() {
use CoverageGapKind::*;
for kind in [
ScannerStructuredParseFailure,
SourceError,
NonBinaryUnreadable,
GitObjectUnreadable,
ArchiveTruncated,
BinarySectionNameUnresolved,
SourceTruncated,
StructuredSourceParseFailure,
ArchiveDuplicateScanUnavailable,
GitLfsPointer,
BinaryDegraded,
BinaryUnreadable,
ScannerLineOffsetMismatch,
] {
assert_eq!(
kind.severity(),
CoverageSeverity::Fail,
"{kind:?} is a genuine coverage miss and must render as FAIL (red)"
);
}
}
#[test]
fn warn_severity_set_is_exact() {
use CoverageGapKind::*;
for kind in [
OverMaxSize,
Binary,
Excluded,
ScannerStructuredOversizeSkip,
ScannerDecodeTruncation,
ScannerDecodeOversizeSkip,
ScannerInvalidPatternIndexSkip,
ScannerBoundaryCardinalityMismatch,
ScannerBinaryStringsNamedExclusion,
] {
assert_eq!(
kind.severity(),
CoverageSeverity::Warn,
"{kind:?} is advisory/partial and must render as WARN (yellow)"
);
}
}
#[test]
fn severity_partition_totals_all_kinds() {
let fail = CoverageGapKind::ALL
.iter()
.filter(|k| k.severity() == CoverageSeverity::Fail)
.count();
let warn = CoverageGapKind::ALL
.iter()
.filter(|k| k.severity() == CoverageSeverity::Warn)
.count();
assert_eq!(fail, 17, "expected 17 FAIL categories, got {fail}");
assert_eq!(warn, 10, "expected 10 WARN categories, got {warn}");
assert_eq!(fail + warn, CoverageGapKind::ALL.len());
assert_eq!(
CoverageGapKind::fail_class_kinds().count(),
fail,
"fail_class_kinds() must match severity()==Fail filter (KH-1410)"
);
}
#[test]
fn daemon_source_coverage_gaps_fail_class_matches_source_skip_fail_kinds() {
use crate::daemon::protocol::SourceCoverageGaps;
let gaps = SourceCoverageGaps {
over_max_size: 1,
binary: 1,
unreadable: 2,
git_object_unreadable: 3,
archive_truncated: 4,
binary_section_name_unresolved: 5,
source_truncated: 6,
structured_source_parse_failures: 7,
archive_duplicate_scan_unavailable: 8,
git_lfs_pointer: 9,
source_failed: 10,
};
assert_eq!(
gaps.fail_class_total(),
2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10,
"daemon fail_class_total must exclude only over_max_size and binary"
);
assert_eq!(gaps.total(), gaps.fail_class_total() + 1 + 1);
}
#[test]
fn fail_class_total_sums_only_fail_kinds() {
let counts = all_ones();
let via_kinds: usize = CoverageGapKind::fail_class_kinds()
.map(|k| k.count(&counts))
.sum();
assert_eq!(counts.fail_class_total(), via_kinds);
for kind in CoverageGapKind::ALL {
if kind.severity() == CoverageSeverity::Warn {
assert!(
kind.count(&counts) > 0,
"{kind:?} should be non-zero in all_ones fixture"
);
}
}
let warn_sum: usize = CoverageGapKind::ALL
.iter()
.filter(|k| k.severity() == CoverageSeverity::Warn)
.map(|k| k.count(&counts))
.sum();
let all_sum: usize = CoverageGapKind::ALL.iter().map(|k| k.count(&counts)).sum();
assert_eq!(
counts.fail_class_total() + warn_sum,
all_sum,
"FAIL+WARN partition must cover every kind count"
);
assert!(
counts.fail_class_total() < all_sum,
"WARN categories must be excluded from fail_class_total"
);
}
#[test]
fn sarif_reasons_are_all_unique() {
let mut reasons: Vec<&str> = CoverageGapKind::ALL
.iter()
.map(|k| k.sarif_reason())
.collect();
let total = reasons.len();
reasons.sort_unstable();
reasons.dedup();
assert_eq!(
reasons.len(),
total,
"two kinds share a SARIF reason string"
);
}
#[test]
fn sarif_reasons_are_all_nonempty() {
for kind in CoverageGapKind::ALL {
assert!(
!kind.sarif_reason().trim().is_empty(),
"{kind:?} has an empty SARIF reason"
);
}
}
#[test]
fn human_reasons_are_all_unique() {
let mut reasons: Vec<String> = CoverageGapKind::ALL
.iter()
.map(|k| k.human_reason(1))
.collect();
let total = reasons.len();
reasons.sort();
reasons.dedup();
assert_eq!(
reasons.len(),
total,
"two kinds share a human reason string"
);
}
#[test]
fn human_reasons_are_all_nonempty() {
for kind in CoverageGapKind::ALL {
assert!(
!kind.human_reason(1).trim().is_empty(),
"{kind:?} has an empty human reason"
);
}
}
#[test]
fn human_reason_embeds_the_count() {
for kind in CoverageGapKind::ALL {
let reason = kind.human_reason(4242);
assert!(
reason.contains("4242"),
"{kind:?} human reason must include its count, got {reason:?}"
);
}
}
#[test]
fn sarif_summary_is_the_projection_of_all_kinds() {
let counts = all_ones();
let summary = coverage_gap_summary(&counts);
for kind in CoverageGapKind::ALL {
assert!(
summary.iter().any(|(r, _)| r == kind.sarif_reason()),
"{kind:?} is non-zero on all_ones but its sarif_reason is missing from the summary"
);
}
assert_eq!(
summary.len(),
CoverageGapKind::ALL.len(),
"the SARIF summary must contain exactly one entry per non-zero kind"
);
}
#[test]
fn both_surfaces_cover_the_identical_kind_set() {
let counts = all_ones();
let summary = coverage_gap_summary(&counts);
let sarif_reasons: Vec<&str> = summary.iter().map(|(r, _)| r.as_str()).collect();
let mut rendered = 0usize;
for kind in CoverageGapKind::ALL {
let n = kind.count(&counts);
if n == 0 {
continue;
}
assert!(
!kind.human_reason(n).trim().is_empty(),
"{kind:?} non-zero but the human surface renders nothing"
);
assert!(
sarif_reasons.contains(&kind.sarif_reason()),
"{kind:?} rendered by the human surface but missing from SARIF"
);
rendered += 1;
}
assert_eq!(
rendered,
summary.len(),
"the human and SARIF surfaces must cover the identical set of kinds"
);
assert_eq!(rendered, 27, "all 27 kinds render on the all_ones snapshot");
}