Skip to main content

supercov_engine/
coverage_query.rs

1//! Language-neutral coverage query operators.
2//!
3//! Querying is deliberately separated from the CLI and storage container.
4//! This module accepts the frozen analyzed view and owns structural query
5//! semantics shared by every language frontend.
6
7use std::collections::{BTreeMap, BTreeSet, HashMap};
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    agent_json::pagination,
13    coverage_analysis::{CoverageSummary, is_independence_pair},
14    coverage_index::{
15        CoverageDimension, CoverageIndex, CoverageIndexError, CoverageViewId, IndexedCoverageModel,
16        IndexedDecisionGap, IndexedDimensionCoverage, IndexedFileGap, IndexedGapDimensions,
17        IndexedHitMetadata, IndexedMeasurement, IndexedOutcomeCounts, IndexedScopeEntry,
18        IndexedSourceScope, IndexedSummaryConfidence, IndexedTestSummary,
19    },
20    coverage_report::{
21        CoverageConfidence, CoverageReportRequest, CoverageView, DecisionMeta, ReportError,
22        SourceLine, TestAttempt, TestProvenance, TransportStats, analyze_coverage_results,
23        coverage_summary_for_tests,
24    },
25};
26use supercov_contracts::AgentPagination;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum MinimizeMetric {
31    All,
32    Lines,
33    Statements,
34    Functions,
35    Branches,
36    Mcdc,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct MinimumTestSetResult {
42    pub optimal: bool,
43    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
44    pub target: f64,
45    pub metric: MinimizeMetric,
46    pub selected: Vec<String>,
47    pub expanded: Vec<String>,
48    pub summary: CoverageSummary,
49    pub explored_states: usize,
50}
51
52#[derive(Debug, Clone, PartialEq, Deserialize)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54pub struct MinimumTestSetRequest {
55    pub coverage: CoverageReportRequest,
56    #[serde(default = "default_target")]
57    pub target: f64,
58    #[serde(default = "default_metric")]
59    pub metric: MinimizeMetric,
60    #[serde(default = "default_max_states")]
61    pub max_states: usize,
62}
63
64fn default_target() -> f64 {
65    100.0
66}
67
68fn default_metric() -> MinimizeMetric {
69    MinimizeMetric::All
70}
71
72fn default_max_states() -> usize {
73    5_000
74}
75
76pub fn minimum_test_set_for_request(
77    request: &MinimumTestSetRequest,
78) -> Result<MinimumTestSetResult, QueryError> {
79    let report = analyze_coverage_results(&request.coverage)?;
80    minimum_test_set(
81        &report.view,
82        request.target,
83        request.metric,
84        request.max_states,
85    )
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
89pub struct CoverageMinimizedTest {
90    pub id: String,
91    pub name: String,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub file: Option<String>,
94    pub runner: String,
95    pub kind: String,
96}
97
98#[derive(Debug, Clone, PartialEq, Serialize)]
99#[serde(rename_all = "camelCase")]
100pub struct CoverageMinimizeData {
101    pub run: String,
102    pub filters: CoverageQueryFilters,
103    pub optimal: bool,
104    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
105    pub target: f64,
106    pub metric: MinimizeMetric,
107    pub selected: Vec<String>,
108    pub expanded: Vec<String>,
109    pub summary: CoverageSummary,
110    pub explored_states: usize,
111    pub selected_count: usize,
112    pub total_candidate_tests: usize,
113    pub tests: Vec<CoverageMinimizedTest>,
114}
115
116#[derive(Debug, Clone, Copy)]
117pub struct CoverageMinimizeQueryOptions<'a> {
118    pub run: &'a str,
119    pub view_id: CoverageViewId,
120    pub kind: Option<&'a str>,
121    pub runner: Option<&'a str>,
122    pub target: f64,
123    pub metric: MinimizeMetric,
124    pub max_states: usize,
125    pub offset: usize,
126    pub limit: usize,
127}
128
129pub fn coverage_minimize_query(
130    view: &CoverageView,
131    options: CoverageMinimizeQueryOptions<'_>,
132) -> Result<(CoverageMinimizeData, AgentPagination), QueryError> {
133    if options.limit == 0 {
134        return Err(QueryError::InvalidPagination);
135    }
136    let selected_ids = if options.kind.is_none() && options.runner.is_none() {
137        None
138    } else {
139        let ids = view
140            .tests
141            .iter()
142            .filter(|test| {
143                options.kind.is_none_or(|kind| test.provenance.kind == kind)
144                    && options
145                        .runner
146                        .is_none_or(|runner| test.provenance.runner == runner)
147            })
148            .map(|test| test.id.clone())
149            .collect::<BTreeSet<_>>();
150        if ids.is_empty() {
151            return Err(QueryError::TestFilterEmpty {
152                kind: options.kind.map(str::to_owned),
153                runner: options.runner.map(str::to_owned),
154            });
155        }
156        Some(ids)
157    };
158    let mut solver_view = view.clone();
159    if let Some(selected) = &selected_ids {
160        solver_view.tests.retain(|test| selected.contains(&test.id));
161    }
162    let minimized = minimum_test_set(
163        &solver_view,
164        options.target,
165        options.metric,
166        options.max_states,
167    )?;
168    let selected_details = minimized
169        .selected
170        .iter()
171        .map(|id| {
172            let test = view
173                .tests
174                .iter()
175                .find(|test| test.id == *id)
176                .ok_or(QueryError::InvalidRecordSelection)?;
177            Ok(CoverageMinimizedTest {
178                id: id.clone(),
179                name: test.name.clone(),
180                file: test.file.clone(),
181                runner: test.provenance.runner.clone(),
182                kind: test.provenance.kind.clone(),
183            })
184        })
185        .collect::<Result<Vec<_>, QueryError>>()?;
186    let total = selected_details.len();
187    let tests = selected_details
188        .iter()
189        .skip(options.offset)
190        .take(options.limit)
191        .cloned()
192        .collect::<Vec<_>>();
193    let returned = tests.len();
194    let total_candidate_tests = solver_view
195        .tests
196        .iter()
197        .filter(|test| test.role == "test")
198        .count();
199    Ok((
200        CoverageMinimizeData {
201            run: options.run.into(),
202            filters: query_filters(options.view_id, options.kind, options.runner),
203            optimal: minimized.optimal,
204            target: minimized.target,
205            metric: minimized.metric,
206            selected: minimized.selected,
207            expanded: minimized.expanded,
208            summary: minimized.summary,
209            explored_states: minimized.explored_states,
210            selected_count: total,
211            total_candidate_tests,
212            tests,
213        },
214        pagination(options.offset, options.limit, returned, total),
215    ))
216}
217
218#[derive(Debug)]
219pub enum QueryError {
220    InvalidTarget(f64),
221    UnattributedEvidence,
222    TargetUnreachable {
223        metric: MinimizeMetric,
224        target: f64,
225        reachable: f64,
226    },
227    ComplexityLimit {
228        candidate_tests: usize,
229        obligations: usize,
230        explored_states: usize,
231        max_states: usize,
232        target: f64,
233        metric: MinimizeMetric,
234    },
235    Analysis(ReportError),
236    Index(CoverageIndexError),
237    InvalidPagination,
238    TestFilterEmpty {
239        kind: Option<String>,
240        runner: Option<String>,
241    },
242    TestNotFound(String),
243    DecisionNotFound(String),
244    SourceNotFound(String),
245    AmbiguousSelector {
246        selector: String,
247        matches: Vec<String>,
248    },
249    InvalidRecordSelection,
250    ScopeUnavailable,
251}
252
253impl From<ReportError> for QueryError {
254    fn from(value: ReportError) -> Self {
255        Self::Analysis(value)
256    }
257}
258
259impl From<CoverageIndexError> for QueryError {
260    fn from(value: CoverageIndexError) -> Self {
261        Self::Index(value)
262    }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct CoverageQueryFilters {
268    pub outcome: String,
269    pub kind: Option<String>,
270    pub runner: Option<String>,
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize)]
274#[serde(rename_all = "camelCase")]
275pub struct CoverageFilesData {
276    pub run: String,
277    pub filters: CoverageQueryFilters,
278    pub metric: MinimizeMetric,
279    pub files: Vec<IndexedFileGap>,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct CoverageGapsData {
285    pub run: String,
286    pub filters: CoverageQueryFilters,
287    pub metric: MinimizeMetric,
288    pub gaps: Vec<IndexedFileGap>,
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize)]
292#[serde(rename_all = "camelCase")]
293pub struct CoverageKindsData {
294    pub run: String,
295    pub filters: CoverageQueryFilters,
296    pub kinds: Vec<IndexedDimensionCoverage>,
297}
298
299#[derive(Debug, Clone, PartialEq, Serialize)]
300#[serde(rename_all = "camelCase")]
301pub struct CoverageRunnersData {
302    pub run: String,
303    pub filters: CoverageQueryFilters,
304    pub runners: Vec<IndexedDimensionCoverage>,
305}
306
307/// Tests by how completely their coverage is their own.
308#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
309#[serde(rename_all = "camelCase")]
310pub struct TestAttributionCounts {
311    /// Credited with exactly what they reached.
312    pub exact: usize,
313    /// Credited with some of what they reached, and no way to say how much.
314    pub partial: usize,
315    /// Credited with none of it; what they reached is in the run's totals.
316    pub run_wide: usize,
317}
318
319impl TestAttributionCounts {
320    pub fn total(&self) -> usize {
321        self.exact + self.partial + self.run_wide
322    }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
326#[serde(rename_all = "camelCase")]
327pub struct CoverageDiagnostic {
328    pub code: String,
329    pub severity: String,
330    pub message: String,
331}
332
333#[derive(Debug, Clone, PartialEq, Serialize)]
334#[serde(rename_all = "camelCase")]
335pub struct CoverageSummaryData {
336    pub test_kind_sources: BTreeMap<String, usize>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub assertion_coverage: Option<serde_json::Value>,
339    pub run: String,
340    #[serde(default)]
341    pub command: Vec<String>,
342    #[serde(default, skip_serializing_if = "Vec::is_empty")]
343    pub hints: Vec<String>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub workspace: Option<String>,
346    pub filters: CoverageQueryFilters,
347    pub model: IndexedCoverageModel,
348    pub generated_at: String,
349    pub valid: bool,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub test_exit_code: Option<i32>,
352    pub stale: bool,
353    pub stale_reasons: Vec<String>,
354    pub structurally_complete: bool,
355    pub complete: bool,
356    pub coverage: CoverageSummary,
357    pub measurement: IndexedMeasurement,
358    pub coverage_by_kind: Vec<IndexedDimensionCoverage>,
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub e2e_gap_context: Option<CoverageKindGapContext>,
361    pub coverage_by_runner: Vec<IndexedDimensionCoverage>,
362    pub attribution: crate::coverage_index::IndexedAttribution,
363    /// How completely each test's coverage could be credited to it.
364    ///
365    /// Counted here rather than stored, because the per-test answer is already
366    /// in the run and a total is a read of it. A run written before the field
367    /// existed reports every test as exact, which is what its records say.
368    pub test_attribution: TestAttributionCounts,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub transport: Option<TransportStats>,
371    pub diagnostics: Vec<CoverageDiagnostic>,
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub confidence: Option<IndexedSummaryConfidence>,
374    pub files_with_gaps: usize,
375    pub files_with_coverage_gaps: usize,
376    pub files_with_measurement_limitations: usize,
377    pub tests: usize,
378    pub setups: usize,
379    pub test_outcomes: IndexedOutcomeCounts,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub source_scope: Option<IndexedSourceScope>,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
385#[serde(rename_all = "camelCase")]
386pub struct CoverageKindGapContext {
387    pub kind: String,
388    pub other_kinds: Vec<String>,
389    pub covered_elsewhere: IndexedGapDimensions,
390    pub uncovered_everywhere: IndexedGapDimensions,
391}
392
393#[derive(Debug, Clone)]
394pub struct CoverageSummaryQueryOptions<'a> {
395    pub run: &'a str,
396    pub view: CoverageViewId,
397    pub kind: Option<&'a str>,
398    pub runner: Option<&'a str>,
399    pub valid: bool,
400    pub test_exit_code: Option<i32>,
401    pub stale: bool,
402    pub stale_reasons: Vec<String>,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
406pub struct ScopeCounts {
407    pub included: usize,
408    pub excluded: usize,
409    pub ambiguous: usize,
410}
411
412#[derive(Debug, Clone, PartialEq, Serialize)]
413#[serde(rename_all = "camelCase")]
414pub struct CoverageScopeData {
415    pub run: String,
416    pub filters: CoverageQueryFilters,
417    pub kind: String,
418    pub language: String,
419    pub model: String,
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub mode: Option<String>,
422    pub roots: Vec<String>,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub unit: Option<String>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub measurement_complete: Option<bool>,
427    pub counts: ScopeCounts,
428    pub measurement: IndexedMeasurement,
429    pub entries: Vec<IndexedScopeEntry>,
430}
431
432#[derive(Debug, Clone, Copy)]
433pub struct CoverageScopeQueryOptions<'a> {
434    pub run: &'a str,
435    pub view: CoverageViewId,
436    pub kind: Option<&'a str>,
437    pub runner: Option<&'a str>,
438    pub offset: usize,
439    pub limit: usize,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
443pub struct CoverageLocation {
444    pub file: String,
445    pub line: usize,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
449pub struct CoverageCoveringTest {
450    pub id: String,
451    pub name: String,
452    pub provenance: TestProvenance,
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
456#[serde(rename_all = "camelCase")]
457pub struct CoverageCoveringPhase {
458    pub id: String,
459    pub kind: String,
460    pub operation: String,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub source: Option<String>,
463    pub test: String,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub status: Option<String>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub caused_by_phase_id: Option<String>,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
471#[serde(rename_all = "camelCase")]
472pub struct CoverageAnchor {
473    pub kind: String,
474    pub id: String,
475    pub column: usize,
476    pub covered: bool,
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub source: Option<String>,
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub missing: Option<String>,
481    pub covering_tests: usize,
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub covered_conditions: Option<usize>,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub conditions: Option<usize>,
486}
487
488#[derive(Debug, Clone, PartialEq, Serialize)]
489#[serde(rename_all = "camelCase")]
490pub struct CoverageCoversLineData {
491    pub run: String,
492    pub filters: CoverageQueryFilters,
493    pub location: CoverageLocation,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub source: Option<String>,
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub source_origin: Option<String>,
498    pub covered: bool,
499    pub confidence: CoverageConfidence,
500    pub total_tests: usize,
501    pub total_phases: usize,
502    pub total_anchored: usize,
503    pub covered_anchored: usize,
504    pub total_limitations: usize,
505    pub total_remaining: usize,
506    pub tests: Vec<CoverageCoveringTest>,
507    pub phases: Vec<CoverageCoveringPhase>,
508    pub anchored: Vec<CoverageAnchor>,
509    pub limitations: Vec<CoverageFileLimitation>,
510    pub remaining: Vec<CoverageFileObligation>,
511}
512
513#[derive(Debug, Clone, PartialEq, Serialize)]
514#[serde(rename_all = "camelCase")]
515pub struct CoverageCoversAnchorsData {
516    pub run: String,
517    pub filters: CoverageQueryFilters,
518    pub location: CoverageLocation,
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub source: Option<String>,
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub source_origin: Option<String>,
523    pub line_obligation: bool,
524    pub anchored: Vec<CoverageAnchor>,
525    pub total_anchored: usize,
526    pub covered_anchored: usize,
527    pub total_limitations: usize,
528    pub limitations: Vec<CoverageFileLimitation>,
529    pub total_remaining: usize,
530    pub remaining: Vec<CoverageFileObligation>,
531    pub total_tests: usize,
532    pub tests: Vec<CoverageCoveringTest>,
533}
534
535#[derive(Debug, Clone, PartialEq, Serialize)]
536#[serde(untagged)]
537pub enum CoverageCoversData {
538    Line(CoverageCoversLineData),
539    Anchors(CoverageCoversAnchorsData),
540}
541
542#[derive(Debug, Clone, Copy)]
543pub struct CoverageCoversQueryOptions<'a> {
544    pub run: &'a str,
545    pub view: CoverageViewId,
546    pub kind: Option<&'a str>,
547    pub runner: Option<&'a str>,
548    pub file: &'a str,
549    pub line: usize,
550    pub offset: usize,
551    pub limit: usize,
552}
553
554fn query_filters(
555    view: CoverageViewId,
556    kind: Option<&str>,
557    runner: Option<&str>,
558) -> CoverageQueryFilters {
559    CoverageQueryFilters {
560        outcome: match view {
561            CoverageViewId::All => "all",
562            CoverageViewId::Passed => "passed",
563            CoverageViewId::Failed => "failed",
564        }
565        .into(),
566        kind: kind.map(str::to_owned),
567        runner: runner.map(str::to_owned),
568    }
569}
570
571fn selected_test_ids(
572    tests: &[IndexedTestSummary],
573    kind: Option<&str>,
574    runner: Option<&str>,
575) -> Result<Option<BTreeSet<String>>, QueryError> {
576    if kind.is_none() && runner.is_none() {
577        return Ok(None);
578    }
579    let selected = tests
580        .iter()
581        .filter(|test| {
582            kind.is_none_or(|kind| test.provenance.kind == kind)
583                && runner.is_none_or(|runner| test.provenance.runner == runner)
584        })
585        .map(|test| test.id.clone())
586        .collect::<BTreeSet<_>>();
587    if selected.is_empty() {
588        return Err(QueryError::TestFilterEmpty {
589            kind: kind.map(str::to_owned),
590            runner: runner.map(str::to_owned),
591        });
592    }
593    Ok(Some(selected))
594}
595
596pub fn coverage_covers_query(
597    index: &CoverageIndex<'_>,
598    options: CoverageCoversQueryOptions<'_>,
599) -> Result<(CoverageCoversData, AgentPagination), QueryError> {
600    if options.limit == 0 {
601        return Err(QueryError::InvalidPagination);
602    }
603    let tests = index.test_summaries(options.view)?;
604    let selected = selected_test_ids(&tests, options.kind, options.runner)?;
605    let selected_includes = |id: &str| selected.as_ref().is_none_or(|ids| ids.contains(id));
606    let filters = query_filters(options.view, options.kind, options.runner);
607    let location = CoverageLocation {
608        file: options.file.into(),
609        line: options.line,
610    };
611    let metadata = index
612        .hit_metadata(options.view)?
613        .into_iter()
614        .map(|value| (value.id.clone(), value))
615        .collect::<HashMap<_, _>>();
616    let decisions = index
617        .decision_details(options.view)?
618        .into_iter()
619        .map(|value| (value.meta.id.clone(), value))
620        .collect::<HashMap<_, _>>();
621    let anchors = index.anchors(options.view, options.file, options.line)?;
622    let tests_by_id = tests
623        .iter()
624        .map(|test| (test.id.clone(), test.clone()))
625        .collect::<HashMap<_, _>>();
626    let mut anchor_test_ids = anchors
627        .iter()
628        .flat_map(|anchor| anchor.tests.iter())
629        .filter(|id| selected_includes(id))
630        .cloned()
631        .collect::<BTreeSet<_>>();
632    for anchor in anchors.iter().filter(|anchor| anchor.kind == "branch") {
633        anchor_test_ids.extend(
634            metadata
635                .values()
636                .filter(|detail| detail.parent_id.as_deref() == Some(anchor.id.as_str()))
637                .flat_map(|detail| detail.tests.iter())
638                .filter(|id| selected_includes(id))
639                .cloned(),
640        );
641    }
642    let all_anchor_tests = anchor_test_ids
643        .iter()
644        .map(|id| {
645            let test = tests_by_id.get(id);
646            CoverageCoveringTest {
647                id: id.clone(),
648                name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
649                provenance: test
650                    .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
651            }
652        })
653        .collect::<Vec<_>>();
654    let total_anchor_tests = all_anchor_tests.len();
655    let anchor_tests_page = all_anchor_tests
656        .iter()
657        .skip(options.offset)
658        .take(options.limit)
659        .cloned()
660        .collect::<Vec<_>>();
661    let render_anchor = |anchor: crate::coverage_index::IndexedAnchor| {
662        let branch_alternatives = if anchor.kind == "branch" {
663            metadata
664                .values()
665                .filter(|detail| {
666                    detail.obligation == "branch"
667                        && detail.parent_id.as_deref() == Some(anchor.id.as_str())
668                })
669                .collect::<Vec<_>>()
670        } else {
671            Vec::new()
672        };
673        let branch_tests = branch_alternatives
674            .iter()
675            .flat_map(|detail| detail.tests.iter())
676            .filter(|test| selected_includes(test))
677            .cloned()
678            .collect::<BTreeSet<_>>();
679        let covering_tests = if anchor.kind == "branch" {
680            branch_tests.len()
681        } else {
682            anchor
683                .tests
684                .iter()
685                .filter(|test| selected_includes(test))
686                .count()
687        };
688        let detail = metadata.get(&anchor.id);
689        let decision = decisions
690            .get(&anchor.id)
691            .cloned()
692            .map(|decision| selected_decision(decision, selected.as_ref()));
693        let conditions = decision.as_ref().map_or(anchor.conditions, |decision| {
694            Some(decision.conditions.len())
695        });
696        let covered_conditions = decision
697            .as_ref()
698            .map_or(anchor.covered_conditions, |decision| {
699                Some(
700                    decision
701                        .conditions
702                        .iter()
703                        .filter(|condition| condition.covered)
704                        .count(),
705                )
706            });
707        let covered = match anchor.kind.as_str() {
708            "decision" => conditions == covered_conditions,
709            "branch" if !branch_alternatives.is_empty() => branch_alternatives
710                .iter()
711                .all(|detail| detail.tests.iter().any(|test| selected_includes(test))),
712            "branch" => anchor.covered,
713            _ => covering_tests > 0,
714        };
715        let branch_source = branch_alternatives
716            .first()
717            .map(|detail| detail.source.clone());
718        let missing_branch_alternatives = branch_alternatives
719            .iter()
720            .filter(|detail| !detail.tests.iter().any(|test| selected_includes(test)))
721            .filter_map(|detail| detail.alternative.clone())
722            .collect::<Vec<_>>();
723        CoverageAnchor {
724            kind: anchor.kind,
725            id: anchor.id,
726            column: anchor.column,
727            covered,
728            source: decision
729                .as_ref()
730                .map(|decision| decision.meta.source.clone())
731                .or(branch_source)
732                .or_else(|| {
733                    detail.map(|detail| {
734                        detail
735                            .label
736                            .clone()
737                            .unwrap_or_else(|| detail.source.clone())
738                    })
739                })
740                .and_then(|source| compact_source(&source)),
741            missing: if missing_branch_alternatives.is_empty() {
742                detail.and_then(|detail| detail.alternative.clone())
743            } else {
744                Some(missing_branch_alternatives.join("; "))
745            },
746            covering_tests,
747            covered_conditions,
748            conditions,
749        }
750    };
751    let rendered_anchors = anchors.into_iter().map(render_anchor).collect::<Vec<_>>();
752    let total_anchored = rendered_anchors.len();
753    let covered_anchored = rendered_anchors
754        .iter()
755        .filter(|anchor| anchor.covered)
756        .count();
757    let all_limitations = index
758        .limitations(options.view)?
759        .into_iter()
760        .filter(|limitation| limitation.file == options.file && limitation.line == options.line)
761        .map(|limitation| CoverageFileLimitation {
762            id: limitation.id,
763            kind: limitation.kind,
764            file: limitation.file,
765            line: limitation.line,
766            column: limitation.column,
767            source: limitation.source,
768            reason: limitation.reason,
769            blocking: limitation.blocking,
770            effect: "outside-measured-denominator".into(),
771        })
772        .collect::<Vec<_>>();
773    let total_limitations = all_limitations.len();
774    let limitations_page = all_limitations
775        .iter()
776        .skip(options.offset)
777        .take(options.limit)
778        .cloned()
779        .collect::<Vec<_>>();
780    let (file_detail, _) = coverage_file_detail_query(
781        index,
782        CoverageFileDetailOptions {
783            run: options.run,
784            view: options.view,
785            kind: options.kind,
786            runner: options.runner,
787            selector: options.file,
788            metric: MinimizeMetric::All,
789            offset: 0,
790            limit: usize::MAX,
791        },
792    )?;
793    let gap_line = file_detail
794        .gap_lines
795        .into_iter()
796        .find(|gap| gap.line == options.line);
797    // Covered lines are absent from the gap projection, but their anchored
798    // statement/branch/decision metadata still carries the source snippet.
799    // Prefer the exact gap-line text when present and otherwise retain that
800    // anchored source so a successful line query never contradicts itself by
801    // claiming the source is unavailable while printing it below.
802    let source = gap_line
803        .as_ref()
804        .and_then(|gap| gap.source.clone())
805        .or_else(|| {
806            rendered_anchors
807                .iter()
808                .filter_map(|anchor| anchor.source.clone())
809                .min_by_key(|source| source.len())
810        });
811    let all_remaining = gap_line.map_or_else(Vec::new, |gap| gap.obligations);
812    let total_remaining = all_remaining.len();
813    let remaining_page = all_remaining
814        .iter()
815        .skip(options.offset)
816        .take(options.limit)
817        .cloned()
818        .collect::<Vec<_>>();
819    let Some(line) = index.line(options.view, options.file, options.line)? else {
820        let anchored = rendered_anchors
821            .iter()
822            .skip(options.offset)
823            .take(options.limit)
824            .cloned()
825            .collect::<Vec<_>>();
826        let total = total_anchored.max(total_limitations).max(total_remaining);
827        let total = total.max(total_anchor_tests);
828        let returned = anchored
829            .len()
830            .max(limitations_page.len())
831            .max(remaining_page.len())
832            .max(anchor_tests_page.len());
833        return Ok((
834            CoverageCoversData::Anchors(CoverageCoversAnchorsData {
835                run: options.run.into(),
836                filters,
837                location,
838                source,
839                source_origin: None,
840                line_obligation: false,
841                anchored,
842                total_anchored,
843                covered_anchored,
844                total_limitations,
845                limitations: limitations_page,
846                total_remaining,
847                remaining: remaining_page,
848                total_tests: total_anchor_tests,
849                tests: anchor_tests_page,
850            }),
851            pagination(options.offset, options.limit, returned, total),
852        ));
853    };
854    let all_tests = line
855        .tests
856        .iter()
857        .filter(|id| selected_includes(id))
858        .map(|id| {
859            let test = tests_by_id.get(id);
860            CoverageCoveringTest {
861                id: id.clone(),
862                name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
863                provenance: test
864                    .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
865            }
866        })
867        .collect::<Vec<_>>();
868    let phases_by_id = index
869        .phase_summaries(options.view)?
870        .into_iter()
871        .map(|phase| (phase.id.clone(), phase))
872        .collect::<HashMap<_, _>>();
873    let all_phases = line
874        .phases
875        .iter()
876        .filter_map(|id| phases_by_id.get(id))
877        .filter(|phase| selected_includes(&phase.test))
878        .map(|phase| CoverageCoveringPhase {
879            id: phase.id.clone(),
880            kind: phase.kind.clone(),
881            operation: phase.operation.clone(),
882            source: phase.source.clone(),
883            test: phase.test.clone(),
884            status: phase.status.clone(),
885            caused_by_phase_id: phase.caused_by_phase_id.clone(),
886        })
887        .collect::<Vec<_>>();
888    let tests_page = all_tests
889        .iter()
890        .skip(options.offset)
891        .take(options.limit)
892        .cloned()
893        .collect::<Vec<_>>();
894    let phases_page = all_phases
895        .iter()
896        .skip(options.offset)
897        .take(options.limit)
898        .cloned()
899        .collect::<Vec<_>>();
900    let anchored_page = rendered_anchors
901        .into_iter()
902        .skip(options.offset)
903        .take(options.limit)
904        .collect::<Vec<_>>();
905    let total = all_tests
906        .len()
907        .max(all_phases.len())
908        .max(total_anchored)
909        .max(total_limitations)
910        .max(total_remaining);
911    let returned = tests_page
912        .len()
913        .max(phases_page.len())
914        .max(anchored_page.len())
915        .max(limitations_page.len())
916        .max(remaining_page.len());
917    Ok((
918        CoverageCoversData::Line(CoverageCoversLineData {
919            run: options.run.into(),
920            filters,
921            location,
922            source,
923            source_origin: None,
924            covered: line.tests.iter().any(|test| selected_includes(test)),
925            confidence: line.confidence,
926            total_tests: all_tests.len(),
927            total_phases: all_phases.len(),
928            total_anchored,
929            covered_anchored,
930            total_limitations,
931            total_remaining,
932            tests: tests_page,
933            phases: phases_page,
934            anchored: anchored_page,
935            limitations: limitations_page,
936            remaining: remaining_page,
937        }),
938        pagination(options.offset, options.limit, returned, total),
939    ))
940}
941
942#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
943pub struct CoverageTestMatch {
944    pub id: String,
945    pub name: String,
946    pub outcome: String,
947    pub provenance: TestProvenance,
948}
949
950#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
951#[serde(rename_all = "camelCase")]
952pub struct CoverageHitDetail {
953    pub id: String,
954    pub obligation: String,
955    #[serde(skip_serializing_if = "Option::is_none")]
956    pub branch_kind: Option<String>,
957    #[serde(skip_serializing_if = "Option::is_none")]
958    pub file: Option<String>,
959    #[serde(skip_serializing_if = "Option::is_none")]
960    pub line: Option<usize>,
961    #[serde(skip_serializing_if = "Option::is_none")]
962    pub column: Option<usize>,
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub label: Option<String>,
965    #[serde(skip_serializing_if = "Option::is_none")]
966    pub alternative: Option<String>,
967}
968
969#[derive(Debug, Clone, PartialEq, Serialize)]
970pub struct CoverageTestDecision {
971    pub id: String,
972    pub vectors: Vec<crate::coverage_analysis::McdcVector>,
973    pub meta: DecisionMeta,
974}
975
976#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
977#[serde(rename_all = "camelCase")]
978pub struct CoverageTestPhase {
979    pub id: String,
980    pub kind: String,
981    pub operation: String,
982    #[serde(skip_serializing_if = "Option::is_none")]
983    pub source: Option<String>,
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub status: Option<String>,
986    #[serde(skip_serializing_if = "Option::is_none")]
987    pub caused_by_phase_id: Option<String>,
988    pub lines: usize,
989    pub decisions: usize,
990}
991
992#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
993pub struct CoverageTestTotals {
994    pub lines: usize,
995    pub hits: usize,
996    pub decisions: usize,
997    pub phases: usize,
998}
999
1000#[derive(Debug, Clone, PartialEq, Serialize)]
1001#[serde(rename_all = "camelCase")]
1002pub struct CoverageSelectedTest {
1003    pub id: String,
1004    pub name: String,
1005    #[serde(skip_serializing_if = "Option::is_none")]
1006    pub file: Option<String>,
1007    #[serde(skip_serializing_if = "Option::is_none")]
1008    pub title: Option<String>,
1009    pub retries: Vec<usize>,
1010    pub attempts: Vec<TestAttempt>,
1011    pub outcome: String,
1012    pub provenance: TestProvenance,
1013    pub role: String,
1014    /// `exact`, or `run-wide` when this test ran and nothing can say what it
1015    /// reached. Under `run-wide` the totals below are zero because the test
1016    /// has no coverage of its own, not because it reached nothing.
1017    pub attribution: String,
1018    pub hits: Vec<String>,
1019    pub decisions: Vec<CoverageTestDecision>,
1020    pub lines: Vec<SourceLine>,
1021    pub hit_details: Vec<CoverageHitDetail>,
1022    pub phases: Vec<CoverageTestPhase>,
1023    pub totals: CoverageTestTotals,
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Serialize)]
1027pub struct CoverageTestMatchesData {
1028    pub run: String,
1029    pub filters: CoverageQueryFilters,
1030    pub tests: Vec<CoverageTestMatch>,
1031}
1032
1033#[derive(Debug, Clone, PartialEq, Serialize)]
1034#[serde(rename_all = "camelCase")]
1035pub struct CoverageTestDetailData {
1036    pub run: String,
1037    pub filters: CoverageQueryFilters,
1038    pub pagination_applies_to: String,
1039    pub tests: Vec<CoverageSelectedTest>,
1040}
1041
1042#[derive(Debug, Clone, PartialEq, Serialize)]
1043#[serde(untagged)]
1044pub enum CoverageTestData {
1045    Matches(CoverageTestMatchesData),
1046    Detail(CoverageTestDetailData),
1047}
1048
1049#[derive(Debug, Clone, Copy)]
1050pub struct CoverageTestQueryOptions<'a> {
1051    pub run: &'a str,
1052    pub view: CoverageViewId,
1053    pub kind: Option<&'a str>,
1054    pub runner: Option<&'a str>,
1055    pub selector: &'a str,
1056    pub offset: usize,
1057    pub limit: usize,
1058}
1059
1060fn hit_detail(id: &str, metadata: Option<&IndexedHitMetadata>) -> CoverageHitDetail {
1061    match metadata {
1062        Some(metadata) => CoverageHitDetail {
1063            id: id.into(),
1064            obligation: metadata.obligation.clone(),
1065            branch_kind: metadata.branch_kind.clone(),
1066            file: Some(metadata.file.clone()),
1067            line: Some(metadata.line),
1068            column: Some(metadata.column),
1069            label: metadata.label.clone(),
1070            alternative: metadata.alternative.clone(),
1071        },
1072        None => CoverageHitDetail {
1073            id: id.into(),
1074            obligation: "unknown".into(),
1075            branch_kind: None,
1076            file: None,
1077            line: None,
1078            column: None,
1079            label: None,
1080            alternative: None,
1081        },
1082    }
1083}
1084
1085pub fn coverage_test_query(
1086    index: &CoverageIndex<'_>,
1087    options: CoverageTestQueryOptions<'_>,
1088) -> Result<(CoverageTestData, AgentPagination), QueryError> {
1089    if options.limit == 0 {
1090        return Err(QueryError::InvalidPagination);
1091    }
1092    let tests = index.test_details(options.view)?;
1093    let summaries = tests
1094        .iter()
1095        .map(|test| test.summary.clone())
1096        .collect::<Vec<_>>();
1097    let selected = selected_test_ids(&summaries, options.kind, options.runner)?;
1098    let selector = options.selector.to_lowercase();
1099    let matches = tests
1100        .into_iter()
1101        .filter(|test| {
1102            selected
1103                .as_ref()
1104                .is_none_or(|ids| ids.contains(&test.summary.id))
1105        })
1106        .filter(|test| {
1107            test.summary.id == selector || test.summary.name.to_lowercase().contains(&selector)
1108        })
1109        .collect::<Vec<_>>();
1110    if matches.is_empty() {
1111        return Err(QueryError::TestNotFound(options.selector.into()));
1112    }
1113    let filters = query_filters(options.view, options.kind, options.runner);
1114    if matches.len() > 1 {
1115        let total = matches.len();
1116        let page = matches
1117            .into_iter()
1118            .skip(options.offset)
1119            .take(options.limit)
1120            .map(|test| CoverageTestMatch {
1121                id: test.summary.id,
1122                name: test.summary.name,
1123                outcome: test.summary.outcome,
1124                provenance: test.summary.provenance,
1125            })
1126            .collect::<Vec<_>>();
1127        let returned = page.len();
1128        return Ok((
1129            CoverageTestData::Matches(CoverageTestMatchesData {
1130                run: options.run.into(),
1131                filters,
1132                tests: page,
1133            }),
1134            pagination(options.offset, options.limit, returned, total),
1135        ));
1136    }
1137    let test = matches.into_iter().next().expect("one test match");
1138    let metadata = index
1139        .hit_metadata(options.view)?
1140        .into_iter()
1141        .map(|metadata| (metadata.id.clone(), metadata))
1142        .collect::<HashMap<_, _>>();
1143    let decisions = index
1144        .decision_metadata(options.view)?
1145        .into_iter()
1146        .map(|decision| (decision.id.clone(), decision))
1147        .collect::<HashMap<_, _>>();
1148    let all_phases = index
1149        .phase_summaries(options.view)?
1150        .into_iter()
1151        .filter(|phase| phase.test == test.summary.id)
1152        .map(|phase| CoverageTestPhase {
1153            id: phase.id,
1154            kind: phase.kind,
1155            operation: phase.operation,
1156            source: phase.source,
1157            status: phase.status,
1158            caused_by_phase_id: phase.caused_by_phase_id,
1159            lines: phase.lines,
1160            decisions: phase.decisions,
1161        })
1162        .collect::<Vec<_>>();
1163    let totals = CoverageTestTotals {
1164        lines: test.lines.len(),
1165        hits: test.hits.len(),
1166        decisions: test.decisions.len(),
1167        phases: all_phases.len(),
1168    };
1169    let total = totals
1170        .lines
1171        .max(totals.hits)
1172        .max(totals.decisions)
1173        .max(totals.phases);
1174    let lines = test
1175        .lines
1176        .iter()
1177        .skip(options.offset)
1178        .take(options.limit)
1179        .cloned()
1180        .collect::<Vec<_>>();
1181    let hits = test
1182        .hits
1183        .iter()
1184        .skip(options.offset)
1185        .take(options.limit)
1186        .cloned()
1187        .collect::<Vec<_>>();
1188    let hit_details = test
1189        .hits
1190        .iter()
1191        .skip(options.offset)
1192        .take(options.limit)
1193        .map(|id| hit_detail(id, metadata.get(id)))
1194        .collect::<Vec<_>>();
1195    let test_decisions = test
1196        .decisions
1197        .iter()
1198        .skip(options.offset)
1199        .take(options.limit)
1200        .map(|decision| {
1201            Ok(CoverageTestDecision {
1202                id: decision.id.clone(),
1203                vectors: decision.vectors.clone(),
1204                meta: decisions
1205                    .get(&decision.id)
1206                    .cloned()
1207                    .ok_or(QueryError::InvalidRecordSelection)?,
1208            })
1209        })
1210        .collect::<Result<Vec<_>, QueryError>>()?;
1211    let phases = all_phases
1212        .into_iter()
1213        .skip(options.offset)
1214        .take(options.limit)
1215        .collect::<Vec<_>>();
1216    let returned = lines
1217        .len()
1218        .max(hits.len())
1219        .max(test_decisions.len())
1220        .max(phases.len());
1221    Ok((
1222        CoverageTestData::Detail(CoverageTestDetailData {
1223            run: options.run.into(),
1224            filters,
1225            pagination_applies_to:
1226                "lines, hits/hitDetails, decisions, and phases independently within the test".into(),
1227            tests: vec![CoverageSelectedTest {
1228                id: test.summary.id,
1229                name: test.summary.name,
1230                file: test.summary.file,
1231                title: test.summary.title,
1232                retries: test.retries,
1233                attempts: test.attempts,
1234                outcome: test.summary.outcome,
1235                provenance: test.summary.provenance,
1236                role: test.summary.role,
1237                attribution: test.summary.attribution,
1238                hits,
1239                decisions: test_decisions,
1240                lines,
1241                hit_details,
1242                phases,
1243                totals,
1244            }],
1245        }),
1246        pagination(options.offset, options.limit, returned, total),
1247    ))
1248}
1249
1250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1251pub struct CoverageDecisionMatch {
1252    pub id: String,
1253    pub file: String,
1254    pub line: usize,
1255    pub column: usize,
1256    pub source: String,
1257}
1258
1259#[derive(Debug, Clone, PartialEq, Serialize)]
1260#[serde(rename_all = "camelCase")]
1261pub struct CoverageDecisionCondition {
1262    pub index: usize,
1263    pub source: String,
1264    pub covered: bool,
1265    #[serde(skip_serializing_if = "Option::is_none")]
1266    pub assertion_covered: Option<bool>,
1267    #[serde(skip_serializing_if = "Option::is_none")]
1268    pub witness: Option<[crate::coverage_analysis::McdcVector; 2]>,
1269    #[serde(skip_serializing_if = "Option::is_none")]
1270    pub witness_tests: Option<[Vec<String>; 2]>,
1271}
1272
1273#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1274#[serde(rename_all = "camelCase")]
1275pub struct CoverageDecisionTotals {
1276    pub conditions: usize,
1277    pub vector_observations: usize,
1278    pub tests: usize,
1279}
1280
1281#[derive(Debug, Clone, PartialEq, Serialize)]
1282#[serde(rename_all = "camelCase")]
1283pub struct CoverageSelectedDecision {
1284    pub meta: DecisionMeta,
1285    pub executed: bool,
1286    pub covered: bool,
1287    pub vectors: Vec<crate::coverage_analysis::McdcVector>,
1288    pub vector_observations: Vec<crate::coverage_report::VectorObservation>,
1289    pub conditions: Vec<CoverageDecisionCondition>,
1290    pub tests: Vec<String>,
1291    pub confidence: CoverageConfidence,
1292    pub totals: CoverageDecisionTotals,
1293}
1294
1295#[derive(Debug, Clone, PartialEq, Serialize)]
1296pub struct CoverageDecisionMatchesData {
1297    pub run: String,
1298    pub filters: CoverageQueryFilters,
1299    pub decisions: Vec<CoverageDecisionMatch>,
1300}
1301
1302#[derive(Debug, Clone, PartialEq, Serialize)]
1303#[serde(rename_all = "camelCase")]
1304pub struct CoverageDecisionDetailData {
1305    pub run: String,
1306    pub filters: CoverageQueryFilters,
1307    pub pagination_applies_to: String,
1308    pub decisions: Vec<CoverageSelectedDecision>,
1309}
1310
1311#[derive(Debug, Clone, PartialEq, Serialize)]
1312#[serde(untagged)]
1313pub enum CoverageDecisionData {
1314    Matches(CoverageDecisionMatchesData),
1315    Detail(CoverageDecisionDetailData),
1316}
1317
1318#[derive(Debug, Clone, Copy)]
1319pub struct CoverageDecisionQueryOptions<'a> {
1320    pub run: &'a str,
1321    pub view: CoverageViewId,
1322    pub kind: Option<&'a str>,
1323    pub runner: Option<&'a str>,
1324    pub selector: &'a str,
1325    pub offset: usize,
1326    pub limit: usize,
1327}
1328
1329fn selector_location(selector: &str) -> Option<(&str, usize)> {
1330    let (prefix, last) = selector.rsplit_once(':')?;
1331    let last = last.parse::<usize>().ok()?;
1332    if let Some((file, possible_line)) = prefix.rsplit_once(':')
1333        && let Ok(line) = possible_line.parse::<usize>()
1334    {
1335        return Some((file, line));
1336    }
1337    Some((prefix, last))
1338}
1339
1340fn selected_decision(
1341    decision: crate::coverage_report::DecisionResult,
1342    selected: Option<&BTreeSet<String>>,
1343) -> crate::coverage_report::DecisionResult {
1344    let Some(selected) = selected else {
1345        return decision;
1346    };
1347    let vector_observations = decision
1348        .vector_observations
1349        .into_iter()
1350        .filter_map(|mut observation| {
1351            observation.tests.retain(|test| selected.contains(test));
1352            (!observation.tests.is_empty()).then_some(observation)
1353        })
1354        .collect::<Vec<_>>();
1355    let vectors = vector_observations
1356        .iter()
1357        .map(|observation| observation.vector.clone())
1358        .collect::<Vec<_>>();
1359    let conditions = decision
1360        .meta
1361        .conditions
1362        .iter()
1363        .enumerate()
1364        .map(|(index, source)| {
1365            let mut witness = None;
1366            let mut witness_tests = None;
1367            'pairs: for left in 0..vector_observations.len() {
1368                for right in (left + 1)..vector_observations.len() {
1369                    let first = &vector_observations[left];
1370                    let second = &vector_observations[right];
1371                    if is_independence_pair(&first.vector, &second.vector, index) {
1372                        witness = Some([first.vector.clone(), second.vector.clone()]);
1373                        witness_tests = Some([first.tests.clone(), second.tests.clone()]);
1374                        break 'pairs;
1375                    }
1376                }
1377            }
1378            crate::coverage_report::ConditionResult {
1379                index,
1380                source: source.clone(),
1381                covered: witness.is_some(),
1382                assertion_covered: false,
1383                witness,
1384                witness_tests,
1385            }
1386        })
1387        .collect::<Vec<_>>();
1388    crate::coverage_report::DecisionResult {
1389        meta: decision.meta,
1390        executed: !vectors.is_empty(),
1391        covered: conditions.iter().all(|condition| condition.covered),
1392        vectors,
1393        vector_observations,
1394        conditions,
1395        tests: decision
1396            .tests
1397            .into_iter()
1398            .filter(|test| selected.contains(test))
1399            .collect(),
1400        confidence: decision.confidence,
1401    }
1402}
1403
1404/// Reconstruct the exact decision view used by provenance-filtered queries.
1405/// Project filters are applied against the immutable query index.
1406pub fn filtered_decisions(
1407    index: &CoverageIndex<'_>,
1408    view: CoverageViewId,
1409    kind: Option<&str>,
1410    runner: Option<&str>,
1411) -> Result<Vec<crate::coverage_report::DecisionResult>, QueryError> {
1412    let tests = index.test_summaries(view)?;
1413    let selected = selected_test_ids(&tests, kind, runner)?;
1414    index
1415        .decision_details(view)?
1416        .into_iter()
1417        .map(|decision| Ok(selected_decision(decision, selected.as_ref())))
1418        .collect()
1419}
1420
1421pub fn coverage_decision_query(
1422    index: &CoverageIndex<'_>,
1423    options: CoverageDecisionQueryOptions<'_>,
1424) -> Result<(CoverageDecisionData, AgentPagination), QueryError> {
1425    if options.limit == 0 {
1426        return Err(QueryError::InvalidPagination);
1427    }
1428    let tests = index.test_summaries(options.view)?;
1429    let selected = selected_test_ids(&tests, options.kind, options.runner)?;
1430    let decisions = index.decision_details(options.view)?;
1431    let mut matches = decisions
1432        .into_iter()
1433        .filter(|decision| decision.meta.id == options.selector)
1434        .collect::<Vec<_>>();
1435    if matches.is_empty()
1436        && let Some((file, line)) = selector_location(options.selector)
1437    {
1438        matches = index
1439            .decision_details(options.view)?
1440            .into_iter()
1441            .filter(|decision| decision.meta.file == file && decision.meta.line == line)
1442            .collect();
1443    }
1444    if matches.is_empty() {
1445        return Err(QueryError::DecisionNotFound(options.selector.into()));
1446    }
1447    let filters = query_filters(options.view, options.kind, options.runner);
1448    if matches.len() > 1 {
1449        let total = matches.len();
1450        let page = matches
1451            .into_iter()
1452            .skip(options.offset)
1453            .take(options.limit)
1454            .map(|decision| CoverageDecisionMatch {
1455                id: decision.meta.id,
1456                file: decision.meta.file,
1457                line: decision.meta.line,
1458                column: decision.meta.column,
1459                source: decision.meta.source,
1460            })
1461            .collect::<Vec<_>>();
1462        let returned = page.len();
1463        return Ok((
1464            CoverageDecisionData::Matches(CoverageDecisionMatchesData {
1465                run: options.run.into(),
1466                filters,
1467                decisions: page,
1468            }),
1469            pagination(options.offset, options.limit, returned, total),
1470        ));
1471    }
1472    let filtered = selected_decision(
1473        matches.into_iter().next().expect("one decision match"),
1474        selected.as_ref(),
1475    );
1476    let totals = CoverageDecisionTotals {
1477        conditions: filtered.conditions.len(),
1478        vector_observations: filtered.vector_observations.len(),
1479        tests: filtered.tests.len(),
1480    };
1481    let total = totals
1482        .conditions
1483        .max(totals.vector_observations)
1484        .max(totals.tests);
1485    let vector_observations = filtered
1486        .vector_observations
1487        .iter()
1488        .skip(options.offset)
1489        .take(options.limit)
1490        .cloned()
1491        .collect::<Vec<_>>();
1492    let vectors = vector_observations
1493        .iter()
1494        .map(|observation| observation.vector.clone())
1495        .collect::<Vec<_>>();
1496    let conditions = filtered
1497        .conditions
1498        .iter()
1499        .skip(options.offset)
1500        .take(options.limit)
1501        .map(|condition| CoverageDecisionCondition {
1502            index: condition.index,
1503            source: condition.source.clone(),
1504            covered: condition.covered,
1505            assertion_covered: selected.is_none().then_some(condition.assertion_covered),
1506            witness: condition.witness.clone(),
1507            witness_tests: condition.witness_tests.clone(),
1508        })
1509        .collect::<Vec<_>>();
1510    let tests = filtered
1511        .tests
1512        .iter()
1513        .skip(options.offset)
1514        .take(options.limit)
1515        .cloned()
1516        .collect::<Vec<_>>();
1517    let returned = vector_observations
1518        .len()
1519        .max(conditions.len())
1520        .max(tests.len());
1521    Ok((
1522        CoverageDecisionData::Detail(CoverageDecisionDetailData {
1523            run: options.run.into(),
1524            filters,
1525            pagination_applies_to:
1526                "conditions, vectorObservations, and tests independently within each decision"
1527                    .into(),
1528            decisions: vec![CoverageSelectedDecision {
1529                meta: filtered.meta,
1530                executed: filtered.executed,
1531                covered: filtered.covered,
1532                vectors,
1533                vector_observations,
1534                conditions,
1535                tests,
1536                confidence: filtered.confidence,
1537                totals,
1538            }],
1539        }),
1540        pagination(options.offset, options.limit, returned, total),
1541    ))
1542}
1543
1544#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1545pub struct CoverageOtherTest {
1546    pub id: String,
1547    pub name: String,
1548}
1549
1550#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1551#[serde(rename_all = "camelCase")]
1552pub struct CoverageOtherCoverage {
1553    pub covered_elsewhere: bool,
1554    pub kinds: Vec<String>,
1555    pub runners: Vec<String>,
1556    pub tests: Vec<CoverageOtherTest>,
1557}
1558
1559#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1560#[serde(rename_all = "camelCase")]
1561pub struct CoverageLineObligation {
1562    pub kind: String,
1563    pub id: String,
1564    pub line: usize,
1565    pub other_coverage: CoverageOtherCoverage,
1566}
1567
1568#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1569#[serde(rename_all = "camelCase")]
1570pub struct CoveragePointObligation {
1571    pub kind: String,
1572    pub id: String,
1573    pub line: usize,
1574    pub column: usize,
1575    pub source: String,
1576    pub other_coverage: CoverageOtherCoverage,
1577}
1578
1579#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1580#[serde(rename_all = "camelCase")]
1581pub struct CoverageBranchObligation {
1582    pub kind: String,
1583    pub id: String,
1584    pub line: usize,
1585    pub column: usize,
1586    pub source: String,
1587    pub missing: String,
1588    pub other_coverage: CoverageOtherCoverage,
1589}
1590
1591#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1592#[serde(rename_all = "camelCase")]
1593pub struct CoverageMcdcObligation {
1594    pub kind: String,
1595    pub id: String,
1596    pub line: usize,
1597    pub column: usize,
1598    pub decision: String,
1599    pub missing_condition: String,
1600    #[serde(skip)]
1601    pub condition_index: usize,
1602    pub observed_vectors: Vec<String>,
1603    pub other_coverage: CoverageOtherCoverage,
1604}
1605
1606#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1607#[serde(untagged)]
1608pub enum CoverageFileObligation {
1609    Line(CoverageLineObligation),
1610    Point(CoveragePointObligation),
1611    Branch(CoverageBranchObligation),
1612    Mcdc(CoverageMcdcObligation),
1613}
1614
1615impl CoverageFileObligation {
1616    fn line(&self) -> usize {
1617        match self {
1618            Self::Line(value) => value.line,
1619            Self::Point(value) => value.line,
1620            Self::Branch(value) => value.line,
1621            Self::Mcdc(value) => value.line,
1622        }
1623    }
1624
1625    fn kind(&self) -> &str {
1626        match self {
1627            Self::Line(value) => &value.kind,
1628            Self::Point(value) => &value.kind,
1629            Self::Branch(value) => &value.kind,
1630            Self::Mcdc(value) => &value.kind,
1631        }
1632    }
1633}
1634
1635#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1636pub struct CoverageFileTest {
1637    pub id: String,
1638    pub name: String,
1639    pub provenance: TestProvenance,
1640}
1641
1642#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1643pub struct CoverageFileLimitation {
1644    pub id: String,
1645    pub kind: String,
1646    pub file: String,
1647    pub line: usize,
1648    pub column: usize,
1649    pub source: String,
1650    pub reason: String,
1651    pub blocking: bool,
1652    pub effect: String,
1653}
1654
1655#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1656#[serde(rename_all = "camelCase")]
1657pub struct CoverageFileCounts {
1658    /// Measured lines in the file, and how many the selected tests covered:
1659    /// the file's own line coverage, comparable with other tools'.
1660    pub total_lines: usize,
1661    pub covered_lines: usize,
1662    pub uncovered_lines: usize,
1663    pub uncovered_statements: usize,
1664    pub uncovered_functions: usize,
1665    pub missing_branches: usize,
1666    pub missing_mcdc_conditions: usize,
1667    pub measurement_limitations: usize,
1668}
1669
1670#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1671#[serde(rename_all = "camelCase")]
1672pub struct CoverageFileGapLine {
1673    pub line: usize,
1674    pub state: String,
1675    #[serde(skip_serializing_if = "Option::is_none")]
1676    pub source: Option<String>,
1677    pub obligations: Vec<CoverageFileObligation>,
1678    pub limitations: Vec<CoverageFileLimitation>,
1679}
1680
1681#[derive(Debug, Clone, PartialEq, Serialize)]
1682#[serde(rename_all = "camelCase")]
1683pub struct CoverageFileDetailData {
1684    pub run: String,
1685    pub filters: CoverageQueryFilters,
1686    pub file: String,
1687    pub metric: MinimizeMetric,
1688    pub counts: CoverageFileCounts,
1689    pub total_tests: usize,
1690    pub total_obligations: usize,
1691    pub total_gap_lines: usize,
1692    pub gap_lines: Vec<CoverageFileGapLine>,
1693    pub total_limitations: usize,
1694}
1695
1696#[derive(Debug, Clone, Copy)]
1697pub struct CoverageFileDetailOptions<'a> {
1698    pub run: &'a str,
1699    pub view: CoverageViewId,
1700    pub kind: Option<&'a str>,
1701    pub runner: Option<&'a str>,
1702    pub selector: &'a str,
1703    pub metric: MinimizeMetric,
1704    pub offset: usize,
1705    pub limit: usize,
1706}
1707
1708fn other_coverage(
1709    test_ids: &[String],
1710    selected: Option<&BTreeSet<String>>,
1711    tests: &HashMap<String, IndexedTestSummary>,
1712) -> CoverageOtherCoverage {
1713    let covered = selected.map_or_else(Vec::new, |selected| {
1714        test_ids
1715            .iter()
1716            .filter(|id| !selected.contains(*id))
1717            .filter_map(|id| tests.get(id))
1718            .collect::<Vec<_>>()
1719    });
1720    CoverageOtherCoverage {
1721        covered_elsewhere: !covered.is_empty(),
1722        kinds: covered
1723            .iter()
1724            .map(|test| test.provenance.kind.clone())
1725            .collect::<BTreeSet<_>>()
1726            .into_iter()
1727            .collect(),
1728        runners: covered
1729            .iter()
1730            .map(|test| test.provenance.runner.clone())
1731            .collect::<BTreeSet<_>>()
1732            .into_iter()
1733            .collect(),
1734        tests: covered
1735            .into_iter()
1736            .map(|test| CoverageOtherTest {
1737                id: test.id.clone(),
1738                name: test.name.clone(),
1739            })
1740            .collect(),
1741    }
1742}
1743
1744fn vector_text(vector: &crate::coverage_analysis::McdcVector) -> String {
1745    let values = vector
1746        .values
1747        .iter()
1748        .map(|value| match value {
1749            None => '-',
1750            Some(false) => 'F',
1751            Some(true) => 'T',
1752        })
1753        .collect::<String>();
1754    format!("{values} -> {}", if vector.outcome { 'T' } else { 'F' })
1755}
1756
1757fn obligation_matches_metric(obligation: &CoverageFileObligation, metric: MinimizeMetric) -> bool {
1758    metric == MinimizeMetric::All
1759        || matches!(
1760            (obligation.kind(), metric),
1761            ("line", MinimizeMetric::Lines)
1762                | ("statement", MinimizeMetric::Statements)
1763                | ("function", MinimizeMetric::Functions)
1764                | ("branch", MinimizeMetric::Branches)
1765                | ("mcdc", MinimizeMetric::Mcdc)
1766        )
1767}
1768
1769fn compact_source(value: &str) -> Option<String> {
1770    let line = value.lines().find(|line| !line.trim().is_empty())?.trim();
1771    let compact = line.split_whitespace().collect::<Vec<_>>().join(" ");
1772    if compact.is_empty() {
1773        None
1774    } else if compact.chars().count() > 120 {
1775        Some(format!(
1776            "{}…",
1777            compact.chars().take(119).collect::<String>()
1778        ))
1779    } else {
1780        Some(compact)
1781    }
1782}
1783
1784fn obligation_source(obligation: &CoverageFileObligation) -> Option<String> {
1785    match obligation {
1786        CoverageFileObligation::Line(_) => None,
1787        CoverageFileObligation::Point(value) => compact_source(&value.source),
1788        CoverageFileObligation::Branch(value) => compact_source(&value.source),
1789        CoverageFileObligation::Mcdc(value) => compact_source(&value.decision),
1790    }
1791}
1792
1793pub fn coverage_file_detail_query(
1794    index: &CoverageIndex<'_>,
1795    options: CoverageFileDetailOptions<'_>,
1796) -> Result<(CoverageFileDetailData, AgentPagination), QueryError> {
1797    if options.limit == 0 {
1798        return Err(QueryError::InvalidPagination);
1799    }
1800    let test_details = index.test_details(options.view)?;
1801    let test_summaries = test_details
1802        .iter()
1803        .map(|test| test.summary.clone())
1804        .collect::<Vec<_>>();
1805    let selected = selected_test_ids(&test_summaries, options.kind, options.runner)?;
1806    let tests_by_id = test_summaries
1807        .into_iter()
1808        .map(|test| (test.id.clone(), test))
1809        .collect::<HashMap<_, _>>();
1810    let lines = index.lines(options.view)?;
1811    let limitation_records = index.limitations(options.view)?;
1812    let files = lines
1813        .iter()
1814        .map(|line| line.file.as_str())
1815        .chain(
1816            limitation_records
1817                .iter()
1818                .map(|limitation| limitation.file.as_str()),
1819        )
1820        .collect::<BTreeSet<_>>();
1821    let file = if files.contains(options.selector) {
1822        options.selector.to_owned()
1823    } else {
1824        let matches = files
1825            .into_iter()
1826            .filter(|file| file.contains(options.selector))
1827            .collect::<Vec<_>>();
1828        if matches.is_empty() {
1829            return Err(QueryError::SourceNotFound(options.selector.into()));
1830        }
1831        if matches.len() != 1 {
1832            return Err(QueryError::AmbiguousSelector {
1833                selector: options.selector.into(),
1834                matches: matches.into_iter().map(str::to_owned).collect(),
1835            });
1836        }
1837        matches[0].to_owned()
1838    };
1839    let selected_includes = |tests: &[String]| {
1840        selected.as_ref().map_or(!tests.is_empty(), |selected| {
1841            tests.iter().any(|test| selected.contains(test))
1842        })
1843    };
1844    let total_lines = lines
1845        .iter()
1846        .filter(|line| line.measured && line.file == file)
1847        .count();
1848    let uncovered_lines = lines
1849        .iter()
1850        .filter(|line| line.measured && line.file == file && !selected_includes(&line.tests))
1851        .map(|line| {
1852            CoverageFileObligation::Line(CoverageLineObligation {
1853                kind: "line".into(),
1854                id: format!("line:{}:{}", line.file, line.line),
1855                line: line.line,
1856                other_coverage: other_coverage(&line.tests, selected.as_ref(), &tests_by_id),
1857            })
1858        })
1859        .collect::<Vec<_>>();
1860    let metadata = index.hit_metadata(options.view)?;
1861    let statements = metadata
1862        .iter()
1863        .filter(|point| {
1864            point.file == file
1865                && point.obligation == "statement"
1866                && !selected_includes(&point.tests)
1867        })
1868        .map(|point| {
1869            CoverageFileObligation::Point(CoveragePointObligation {
1870                kind: "statement".into(),
1871                id: point.id.clone(),
1872                line: point.line,
1873                column: point.column,
1874                source: point.label.clone().unwrap_or_else(|| point.source.clone()),
1875                other_coverage: other_coverage(&point.tests, selected.as_ref(), &tests_by_id),
1876            })
1877        })
1878        .collect::<Vec<_>>();
1879    let functions = metadata
1880        .iter()
1881        .filter(|point| {
1882            point.file == file && point.obligation == "function" && !selected_includes(&point.tests)
1883        })
1884        .map(|point| {
1885            CoverageFileObligation::Point(CoveragePointObligation {
1886                kind: "function".into(),
1887                id: point.id.clone(),
1888                line: point.line,
1889                column: point.column,
1890                source: point.label.clone().unwrap_or_else(|| point.source.clone()),
1891                other_coverage: other_coverage(&point.tests, selected.as_ref(), &tests_by_id),
1892            })
1893        })
1894        .collect::<Vec<_>>();
1895    let branches = metadata
1896        .iter()
1897        .filter(|branch| {
1898            branch.file == file
1899                && branch.obligation == "branch"
1900                && !selected_includes(&branch.tests)
1901        })
1902        .map(|branch| {
1903            CoverageFileObligation::Branch(CoverageBranchObligation {
1904                kind: "branch".into(),
1905                id: branch.id.clone(),
1906                line: branch.line,
1907                column: branch.column,
1908                source: branch.source.clone(),
1909                missing: branch.alternative.clone().unwrap_or_default(),
1910                other_coverage: other_coverage(&branch.tests, selected.as_ref(), &tests_by_id),
1911            })
1912        })
1913        .collect::<Vec<_>>();
1914    let original_decisions = index.decision_details(options.view)?;
1915    let mut mcdc = Vec::new();
1916    for original in original_decisions
1917        .iter()
1918        .filter(|decision| decision.meta.file == file)
1919    {
1920        let filtered = selected_decision(original.clone(), selected.as_ref());
1921        for condition in filtered
1922            .conditions
1923            .iter()
1924            .filter(|condition| !condition.covered)
1925        {
1926            let original_tests = original.conditions[condition.index]
1927                .witness_tests
1928                .clone()
1929                .unwrap_or_default()
1930                .into_iter()
1931                .flatten()
1932                .collect::<Vec<_>>();
1933            mcdc.push(CoverageFileObligation::Mcdc(CoverageMcdcObligation {
1934                kind: "mcdc".into(),
1935                id: original.meta.id.clone(),
1936                line: original.meta.line,
1937                column: original.meta.column,
1938                decision: original.meta.source.clone(),
1939                missing_condition: condition.source.clone(),
1940                condition_index: condition.index,
1941                observed_vectors: filtered
1942                    .vector_observations
1943                    .iter()
1944                    .map(|observation| vector_text(&observation.vector))
1945                    .collect(),
1946                other_coverage: other_coverage(&original_tests, selected.as_ref(), &tests_by_id),
1947            }));
1948        }
1949    }
1950    let mut obligations = uncovered_lines
1951        .iter()
1952        .chain(statements.iter())
1953        .chain(functions.iter())
1954        .chain(branches.iter())
1955        .chain(mcdc.iter())
1956        .filter(|obligation| obligation_matches_metric(obligation, options.metric))
1957        .cloned()
1958        .collect::<Vec<_>>();
1959    obligations.sort_by(|left, right| {
1960        left.line()
1961            .cmp(&right.line())
1962            .then_with(|| left.kind().cmp(right.kind()))
1963    });
1964    let mut limitations = limitation_records
1965        .into_iter()
1966        .filter(|limitation| limitation.file == file)
1967        .map(|limitation| CoverageFileLimitation {
1968            id: limitation.id,
1969            kind: limitation.kind,
1970            file: limitation.file,
1971            line: limitation.line,
1972            column: limitation.column,
1973            source: limitation.source,
1974            reason: limitation.reason,
1975            blocking: limitation.blocking,
1976            effect: "outside-measured-denominator".into(),
1977        })
1978        .collect::<Vec<_>>();
1979    limitations.sort_by(|left, right| {
1980        left.line
1981            .cmp(&right.line)
1982            .then_with(|| left.column.cmp(&right.column))
1983            .then_with(|| left.id.cmp(&right.id))
1984    });
1985    let total_tests = test_details
1986        .iter()
1987        .filter(|test| {
1988            selected
1989                .as_ref()
1990                .is_none_or(|selected| selected.contains(&test.summary.id))
1991                && test.lines.iter().any(|line| line.file == file)
1992        })
1993        .count();
1994    let total_obligations = obligations.len();
1995    let total_limitations = limitations.len();
1996    let mut grouped =
1997        BTreeMap::<usize, (Vec<CoverageFileObligation>, Vec<CoverageFileLimitation>)>::new();
1998    for obligation in obligations {
1999        grouped
2000            .entry(obligation.line())
2001            .or_default()
2002            .0
2003            .push(obligation);
2004    }
2005    for limitation in limitations {
2006        grouped
2007            .entry(limitation.line)
2008            .or_default()
2009            .1
2010            .push(limitation);
2011    }
2012    let gap_lines = grouped
2013        .into_iter()
2014        .map(|(line, (obligations, limitations))| {
2015            let source = obligations.iter().find_map(obligation_source).or_else(|| {
2016                limitations
2017                    .iter()
2018                    .find_map(|value| compact_source(&value.source))
2019            });
2020            let state = if obligations
2021                .iter()
2022                .any(|value| matches!(value, CoverageFileObligation::Line(_)))
2023            {
2024                "missing"
2025            } else if !obligations.is_empty() {
2026                "part"
2027            } else {
2028                "limited"
2029            };
2030            CoverageFileGapLine {
2031                line,
2032                state: state.into(),
2033                source,
2034                obligations,
2035                limitations,
2036            }
2037        })
2038        .collect::<Vec<_>>();
2039    let total_gap_lines = gap_lines.len();
2040    let selected_gap_lines = gap_lines
2041        .into_iter()
2042        .skip(options.offset)
2043        .take(options.limit)
2044        .collect::<Vec<_>>();
2045    let returned = selected_gap_lines.len();
2046    Ok((
2047        CoverageFileDetailData {
2048            run: options.run.into(),
2049            filters: query_filters(options.view, options.kind, options.runner),
2050            file,
2051            metric: options.metric,
2052            counts: CoverageFileCounts {
2053                total_lines,
2054                covered_lines: total_lines - uncovered_lines.len(),
2055                uncovered_lines: uncovered_lines.len(),
2056                uncovered_statements: statements.len(),
2057                uncovered_functions: functions.len(),
2058                missing_branches: branches.len(),
2059                missing_mcdc_conditions: mcdc.len(),
2060                measurement_limitations: total_limitations,
2061            },
2062            total_tests,
2063            total_obligations,
2064            total_gap_lines,
2065            gap_lines: selected_gap_lines,
2066            total_limitations,
2067        },
2068        pagination(options.offset, options.limit, returned, total_gap_lines),
2069    ))
2070}
2071
2072#[derive(Debug, Clone, PartialEq, Serialize)]
2073pub struct CoverageDiffDelta {
2074    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2075    pub lines: f64,
2076    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2077    pub branches: f64,
2078    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2079    pub mcdc: f64,
2080}
2081
2082#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2083#[serde(rename_all = "camelCase")]
2084pub struct CoverageDiffSide {
2085    pub line_count: usize,
2086    pub branch_count: usize,
2087    pub mcdc_count: usize,
2088    pub lines: Vec<String>,
2089    pub branches: Vec<String>,
2090    pub mcdc: Vec<String>,
2091}
2092
2093#[derive(Debug, Clone, PartialEq, Serialize)]
2094pub struct CoverageDiffData {
2095    pub filters: CoverageQueryFilters,
2096    pub older: String,
2097    pub newer: String,
2098    pub delta: CoverageDiffDelta,
2099    pub gained: CoverageDiffSide,
2100    pub lost: CoverageDiffSide,
2101}
2102
2103#[derive(Debug, Clone, Copy)]
2104pub struct CoverageDiffQueryOptions<'a> {
2105    pub older_run: &'a str,
2106    pub newer_run: &'a str,
2107    pub view: CoverageViewId,
2108    pub kind: Option<&'a str>,
2109    pub runner: Option<&'a str>,
2110    pub offset: usize,
2111    pub limit: usize,
2112}
2113
2114struct DiffSnapshot {
2115    summary: CoverageSummary,
2116    lines: BTreeSet<String>,
2117    branches: HashMap<String, String>,
2118    mcdc: HashMap<String, String>,
2119}
2120
2121fn diff_snapshot(
2122    index: &CoverageIndex<'_>,
2123    view: CoverageViewId,
2124) -> Result<DiffSnapshot, QueryError> {
2125    let summary = index.summary(view)?;
2126    let lines = index
2127        .lines(view)?
2128        .into_iter()
2129        .filter(|line| line.covered)
2130        .map(|line| format!("{}:{}", line.file, line.line))
2131        .collect();
2132    let branches = index
2133        .hit_metadata(view)?
2134        .into_iter()
2135        .filter(|metadata| metadata.obligation == "branch" && !metadata.tests.is_empty())
2136        .map(|metadata| {
2137            let parent = metadata
2138                .parent_id
2139                .ok_or(QueryError::InvalidRecordSelection)?;
2140            Ok((
2141                format!("{parent}:{}", metadata.id),
2142                format!(
2143                    "{}:{} {}",
2144                    metadata.file,
2145                    metadata.line,
2146                    metadata.alternative.unwrap_or_default()
2147                ),
2148            ))
2149        })
2150        .collect::<Result<HashMap<_, _>, QueryError>>()?;
2151    let mcdc = index
2152        .decision_details(view)?
2153        .into_iter()
2154        .flat_map(|decision| {
2155            decision
2156                .conditions
2157                .into_iter()
2158                .filter(|condition| condition.covered)
2159                .map(move |condition| {
2160                    (
2161                        format!("{}:c{}", decision.meta.id, condition.index),
2162                        format!(
2163                            "{}:{} C{} {}",
2164                            decision.meta.file,
2165                            decision.meta.line,
2166                            condition.index + 1,
2167                            condition.source
2168                        ),
2169                    )
2170                })
2171        })
2172        .collect();
2173    Ok(DiffSnapshot {
2174        summary,
2175        lines,
2176        branches,
2177        mcdc,
2178    })
2179}
2180
2181fn js_string_cmp(left: &str, right: &str) -> std::cmp::Ordering {
2182    left.encode_utf16().cmp(right.encode_utf16())
2183}
2184
2185fn rounded_delta(newer: f64, older: f64) -> f64 {
2186    ((newer - older) * 100.0).round() / 100.0
2187}
2188
2189pub fn coverage_diff_query(
2190    older: &CoverageIndex<'_>,
2191    newer: &CoverageIndex<'_>,
2192    options: CoverageDiffQueryOptions<'_>,
2193) -> Result<(CoverageDiffData, AgentPagination), QueryError> {
2194    if options.limit == 0 {
2195        return Err(QueryError::InvalidPagination);
2196    }
2197    let older = diff_snapshot(older, options.view)?;
2198    let newer = diff_snapshot(newer, options.view)?;
2199    let mut gained_lines = newer
2200        .lines
2201        .difference(&older.lines)
2202        .cloned()
2203        .collect::<Vec<_>>();
2204    let mut lost_lines = older
2205        .lines
2206        .difference(&newer.lines)
2207        .cloned()
2208        .collect::<Vec<_>>();
2209    let mut gained_branches = newer
2210        .branches
2211        .iter()
2212        .filter(|(id, _)| !older.branches.contains_key(*id))
2213        .map(|(_, label)| label.clone())
2214        .collect::<Vec<_>>();
2215    let mut lost_branches = older
2216        .branches
2217        .iter()
2218        .filter(|(id, _)| !newer.branches.contains_key(*id))
2219        .map(|(_, label)| label.clone())
2220        .collect::<Vec<_>>();
2221    let mut gained_mcdc = newer
2222        .mcdc
2223        .iter()
2224        .filter(|(id, _)| !older.mcdc.contains_key(*id))
2225        .map(|(_, label)| label.clone())
2226        .collect::<Vec<_>>();
2227    let mut lost_mcdc = older
2228        .mcdc
2229        .iter()
2230        .filter(|(id, _)| !newer.mcdc.contains_key(*id))
2231        .map(|(_, label)| label.clone())
2232        .collect::<Vec<_>>();
2233    for values in [
2234        &mut gained_lines,
2235        &mut lost_lines,
2236        &mut gained_branches,
2237        &mut lost_branches,
2238        &mut gained_mcdc,
2239        &mut lost_mcdc,
2240    ] {
2241        values.sort_by(|left, right| js_string_cmp(left, right));
2242    }
2243    let total = [
2244        gained_lines.len(),
2245        gained_branches.len(),
2246        gained_mcdc.len(),
2247        lost_lines.len(),
2248        lost_branches.len(),
2249        lost_mcdc.len(),
2250    ]
2251    .into_iter()
2252    .max()
2253    .unwrap_or(0);
2254    let page = |values: &[String]| {
2255        values
2256            .iter()
2257            .skip(options.offset)
2258            .take(options.limit)
2259            .cloned()
2260            .collect::<Vec<_>>()
2261    };
2262    let gained = CoverageDiffSide {
2263        line_count: gained_lines.len(),
2264        branch_count: gained_branches.len(),
2265        mcdc_count: gained_mcdc.len(),
2266        lines: page(&gained_lines),
2267        branches: page(&gained_branches),
2268        mcdc: page(&gained_mcdc),
2269    };
2270    let lost = CoverageDiffSide {
2271        line_count: lost_lines.len(),
2272        branch_count: lost_branches.len(),
2273        mcdc_count: lost_mcdc.len(),
2274        lines: page(&lost_lines),
2275        branches: page(&lost_branches),
2276        mcdc: page(&lost_mcdc),
2277    };
2278    let returned = [
2279        gained.lines.len(),
2280        gained.branches.len(),
2281        gained.mcdc.len(),
2282        lost.lines.len(),
2283        lost.branches.len(),
2284        lost.mcdc.len(),
2285    ]
2286    .into_iter()
2287    .max()
2288    .unwrap_or(0);
2289    Ok((
2290        CoverageDiffData {
2291            filters: query_filters(options.view, options.kind, options.runner),
2292            older: options.older_run.into(),
2293            newer: options.newer_run.into(),
2294            delta: CoverageDiffDelta {
2295                lines: rounded_delta(
2296                    newer.summary.lines.percentage,
2297                    older.summary.lines.percentage,
2298                ),
2299                branches: rounded_delta(
2300                    newer.summary.branches.percentage,
2301                    older.summary.branches.percentage,
2302                ),
2303                mcdc: rounded_delta(
2304                    newer.summary.condition_coverage_pct,
2305                    older.summary.condition_coverage_pct,
2306                ),
2307            },
2308            gained,
2309            lost,
2310        },
2311        pagination(options.offset, options.limit, returned, total),
2312    ))
2313}
2314
2315pub fn coverage_scope_query(
2316    index: &CoverageIndex<'_>,
2317    options: CoverageScopeQueryOptions<'_>,
2318) -> Result<(CoverageScopeData, AgentPagination), QueryError> {
2319    if options.limit == 0 {
2320        return Err(QueryError::InvalidPagination);
2321    }
2322    let projection = index.projection(options.view, options.kind, options.runner)?;
2323    let scope = projection
2324        .source_scope
2325        .ok_or(QueryError::ScopeUnavailable)?;
2326    let mut entries = index.scope_entries(options.view)?;
2327    entries.sort_by(|left, right| {
2328        let rank = |status: &str| match status {
2329            "ambiguous" => 0,
2330            "included" => 1,
2331            "excluded" => 2,
2332            _ => 3,
2333        };
2334        rank(&left.status)
2335            .cmp(&rank(&right.status))
2336            .then_with(|| left.file.cmp(&right.file))
2337    });
2338    let total = entries.len();
2339    let selected = entries
2340        .into_iter()
2341        .skip(options.offset)
2342        .take(options.limit)
2343        .collect::<Vec<_>>();
2344    let returned = selected.len();
2345    Ok((
2346        CoverageScopeData {
2347            run: options.run.into(),
2348            filters: CoverageQueryFilters {
2349                outcome: match options.view {
2350                    CoverageViewId::All => "all",
2351                    CoverageViewId::Passed => "passed",
2352                    CoverageViewId::Failed => "failed",
2353                }
2354                .into(),
2355                kind: options.kind.map(str::to_owned),
2356                runner: options.runner.map(str::to_owned),
2357            },
2358            kind: scope.kind,
2359            language: scope.language,
2360            model: scope.model,
2361            mode: scope.mode,
2362            roots: scope.roots,
2363            unit: scope.unit,
2364            measurement_complete: scope.measurement_complete,
2365            counts: ScopeCounts {
2366                included: scope.included,
2367                excluded: scope.excluded,
2368                ambiguous: scope.ambiguous,
2369            },
2370            measurement: projection.measurement,
2371            entries: selected,
2372        },
2373        pagination(options.offset, options.limit, returned, total),
2374    ))
2375}
2376
2377pub fn coverage_summary_query(
2378    index: &CoverageIndex<'_>,
2379    options: CoverageSummaryQueryOptions<'_>,
2380) -> Result<CoverageSummaryData, QueryError> {
2381    let projection = index.projection(options.view, options.kind, options.runner)?;
2382    let mut test_kind_sources = BTreeMap::new();
2383    for test in index.test_summaries(options.view)?.iter().filter(|t| {
2384        t.role == "test"
2385            && options.kind.is_none_or(|k| t.provenance.kind == k)
2386            && options.runner.is_none_or(|r| t.provenance.runner == r)
2387    }) {
2388        *test_kind_sources
2389            .entry(test.provenance.source.clone())
2390            .or_insert(0) += 1;
2391    }
2392    let mut diagnostics = Vec::new();
2393    let mut transport_blockers = 0usize;
2394    if projection.empty_evidence_tests > 0 {
2395        diagnostics.push(CoverageDiagnostic {
2396            code: "TEST_EVIDENCE_MISSING".into(),
2397            severity: "warning".into(),
2398            message: format!(
2399                "{} test(s) recorded assertion phases but attributed zero coverage evidence; possible causes include checks of uninstrumented data, shared setup, lost async context or missing probe transport. First: {}",
2400                projection.empty_evidence_tests,
2401                projection.first_empty_evidence_test.as_deref().unwrap_or("unknown")
2402            ),
2403        });
2404    }
2405    if let Some(transport) = &projection.transport {
2406        if transport.corrupt_records > 0 {
2407            transport_blockers += 1;
2408            diagnostics.push(CoverageDiagnostic {
2409                code: "CORRUPT_EVIDENCE_RECORDS".into(),
2410                severity: "error".into(),
2411                message: format!(
2412                    "{} malformed evidence record(s) in {} file(s) were excluded; coverage is incomplete.",
2413                    transport.corrupt_records, transport.corrupt_files
2414                ),
2415            });
2416        }
2417        if transport.remote_launches > 0
2418            && transport.scoped_server_records == 0
2419            && transport.background_server_records == 0
2420            && projection.attribution.server_explicit == 0
2421            && projection.attribution.server_fallback == 0
2422        {
2423            transport_blockers += 1;
2424            diagnostics.push(CoverageDiagnostic {
2425                code: "REMOTE_SERVER_EVIDENCE_MISSING".into(),
2426                severity: "error".into(),
2427                message: "Remote launches were supervised, but neither scoped nor background server evidence returned. Supercov refuses to describe this measurement as complete.".into(),
2428            });
2429        }
2430    }
2431    let coverage_by_kind = index.dimensions(options.view, CoverageDimension::Kind)?;
2432    let coverage_by_runner = index.dimensions(options.view, CoverageDimension::Runner)?;
2433    let other_e2e_kinds = coverage_by_kind
2434        .iter()
2435        .filter(|dimension| dimension.tests > 0)
2436        .filter_map(|dimension| dimension.kind.clone())
2437        .filter(|kind| kind != "e2e")
2438        .collect::<Vec<_>>();
2439    let e2e_observed = coverage_by_kind
2440        .iter()
2441        .any(|dimension| dimension.tests > 0 && dimension.kind.as_deref() == Some("e2e"));
2442    let e2e_gap_context = if options.kind.is_none()
2443        && options.runner.is_none()
2444        && e2e_observed
2445        && !other_e2e_kinds.is_empty()
2446    {
2447        let mut covered_elsewhere = IndexedGapDimensions {
2448            lines: 0,
2449            statements: 0,
2450            functions: 0,
2451            branches: 0,
2452            mcdc_conditions: 0,
2453        };
2454        let mut uncovered_everywhere = covered_elsewhere.clone();
2455        for gap in index.file_gaps(options.view, Some("e2e"), None)? {
2456            covered_elsewhere.lines += gap.covered_by_other_tests.lines;
2457            covered_elsewhere.statements += gap.covered_by_other_tests.statements;
2458            covered_elsewhere.functions += gap.covered_by_other_tests.functions;
2459            covered_elsewhere.branches += gap.covered_by_other_tests.branches;
2460            covered_elsewhere.mcdc_conditions += gap.covered_by_other_tests.mcdc_conditions;
2461            uncovered_everywhere.lines += gap.uncovered_everywhere.lines;
2462            uncovered_everywhere.statements += gap.uncovered_everywhere.statements;
2463            uncovered_everywhere.functions += gap.uncovered_everywhere.functions;
2464            uncovered_everywhere.branches += gap.uncovered_everywhere.branches;
2465            uncovered_everywhere.mcdc_conditions += gap.uncovered_everywhere.mcdc_conditions;
2466        }
2467        Some(CoverageKindGapContext {
2468            kind: "e2e".into(),
2469            other_kinds: other_e2e_kinds,
2470            covered_elsewhere,
2471            uncovered_everywhere,
2472        })
2473    } else {
2474        None
2475    };
2476    let filters = CoverageQueryFilters {
2477        outcome: match options.view {
2478            CoverageViewId::All => "all",
2479            CoverageViewId::Passed => "passed",
2480            CoverageViewId::Failed => "failed",
2481        }
2482        .into(),
2483        kind: options.kind.map(str::to_owned),
2484        runner: options.runner.map(str::to_owned),
2485    };
2486    let mut measurement = projection.measurement.clone();
2487    if transport_blockers > 0 {
2488        measurement.complete = false;
2489        measurement.limitations = measurement.limitations.saturating_add(transport_blockers);
2490        measurement.blocking = measurement.blocking.saturating_add(transport_blockers);
2491    }
2492    let structurally_complete = projection.summary.coverage_complete && measurement.complete;
2493    let complete = options.view == CoverageViewId::Passed
2494        && options.valid
2495        && !options.stale
2496        && structurally_complete;
2497    Ok(CoverageSummaryData {
2498        test_kind_sources,
2499        assertion_coverage: None,
2500        run: options.run.into(),
2501        command: Vec::new(),
2502        hints: Vec::new(),
2503        workspace: None,
2504        filters,
2505        model: index.model()?,
2506        generated_at: projection.generated_at,
2507        valid: options.valid,
2508        test_exit_code: options.test_exit_code,
2509        stale: options.stale,
2510        stale_reasons: options.stale_reasons,
2511        structurally_complete,
2512        complete,
2513        coverage: projection.summary,
2514        measurement,
2515        coverage_by_kind,
2516        e2e_gap_context,
2517        coverage_by_runner,
2518        attribution: projection.attribution,
2519        test_attribution: {
2520            let mut counts = TestAttributionCounts::default();
2521            for test in index
2522                .test_summaries(options.view)?
2523                .iter()
2524                .filter(|test| test.role == "test")
2525            {
2526                match test.attribution.as_str() {
2527                    crate::coverage_report::ATTRIBUTION_RUN_WIDE => counts.run_wide += 1,
2528                    crate::coverage_report::ATTRIBUTION_PARTIAL => counts.partial += 1,
2529                    _ => counts.exact += 1,
2530                }
2531            }
2532            counts
2533        },
2534        transport: projection.transport,
2535        diagnostics,
2536        confidence: (options.kind.is_none() && options.runner.is_none())
2537            .then_some(projection.confidence),
2538        files_with_gaps: projection.files_with_gaps,
2539        files_with_coverage_gaps: projection.files_with_coverage_gaps,
2540        files_with_measurement_limitations: projection.measurement.files,
2541        tests: projection.tests,
2542        setups: projection.setups,
2543        test_outcomes: projection.test_outcomes,
2544        source_scope: projection.source_scope,
2545    })
2546}
2547
2548#[derive(Debug, Clone, PartialEq)]
2549pub enum CoverageDimensionQueryData {
2550    Kinds(CoverageKindsData),
2551    Runners(CoverageRunnersData),
2552}
2553
2554#[derive(Debug, Clone, PartialEq)]
2555pub enum CoverageFileQueryData {
2556    Files(CoverageFilesData),
2557    Gaps(CoverageGapsData),
2558}
2559
2560#[derive(Debug, Clone, PartialEq)]
2561pub struct CoverageFileQueryResult {
2562    pub data: CoverageFileQueryData,
2563    pub pagination: AgentPagination,
2564}
2565
2566#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2567#[serde(rename_all = "lowercase")]
2568pub enum DecisionSort {
2569    Location,
2570    Missing,
2571}
2572
2573#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2574#[serde(rename_all = "camelCase")]
2575pub struct DecisionGapTotals {
2576    pub decisions: usize,
2577    pub decisions_with_missing_conditions: usize,
2578    pub conditions: usize,
2579    pub missing_conditions: usize,
2580}
2581
2582#[derive(Debug, Clone, PartialEq, Serialize)]
2583#[serde(rename_all = "camelCase")]
2584pub struct CoverageFileDecisionsData {
2585    pub run: String,
2586    pub filters: CoverageQueryFilters,
2587    pub file: String,
2588    pub group: String,
2589    pub sort: DecisionSort,
2590    pub totals: DecisionGapTotals,
2591    pub decisions: Vec<IndexedDecisionGap>,
2592}
2593
2594#[derive(Debug, Clone, Copy)]
2595pub struct CoverageFileDecisionsOptions<'a> {
2596    pub run: &'a str,
2597    pub view: CoverageViewId,
2598    pub kind: Option<&'a str>,
2599    pub runner: Option<&'a str>,
2600    pub file: &'a str,
2601    pub sort: DecisionSort,
2602    pub offset: usize,
2603    pub limit: usize,
2604}
2605
2606#[derive(Debug, Clone)]
2607pub struct CoverageDimensionQueryOptions<'a> {
2608    pub run: &'a str,
2609    pub view: CoverageViewId,
2610    pub dimension: CoverageDimension,
2611    pub filters: CoverageQueryFilters,
2612    pub offset: usize,
2613    pub limit: usize,
2614}
2615
2616pub fn coverage_dimension_query(
2617    index: &CoverageIndex<'_>,
2618    options: CoverageDimensionQueryOptions<'_>,
2619) -> Result<(CoverageDimensionQueryData, AgentPagination), QueryError> {
2620    let CoverageDimensionQueryOptions {
2621        run,
2622        view,
2623        dimension,
2624        filters,
2625        offset,
2626        limit,
2627    } = options;
2628    if limit == 0 {
2629        return Err(QueryError::InvalidPagination);
2630    }
2631    let values = index.dimensions(view, dimension)?;
2632    let total = values.len();
2633    let selected = values
2634        .into_iter()
2635        .skip(offset)
2636        .take(limit)
2637        .collect::<Vec<_>>();
2638    let returned = selected.len();
2639    let data = match dimension {
2640        CoverageDimension::Kind => CoverageDimensionQueryData::Kinds(CoverageKindsData {
2641            run: run.into(),
2642            filters,
2643            kinds: selected,
2644        }),
2645        CoverageDimension::Runner => CoverageDimensionQueryData::Runners(CoverageRunnersData {
2646            run: run.into(),
2647            filters,
2648            runners: selected,
2649        }),
2650    };
2651    Ok((data, pagination(offset, limit, returned, total)))
2652}
2653
2654#[derive(Debug, Clone, Copy)]
2655pub struct CoverageFileQueryOptions<'a> {
2656    pub run: &'a str,
2657    pub view: CoverageViewId,
2658    pub metric: MinimizeMetric,
2659    pub gaps_only: bool,
2660    pub kind: Option<&'a str>,
2661    pub runner: Option<&'a str>,
2662    pub offset: usize,
2663    pub limit: usize,
2664}
2665
2666fn gap_metric_value(gap: &IndexedFileGap, metric: MinimizeMetric) -> usize {
2667    match metric {
2668        MinimizeMetric::All => gap.score,
2669        MinimizeMetric::Lines => gap.uncovered_lines,
2670        MinimizeMetric::Statements => gap.uncovered_statements,
2671        MinimizeMetric::Functions => gap.uncovered_functions,
2672        MinimizeMetric::Branches => gap.missing_branches,
2673        MinimizeMetric::Mcdc => gap.missing_mcdc_conditions,
2674    }
2675}
2676
2677fn has_gap_for_metric(gap: &IndexedFileGap, metric: MinimizeMetric) -> bool {
2678    match metric {
2679        MinimizeMetric::All => {
2680            gap.uncovered_lines > 0
2681                || gap.uncovered_statements > 0
2682                || gap.uncovered_functions > 0
2683                || gap.missing_branches > 0
2684                || gap.missing_mcdc_conditions > 0
2685        }
2686        _ => gap_metric_value(gap, metric) > 0,
2687    }
2688}
2689
2690pub fn coverage_file_decisions_query(
2691    index: &CoverageIndex<'_>,
2692    options: CoverageFileDecisionsOptions<'_>,
2693) -> Result<(CoverageFileDecisionsData, AgentPagination), QueryError> {
2694    if options.limit == 0 {
2695        return Err(QueryError::InvalidPagination);
2696    }
2697    let all = index.decision_gaps(options.view, options.kind, options.runner, options.file)?;
2698    if (options.kind.is_some() || options.runner.is_some()) && all.is_empty() {
2699        // A file with no decisions is valid, so consult the file projection to
2700        // distinguish it from a nonexistent test provenance projection.
2701        if index
2702            .file_gaps(options.view, options.kind, options.runner)?
2703            .is_empty()
2704        {
2705            return Err(QueryError::TestFilterEmpty {
2706                kind: options.kind.map(str::to_owned),
2707                runner: options.runner.map(str::to_owned),
2708            });
2709        }
2710    }
2711    let totals = DecisionGapTotals {
2712        decisions: all.len(),
2713        decisions_with_missing_conditions: all
2714            .iter()
2715            .filter(|decision| decision.missing_conditions > 0)
2716            .count(),
2717        conditions: all.iter().map(|decision| decision.conditions).sum(),
2718        missing_conditions: all.iter().map(|decision| decision.missing_conditions).sum(),
2719    };
2720    let mut missing = all
2721        .into_iter()
2722        .filter(|decision| decision.missing_conditions > 0)
2723        .collect::<Vec<_>>();
2724    missing.sort_by(|left, right| match options.sort {
2725        DecisionSort::Missing => right
2726            .missing_conditions
2727            .cmp(&left.missing_conditions)
2728            .then_with(|| left.line.cmp(&right.line))
2729            .then_with(|| left.column.cmp(&right.column)),
2730        DecisionSort::Location => left
2731            .line
2732            .cmp(&right.line)
2733            .then_with(|| left.column.cmp(&right.column))
2734            .then_with(|| left.id.cmp(&right.id)),
2735    });
2736    let total = missing.len();
2737    let rows = missing
2738        .into_iter()
2739        .skip(options.offset)
2740        .take(options.limit)
2741        .collect::<Vec<_>>();
2742    let returned = rows.len();
2743    let filters = CoverageQueryFilters {
2744        outcome: match options.view {
2745            CoverageViewId::All => "all",
2746            CoverageViewId::Passed => "passed",
2747            CoverageViewId::Failed => "failed",
2748        }
2749        .into(),
2750        kind: options.kind.map(str::to_owned),
2751        runner: options.runner.map(str::to_owned),
2752    };
2753    Ok((
2754        CoverageFileDecisionsData {
2755            run: options.run.into(),
2756            filters,
2757            file: options.file.into(),
2758            group: "decision".into(),
2759            sort: options.sort,
2760            totals,
2761            decisions: rows,
2762        },
2763        pagination(options.offset, options.limit, returned, total),
2764    ))
2765}
2766
2767pub fn coverage_file_query(
2768    index: &CoverageIndex<'_>,
2769    options: CoverageFileQueryOptions<'_>,
2770) -> Result<CoverageFileQueryResult, QueryError> {
2771    let CoverageFileQueryOptions {
2772        run,
2773        view,
2774        metric,
2775        gaps_only,
2776        kind,
2777        runner,
2778        offset,
2779        limit,
2780    } = options;
2781    if limit == 0 {
2782        return Err(QueryError::InvalidPagination);
2783    }
2784    let mut files = index.file_gaps(view, kind, runner)?;
2785    if (kind.is_some() || runner.is_some()) && files.is_empty() {
2786        return Err(QueryError::TestFilterEmpty {
2787            kind: kind.map(str::to_owned),
2788            runner: runner.map(str::to_owned),
2789        });
2790    }
2791    if gaps_only {
2792        files.retain(|gap| has_gap_for_metric(gap, metric) || gap.measurement_limitations > 0);
2793    }
2794    files.sort_by(|left, right| {
2795        gap_metric_value(right, metric)
2796            .cmp(&gap_metric_value(left, metric))
2797            .then_with(|| {
2798                right
2799                    .measurement_limitations
2800                    .cmp(&left.measurement_limitations)
2801            })
2802            .then_with(|| left.file.cmp(&right.file))
2803    });
2804    let total = files.len();
2805    let page = files
2806        .into_iter()
2807        .skip(offset)
2808        .take(limit)
2809        .collect::<Vec<_>>();
2810    let page_info = pagination(offset, limit, page.len(), total);
2811    let filters = CoverageQueryFilters {
2812        outcome: match view {
2813            CoverageViewId::All => "all",
2814            CoverageViewId::Passed => "passed",
2815            CoverageViewId::Failed => "failed",
2816        }
2817        .into(),
2818        kind: kind.map(str::to_owned),
2819        runner: runner.map(str::to_owned),
2820    };
2821    let data = if gaps_only {
2822        CoverageFileQueryData::Gaps(CoverageGapsData {
2823            run: run.into(),
2824            filters,
2825            metric,
2826            gaps: page,
2827        })
2828    } else {
2829        CoverageFileQueryData::Files(CoverageFilesData {
2830            run: run.into(),
2831            filters,
2832            metric,
2833            files: page,
2834        })
2835    };
2836    Ok(CoverageFileQueryResult {
2837        data,
2838        pagination: page_info,
2839    })
2840}
2841
2842#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2843enum ObligationMetric {
2844    Lines,
2845    Statements,
2846    Functions,
2847    Branches,
2848    Mcdc,
2849}
2850
2851impl ObligationMetric {
2852    fn selected(self, metric: MinimizeMetric) -> bool {
2853        metric == MinimizeMetric::All
2854            || matches!(
2855                (self, metric),
2856                (Self::Lines, MinimizeMetric::Lines)
2857                    | (Self::Statements, MinimizeMetric::Statements)
2858                    | (Self::Functions, MinimizeMetric::Functions)
2859                    | (Self::Branches, MinimizeMetric::Branches)
2860                    | (Self::Mcdc, MinimizeMetric::Mcdc)
2861            )
2862    }
2863
2864    fn public(self) -> MinimizeMetric {
2865        match self {
2866            Self::Lines => MinimizeMetric::Lines,
2867            Self::Statements => MinimizeMetric::Statements,
2868            Self::Functions => MinimizeMetric::Functions,
2869            Self::Branches => MinimizeMetric::Branches,
2870            Self::Mcdc => MinimizeMetric::Mcdc,
2871        }
2872    }
2873}
2874
2875#[derive(Clone)]
2876struct Obligation {
2877    id: String,
2878    metric: ObligationMetric,
2879    /// Any one complete option satisfies this obligation.
2880    options: Vec<Vec<String>>,
2881}
2882
2883struct ObligationModel {
2884    obligations: Vec<Obligation>,
2885    setup_by_file: BTreeMap<String, Vec<String>>,
2886    tests_by_file: BTreeMap<String, Vec<String>>,
2887    background: Vec<String>,
2888}
2889
2890impl ObligationModel {
2891    fn expand(&self, selected: &BTreeSet<String>) -> BTreeSet<String> {
2892        let mut expanded = selected.clone();
2893        expanded.extend(self.background.iter().cloned());
2894        for (file, setup_ids) in &self.setup_by_file {
2895            if self
2896                .tests_by_file
2897                .get(file)
2898                .is_some_and(|tests| tests.iter().any(|id| selected.contains(id)))
2899            {
2900                expanded.extend(setup_ids.iter().cloned());
2901            }
2902        }
2903        expanded
2904    }
2905}
2906
2907fn deduplicate_options(options: impl IntoIterator<Item = Vec<String>>) -> Vec<Vec<String>> {
2908    let mut unique = BTreeMap::<String, Vec<String>>::new();
2909    for mut option in options {
2910        option.sort();
2911        option.dedup();
2912        let key = option.join("\0");
2913        unique.entry(key).or_insert(option);
2914    }
2915    unique.into_values().collect()
2916}
2917
2918fn evidence_choices(
2919    ids: &[String],
2920    tests: &HashMap<&str, &crate::coverage_report::TestCoverageResult>,
2921    candidates: &BTreeSet<String>,
2922    tests_by_file: &BTreeMap<String, Vec<String>>,
2923) -> Vec<Vec<String>> {
2924    let mut choices = Vec::new();
2925    for id in ids {
2926        let Some(test) = tests.get(id.as_str()) else {
2927            continue;
2928        };
2929        if test.role == "background" {
2930            choices.push(Vec::new());
2931        } else if test.role == "setup" {
2932            if let Some(file) = &test.file {
2933                choices.extend(
2934                    tests_by_file
2935                        .get(file)
2936                        .into_iter()
2937                        .flatten()
2938                        .map(|candidate| vec![candidate.clone()]),
2939                );
2940            }
2941        } else if candidates.contains(id) {
2942            choices.push(vec![id.clone()]);
2943        }
2944    }
2945    deduplicate_options(choices)
2946}
2947
2948fn build_obligations(view: &CoverageView, candidates: &BTreeSet<String>) -> ObligationModel {
2949    let tests = view
2950        .tests
2951        .iter()
2952        .map(|test| (test.id.as_str(), test))
2953        .collect::<HashMap<_, _>>();
2954    let mut tests_by_file = BTreeMap::<String, Vec<String>>::new();
2955    let mut setup_by_file = BTreeMap::<String, Vec<String>>::new();
2956    let mut background = Vec::new();
2957    for test in &view.tests {
2958        match test.role.as_str() {
2959            "test" if candidates.contains(&test.id) => {
2960                if let Some(file) = &test.file {
2961                    tests_by_file
2962                        .entry(file.clone())
2963                        .or_default()
2964                        .push(test.id.clone());
2965                }
2966            }
2967            "setup" => {
2968                if let Some(file) = &test.file {
2969                    setup_by_file
2970                        .entry(file.clone())
2971                        .or_default()
2972                        .push(test.id.clone());
2973                }
2974            }
2975            "background" => background.push(test.id.clone()),
2976            _ => {}
2977        }
2978    }
2979    let choices = |ids: &[String]| evidence_choices(ids, &tests, candidates, &tests_by_file);
2980    let mut obligations = Vec::new();
2981    let mut unique_lines = BTreeMap::new();
2982    for line in &view.lines {
2983        unique_lines.insert((line.file.as_str(), line.line), line);
2984    }
2985    for ((file, line), result) in unique_lines {
2986        obligations.push(Obligation {
2987            id: format!("line:{file}:{line}"),
2988            metric: ObligationMetric::Lines,
2989            options: choices(&result.tests),
2990        });
2991    }
2992    for point in &view.points {
2993        let (kind, metric) = match point.meta.kind {
2994            crate::coverage_analysis::PointKind::Statement => {
2995                ("statement", ObligationMetric::Statements)
2996            }
2997            crate::coverage_analysis::PointKind::Function => {
2998                ("function", ObligationMetric::Functions)
2999            }
3000        };
3001        obligations.push(Obligation {
3002            id: format!("{kind}:{}", point.meta.id),
3003            metric,
3004            options: choices(&point.tests),
3005        });
3006    }
3007    for branch in &view.branches {
3008        for alternative in &branch.alternatives {
3009            obligations.push(Obligation {
3010                id: format!("branch:{}:{}", branch.meta.id, alternative.id),
3011                metric: ObligationMetric::Branches,
3012                options: choices(&alternative.tests),
3013            });
3014        }
3015    }
3016    for decision in &view.decisions {
3017        for condition in 0..decision.meta.conditions.len() {
3018            let mut options = Vec::new();
3019            for left in 0..decision.vector_observations.len() {
3020                for right in (left + 1)..decision.vector_observations.len() {
3021                    let first = &decision.vector_observations[left];
3022                    let second = &decision.vector_observations[right];
3023                    if !is_independence_pair(&first.vector, &second.vector, condition) {
3024                        continue;
3025                    }
3026                    for first_choice in choices(&first.tests) {
3027                        for second_choice in choices(&second.tests) {
3028                            let mut combined = first_choice.clone();
3029                            combined.extend(second_choice);
3030                            options.push(combined);
3031                        }
3032                    }
3033                }
3034            }
3035            obligations.push(Obligation {
3036                id: format!("mcdc:{}:{condition}", decision.meta.id),
3037                metric: ObligationMetric::Mcdc,
3038                options: deduplicate_options(options),
3039            });
3040        }
3041    }
3042    ObligationModel {
3043        obligations,
3044        setup_by_file,
3045        tests_by_file,
3046        background,
3047    }
3048}
3049
3050fn percentage(metric: ObligationMetric, summary: &CoverageSummary) -> f64 {
3051    match metric {
3052        ObligationMetric::Lines => summary.lines.percentage,
3053        ObligationMetric::Statements => summary.statements.percentage,
3054        ObligationMetric::Functions => summary.functions.percentage,
3055        ObligationMetric::Branches => summary.branches.percentage,
3056        ObligationMetric::Mcdc => summary.condition_coverage_pct,
3057    }
3058}
3059
3060fn obligation_satisfied(obligation: &Obligation, selected: &BTreeSet<String>) -> bool {
3061    obligation
3062        .options
3063        .iter()
3064        .any(|option| option.iter().all(|test| selected.contains(test)))
3065}
3066
3067struct Search<'a> {
3068    obligations: &'a [Obligation],
3069    skip_limits: BTreeMap<ObligationMetric, usize>,
3070    best: BTreeSet<String>,
3071    explored_states: usize,
3072    max_states: usize,
3073    seen: BTreeSet<String>,
3074    candidate_tests: usize,
3075    target: f64,
3076    metric: MinimizeMetric,
3077}
3078
3079impl Search<'_> {
3080    fn visit(
3081        &mut self,
3082        selected: BTreeSet<String>,
3083        skipped: BTreeSet<String>,
3084        skipped_by_metric: BTreeMap<ObligationMetric, usize>,
3085    ) -> Result<(), QueryError> {
3086        self.explored_states += 1;
3087        if self.explored_states > self.max_states {
3088            return Err(QueryError::ComplexityLimit {
3089                candidate_tests: self.candidate_tests,
3090                obligations: self.obligations.len(),
3091                explored_states: self.explored_states,
3092                max_states: self.max_states,
3093                target: self.target,
3094                metric: self.metric,
3095            });
3096        }
3097        if selected.len() >= self.best.len() {
3098            return Ok(());
3099        }
3100        let state_key = format!(
3101            "{}|{}",
3102            selected.iter().cloned().collect::<Vec<_>>().join(","),
3103            skipped.iter().cloned().collect::<Vec<_>>().join(",")
3104        );
3105        if !self.seen.insert(state_key) {
3106            return Ok(());
3107        }
3108        let mut unmet = self
3109            .obligations
3110            .iter()
3111            .filter(|obligation| {
3112                !skipped.contains(&obligation.id) && !obligation_satisfied(obligation, &selected)
3113            })
3114            .collect::<Vec<_>>();
3115        if unmet.is_empty() {
3116            self.best = selected;
3117            return Ok(());
3118        }
3119        unmet.sort_by(|left, right| {
3120            let feasible = |obligation: &Obligation| {
3121                obligation
3122                    .options
3123                    .iter()
3124                    .filter(|option| option.iter().any(|test| !selected.contains(test)))
3125                    .count()
3126            };
3127            feasible(left)
3128                .cmp(&feasible(right))
3129                .then_with(|| left.id.cmp(&right.id))
3130        });
3131        let obligation = unmet[0];
3132        let mut additions = deduplicate_options(obligation.options.iter().filter_map(|option| {
3133            let addition = option
3134                .iter()
3135                .filter(|test| !selected.contains(*test))
3136                .cloned()
3137                .collect::<Vec<_>>();
3138            (!addition.is_empty()).then_some(addition)
3139        }));
3140        additions.sort_by(|left, right| {
3141            left.len()
3142                .cmp(&right.len())
3143                .then_with(|| left.join("\0").cmp(&right.join("\0")))
3144        });
3145        for addition in additions {
3146            if selected.len() + addition.len() >= self.best.len() {
3147                continue;
3148            }
3149            let mut next = selected.clone();
3150            next.extend(addition);
3151            self.visit(next, skipped.clone(), skipped_by_metric.clone())?;
3152        }
3153        let skipped_count = skipped_by_metric
3154            .get(&obligation.metric)
3155            .copied()
3156            .unwrap_or(0);
3157        if skipped_count
3158            < self
3159                .skip_limits
3160                .get(&obligation.metric)
3161                .copied()
3162                .unwrap_or(0)
3163        {
3164            let mut next_skipped = skipped;
3165            next_skipped.insert(obligation.id.clone());
3166            let mut next_counts = skipped_by_metric;
3167            next_counts.insert(obligation.metric, skipped_count + 1);
3168            self.visit(selected, next_skipped, next_counts)?;
3169        }
3170        Ok(())
3171    }
3172}
3173
3174pub fn minimum_test_set(
3175    view: &CoverageView,
3176    target: f64,
3177    metric: MinimizeMetric,
3178    max_states: usize,
3179) -> Result<MinimumTestSetResult, QueryError> {
3180    use crate::coverage_report::coverage_is_complete;
3181    if !target.is_finite() || !(0.0..=100.0).contains(&target) {
3182        return Err(QueryError::InvalidTarget(target));
3183    }
3184    if view.tests.iter().any(|test| {
3185        test.role == "background" && (!test.hits.is_empty() || !test.decisions.is_empty())
3186    }) {
3187        return Err(QueryError::UnattributedEvidence);
3188    }
3189    // And a test whose own coverage nothing recorded cannot be minimized
3190    // around either. Its empty hit set would read as a test that adds nothing
3191    // to any set, so the smallest set would drop it -- and what it covers is
3192    // precisely what nothing knows. The check above catches this whenever the
3193    // run-wide record holds anything, which is nearly always; this catches the
3194    // case where those tests happened to reach nothing measured.
3195    if view
3196        .tests
3197        .iter()
3198        .any(|test| test.role == "test" && !coverage_is_complete(&test.attribution))
3199    {
3200        return Err(QueryError::UnattributedEvidence);
3201    }
3202    let candidate_tests = view
3203        .tests
3204        .iter()
3205        .filter(|test| test.role == "test")
3206        .map(|test| test.id.clone())
3207        .collect::<BTreeSet<_>>();
3208    let model = build_obligations(view, &candidate_tests);
3209    let obligations = model
3210        .obligations
3211        .iter()
3212        .filter(|obligation| obligation.metric.selected(metric))
3213        .cloned()
3214        .collect::<Vec<_>>();
3215    let mut totals = BTreeMap::<ObligationMetric, usize>::new();
3216    for obligation in &obligations {
3217        *totals.entry(obligation.metric).or_default() += 1;
3218    }
3219    let metrics = [
3220        ObligationMetric::Lines,
3221        ObligationMetric::Statements,
3222        ObligationMetric::Functions,
3223        ObligationMetric::Branches,
3224        ObligationMetric::Mcdc,
3225    ]
3226    .into_iter()
3227    .filter(|candidate| candidate.selected(metric))
3228    .collect::<Vec<_>>();
3229    let skip_limits = metrics
3230        .iter()
3231        .map(|selected_metric| {
3232            let total = totals.get(selected_metric).copied().unwrap_or(0);
3233            let required = ((total as f64 * target) / 100.0).ceil() as usize;
3234            (*selected_metric, total.saturating_sub(required))
3235        })
3236        .collect::<BTreeMap<_, _>>();
3237    let full_expanded = model.expand(&candidate_tests);
3238    let full_summary = coverage_summary_for_tests(view, &full_expanded)?;
3239    for selected_metric in &metrics {
3240        let reachable = percentage(*selected_metric, &full_summary);
3241        if reachable + 1e-9 < target {
3242            return Err(QueryError::TargetUnreachable {
3243                metric: selected_metric.public(),
3244                target,
3245                reachable,
3246            });
3247        }
3248    }
3249    let mut search = Search {
3250        obligations: &obligations,
3251        skip_limits,
3252        best: candidate_tests.clone(),
3253        explored_states: 0,
3254        max_states,
3255        seen: BTreeSet::new(),
3256        candidate_tests: candidate_tests.len(),
3257        target,
3258        metric,
3259    };
3260    search.visit(BTreeSet::new(), BTreeSet::new(), BTreeMap::new())?;
3261    let expanded = model.expand(&search.best);
3262    let summary = coverage_summary_for_tests(view, &expanded)?;
3263    Ok(MinimumTestSetResult {
3264        optimal: true,
3265        target,
3266        metric,
3267        selected: search.best.into_iter().collect(),
3268        expanded: expanded.into_iter().collect(),
3269        summary,
3270        explored_states: search.explored_states,
3271    })
3272}
3273
3274#[cfg(test)]
3275mod tests {
3276    use crate::{
3277        coverage_analysis::McdcVector,
3278        coverage_report::{
3279            CoverageManifest, CoverageReport, CoverageReportRequest, DecisionMeta, ExitCodeInput,
3280            RawTestResult, RuntimeSnapshot, TestProvenance, analyze_coverage_results,
3281        },
3282    };
3283
3284    use super::*;
3285
3286    #[test]
3287    fn all_metric_keeps_statement_only_files_in_the_gap_set() {
3288        let gap = IndexedFileGap {
3289            view: CoverageViewId::All,
3290            file: "src/statement.js".into(),
3291            uncovered_lines: 0,
3292            uncovered_statements: 1,
3293            uncovered_functions: 0,
3294            missing_branches: 0,
3295            missing_mcdc_conditions: 0,
3296            measurement_limitations: 0,
3297            limitation_kinds: Vec::new(),
3298            covered_by_other_tests: crate::coverage_index::IndexedGapDimensions {
3299                lines: 0,
3300                statements: 0,
3301                functions: 0,
3302                branches: 0,
3303                mcdc_conditions: 0,
3304            },
3305            uncovered_everywhere: crate::coverage_index::IndexedGapDimensions {
3306                lines: 0,
3307                statements: 1,
3308                functions: 0,
3309                branches: 0,
3310                mcdc_conditions: 0,
3311            },
3312            score: 0,
3313        };
3314        assert!(has_gap_for_metric(&gap, MinimizeMetric::All));
3315        assert!(has_gap_for_metric(&gap, MinimizeMetric::Statements));
3316        assert!(!has_gap_for_metric(&gap, MinimizeMetric::Lines));
3317    }
3318
3319    fn result(id: &str, vector: McdcVector) -> RawTestResult {
3320        RawTestResult {
3321            test_id: Some(id.into()),
3322            scope: None,
3323            test: id.into(),
3324            test_file: Some("tests/permission.test.js".into()),
3325            title: None,
3326            retry: Some(0),
3327            status: Some("passed".into()),
3328            expected_status: None,
3329            flaky: false,
3330            provenance: TestProvenance {
3331                runner: "node:test".into(),
3332                kind: "unit".into(),
3333                project: None,
3334                source: "runner-default".into(),
3335            },
3336            role: "test".into(),
3337            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
3338            phases: Vec::new(),
3339            runtime: vec![RuntimeSnapshot {
3340                decisions: vec![crate::coverage_report::DecisionSnapshot {
3341                    meta: decision(),
3342                    vectors: vec![vector],
3343                }],
3344                hits: Vec::new(),
3345                events: Vec::new(),
3346                logicals: Vec::new(),
3347            }],
3348            browser: Vec::new(),
3349            server: Vec::new(),
3350        }
3351    }
3352
3353    fn decision() -> DecisionMeta {
3354        DecisionMeta {
3355            id: "decision".into(),
3356            file: "src/permission.js".into(),
3357            line: 1,
3358            column: 1,
3359            source: "admin || owner".into(),
3360            conditions: vec!["admin".into(), "owner".into()],
3361            kind: "if".into(),
3362        }
3363    }
3364
3365    fn report(mut results: Vec<RawTestResult>) -> CoverageReport {
3366        analyze_coverage_results(&CoverageReportRequest {
3367            run_id: "run".into(),
3368            manifest: CoverageManifest {
3369                unmeasured: Vec::new(),
3370                decisions: vec![decision()],
3371                points: Vec::new(),
3372                branches: Vec::new(),
3373                limitations: Vec::new(),
3374                scope: None,
3375            },
3376            raw_results: std::mem::take(&mut results),
3377            generated_at: "time".into(),
3378            coverage_model: None,
3379            integrity: None,
3380            test_exit_code: ExitCodeInput::Present(Some(0)),
3381        })
3382        .unwrap()
3383    }
3384
3385    #[test]
3386    fn recomputes_mcdc_witnesses_and_removes_a_redundant_vector() {
3387        let report = report(vec![
3388            result(
3389                "admin",
3390                McdcVector {
3391                    values: vec![Some(true), None],
3392                    outcome: true,
3393                },
3394            ),
3395            result(
3396                "owner",
3397                McdcVector {
3398                    values: vec![Some(false), Some(true)],
3399                    outcome: true,
3400                },
3401            ),
3402            result(
3403                "both",
3404                McdcVector {
3405                    values: vec![Some(true), None],
3406                    outcome: true,
3407                },
3408            ),
3409            result(
3410                "neither",
3411                McdcVector {
3412                    values: vec![Some(false), Some(false)],
3413                    outcome: false,
3414                },
3415            ),
3416        ]);
3417        let minimized = minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 5_000).unwrap();
3418        assert_eq!(minimized.selected.len(), 3);
3419        assert!(minimized.selected.contains(&"owner".into()));
3420        assert!(minimized.selected.contains(&"neither".into()));
3421        assert_eq!(minimized.summary.condition_coverage_pct, 100.0);
3422    }
3423
3424    #[test]
3425    fn a_test_that_belongs_to_no_test_is_not_minimized_away() {
3426        // The first guard catches this whenever the run-wide record holds
3427        // anything, which is nearly always -- so it is the rare run, where
3428        // those tests happened to reach nothing measured, that reaches this
3429        // one. Without it the smallest set would quietly drop a test whose
3430        // coverage is unknown rather than absent, and report the drop as a
3431        // saving.
3432        let mut parallel = result(
3433            "parallel",
3434            McdcVector {
3435                values: vec![Some(false), Some(false)],
3436                outcome: false,
3437            },
3438        );
3439        parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
3440        assert!(matches!(
3441            minimum_test_set(
3442                &report(vec![parallel]).view,
3443                100.0,
3444                MinimizeMetric::Mcdc,
3445                5_000,
3446            ),
3447            Err(QueryError::UnattributedEvidence)
3448        ));
3449    }
3450
3451    #[test]
3452    fn the_summary_counts_each_test_under_the_attribution_it_carries() {
3453        // These three numbers are what the `Attribution` line reads out, and
3454        // nothing -- here or in the integration scripts -- had ever checked
3455        // that a test lands in the bucket it was written with.
3456        let vector = || McdcVector {
3457            values: vec![Some(false), Some(false)],
3458            outcome: false,
3459        };
3460        let mut serial = result("serial", vector());
3461        serial.attribution = crate::coverage_report::ATTRIBUTION_EXACT.into();
3462        let mut parallel = result("parallel", vector());
3463        parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
3464        let mut ruby = result("ruby", vector());
3465        ruby.attribution = crate::coverage_report::ATTRIBUTION_PARTIAL.into();
3466        let mut also_parallel = result("also-parallel", vector());
3467        also_parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
3468        let report = report(vec![serial, parallel, ruby, also_parallel]);
3469
3470        let nonce = std::time::SystemTime::now()
3471            .duration_since(std::time::UNIX_EPOCH)
3472            .unwrap()
3473            .as_nanos();
3474        let root = std::env::temp_dir().join(format!(
3475            "supercov-attribution-counts-{}-{nonce}",
3476            std::process::id()
3477        ));
3478        std::fs::create_dir(&root).unwrap();
3479        let path = root.join("query-index.v1.bin");
3480        let identity = crate::query_index::QueryIndexIdentity {
3481            evidence_sha256: [1; 32],
3482            evidence_bytes: 100,
3483            analysis_sha256: [2; 32],
3484            producer_sha256: [3; 32],
3485            archive_schema_version: 2,
3486        };
3487        crate::query_index::write_query_index(
3488            &crate::coverage_index::coverage_index_sections(&report).unwrap(),
3489            &identity,
3490            &path,
3491        )
3492        .unwrap();
3493        let container = crate::query_index::QueryIndex::open(&path, &identity).unwrap();
3494        let index = crate::coverage_index::CoverageIndex::new(&container).unwrap();
3495
3496        let summary = coverage_summary_query(
3497            &index,
3498            CoverageSummaryQueryOptions {
3499                run: "run",
3500                view: CoverageViewId::All,
3501                kind: None,
3502                runner: None,
3503                valid: true,
3504                test_exit_code: Some(0),
3505                stale: false,
3506                stale_reasons: Vec::new(),
3507            },
3508        )
3509        .unwrap();
3510        assert_eq!(summary.test_attribution.exact, 1);
3511        assert_eq!(summary.test_attribution.run_wide, 2);
3512        assert_eq!(summary.test_attribution.partial, 1);
3513        assert_eq!(summary.test_attribution.total(), 4);
3514        std::fs::remove_dir_all(&root).ok();
3515    }
3516
3517    #[test]
3518    fn refuses_background_evidence() {
3519        let mut aggregate = result(
3520            "aggregate",
3521            McdcVector {
3522                values: vec![Some(false), Some(false)],
3523                outcome: false,
3524            },
3525        );
3526        aggregate.role = "background".into();
3527        assert!(matches!(
3528            minimum_test_set(
3529                &report(vec![aggregate]).view,
3530                100.0,
3531                MinimizeMetric::Mcdc,
3532                5_000,
3533            ),
3534            Err(QueryError::UnattributedEvidence)
3535        ));
3536    }
3537
3538    #[test]
3539    fn bounds_the_exact_search() {
3540        let report = report(vec![
3541            result(
3542                "admin",
3543                McdcVector {
3544                    values: vec![Some(true), None],
3545                    outcome: true,
3546                },
3547            ),
3548            result(
3549                "owner",
3550                McdcVector {
3551                    values: vec![Some(false), Some(true)],
3552                    outcome: true,
3553                },
3554            ),
3555            result(
3556                "neither",
3557                McdcVector {
3558                    values: vec![Some(false), Some(false)],
3559                    outcome: false,
3560                },
3561            ),
3562        ]);
3563        assert!(matches!(
3564            minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 1),
3565            Err(QueryError::ComplexityLimit { .. })
3566        ));
3567    }
3568}