diff --git a/src/db.rs b/src/db.rs index 01ec39b..3c8e5ac 100644 --- a/src/db.rs +++ b/src/db.rs @@ -157,7 +157,8 @@ pub(crate) struct TestHit { /// Why a test was selected. The triple (`file`, `matched_hunk`, `kind`) /// describes one path through the selection logic; `stored_range` names the /// row that matched (absent for [`HitKind::StructuralBackstop`], where the -/// whole point is that nothing matched). +/// whole point is that nothing matched, and for [`HitKind::ConfigRule`], +/// which is anchored to a config rule rather than to coverage). #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct HitReason { /// `collect_sha` the diff was anchored against. Hunks live in this @@ -170,7 +171,8 @@ pub(crate) struct HitReason { /// The diff hunk that triggered the selection. Inclusive `(start, end)`. pub(crate) matched_hunk: (i64, i64), /// The stored row that overlapped the hunk. `None` when [`HitKind::StructuralBackstop`] - /// fired (no row overlapped — the test was pulled in by file-presence alone). + /// fired (no row overlapped — the test was pulled in by file-presence alone), + /// and for [`HitKind::ConfigRule`], which isn't anchored to coverage at all. pub(crate) stored_range: Option<(i64, i64)>, } diff --git a/src/plan.rs b/src/plan.rs index d8823ff..86d2795 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -274,7 +274,6 @@ pub(crate) fn write_selection_report(inputs: SelectionReport, path: &Path) -> Re &plan.changed_ranges, changed_files, )?, - include_changed_files: true, }; Report::build_selection(report_inputs).write_json(path) } diff --git a/src/report.rs b/src/report.rs index da6ba45..0ba6835 100644 --- a/src/report.rs +++ b/src/report.rs @@ -252,7 +252,8 @@ pub(crate) struct ReasonEntry { pub(crate) file: String, pub(crate) kind: ReasonKind, /// `[line_start, line_end]` of the stored row that matched. `None` - /// for `structural_backstop` (no row matched by definition). + /// for `structural_backstop` and `config_rule` — neither is anchored + /// to a coverage row, by definition. pub(crate) stored_range: Option<[i64; 2]>, /// `[start, end]` of the diff hunk that triggered selection. pub(crate) matched_hunk: [i64; 2], @@ -294,9 +295,6 @@ pub(crate) struct SelectionInputs<'a> { pub(crate) status: CacheStatus, pub(crate) selection: &'a Selection, pub(crate) changed_files: Vec, - /// `false` collapses to `selection.changed_files = None` (no diff - /// anchor was usable). - pub(crate) include_changed_files: bool, } /// Inputs for [`Report::build_full_suite`] — the partial-report path for @@ -486,14 +484,12 @@ impl Report { mode: SelectionMode::Selection, }; - let changed_files = if inputs.include_changed_files { - Some(build_changed_files_entries( - &inputs.changed_files, - &selection.diagnostics.per_file, - )) - } else { - None - }; + // Selection mode always has a usable diff anchor, so this is always + // `Some`; the `null` shape belongs to `build_full_suite`. + let changed_files = Some(build_changed_files_entries( + &inputs.changed_files, + &selection.diagnostics.per_file, + )); let selected_tests = selection.diagnostics.per_test.as_ref().map(|per_test| { build_selected_tests( @@ -953,6 +949,50 @@ mod tests { assert_eq!(entries[2].commits_ahead, None); } + /// A `config_rule` reason names the triggering input rather than a + /// coverage hunk: `stored_range` is null, `collect_sha` is empty and + /// `matched_hunk` is `[0, 0]`. `selection.rs` fills those three fields by + /// hand, so nothing else pins the encoding the v1 schema documents. + #[test] + fn config_rule_reason_carries_no_coverage_anchor() { + let test = TestId::new("sample::tests", "reads_snapshot"); + let per_test: BTreeMap> = [( + test.clone(), + vec![HitReason { + collect_sha: String::new(), + file: "tests/snapshots/x.snap".to_string(), + kind: HitKind::ConfigRule, + matched_hunk: (0, 0), + stored_range: None, + }], + )] + .into_iter() + .collect(); + let config_tests: BTreeSet = [test].into_iter().collect(); + + let entries = build_selected_tests( + &BTreeSet::new(), + &config_tests, + &BTreeSet::new(), + &BTreeSet::new(), + &per_test, + ); + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].kind, SelectedTestKind::ConfigRule); + let json = serde_json::to_value(&entries[0]).unwrap(); + assert_eq!(json["reasons"][0]["kind"], "config_rule"); + assert!( + json["reasons"][0]["stored_range"].is_null(), + "config_rule matches no coverage row, so stored_range must be null" + ); + assert_eq!(json["reasons"][0]["collect_sha"], ""); + assert_eq!( + json["reasons"][0]["matched_hunk"], + serde_json::json!([0, 0]) + ); + } + /// Build a minimal full-suite report and verify the shape callers /// expect — null counts, omitted changed_files/selected_tests, mode /// = "full-suite-no-listing".