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