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    /// What the tests cover between them. `coverage` less this ran while no
324    /// test was running, which is why a run with one kind of test can show a
325    /// total above that kind's own.
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub coverage_by_tests: Option<CoverageSummary>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub e2e_gap_context: Option<CoverageKindGapContext>,
330    pub coverage_by_runner: Vec<IndexedDimensionCoverage>,
331    pub attribution: crate::coverage_index::IndexedAttribution,
332    /// How completely each test's coverage could be credited to it.
333    ///
334    /// Counted here rather than stored, because the per-test answer is already
335    /// in the run and a total is a read of it. A run written before the field
336    /// existed reports every test as exact, which is what its records say.
337    pub test_attribution: TestAttributionCounts,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub transport: Option<TransportStats>,
340    pub diagnostics: Vec<CoverageDiagnostic>,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub confidence: Option<IndexedSummaryConfidence>,
343    pub files_with_gaps: usize,
344    pub files_with_coverage_gaps: usize,
345    pub files_with_measurement_limitations: usize,
346    pub tests: usize,
347    pub setups: usize,
348    pub test_outcomes: IndexedOutcomeCounts,
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub source_scope: Option<IndexedSourceScope>,
351    /// Where the files discovery could not classify are, and the roots that
352    /// would settle them.
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub unclassified_source: Option<UnclassifiedSource>,
355}
356
357/// One row of the scope overview: the files of one place that share a status
358/// and the reason for it.
359#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
360#[serde(rename_all = "camelCase")]
361pub struct ScopeGroup {
362    pub status: String,
363    pub directory: String,
364    pub reason: String,
365    pub files: usize,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
369#[serde(rename_all = "camelCase")]
370pub struct ScopeDirectory {
371    pub directory: String,
372    pub files: usize,
373}
374
375/// The unclassified files of a run by where they are, and the source roots
376/// to name if they are the project's own code: the roots discovery found
377/// plus each of those places.
378#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
379#[serde(rename_all = "camelCase")]
380pub struct UnclassifiedSource {
381    pub directories: Vec<ScopeDirectory>,
382    pub roots: Vec<String>,
383}
384
385/// Where a reader would look for a file: the directory at the top of the
386/// project, or of its package in a workspace. A file that sits at that top
387/// is in `.`.
388fn scope_directory(entry: &IndexedScopeEntry) -> String {
389    let package = entry.package_root.as_deref().filter(|root| *root != ".");
390    let local = package
391        .and_then(|root| entry.file.strip_prefix(root))
392        .and_then(|rest| rest.strip_prefix('/'))
393        .unwrap_or(&entry.file);
394    match (package, local.split_once('/')) {
395        (Some(package), Some((first, _))) => format!("{package}/{first}"),
396        (Some(package), None) => package.to_owned(),
397        (None, Some((first, _))) => first.to_owned(),
398        (None, None) => ".".to_owned(),
399    }
400}
401
402fn scope_status_rank(status: &str) -> u8 {
403    match status {
404        "ambiguous" => 0,
405        "included" => 1,
406        "excluded" => 2,
407        _ => 3,
408    }
409}
410
411/// Every scope entry, grouped. 845 entries took five pages to read and an
412/// `awk` to see that 79 of 89 unclassified files were one directory.
413pub fn scope_groups(entries: &[IndexedScopeEntry]) -> Vec<ScopeGroup> {
414    let mut groups = BTreeMap::<(u8, String, String, String), usize>::new();
415    for entry in entries {
416        *groups
417            .entry((
418                scope_status_rank(&entry.status),
419                entry.status.clone(),
420                scope_directory(entry),
421                entry.reason.clone(),
422            ))
423            .or_default() += 1;
424    }
425    let mut groups = groups
426        .into_iter()
427        .map(|((rank, status, directory, reason), files)| {
428            (
429                rank,
430                ScopeGroup {
431                    status,
432                    directory,
433                    reason,
434                    files,
435                },
436            )
437        })
438        .collect::<Vec<_>>();
439    groups.sort_by(|(left_rank, left), (right_rank, right)| {
440        left_rank
441            .cmp(right_rank)
442            .then_with(|| right.files.cmp(&left.files))
443            .then_with(|| left.directory.cmp(&right.directory))
444            .then_with(|| left.reason.cmp(&right.reason))
445    });
446    groups.into_iter().map(|(_, group)| group).collect()
447}
448
449pub fn unclassified_source(
450    roots: &[String],
451    entries: &[IndexedScopeEntry],
452) -> Option<UnclassifiedSource> {
453    let mut places = BTreeMap::<String, usize>::new();
454    let mut named = Vec::<(String, usize)>::new();
455    for entry in entries.iter().filter(|entry| entry.status == "ambiguous") {
456        let directory = scope_directory(entry);
457        *places.entry(directory.clone()).or_default() += 1;
458        // A file at the top is named itself; naming `.` would take the tests
459        // and the configuration with it.
460        let root = if entry.file.contains('/') && directory != "." {
461            directory
462        } else {
463            entry.file.clone()
464        };
465        match named.iter_mut().find(|(name, _)| *name == root) {
466            Some((_, files)) => *files += 1,
467            None => named.push((root, 1)),
468        }
469    }
470    if places.is_empty() {
471        return None;
472    }
473    let mut directories = places
474        .into_iter()
475        .map(|(directory, files)| ScopeDirectory { directory, files })
476        .collect::<Vec<_>>();
477    directories.sort_by(|left, right| {
478        right
479            .files
480            .cmp(&left.files)
481            .then_with(|| left.directory.cmp(&right.directory))
482    });
483    named.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
484    let mut all = roots.to_vec();
485    for (root, _) in named {
486        if !all.contains(&root) {
487            all.push(root);
488        }
489    }
490    Some(UnclassifiedSource {
491        directories,
492        roots: all,
493    })
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
497#[serde(rename_all = "camelCase")]
498pub struct CoverageKindGapContext {
499    pub kind: String,
500    pub other_kinds: Vec<String>,
501    pub covered_elsewhere: IndexedGapDimensions,
502    pub uncovered_everywhere: IndexedGapDimensions,
503}
504
505#[derive(Debug, Clone)]
506pub struct CoverageSummaryQueryOptions<'a> {
507    pub run: &'a str,
508    pub view: CoverageViewId,
509    pub kind: Option<&'a str>,
510    pub runner: Option<&'a str>,
511    pub valid: bool,
512    pub test_exit_code: Option<i32>,
513    pub stale: bool,
514    pub stale_reasons: Vec<String>,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
518pub struct ScopeCounts {
519    pub included: usize,
520    pub excluded: usize,
521    pub ambiguous: usize,
522}
523
524#[derive(Debug, Clone, PartialEq, Serialize)]
525#[serde(rename_all = "camelCase")]
526pub struct CoverageScopeData {
527    pub run: String,
528    pub filters: CoverageQueryFilters,
529    pub kind: String,
530    pub language: String,
531    pub model: String,
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub mode: Option<String>,
534    pub roots: Vec<String>,
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub unit: Option<String>,
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub measurement_complete: Option<bool>,
539    pub counts: ScopeCounts,
540    pub measurement: IndexedMeasurement,
541    /// Every entry of the scope, grouped; `entries` is one page of them.
542    pub groups: Vec<ScopeGroup>,
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub unclassified_source: Option<UnclassifiedSource>,
545    pub entries: Vec<IndexedScopeEntry>,
546}
547
548#[derive(Debug, Clone, Copy)]
549pub struct CoverageScopeQueryOptions<'a> {
550    pub run: &'a str,
551    pub view: CoverageViewId,
552    pub kind: Option<&'a str>,
553    pub runner: Option<&'a str>,
554    pub offset: usize,
555    pub limit: usize,
556}
557
558#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
559pub struct CoverageLocation {
560    pub file: String,
561    pub line: usize,
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
565pub struct CoverageCoveringTest {
566    pub id: String,
567    pub name: String,
568    pub provenance: TestProvenance,
569}
570
571#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
572#[serde(rename_all = "camelCase")]
573pub struct CoverageCoveringPhase {
574    pub id: String,
575    pub kind: String,
576    pub operation: String,
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub source: Option<String>,
579    pub test: String,
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub status: Option<String>,
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub caused_by_phase_id: Option<String>,
584}
585
586#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
587#[serde(rename_all = "camelCase")]
588pub struct CoverageAnchor {
589    pub kind: String,
590    pub id: String,
591    pub column: usize,
592    pub covered: bool,
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub source: Option<String>,
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub missing: Option<String>,
597    pub covering_tests: usize,
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub covered_conditions: Option<usize>,
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub conditions: Option<usize>,
602}
603
604#[derive(Debug, Clone, PartialEq, Serialize)]
605#[serde(rename_all = "camelCase")]
606pub struct CoverageCoversLineData {
607    pub run: String,
608    pub filters: CoverageQueryFilters,
609    pub location: CoverageLocation,
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub source: Option<String>,
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub source_origin: Option<String>,
614    pub covered: bool,
615    pub confidence: CoverageConfidence,
616    pub total_tests: usize,
617    pub total_phases: usize,
618    pub total_anchored: usize,
619    pub covered_anchored: usize,
620    pub total_limitations: usize,
621    pub total_remaining: usize,
622    pub tests: Vec<CoverageCoveringTest>,
623    pub phases: Vec<CoverageCoveringPhase>,
624    pub anchored: Vec<CoverageAnchor>,
625    pub limitations: Vec<CoverageFileLimitation>,
626    pub remaining: Vec<CoverageFileObligation>,
627    /// The run's saved assertion assessment for this place, attached by the
628    /// CLI when the run has one.
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub assertions: Option<serde_json::Value>,
631}
632
633#[derive(Debug, Clone, PartialEq, Serialize)]
634#[serde(rename_all = "camelCase")]
635pub struct CoverageCoversAnchorsData {
636    pub run: String,
637    pub filters: CoverageQueryFilters,
638    pub location: CoverageLocation,
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub source: Option<String>,
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub source_origin: Option<String>,
643    pub line_obligation: bool,
644    pub anchored: Vec<CoverageAnchor>,
645    pub total_anchored: usize,
646    pub covered_anchored: usize,
647    pub total_limitations: usize,
648    pub limitations: Vec<CoverageFileLimitation>,
649    pub total_remaining: usize,
650    pub remaining: Vec<CoverageFileObligation>,
651    pub total_tests: usize,
652    pub tests: Vec<CoverageCoveringTest>,
653    /// The run's saved assertion assessment for this place, attached by the
654    /// CLI when the run has one.
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub assertions: Option<serde_json::Value>,
657}
658
659#[derive(Debug, Clone, PartialEq, Serialize)]
660#[serde(untagged)]
661pub enum CoverageCoversData {
662    Line(CoverageCoversLineData),
663    Anchors(CoverageCoversAnchorsData),
664}
665
666#[derive(Debug, Clone, Copy)]
667pub struct CoverageCoversQueryOptions<'a> {
668    pub run: &'a str,
669    pub view: CoverageViewId,
670    pub kind: Option<&'a str>,
671    pub runner: Option<&'a str>,
672    pub file: &'a str,
673    pub line: usize,
674    pub offset: usize,
675    pub limit: usize,
676}
677
678fn query_filters(
679    view: CoverageViewId,
680    kind: Option<&str>,
681    runner: Option<&str>,
682) -> CoverageQueryFilters {
683    CoverageQueryFilters {
684        outcome: match view {
685            CoverageViewId::All => "all",
686            CoverageViewId::Passed => "passed",
687            CoverageViewId::Failed => "failed",
688        }
689        .into(),
690        kind: kind.map(str::to_owned),
691        runner: runner.map(str::to_owned),
692    }
693}
694
695fn selected_test_ids(
696    tests: &[IndexedTestSummary],
697    kind: Option<&str>,
698    runner: Option<&str>,
699) -> Result<Option<BTreeSet<String>>, QueryError> {
700    if kind.is_none() && runner.is_none() {
701        return Ok(None);
702    }
703    let selected = tests
704        .iter()
705        .filter(|test| {
706            kind.is_none_or(|kind| test.provenance.kind == kind)
707                && runner.is_none_or(|runner| test.provenance.runner == runner)
708        })
709        .map(|test| test.id.clone())
710        .collect::<BTreeSet<_>>();
711    if selected.is_empty() {
712        return Err(QueryError::TestFilterEmpty {
713            kind: kind.map(str::to_owned),
714            runner: runner.map(str::to_owned),
715        });
716    }
717    Ok(Some(selected))
718}
719
720pub fn coverage_covers_query(
721    index: &CoverageIndex<'_>,
722    options: CoverageCoversQueryOptions<'_>,
723) -> Result<(CoverageCoversData, AgentPagination), QueryError> {
724    if options.limit == 0 {
725        return Err(QueryError::InvalidPagination);
726    }
727    let tests = index.test_summaries(options.view)?;
728    let selected = selected_test_ids(&tests, options.kind, options.runner)?;
729    let selected_includes = |id: &str| selected.as_ref().is_none_or(|ids| ids.contains(id));
730    let filters = query_filters(options.view, options.kind, options.runner);
731    let location = CoverageLocation {
732        file: options.file.into(),
733        line: options.line,
734    };
735    let metadata = index
736        .hit_metadata(options.view)?
737        .into_iter()
738        .map(|value| (value.id.clone(), value))
739        .collect::<HashMap<_, _>>();
740    let decisions = index
741        .decision_details(options.view)?
742        .into_iter()
743        .map(|value| (value.meta.id.clone(), value))
744        .collect::<HashMap<_, _>>();
745    let anchors = index.anchors(options.view, options.file, options.line)?;
746    let tests_by_id = tests
747        .iter()
748        .map(|test| (test.id.clone(), test.clone()))
749        .collect::<HashMap<_, _>>();
750    let mut anchor_test_ids = anchors
751        .iter()
752        .flat_map(|anchor| anchor.tests.iter())
753        .filter(|id| selected_includes(id))
754        .cloned()
755        .collect::<BTreeSet<_>>();
756    for anchor in anchors.iter().filter(|anchor| anchor.kind == "branch") {
757        anchor_test_ids.extend(
758            metadata
759                .values()
760                .filter(|detail| detail.parent_id.as_deref() == Some(anchor.id.as_str()))
761                .flat_map(|detail| detail.tests.iter())
762                .filter(|id| selected_includes(id))
763                .cloned(),
764        );
765    }
766    let all_anchor_tests = anchor_test_ids
767        .iter()
768        .map(|id| {
769            let test = tests_by_id.get(id);
770            CoverageCoveringTest {
771                id: id.clone(),
772                name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
773                provenance: test
774                    .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
775            }
776        })
777        .collect::<Vec<_>>();
778    let total_anchor_tests = all_anchor_tests.len();
779    let anchor_tests_page = all_anchor_tests
780        .iter()
781        .skip(options.offset)
782        .take(options.limit)
783        .cloned()
784        .collect::<Vec<_>>();
785    let render_anchor = |anchor: crate::coverage_index::IndexedAnchor| {
786        let branch_alternatives = if anchor.kind == "branch" {
787            metadata
788                .values()
789                .filter(|detail| {
790                    detail.obligation == "branch"
791                        && detail.parent_id.as_deref() == Some(anchor.id.as_str())
792                })
793                .collect::<Vec<_>>()
794        } else {
795            Vec::new()
796        };
797        let branch_tests = branch_alternatives
798            .iter()
799            .flat_map(|detail| detail.tests.iter())
800            .filter(|test| selected_includes(test))
801            .cloned()
802            .collect::<BTreeSet<_>>();
803        let covering_tests = if anchor.kind == "branch" {
804            branch_tests.len()
805        } else {
806            anchor
807                .tests
808                .iter()
809                .filter(|test| selected_includes(test))
810                .count()
811        };
812        let detail = metadata.get(&anchor.id);
813        let decision = decisions
814            .get(&anchor.id)
815            .cloned()
816            .map(|decision| selected_decision(decision, selected.as_ref()));
817        let conditions = decision.as_ref().map_or(anchor.conditions, |decision| {
818            Some(decision.conditions.len())
819        });
820        let covered_conditions = decision
821            .as_ref()
822            .map_or(anchor.covered_conditions, |decision| {
823                Some(
824                    decision
825                        .conditions
826                        .iter()
827                        .filter(|condition| condition.covered)
828                        .count(),
829                )
830            });
831        let covered = match anchor.kind.as_str() {
832            "decision" => conditions == covered_conditions,
833            "branch" if !branch_alternatives.is_empty() => branch_alternatives
834                .iter()
835                .all(|detail| detail.tests.iter().any(|test| selected_includes(test))),
836            "branch" => anchor.covered,
837            _ => covering_tests > 0,
838        };
839        let branch_source = branch_alternatives
840            .first()
841            .map(|detail| detail.source.clone());
842        let missing_branch_alternatives = branch_alternatives
843            .iter()
844            .filter(|detail| !detail.tests.iter().any(|test| selected_includes(test)))
845            .filter_map(|detail| detail.alternative.clone())
846            .collect::<Vec<_>>();
847        CoverageAnchor {
848            kind: anchor.kind,
849            id: anchor.id,
850            column: anchor.column,
851            covered,
852            source: decision
853                .as_ref()
854                .map(|decision| decision.meta.source.clone())
855                .or(branch_source)
856                .or_else(|| {
857                    detail.map(|detail| {
858                        detail
859                            .label
860                            .clone()
861                            .unwrap_or_else(|| detail.source.clone())
862                    })
863                })
864                .and_then(|source| compact_source(&source)),
865            missing: if missing_branch_alternatives.is_empty() {
866                detail.and_then(|detail| detail.alternative.clone())
867            } else {
868                Some(missing_branch_alternatives.join("; "))
869            },
870            covering_tests,
871            covered_conditions,
872            conditions,
873        }
874    };
875    let rendered_anchors = anchors.into_iter().map(render_anchor).collect::<Vec<_>>();
876    let total_anchored = rendered_anchors.len();
877    let covered_anchored = rendered_anchors
878        .iter()
879        .filter(|anchor| anchor.covered)
880        .count();
881    let all_limitations = index
882        .limitations(options.view)?
883        .into_iter()
884        .filter(|limitation| limitation.file == options.file && limitation.line == options.line)
885        .map(|limitation| CoverageFileLimitation {
886            id: limitation.id,
887            kind: limitation.kind,
888            file: limitation.file,
889            line: limitation.line,
890            column: limitation.column,
891            source: limitation.source,
892            reason: limitation.reason,
893            blocking: limitation.blocking,
894            effect: "outside-measured-denominator".into(),
895        })
896        .collect::<Vec<_>>();
897    let total_limitations = all_limitations.len();
898    let limitations_page = all_limitations
899        .iter()
900        .skip(options.offset)
901        .take(options.limit)
902        .cloned()
903        .collect::<Vec<_>>();
904    let (file_detail, _) = coverage_file_detail_query(
905        index,
906        CoverageFileDetailOptions {
907            run: options.run,
908            view: options.view,
909            kind: options.kind,
910            runner: options.runner,
911            selector: options.file,
912            metric: MinimizeMetric::All,
913            offset: 0,
914            limit: usize::MAX,
915        },
916    )?;
917    let gap_line = file_detail
918        .gap_lines
919        .into_iter()
920        .find(|gap| gap.line == options.line);
921    // Covered lines are absent from the gap projection, but their anchored
922    // statement/branch/decision metadata still carries the source snippet.
923    // Prefer the exact gap-line text when present and otherwise retain that
924    // anchored source so a successful line query never contradicts itself by
925    // claiming the source is unavailable while printing it below.
926    let source = gap_line
927        .as_ref()
928        .and_then(|gap| gap.source.clone())
929        .or_else(|| {
930            rendered_anchors
931                .iter()
932                .filter_map(|anchor| anchor.source.clone())
933                .min_by_key(|source| source.len())
934        });
935    let all_remaining = gap_line.map_or_else(Vec::new, |gap| gap.obligations);
936    let total_remaining = all_remaining.len();
937    let remaining_page = all_remaining
938        .iter()
939        .skip(options.offset)
940        .take(options.limit)
941        .cloned()
942        .collect::<Vec<_>>();
943    let Some(line) = index.line(options.view, options.file, options.line)? else {
944        let anchored = rendered_anchors
945            .iter()
946            .skip(options.offset)
947            .take(options.limit)
948            .cloned()
949            .collect::<Vec<_>>();
950        let total = total_anchored.max(total_limitations).max(total_remaining);
951        let total = total.max(total_anchor_tests);
952        let returned = anchored
953            .len()
954            .max(limitations_page.len())
955            .max(remaining_page.len())
956            .max(anchor_tests_page.len());
957        return Ok((
958            CoverageCoversData::Anchors(CoverageCoversAnchorsData {
959                run: options.run.into(),
960                filters,
961                location,
962                source,
963                source_origin: None,
964                line_obligation: false,
965                anchored,
966                total_anchored,
967                covered_anchored,
968                total_limitations,
969                limitations: limitations_page,
970                total_remaining,
971                remaining: remaining_page,
972                total_tests: total_anchor_tests,
973                tests: anchor_tests_page,
974                assertions: None,
975            }),
976            pagination(options.offset, options.limit, returned, total),
977        ));
978    };
979    let all_tests = line
980        .tests
981        .iter()
982        .filter(|id| selected_includes(id))
983        .map(|id| {
984            let test = tests_by_id.get(id);
985            CoverageCoveringTest {
986                id: id.clone(),
987                name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
988                provenance: test
989                    .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
990            }
991        })
992        .collect::<Vec<_>>();
993    let phases_by_id = index
994        .phase_summaries(options.view)?
995        .into_iter()
996        .map(|phase| (phase.id.clone(), phase))
997        .collect::<HashMap<_, _>>();
998    let all_phases = line
999        .phases
1000        .iter()
1001        .filter_map(|id| phases_by_id.get(id))
1002        .filter(|phase| selected_includes(&phase.test))
1003        .map(|phase| CoverageCoveringPhase {
1004            id: phase.id.clone(),
1005            kind: phase.kind.clone(),
1006            operation: phase.operation.clone(),
1007            source: phase.source.clone(),
1008            test: phase.test.clone(),
1009            status: phase.status.clone(),
1010            caused_by_phase_id: phase.caused_by_phase_id.clone(),
1011        })
1012        .collect::<Vec<_>>();
1013    let tests_page = all_tests
1014        .iter()
1015        .skip(options.offset)
1016        .take(options.limit)
1017        .cloned()
1018        .collect::<Vec<_>>();
1019    let phases_page = all_phases
1020        .iter()
1021        .skip(options.offset)
1022        .take(options.limit)
1023        .cloned()
1024        .collect::<Vec<_>>();
1025    let anchored_page = rendered_anchors
1026        .into_iter()
1027        .skip(options.offset)
1028        .take(options.limit)
1029        .collect::<Vec<_>>();
1030    let total = all_tests
1031        .len()
1032        .max(all_phases.len())
1033        .max(total_anchored)
1034        .max(total_limitations)
1035        .max(total_remaining);
1036    let returned = tests_page
1037        .len()
1038        .max(phases_page.len())
1039        .max(anchored_page.len())
1040        .max(limitations_page.len())
1041        .max(remaining_page.len());
1042    Ok((
1043        CoverageCoversData::Line(CoverageCoversLineData {
1044            run: options.run.into(),
1045            filters,
1046            location,
1047            source,
1048            source_origin: None,
1049            covered: line.tests.iter().any(|test| selected_includes(test)),
1050            confidence: line.confidence,
1051            total_tests: all_tests.len(),
1052            total_phases: all_phases.len(),
1053            total_anchored,
1054            covered_anchored,
1055            total_limitations,
1056            total_remaining,
1057            tests: tests_page,
1058            phases: phases_page,
1059            anchored: anchored_page,
1060            limitations: limitations_page,
1061            remaining: remaining_page,
1062            assertions: None,
1063        }),
1064        pagination(options.offset, options.limit, returned, total),
1065    ))
1066}
1067
1068#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1069pub struct CoverageTestMatch {
1070    pub id: String,
1071    pub name: String,
1072    pub outcome: String,
1073    pub provenance: TestProvenance,
1074}
1075
1076#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct CoverageHitDetail {
1079    pub id: String,
1080    pub obligation: String,
1081    #[serde(skip_serializing_if = "Option::is_none")]
1082    pub branch_kind: Option<String>,
1083    #[serde(skip_serializing_if = "Option::is_none")]
1084    pub file: Option<String>,
1085    #[serde(skip_serializing_if = "Option::is_none")]
1086    pub line: Option<usize>,
1087    #[serde(skip_serializing_if = "Option::is_none")]
1088    pub column: Option<usize>,
1089    #[serde(skip_serializing_if = "Option::is_none")]
1090    pub label: Option<String>,
1091    #[serde(skip_serializing_if = "Option::is_none")]
1092    pub alternative: Option<String>,
1093}
1094
1095#[derive(Debug, Clone, PartialEq, Serialize)]
1096pub struct CoverageTestDecision {
1097    pub id: String,
1098    pub vectors: Vec<crate::coverage_analysis::McdcVector>,
1099    pub meta: DecisionMeta,
1100}
1101
1102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct CoverageTestPhase {
1105    pub id: String,
1106    pub kind: String,
1107    pub operation: String,
1108    #[serde(skip_serializing_if = "Option::is_none")]
1109    pub source: Option<String>,
1110    #[serde(skip_serializing_if = "Option::is_none")]
1111    pub status: Option<String>,
1112    #[serde(skip_serializing_if = "Option::is_none")]
1113    pub caused_by_phase_id: Option<String>,
1114    pub lines: usize,
1115    pub decisions: usize,
1116}
1117
1118#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1119pub struct CoverageTestTotals {
1120    pub lines: usize,
1121    pub hits: usize,
1122    pub decisions: usize,
1123    pub phases: usize,
1124}
1125
1126#[derive(Debug, Clone, PartialEq, Serialize)]
1127#[serde(rename_all = "camelCase")]
1128pub struct CoverageSelectedTest {
1129    pub id: String,
1130    pub name: String,
1131    #[serde(skip_serializing_if = "Option::is_none")]
1132    pub file: Option<String>,
1133    #[serde(skip_serializing_if = "Option::is_none")]
1134    pub title: Option<String>,
1135    pub retries: Vec<usize>,
1136    pub attempts: Vec<TestAttempt>,
1137    pub outcome: String,
1138    pub provenance: TestProvenance,
1139    pub role: String,
1140    /// `exact`, or `run-wide` when this test ran and nothing can say what it
1141    /// reached. Under `run-wide` the totals below are zero because the test
1142    /// has no coverage of its own, not because it reached nothing.
1143    pub attribution: String,
1144    pub hits: Vec<String>,
1145    pub decisions: Vec<CoverageTestDecision>,
1146    pub lines: Vec<SourceLine>,
1147    pub hit_details: Vec<CoverageHitDetail>,
1148    pub phases: Vec<CoverageTestPhase>,
1149    pub totals: CoverageTestTotals,
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Serialize)]
1153pub struct CoverageTestMatchesData {
1154    pub run: String,
1155    pub filters: CoverageQueryFilters,
1156    pub tests: Vec<CoverageTestMatch>,
1157}
1158
1159#[derive(Debug, Clone, PartialEq, Serialize)]
1160#[serde(rename_all = "camelCase")]
1161pub struct CoverageTestDetailData {
1162    pub run: String,
1163    pub filters: CoverageQueryFilters,
1164    pub pagination_applies_to: String,
1165    pub tests: Vec<CoverageSelectedTest>,
1166}
1167
1168#[derive(Debug, Clone, PartialEq, Serialize)]
1169#[serde(untagged)]
1170pub enum CoverageTestData {
1171    Matches(CoverageTestMatchesData),
1172    Detail(CoverageTestDetailData),
1173}
1174
1175#[derive(Debug, Clone, Copy)]
1176pub struct CoverageTestQueryOptions<'a> {
1177    pub run: &'a str,
1178    pub view: CoverageViewId,
1179    pub kind: Option<&'a str>,
1180    pub runner: Option<&'a str>,
1181    pub selector: &'a str,
1182    pub offset: usize,
1183    pub limit: usize,
1184}
1185
1186fn hit_detail(id: &str, metadata: Option<&IndexedHitMetadata>) -> CoverageHitDetail {
1187    match metadata {
1188        Some(metadata) => CoverageHitDetail {
1189            id: id.into(),
1190            obligation: metadata.obligation.clone(),
1191            branch_kind: metadata.branch_kind.clone(),
1192            file: Some(metadata.file.clone()),
1193            line: Some(metadata.line),
1194            column: Some(metadata.column),
1195            label: metadata.label.clone(),
1196            alternative: metadata.alternative.clone(),
1197        },
1198        None => CoverageHitDetail {
1199            id: id.into(),
1200            obligation: "unknown".into(),
1201            branch_kind: None,
1202            file: None,
1203            line: None,
1204            column: None,
1205            label: None,
1206            alternative: None,
1207        },
1208    }
1209}
1210
1211pub fn coverage_test_query(
1212    index: &CoverageIndex<'_>,
1213    options: CoverageTestQueryOptions<'_>,
1214) -> Result<(CoverageTestData, AgentPagination), QueryError> {
1215    if options.limit == 0 {
1216        return Err(QueryError::InvalidPagination);
1217    }
1218    let tests = index.test_details(options.view)?;
1219    let summaries = tests
1220        .iter()
1221        .map(|test| test.summary.clone())
1222        .collect::<Vec<_>>();
1223    let selected = selected_test_ids(&summaries, options.kind, options.runner)?;
1224    let selector = options.selector.to_lowercase();
1225    let matches = tests
1226        .into_iter()
1227        .filter(|test| {
1228            selected
1229                .as_ref()
1230                .is_none_or(|ids| ids.contains(&test.summary.id))
1231        })
1232        .filter(|test| {
1233            test.summary.id == selector || test.summary.name.to_lowercase().contains(&selector)
1234        })
1235        .collect::<Vec<_>>();
1236    if matches.is_empty() {
1237        return Err(QueryError::TestNotFound(options.selector.into()));
1238    }
1239    let filters = query_filters(options.view, options.kind, options.runner);
1240    if matches.len() > 1 {
1241        let total = matches.len();
1242        let page = matches
1243            .into_iter()
1244            .skip(options.offset)
1245            .take(options.limit)
1246            .map(|test| CoverageTestMatch {
1247                id: test.summary.id,
1248                name: test.summary.name,
1249                outcome: test.summary.outcome,
1250                provenance: test.summary.provenance,
1251            })
1252            .collect::<Vec<_>>();
1253        let returned = page.len();
1254        return Ok((
1255            CoverageTestData::Matches(CoverageTestMatchesData {
1256                run: options.run.into(),
1257                filters,
1258                tests: page,
1259            }),
1260            pagination(options.offset, options.limit, returned, total),
1261        ));
1262    }
1263    let test = matches.into_iter().next().expect("one test match");
1264    let metadata = index
1265        .hit_metadata(options.view)?
1266        .into_iter()
1267        .map(|metadata| (metadata.id.clone(), metadata))
1268        .collect::<HashMap<_, _>>();
1269    let decisions = index
1270        .decision_metadata(options.view)?
1271        .into_iter()
1272        .map(|decision| (decision.id.clone(), decision))
1273        .collect::<HashMap<_, _>>();
1274    let all_phases = index
1275        .phase_summaries(options.view)?
1276        .into_iter()
1277        .filter(|phase| phase.test == test.summary.id)
1278        .map(|phase| CoverageTestPhase {
1279            id: phase.id,
1280            kind: phase.kind,
1281            operation: phase.operation,
1282            source: phase.source,
1283            status: phase.status,
1284            caused_by_phase_id: phase.caused_by_phase_id,
1285            lines: phase.lines,
1286            decisions: phase.decisions,
1287        })
1288        .collect::<Vec<_>>();
1289    let totals = CoverageTestTotals {
1290        lines: test.lines.len(),
1291        hits: test.hits.len(),
1292        decisions: test.decisions.len(),
1293        phases: all_phases.len(),
1294    };
1295    let total = totals
1296        .lines
1297        .max(totals.hits)
1298        .max(totals.decisions)
1299        .max(totals.phases);
1300    let lines = test
1301        .lines
1302        .iter()
1303        .skip(options.offset)
1304        .take(options.limit)
1305        .cloned()
1306        .collect::<Vec<_>>();
1307    let hits = test
1308        .hits
1309        .iter()
1310        .skip(options.offset)
1311        .take(options.limit)
1312        .cloned()
1313        .collect::<Vec<_>>();
1314    let hit_details = test
1315        .hits
1316        .iter()
1317        .skip(options.offset)
1318        .take(options.limit)
1319        .map(|id| hit_detail(id, metadata.get(id)))
1320        .collect::<Vec<_>>();
1321    let test_decisions = test
1322        .decisions
1323        .iter()
1324        .skip(options.offset)
1325        .take(options.limit)
1326        .map(|decision| {
1327            Ok(CoverageTestDecision {
1328                id: decision.id.clone(),
1329                vectors: decision.vectors.clone(),
1330                meta: decisions
1331                    .get(&decision.id)
1332                    .cloned()
1333                    .ok_or(QueryError::InvalidRecordSelection)?,
1334            })
1335        })
1336        .collect::<Result<Vec<_>, QueryError>>()?;
1337    let phases = all_phases
1338        .into_iter()
1339        .skip(options.offset)
1340        .take(options.limit)
1341        .collect::<Vec<_>>();
1342    let returned = lines
1343        .len()
1344        .max(hits.len())
1345        .max(test_decisions.len())
1346        .max(phases.len());
1347    Ok((
1348        CoverageTestData::Detail(CoverageTestDetailData {
1349            run: options.run.into(),
1350            filters,
1351            pagination_applies_to:
1352                "lines, hits/hitDetails, decisions, and phases independently within the test".into(),
1353            tests: vec![CoverageSelectedTest {
1354                id: test.summary.id,
1355                name: test.summary.name,
1356                file: test.summary.file,
1357                title: test.summary.title,
1358                retries: test.retries,
1359                attempts: test.attempts,
1360                outcome: test.summary.outcome,
1361                provenance: test.summary.provenance,
1362                role: test.summary.role,
1363                attribution: test.summary.attribution,
1364                hits,
1365                decisions: test_decisions,
1366                lines,
1367                hit_details,
1368                phases,
1369                totals,
1370            }],
1371        }),
1372        pagination(options.offset, options.limit, returned, total),
1373    ))
1374}
1375
1376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1377pub struct CoverageDecisionMatch {
1378    pub id: String,
1379    pub file: String,
1380    pub line: usize,
1381    pub column: usize,
1382    pub source: String,
1383}
1384
1385#[derive(Debug, Clone, PartialEq, Serialize)]
1386#[serde(rename_all = "camelCase")]
1387pub struct CoverageDecisionCondition {
1388    pub index: usize,
1389    pub source: String,
1390    pub covered: bool,
1391    #[serde(skip_serializing_if = "Option::is_none")]
1392    pub assertion_covered: Option<bool>,
1393    #[serde(skip_serializing_if = "Option::is_none")]
1394    pub witness: Option<[crate::coverage_analysis::McdcVector; 2]>,
1395    #[serde(skip_serializing_if = "Option::is_none")]
1396    pub witness_tests: Option<[Vec<Id>; 2]>,
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1400#[serde(rename_all = "camelCase")]
1401pub struct CoverageDecisionTotals {
1402    pub conditions: usize,
1403    pub vector_observations: usize,
1404    pub tests: usize,
1405}
1406
1407#[derive(Debug, Clone, PartialEq, Serialize)]
1408#[serde(rename_all = "camelCase")]
1409pub struct CoverageSelectedDecision {
1410    pub meta: DecisionMeta,
1411    pub executed: bool,
1412    pub covered: bool,
1413    pub vectors: Vec<crate::coverage_analysis::McdcVector>,
1414    pub vector_observations: Vec<crate::coverage_report::VectorObservation>,
1415    pub conditions: Vec<CoverageDecisionCondition>,
1416    pub tests: Vec<Id>,
1417    pub confidence: CoverageConfidence,
1418    pub totals: CoverageDecisionTotals,
1419}
1420
1421#[derive(Debug, Clone, PartialEq, Serialize)]
1422pub struct CoverageDecisionMatchesData {
1423    pub run: String,
1424    pub filters: CoverageQueryFilters,
1425    pub decisions: Vec<CoverageDecisionMatch>,
1426}
1427
1428#[derive(Debug, Clone, PartialEq, Serialize)]
1429#[serde(rename_all = "camelCase")]
1430pub struct CoverageDecisionDetailData {
1431    pub run: String,
1432    pub filters: CoverageQueryFilters,
1433    pub pagination_applies_to: String,
1434    pub decisions: Vec<CoverageSelectedDecision>,
1435}
1436
1437#[derive(Debug, Clone, PartialEq, Serialize)]
1438#[serde(untagged)]
1439pub enum CoverageDecisionData {
1440    Matches(CoverageDecisionMatchesData),
1441    Detail(CoverageDecisionDetailData),
1442}
1443
1444#[derive(Debug, Clone, Copy)]
1445pub struct CoverageDecisionQueryOptions<'a> {
1446    pub run: &'a str,
1447    pub view: CoverageViewId,
1448    pub kind: Option<&'a str>,
1449    pub runner: Option<&'a str>,
1450    pub selector: &'a str,
1451    pub offset: usize,
1452    pub limit: usize,
1453}
1454
1455fn selector_location(selector: &str) -> Option<(&str, usize)> {
1456    let (prefix, last) = selector.rsplit_once(':')?;
1457    let last = last.parse::<usize>().ok()?;
1458    if let Some((file, possible_line)) = prefix.rsplit_once(':')
1459        && let Ok(line) = possible_line.parse::<usize>()
1460    {
1461        return Some((file, line));
1462    }
1463    Some((prefix, last))
1464}
1465
1466fn selected_decision(
1467    decision: crate::coverage_report::DecisionResult,
1468    selected: Option<&BTreeSet<String>>,
1469) -> crate::coverage_report::DecisionResult {
1470    let Some(selected) = selected else {
1471        return decision;
1472    };
1473    let vector_observations = decision
1474        .vector_observations
1475        .into_iter()
1476        .filter_map(|mut observation| {
1477            observation
1478                .tests
1479                .retain(|test| selected.contains(test.as_str()));
1480            (!observation.tests.is_empty()).then_some(observation)
1481        })
1482        .collect::<Vec<_>>();
1483    let vectors = vector_observations
1484        .iter()
1485        .map(|observation| observation.vector.clone())
1486        .collect::<Vec<_>>();
1487    let conditions = decision
1488        .meta
1489        .conditions
1490        .iter()
1491        .enumerate()
1492        .map(|(index, source)| {
1493            let mut witness = None;
1494            let mut witness_tests = None;
1495            'pairs: for left in 0..vector_observations.len() {
1496                for right in (left + 1)..vector_observations.len() {
1497                    let first = &vector_observations[left];
1498                    let second = &vector_observations[right];
1499                    if is_independence_pair(&first.vector, &second.vector, index) {
1500                        witness = Some([first.vector.clone(), second.vector.clone()]);
1501                        witness_tests = Some([first.tests.clone(), second.tests.clone()]);
1502                        break 'pairs;
1503                    }
1504                }
1505            }
1506            crate::coverage_report::ConditionResult {
1507                index,
1508                source: source.clone(),
1509                covered: witness.is_some(),
1510                assertion_covered: false,
1511                witness,
1512                witness_tests,
1513            }
1514        })
1515        .collect::<Vec<_>>();
1516    crate::coverage_report::DecisionResult {
1517        meta: decision.meta,
1518        executed: !vectors.is_empty(),
1519        covered: conditions.iter().all(|condition| condition.covered),
1520        vectors,
1521        vector_observations,
1522        conditions,
1523        tests: decision
1524            .tests
1525            .into_iter()
1526            .filter(|test| selected.contains(test.as_str()))
1527            .collect(),
1528        confidence: decision.confidence,
1529    }
1530}
1531
1532pub fn coverage_decision_query(
1533    index: &CoverageIndex<'_>,
1534    options: CoverageDecisionQueryOptions<'_>,
1535) -> Result<(CoverageDecisionData, AgentPagination), QueryError> {
1536    if options.limit == 0 {
1537        return Err(QueryError::InvalidPagination);
1538    }
1539    let tests = index.test_summaries(options.view)?;
1540    let selected = selected_test_ids(&tests, options.kind, options.runner)?;
1541    let decisions = index.decision_details(options.view)?;
1542    let mut matches = decisions
1543        .into_iter()
1544        .filter(|decision| decision.meta.id == options.selector)
1545        .collect::<Vec<_>>();
1546    if matches.is_empty()
1547        && let Some((file, line)) = selector_location(options.selector)
1548    {
1549        matches = index
1550            .decision_details(options.view)?
1551            .into_iter()
1552            .filter(|decision| decision.meta.file == file && decision.meta.line == line)
1553            .collect();
1554    }
1555    if matches.is_empty() {
1556        return Err(QueryError::DecisionNotFound(options.selector.into()));
1557    }
1558    let filters = query_filters(options.view, options.kind, options.runner);
1559    if matches.len() > 1 {
1560        let total = matches.len();
1561        let page = matches
1562            .into_iter()
1563            .skip(options.offset)
1564            .take(options.limit)
1565            .map(|decision| CoverageDecisionMatch {
1566                id: decision.meta.id,
1567                file: decision.meta.file,
1568                line: decision.meta.line,
1569                column: decision.meta.column,
1570                source: decision.meta.source,
1571            })
1572            .collect::<Vec<_>>();
1573        let returned = page.len();
1574        return Ok((
1575            CoverageDecisionData::Matches(CoverageDecisionMatchesData {
1576                run: options.run.into(),
1577                filters,
1578                decisions: page,
1579            }),
1580            pagination(options.offset, options.limit, returned, total),
1581        ));
1582    }
1583    let filtered = selected_decision(
1584        matches.into_iter().next().expect("one decision match"),
1585        selected.as_ref(),
1586    );
1587    let totals = CoverageDecisionTotals {
1588        conditions: filtered.conditions.len(),
1589        vector_observations: filtered.vector_observations.len(),
1590        tests: filtered.tests.len(),
1591    };
1592    let total = totals
1593        .conditions
1594        .max(totals.vector_observations)
1595        .max(totals.tests);
1596    let vector_observations = filtered
1597        .vector_observations
1598        .iter()
1599        .skip(options.offset)
1600        .take(options.limit)
1601        .cloned()
1602        .collect::<Vec<_>>();
1603    let vectors = vector_observations
1604        .iter()
1605        .map(|observation| observation.vector.clone())
1606        .collect::<Vec<_>>();
1607    let conditions = filtered
1608        .conditions
1609        .iter()
1610        .skip(options.offset)
1611        .take(options.limit)
1612        .map(|condition| CoverageDecisionCondition {
1613            index: condition.index,
1614            source: condition.source.clone(),
1615            covered: condition.covered,
1616            assertion_covered: selected.is_none().then_some(condition.assertion_covered),
1617            witness: condition.witness.clone(),
1618            witness_tests: condition.witness_tests.clone(),
1619        })
1620        .collect::<Vec<_>>();
1621    let tests = filtered
1622        .tests
1623        .iter()
1624        .skip(options.offset)
1625        .take(options.limit)
1626        .cloned()
1627        .collect::<Vec<_>>();
1628    let returned = vector_observations
1629        .len()
1630        .max(conditions.len())
1631        .max(tests.len());
1632    Ok((
1633        CoverageDecisionData::Detail(CoverageDecisionDetailData {
1634            run: options.run.into(),
1635            filters,
1636            pagination_applies_to:
1637                "conditions, vectorObservations, and tests independently within each decision"
1638                    .into(),
1639            decisions: vec![CoverageSelectedDecision {
1640                meta: filtered.meta,
1641                executed: filtered.executed,
1642                covered: filtered.covered,
1643                vectors,
1644                vector_observations,
1645                conditions,
1646                tests,
1647                confidence: filtered.confidence,
1648                totals,
1649            }],
1650        }),
1651        pagination(options.offset, options.limit, returned, total),
1652    ))
1653}
1654
1655#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1656pub struct CoverageOtherTest {
1657    pub id: String,
1658    pub name: String,
1659}
1660
1661#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1662#[serde(rename_all = "camelCase")]
1663pub struct CoverageOtherCoverage {
1664    pub covered_elsewhere: bool,
1665    pub kinds: Vec<String>,
1666    pub runners: Vec<String>,
1667    pub tests: Vec<CoverageOtherTest>,
1668}
1669
1670#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1671#[serde(rename_all = "camelCase")]
1672pub struct CoverageLineObligation {
1673    pub kind: String,
1674    pub id: String,
1675    pub line: usize,
1676    pub other_coverage: CoverageOtherCoverage,
1677}
1678
1679#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1680#[serde(rename_all = "camelCase")]
1681pub struct CoveragePointObligation {
1682    pub kind: String,
1683    pub id: String,
1684    pub line: usize,
1685    pub column: usize,
1686    pub source: String,
1687    pub other_coverage: CoverageOtherCoverage,
1688}
1689
1690#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1691#[serde(rename_all = "camelCase")]
1692pub struct CoverageBranchObligation {
1693    pub kind: String,
1694    pub id: String,
1695    pub line: usize,
1696    pub column: usize,
1697    pub source: String,
1698    pub missing: String,
1699    pub other_coverage: CoverageOtherCoverage,
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct CoverageMcdcObligation {
1705    pub kind: String,
1706    pub id: String,
1707    pub line: usize,
1708    pub column: usize,
1709    pub decision: String,
1710    pub missing_condition: String,
1711    #[serde(skip)]
1712    pub condition_index: usize,
1713    pub observed_vectors: Vec<String>,
1714    pub other_coverage: CoverageOtherCoverage,
1715}
1716
1717#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1718#[serde(untagged)]
1719pub enum CoverageFileObligation {
1720    Line(CoverageLineObligation),
1721    Point(CoveragePointObligation),
1722    Branch(CoverageBranchObligation),
1723    Mcdc(CoverageMcdcObligation),
1724}
1725
1726impl CoverageFileObligation {
1727    fn line(&self) -> usize {
1728        match self {
1729            Self::Line(value) => value.line,
1730            Self::Point(value) => value.line,
1731            Self::Branch(value) => value.line,
1732            Self::Mcdc(value) => value.line,
1733        }
1734    }
1735
1736    fn kind(&self) -> &str {
1737        match self {
1738            Self::Line(value) => &value.kind,
1739            Self::Point(value) => &value.kind,
1740            Self::Branch(value) => &value.kind,
1741            Self::Mcdc(value) => &value.kind,
1742        }
1743    }
1744}
1745
1746#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1747pub struct CoverageFileLimitation {
1748    pub id: String,
1749    pub kind: String,
1750    pub file: String,
1751    pub line: usize,
1752    pub column: usize,
1753    pub source: String,
1754    pub reason: String,
1755    pub blocking: bool,
1756    pub effect: String,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1760#[serde(rename_all = "camelCase")]
1761pub struct CoverageFileCounts {
1762    /// Measured lines in the file, and how many the selected tests covered:
1763    /// the file's own line coverage, comparable with other tools'.
1764    pub total_lines: usize,
1765    pub covered_lines: usize,
1766    pub uncovered_lines: usize,
1767    pub uncovered_statements: usize,
1768    pub uncovered_functions: usize,
1769    pub missing_branches: usize,
1770    pub missing_mcdc_conditions: usize,
1771    pub measurement_limitations: usize,
1772}
1773
1774#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1775#[serde(rename_all = "camelCase")]
1776pub struct CoverageFileGapLine {
1777    pub line: usize,
1778    pub state: String,
1779    #[serde(skip_serializing_if = "Option::is_none")]
1780    pub source: Option<String>,
1781    pub obligations: Vec<CoverageFileObligation>,
1782    pub limitations: Vec<CoverageFileLimitation>,
1783}
1784
1785#[derive(Debug, Clone, PartialEq, Serialize)]
1786#[serde(rename_all = "camelCase")]
1787pub struct CoverageFileDetailData {
1788    pub run: String,
1789    pub filters: CoverageQueryFilters,
1790    pub file: String,
1791    pub metric: MinimizeMetric,
1792    pub counts: CoverageFileCounts,
1793    pub total_tests: usize,
1794    pub total_obligations: usize,
1795    pub total_gap_lines: usize,
1796    pub gap_lines: Vec<CoverageFileGapLine>,
1797    pub total_limitations: usize,
1798    /// The run's saved assertion assessment for this place, attached by the
1799    /// CLI when the run has one.
1800    #[serde(skip_serializing_if = "Option::is_none")]
1801    pub assertions: Option<serde_json::Value>,
1802}
1803
1804#[derive(Debug, Clone, Copy)]
1805pub struct CoverageFileDetailOptions<'a> {
1806    pub run: &'a str,
1807    pub view: CoverageViewId,
1808    pub kind: Option<&'a str>,
1809    pub runner: Option<&'a str>,
1810    pub selector: &'a str,
1811    pub metric: MinimizeMetric,
1812    pub offset: usize,
1813    pub limit: usize,
1814}
1815
1816fn other_coverage<T: AsRef<str>>(
1817    test_ids: &[T],
1818    selected: Option<&BTreeSet<String>>,
1819    tests: &HashMap<String, IndexedTestSummary>,
1820) -> CoverageOtherCoverage {
1821    let covered = selected.map_or_else(Vec::new, |selected| {
1822        test_ids
1823            .iter()
1824            .filter(|id| !selected.contains(id.as_ref()))
1825            .filter_map(|id| tests.get(id.as_ref()))
1826            .collect::<Vec<_>>()
1827    });
1828    CoverageOtherCoverage {
1829        covered_elsewhere: !covered.is_empty(),
1830        kinds: covered
1831            .iter()
1832            .map(|test| test.provenance.kind.clone())
1833            .collect::<BTreeSet<_>>()
1834            .into_iter()
1835            .collect(),
1836        runners: covered
1837            .iter()
1838            .map(|test| test.provenance.runner.clone())
1839            .collect::<BTreeSet<_>>()
1840            .into_iter()
1841            .collect(),
1842        tests: covered
1843            .into_iter()
1844            .map(|test| CoverageOtherTest {
1845                id: test.id.clone(),
1846                name: test.name.clone(),
1847            })
1848            .collect(),
1849    }
1850}
1851
1852fn vector_text(vector: &crate::coverage_analysis::McdcVector) -> String {
1853    let values = vector
1854        .values
1855        .iter()
1856        .map(|value| match value {
1857            None => '-',
1858            Some(false) => 'F',
1859            Some(true) => 'T',
1860        })
1861        .collect::<String>();
1862    format!("{values} -> {}", if vector.outcome { 'T' } else { 'F' })
1863}
1864
1865fn obligation_matches_metric(obligation: &CoverageFileObligation, metric: MinimizeMetric) -> bool {
1866    metric == MinimizeMetric::All
1867        || matches!(
1868            (obligation.kind(), metric),
1869            ("line", MinimizeMetric::Lines)
1870                | ("statement", MinimizeMetric::Statements)
1871                | ("function", MinimizeMetric::Functions)
1872                | ("branch", MinimizeMetric::Branches)
1873                | ("mcdc", MinimizeMetric::Mcdc)
1874        )
1875}
1876
1877fn compact_source(value: &str) -> Option<String> {
1878    let line = value.lines().find(|line| !line.trim().is_empty())?.trim();
1879    let compact = line.split_whitespace().collect::<Vec<_>>().join(" ");
1880    if compact.is_empty() {
1881        None
1882    } else if compact.chars().count() > 120 {
1883        Some(format!(
1884            "{}…",
1885            compact.chars().take(119).collect::<String>()
1886        ))
1887    } else {
1888        Some(compact)
1889    }
1890}
1891
1892fn obligation_source(obligation: &CoverageFileObligation) -> Option<String> {
1893    match obligation {
1894        CoverageFileObligation::Line(_) => None,
1895        CoverageFileObligation::Point(value) => compact_source(&value.source),
1896        CoverageFileObligation::Branch(value) => compact_source(&value.source),
1897        CoverageFileObligation::Mcdc(value) => compact_source(&value.decision),
1898    }
1899}
1900
1901/// What a file's detail is read from: the run's lines, points, decisions,
1902/// limitations and tests, read once and kept by file.
1903///
1904/// Reading them is most of what a file's detail costs, and none of it
1905/// depends on the file. The HTML report asks for every file with a gap, and
1906/// read the whole run again for each: six minutes for a merged run of 2,094
1907/// tests that is exported in five seconds.
1908pub struct FileDetailSource {
1909    view: CoverageViewId,
1910    tests: Vec<crate::coverage_index::IndexedTestDetail>,
1911    test_summaries: Vec<IndexedTestSummary>,
1912    tests_by_id: HashMap<String, IndexedTestSummary>,
1913    /// The tests with a line in each file.
1914    tests_by_file: HashMap<String, Vec<String>>,
1915    lines: Vec<crate::coverage_index::IndexedLine>,
1916    lines_by_file: BTreeMap<String, Vec<usize>>,
1917    points: Vec<crate::coverage_index::IndexedHitMetadata>,
1918    points_by_file: HashMap<String, Vec<usize>>,
1919    decisions: Vec<crate::coverage_report::DecisionResult>,
1920    decisions_by_file: HashMap<String, Vec<usize>>,
1921    limitations: BTreeMap<String, Vec<crate::coverage_index::IndexedLimitation>>,
1922}
1923
1924impl FileDetailSource {
1925    pub fn read(index: &CoverageIndex<'_>, view: CoverageViewId) -> Result<Self, QueryError> {
1926        let tests = index.test_details(view)?;
1927        let mut tests_by_file = HashMap::<String, Vec<String>>::new();
1928        for test in &tests {
1929            let mut files = test
1930                .lines
1931                .iter()
1932                .map(|line| line.file.as_str())
1933                .collect::<Vec<_>>();
1934            files.sort_unstable();
1935            files.dedup();
1936            for file in files {
1937                tests_by_file
1938                    .entry(file.to_owned())
1939                    .or_default()
1940                    .push(test.summary.id.clone());
1941            }
1942        }
1943        let test_summaries = tests
1944            .iter()
1945            .map(|test| test.summary.clone())
1946            .collect::<Vec<_>>();
1947        let tests_by_id = test_summaries
1948            .iter()
1949            .map(|test| (test.id.clone(), test.clone()))
1950            .collect();
1951        let lines = index.lines(view)?;
1952        let mut lines_by_file = BTreeMap::<String, Vec<usize>>::new();
1953        for (at, line) in lines.iter().enumerate() {
1954            lines_by_file.entry(line.file.clone()).or_default().push(at);
1955        }
1956        let points = index.hit_metadata(view)?;
1957        let mut points_by_file = HashMap::<String, Vec<usize>>::new();
1958        for (at, point) in points.iter().enumerate() {
1959            points_by_file
1960                .entry(point.file.clone())
1961                .or_default()
1962                .push(at);
1963        }
1964        let decisions = index.decision_details(view)?;
1965        let mut decisions_by_file = HashMap::<String, Vec<usize>>::new();
1966        for (at, decision) in decisions.iter().enumerate() {
1967            decisions_by_file
1968                .entry(decision.meta.file.clone())
1969                .or_default()
1970                .push(at);
1971        }
1972        let mut limitations = BTreeMap::<String, Vec<_>>::new();
1973        for limitation in index.limitations(view)? {
1974            limitations
1975                .entry(limitation.file.clone())
1976                .or_default()
1977                .push(limitation);
1978        }
1979        Ok(Self {
1980            view,
1981            tests,
1982            test_summaries,
1983            tests_by_id,
1984            tests_by_file,
1985            lines,
1986            lines_by_file,
1987            points,
1988            points_by_file,
1989            decisions,
1990            decisions_by_file,
1991            limitations,
1992        })
1993    }
1994
1995    /// Every test, line, point and decision of the run, in the index's
1996    /// order, for a reader that wants them all as well as a file's detail.
1997    pub fn tests(&self) -> &[crate::coverage_index::IndexedTestDetail] {
1998        &self.tests
1999    }
2000
2001    pub fn lines(&self) -> &[crate::coverage_index::IndexedLine] {
2002        &self.lines
2003    }
2004
2005    pub fn points(&self) -> &[crate::coverage_index::IndexedHitMetadata] {
2006        &self.points
2007    }
2008
2009    pub fn decisions(&self) -> &[crate::coverage_report::DecisionResult] {
2010        &self.decisions
2011    }
2012}
2013
2014pub fn coverage_file_detail_query(
2015    index: &CoverageIndex<'_>,
2016    options: CoverageFileDetailOptions<'_>,
2017) -> Result<(CoverageFileDetailData, AgentPagination), QueryError> {
2018    if options.limit == 0 {
2019        return Err(QueryError::InvalidPagination);
2020    }
2021    coverage_file_detail_from(&FileDetailSource::read(index, options.view)?, options)
2022}
2023
2024/// One file's detail from a run already read. `options.view` is the view the
2025/// run was read in.
2026pub fn coverage_file_detail_from(
2027    read: &FileDetailSource,
2028    options: CoverageFileDetailOptions<'_>,
2029) -> Result<(CoverageFileDetailData, AgentPagination), QueryError> {
2030    if options.limit == 0 {
2031        return Err(QueryError::InvalidPagination);
2032    }
2033    debug_assert!(read.view == options.view);
2034    let selected = selected_test_ids(&read.test_summaries, options.kind, options.runner)?;
2035    let tests_by_id = &read.tests_by_id;
2036    let files = read
2037        .lines_by_file
2038        .keys()
2039        .chain(read.limitations.keys())
2040        .map(String::as_str)
2041        .collect::<BTreeSet<_>>();
2042    let file = if files.contains(options.selector) {
2043        options.selector.to_owned()
2044    } else {
2045        let matches = files
2046            .into_iter()
2047            .filter(|file| file.contains(options.selector))
2048            .collect::<Vec<_>>();
2049        if matches.is_empty() {
2050            return Err(QueryError::SourceNotFound(options.selector.into()));
2051        }
2052        if matches.len() != 1 {
2053            return Err(QueryError::AmbiguousSelector {
2054                selector: options.selector.into(),
2055                matches: matches.into_iter().map(str::to_owned).collect(),
2056            });
2057        }
2058        matches[0].to_owned()
2059    };
2060    let lines = read
2061        .lines_by_file
2062        .get(&file)
2063        .into_iter()
2064        .flatten()
2065        .map(|at| &read.lines[*at])
2066        .collect::<Vec<_>>();
2067    let metadata = read
2068        .points_by_file
2069        .get(&file)
2070        .into_iter()
2071        .flatten()
2072        .map(|at| &read.points[*at])
2073        .collect::<Vec<_>>();
2074    let selected_includes = |tests: &[String]| {
2075        selected.as_ref().map_or(!tests.is_empty(), |selected| {
2076            tests.iter().any(|test| selected.contains(test))
2077        })
2078    };
2079    let total_lines = lines.iter().filter(|line| line.measured).count();
2080    let uncovered_lines = lines
2081        .iter()
2082        .filter(|line| line.measured && !selected_includes(&line.tests))
2083        .map(|line| {
2084            CoverageFileObligation::Line(CoverageLineObligation {
2085                kind: "line".into(),
2086                id: format!("line:{}:{}", line.file, line.line),
2087                line: line.line,
2088                other_coverage: other_coverage(&line.tests, selected.as_ref(), tests_by_id),
2089            })
2090        })
2091        .collect::<Vec<_>>();
2092    let statements = metadata
2093        .iter()
2094        .filter(|point| point.obligation == "statement" && !selected_includes(&point.tests))
2095        .map(|point| {
2096            CoverageFileObligation::Point(CoveragePointObligation {
2097                kind: "statement".into(),
2098                id: point.id.clone(),
2099                line: point.line,
2100                column: point.column,
2101                source: point.label.clone().unwrap_or_else(|| point.source.clone()),
2102                other_coverage: other_coverage(&point.tests, selected.as_ref(), tests_by_id),
2103            })
2104        })
2105        .collect::<Vec<_>>();
2106    let functions = metadata
2107        .iter()
2108        .filter(|point| point.obligation == "function" && !selected_includes(&point.tests))
2109        .map(|point| {
2110            CoverageFileObligation::Point(CoveragePointObligation {
2111                kind: "function".into(),
2112                id: point.id.clone(),
2113                line: point.line,
2114                column: point.column,
2115                source: point.label.clone().unwrap_or_else(|| point.source.clone()),
2116                other_coverage: other_coverage(&point.tests, selected.as_ref(), tests_by_id),
2117            })
2118        })
2119        .collect::<Vec<_>>();
2120    let branches = metadata
2121        .iter()
2122        .filter(|branch| branch.obligation == "branch" && !selected_includes(&branch.tests))
2123        .map(|branch| {
2124            CoverageFileObligation::Branch(CoverageBranchObligation {
2125                kind: "branch".into(),
2126                id: branch.id.clone(),
2127                line: branch.line,
2128                column: branch.column,
2129                source: branch.source.clone(),
2130                missing: branch.alternative.clone().unwrap_or_default(),
2131                other_coverage: other_coverage(&branch.tests, selected.as_ref(), tests_by_id),
2132            })
2133        })
2134        .collect::<Vec<_>>();
2135    // A line ran when anything anchored on it was observed. A line inside a
2136    // multi-line expression has no line obligation of its own, so this is
2137    // what tells one that never ran from one that ran part of the way.
2138    let observed_lines = metadata
2139        .iter()
2140        .filter(|point| selected_includes(&point.tests))
2141        .map(|point| point.line)
2142        .collect::<BTreeSet<_>>();
2143    let mut mcdc = Vec::new();
2144    for original in read
2145        .decisions_by_file
2146        .get(&file)
2147        .into_iter()
2148        .flatten()
2149        .map(|at| &read.decisions[*at])
2150    {
2151        let filtered = selected_decision(original.clone(), selected.as_ref());
2152        for condition in filtered
2153            .conditions
2154            .iter()
2155            .filter(|condition| !condition.covered)
2156        {
2157            let original_tests = original.conditions[condition.index]
2158                .witness_tests
2159                .clone()
2160                .unwrap_or_default()
2161                .into_iter()
2162                .flatten()
2163                .collect::<Vec<_>>();
2164            mcdc.push(CoverageFileObligation::Mcdc(CoverageMcdcObligation {
2165                kind: "mcdc".into(),
2166                id: original.meta.id.clone(),
2167                line: original.meta.line,
2168                column: original.meta.column,
2169                decision: original.meta.source.clone(),
2170                missing_condition: condition.source.clone(),
2171                condition_index: condition.index,
2172                observed_vectors: filtered
2173                    .vector_observations
2174                    .iter()
2175                    .map(|observation| vector_text(&observation.vector))
2176                    .collect(),
2177                other_coverage: other_coverage(&original_tests, selected.as_ref(), tests_by_id),
2178            }));
2179        }
2180    }
2181    let mut obligations = uncovered_lines
2182        .iter()
2183        .chain(statements.iter())
2184        .chain(functions.iter())
2185        .chain(branches.iter())
2186        .chain(mcdc.iter())
2187        .filter(|obligation| obligation_matches_metric(obligation, options.metric))
2188        .cloned()
2189        .collect::<Vec<_>>();
2190    obligations.sort_by(|left, right| {
2191        left.line()
2192            .cmp(&right.line())
2193            .then_with(|| left.kind().cmp(right.kind()))
2194    });
2195    let mut limitations = read
2196        .limitations
2197        .get(&file)
2198        .into_iter()
2199        .flatten()
2200        .cloned()
2201        .map(|limitation| CoverageFileLimitation {
2202            id: limitation.id,
2203            kind: limitation.kind,
2204            file: limitation.file,
2205            line: limitation.line,
2206            column: limitation.column,
2207            source: limitation.source,
2208            reason: limitation.reason,
2209            blocking: limitation.blocking,
2210            effect: "outside-measured-denominator".into(),
2211        })
2212        .collect::<Vec<_>>();
2213    limitations.sort_by(|left, right| {
2214        left.line
2215            .cmp(&right.line)
2216            .then_with(|| left.column.cmp(&right.column))
2217            .then_with(|| left.id.cmp(&right.id))
2218    });
2219    let total_tests = read
2220        .tests_by_file
2221        .get(&file)
2222        .into_iter()
2223        .flatten()
2224        .filter(|test| {
2225            selected
2226                .as_ref()
2227                .is_none_or(|selected| selected.contains(*test))
2228        })
2229        .count();
2230    let total_obligations = obligations.len();
2231    let total_limitations = limitations.len();
2232    let mut grouped =
2233        BTreeMap::<usize, (Vec<CoverageFileObligation>, Vec<CoverageFileLimitation>)>::new();
2234    for obligation in obligations {
2235        grouped
2236            .entry(obligation.line())
2237            .or_default()
2238            .0
2239            .push(obligation);
2240    }
2241    for limitation in limitations {
2242        grouped
2243            .entry(limitation.line)
2244            .or_default()
2245            .1
2246            .push(limitation);
2247    }
2248    let gap_lines = grouped
2249        .into_iter()
2250        .map(|(line, (obligations, limitations))| {
2251            let source = obligations.iter().find_map(obligation_source).or_else(|| {
2252                limitations
2253                    .iter()
2254                    .find_map(|value| compact_source(&value.source))
2255            });
2256            // An MC/DC condition can be evaluated without anything on its own
2257            // line being recorded, so only branch and point gaps can say a
2258            // line never ran.
2259            let never_ran = !observed_lines.contains(&line)
2260                && !obligations.is_empty()
2261                && obligations
2262                    .iter()
2263                    .all(|value| !matches!(value, CoverageFileObligation::Mcdc(_)));
2264            let state = if never_ran
2265                || obligations
2266                    .iter()
2267                    .any(|value| matches!(value, CoverageFileObligation::Line(_)))
2268            {
2269                "missing"
2270            } else if !obligations.is_empty() || observed_lines.contains(&line) {
2271                // A line that ran with part of it outside measurement ran part
2272                // of the way, as its line view says.
2273                "part"
2274            } else {
2275                "limited"
2276            };
2277            CoverageFileGapLine {
2278                line,
2279                state: state.into(),
2280                source,
2281                obligations,
2282                limitations,
2283            }
2284        })
2285        .collect::<Vec<_>>();
2286    let total_gap_lines = gap_lines.len();
2287    let selected_gap_lines = gap_lines
2288        .into_iter()
2289        .skip(options.offset)
2290        .take(options.limit)
2291        .collect::<Vec<_>>();
2292    let returned = selected_gap_lines.len();
2293    Ok((
2294        CoverageFileDetailData {
2295            run: options.run.into(),
2296            filters: query_filters(options.view, options.kind, options.runner),
2297            file,
2298            metric: options.metric,
2299            counts: CoverageFileCounts {
2300                total_lines,
2301                covered_lines: total_lines - uncovered_lines.len(),
2302                uncovered_lines: uncovered_lines.len(),
2303                uncovered_statements: statements.len(),
2304                uncovered_functions: functions.len(),
2305                missing_branches: branches.len(),
2306                missing_mcdc_conditions: mcdc.len(),
2307                measurement_limitations: total_limitations,
2308            },
2309            total_tests,
2310            total_obligations,
2311            total_gap_lines,
2312            gap_lines: selected_gap_lines,
2313            total_limitations,
2314            assertions: None,
2315        },
2316        pagination(options.offset, options.limit, returned, total_gap_lines),
2317    ))
2318}
2319
2320#[derive(Debug, Clone, PartialEq, Serialize)]
2321pub struct CoverageDiffDelta {
2322    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2323    pub lines: f64,
2324    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2325    pub branches: f64,
2326    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2327    pub mcdc: f64,
2328}
2329
2330#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2331#[serde(rename_all = "camelCase")]
2332pub struct CoverageDiffSide {
2333    pub line_count: usize,
2334    pub branch_count: usize,
2335    pub mcdc_count: usize,
2336    pub lines: Vec<String>,
2337    pub branches: Vec<String>,
2338    pub mcdc: Vec<String>,
2339}
2340
2341/// How much of a comparison falls in one file.
2342#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2343#[serde(rename_all = "camelCase")]
2344pub struct CoverageDiffFile {
2345    pub file: String,
2346    pub lost: CoverageDiffCounts,
2347    pub gained: CoverageDiffCounts,
2348}
2349
2350#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
2351pub struct CoverageDiffCounts {
2352    pub lines: usize,
2353    pub branches: usize,
2354    pub mcdc: usize,
2355}
2356
2357impl CoverageDiffCounts {
2358    fn total(&self) -> usize {
2359        self.lines + self.branches + self.mcdc
2360    }
2361}
2362
2363/// The files of a comparison that are listed whole; the rest are counted.
2364const DIFF_FILES: usize = 20;
2365
2366#[derive(Debug, Clone, PartialEq, Serialize)]
2367#[serde(rename_all = "camelCase")]
2368pub struct CoverageDiffData {
2369    pub filters: CoverageQueryFilters,
2370    pub older: String,
2371    pub newer: String,
2372    pub delta: CoverageDiffDelta,
2373    pub gained: CoverageDiffSide,
2374    pub lost: CoverageDiffSide,
2375    /// How many files the difference is in, and the ones with the most lost,
2376    /// whatever page of items is shown. A list of 79 rows does not say that
2377    /// 45 of its 63 lost lines are one component that did not render.
2378    pub files: usize,
2379    pub by_file: Vec<CoverageDiffFile>,
2380    /// Whether what the two runs measured differs, from their fingerprints.
2381    /// Absent where a run has none.
2382    #[serde(skip_serializing_if = "Option::is_none")]
2383    pub inputs: Option<CoverageDiffInputs>,
2384}
2385
2386/// What two runs of a comparison had in common. With the same source, a
2387/// difference between them is not a change in the code: two e2e runs of one
2388/// commit read "lost: 11 lines, 31 branches" with nothing to say so.
2389#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2390#[serde(rename_all = "camelCase")]
2391pub struct CoverageDiffInputs {
2392    /// The measured source, its dependencies or its configuration.
2393    pub source_changed: bool,
2394    pub tests_changed: bool,
2395}
2396
2397#[derive(Debug, Clone, Copy)]
2398pub struct CoverageDiffQueryOptions<'a> {
2399    pub older_run: &'a str,
2400    pub newer_run: &'a str,
2401    pub view: CoverageViewId,
2402    pub kind: Option<&'a str>,
2403    pub runner: Option<&'a str>,
2404    pub offset: usize,
2405    pub limit: usize,
2406}
2407
2408struct DiffSnapshot {
2409    summary: CoverageSummary,
2410    lines: BTreeSet<String>,
2411    branches: HashMap<String, String>,
2412    mcdc: HashMap<String, String>,
2413}
2414
2415fn diff_snapshot(
2416    index: &CoverageIndex<'_>,
2417    view: CoverageViewId,
2418) -> Result<DiffSnapshot, QueryError> {
2419    let summary = index.summary(view)?;
2420    let lines = index
2421        .lines(view)?
2422        .into_iter()
2423        .filter(|line| line.covered)
2424        .map(|line| format!("{}:{}", line.file, line.line))
2425        .collect();
2426    let branches = index
2427        .hit_metadata(view)?
2428        .into_iter()
2429        .filter(|metadata| metadata.obligation == "branch" && !metadata.tests.is_empty())
2430        .map(|metadata| {
2431            let parent = metadata
2432                .parent_id
2433                .ok_or(QueryError::InvalidRecordSelection)?;
2434            Ok((
2435                format!("{parent}:{}", metadata.id),
2436                format!(
2437                    "{}:{} {}",
2438                    metadata.file,
2439                    metadata.line,
2440                    metadata.alternative.unwrap_or_default()
2441                ),
2442            ))
2443        })
2444        .collect::<Result<HashMap<_, _>, QueryError>>()?;
2445    let mcdc = index
2446        .decision_details(view)?
2447        .into_iter()
2448        .flat_map(|decision| {
2449            decision
2450                .conditions
2451                .into_iter()
2452                .filter(|condition| condition.covered)
2453                .map(move |condition| {
2454                    (
2455                        format!("{}:c{}", decision.meta.id, condition.index),
2456                        format!(
2457                            "{}:{} C{} {}",
2458                            decision.meta.file,
2459                            decision.meta.line,
2460                            condition.index + 1,
2461                            condition.source
2462                        ),
2463                    )
2464                })
2465        })
2466        .collect();
2467    Ok(DiffSnapshot {
2468        summary,
2469        lines,
2470        branches,
2471        mcdc,
2472    })
2473}
2474
2475fn js_string_cmp(left: &str, right: &str) -> std::cmp::Ordering {
2476    left.encode_utf16().cmp(right.encode_utf16())
2477}
2478
2479fn rounded_delta(newer: f64, older: f64) -> f64 {
2480    ((newer - older) * 100.0).round() / 100.0
2481}
2482
2483pub fn coverage_diff_query(
2484    older: &CoverageIndex<'_>,
2485    newer: &CoverageIndex<'_>,
2486    options: CoverageDiffQueryOptions<'_>,
2487) -> Result<(CoverageDiffData, AgentPagination), QueryError> {
2488    if options.limit == 0 {
2489        return Err(QueryError::InvalidPagination);
2490    }
2491    let older = diff_snapshot(older, options.view)?;
2492    let newer = diff_snapshot(newer, options.view)?;
2493    let mut gained_lines = newer
2494        .lines
2495        .difference(&older.lines)
2496        .cloned()
2497        .collect::<Vec<_>>();
2498    let mut lost_lines = older
2499        .lines
2500        .difference(&newer.lines)
2501        .cloned()
2502        .collect::<Vec<_>>();
2503    let mut gained_branches = newer
2504        .branches
2505        .iter()
2506        .filter(|(id, _)| !older.branches.contains_key(*id))
2507        .map(|(_, label)| label.clone())
2508        .collect::<Vec<_>>();
2509    let mut lost_branches = older
2510        .branches
2511        .iter()
2512        .filter(|(id, _)| !newer.branches.contains_key(*id))
2513        .map(|(_, label)| label.clone())
2514        .collect::<Vec<_>>();
2515    let mut gained_mcdc = newer
2516        .mcdc
2517        .iter()
2518        .filter(|(id, _)| !older.mcdc.contains_key(*id))
2519        .map(|(_, label)| label.clone())
2520        .collect::<Vec<_>>();
2521    let mut lost_mcdc = older
2522        .mcdc
2523        .iter()
2524        .filter(|(id, _)| !newer.mcdc.contains_key(*id))
2525        .map(|(_, label)| label.clone())
2526        .collect::<Vec<_>>();
2527    for values in [
2528        &mut gained_lines,
2529        &mut lost_lines,
2530        &mut gained_branches,
2531        &mut lost_branches,
2532        &mut gained_mcdc,
2533        &mut lost_mcdc,
2534    ] {
2535        values.sort_by(|left, right| js_string_cmp(left, right));
2536    }
2537    // Every label starts `file:line`, and a file's path holds no space.
2538    let mut by_file = BTreeMap::<&str, (CoverageDiffCounts, CoverageDiffCounts)>::new();
2539    for (values, count) in [
2540        (&lost_lines, 0),
2541        (&lost_branches, 1),
2542        (&lost_mcdc, 2),
2543        (&gained_lines, 3),
2544        (&gained_branches, 4),
2545        (&gained_mcdc, 5),
2546    ] {
2547        for label in values {
2548            let place = label.split(' ').next().unwrap_or(label);
2549            let file = place.rsplit_once(':').map_or(place, |(file, _)| file);
2550            let (lost, gained) = by_file.entry(file).or_default();
2551            let side = if count < 3 { lost } else { gained };
2552            match count % 3 {
2553                0 => side.lines += 1,
2554                1 => side.branches += 1,
2555                _ => side.mcdc += 1,
2556            }
2557        }
2558    }
2559    let files = by_file.len();
2560    let mut by_file = by_file
2561        .into_iter()
2562        .map(|(file, (lost, gained))| CoverageDiffFile {
2563            file: file.to_owned(),
2564            lost,
2565            gained,
2566        })
2567        .collect::<Vec<_>>();
2568    by_file.sort_by(|left, right| {
2569        (right.lost.total(), right.gained.total())
2570            .cmp(&(left.lost.total(), left.gained.total()))
2571            .then_with(|| js_string_cmp(&left.file, &right.file))
2572    });
2573    by_file.truncate(DIFF_FILES);
2574    let total = [
2575        gained_lines.len(),
2576        gained_branches.len(),
2577        gained_mcdc.len(),
2578        lost_lines.len(),
2579        lost_branches.len(),
2580        lost_mcdc.len(),
2581    ]
2582    .into_iter()
2583    .max()
2584    .unwrap_or(0);
2585    let page = |values: &[String]| {
2586        values
2587            .iter()
2588            .skip(options.offset)
2589            .take(options.limit)
2590            .cloned()
2591            .collect::<Vec<_>>()
2592    };
2593    let gained = CoverageDiffSide {
2594        line_count: gained_lines.len(),
2595        branch_count: gained_branches.len(),
2596        mcdc_count: gained_mcdc.len(),
2597        lines: page(&gained_lines),
2598        branches: page(&gained_branches),
2599        mcdc: page(&gained_mcdc),
2600    };
2601    let lost = CoverageDiffSide {
2602        line_count: lost_lines.len(),
2603        branch_count: lost_branches.len(),
2604        mcdc_count: lost_mcdc.len(),
2605        lines: page(&lost_lines),
2606        branches: page(&lost_branches),
2607        mcdc: page(&lost_mcdc),
2608    };
2609    let returned = [
2610        gained.lines.len(),
2611        gained.branches.len(),
2612        gained.mcdc.len(),
2613        lost.lines.len(),
2614        lost.branches.len(),
2615        lost.mcdc.len(),
2616    ]
2617    .into_iter()
2618    .max()
2619    .unwrap_or(0);
2620    Ok((
2621        CoverageDiffData {
2622            filters: query_filters(options.view, options.kind, options.runner),
2623            older: options.older_run.into(),
2624            newer: options.newer_run.into(),
2625            delta: CoverageDiffDelta {
2626                lines: rounded_delta(
2627                    newer.summary.lines.percentage,
2628                    older.summary.lines.percentage,
2629                ),
2630                branches: rounded_delta(
2631                    newer.summary.branches.percentage,
2632                    older.summary.branches.percentage,
2633                ),
2634                mcdc: rounded_delta(
2635                    newer.summary.condition_coverage_pct,
2636                    older.summary.condition_coverage_pct,
2637                ),
2638            },
2639            gained,
2640            lost,
2641            files,
2642            by_file,
2643            inputs: None,
2644        },
2645        pagination(options.offset, options.limit, returned, total),
2646    ))
2647}
2648
2649pub fn coverage_scope_query(
2650    index: &CoverageIndex<'_>,
2651    options: CoverageScopeQueryOptions<'_>,
2652) -> Result<(CoverageScopeData, AgentPagination), QueryError> {
2653    if options.limit == 0 {
2654        return Err(QueryError::InvalidPagination);
2655    }
2656    let projection = index.projection(options.view, options.kind, options.runner)?;
2657    let scope = projection
2658        .source_scope
2659        .ok_or(QueryError::ScopeUnavailable)?;
2660    let mut entries = index.scope_entries(options.view)?;
2661    entries.sort_by(|left, right| {
2662        scope_status_rank(&left.status)
2663            .cmp(&scope_status_rank(&right.status))
2664            .then_with(|| left.file.cmp(&right.file))
2665    });
2666    let groups = scope_groups(&entries);
2667    let unclassified = unclassified_source(&scope.roots, &entries);
2668    let total = entries.len();
2669    let selected = entries
2670        .into_iter()
2671        .skip(options.offset)
2672        .take(options.limit)
2673        .collect::<Vec<_>>();
2674    let returned = selected.len();
2675    Ok((
2676        CoverageScopeData {
2677            run: options.run.into(),
2678            filters: CoverageQueryFilters {
2679                outcome: match options.view {
2680                    CoverageViewId::All => "all",
2681                    CoverageViewId::Passed => "passed",
2682                    CoverageViewId::Failed => "failed",
2683                }
2684                .into(),
2685                kind: options.kind.map(str::to_owned),
2686                runner: options.runner.map(str::to_owned),
2687            },
2688            kind: scope.kind,
2689            language: scope.language,
2690            model: scope.model,
2691            mode: scope.mode,
2692            roots: scope.roots,
2693            unit: scope.unit,
2694            measurement_complete: scope.measurement_complete,
2695            counts: ScopeCounts {
2696                included: scope.included,
2697                excluded: scope.excluded,
2698                ambiguous: scope.ambiguous,
2699            },
2700            measurement: projection.measurement,
2701            groups,
2702            unclassified_source: unclassified,
2703            entries: selected,
2704        },
2705        pagination(options.offset, options.limit, returned, total),
2706    ))
2707}
2708
2709pub fn coverage_summary_query(
2710    index: &CoverageIndex<'_>,
2711    options: CoverageSummaryQueryOptions<'_>,
2712) -> Result<CoverageSummaryData, QueryError> {
2713    let projection = index.projection(options.view, options.kind, options.runner)?;
2714    let mut test_kind_sources = BTreeMap::new();
2715    for test in index.test_summaries(options.view)?.iter().filter(|t| {
2716        t.role == "test"
2717            && options.kind.is_none_or(|k| t.provenance.kind == k)
2718            && options.runner.is_none_or(|r| t.provenance.runner == r)
2719    }) {
2720        *test_kind_sources
2721            .entry(test.provenance.source.clone())
2722            .or_insert(0) += 1;
2723    }
2724    let mut diagnostics = Vec::new();
2725    let mut transport_blockers = 0usize;
2726    if projection.empty_evidence_tests > 0 {
2727        diagnostics.push(CoverageDiagnostic {
2728            code: "TEST_EVIDENCE_MISSING".into(),
2729            severity: "warning".into(),
2730            message: format!(
2731                "{} 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: {}",
2732                projection.empty_evidence_tests,
2733                projection.first_empty_evidence_test.as_deref().unwrap_or("unknown")
2734            ),
2735        });
2736    }
2737    if let Some(transport) = &projection.transport {
2738        if transport.corrupt_records > 0 {
2739            transport_blockers += 1;
2740            diagnostics.push(CoverageDiagnostic {
2741                code: "CORRUPT_EVIDENCE_RECORDS".into(),
2742                severity: "error".into(),
2743                message: format!(
2744                    "{} malformed evidence record(s) in {} file(s) were excluded; coverage is incomplete.",
2745                    transport.corrupt_records, transport.corrupt_files
2746                ),
2747            });
2748        }
2749        if transport.remote_launches > 0
2750            && transport.scoped_server_records == 0
2751            && transport.background_server_records == 0
2752            && projection.attribution.server_explicit == 0
2753            && projection.attribution.server_fallback == 0
2754        {
2755            transport_blockers += 1;
2756            diagnostics.push(CoverageDiagnostic {
2757                code: "REMOTE_SERVER_EVIDENCE_MISSING".into(),
2758                severity: "error".into(),
2759                message: "Remote launches were supervised, but neither scoped nor background server evidence returned. Supercov refuses to describe this measurement as complete.".into(),
2760            });
2761        }
2762    }
2763    let coverage_by_kind = index.dimensions(options.view, CoverageDimension::Kind)?;
2764    let coverage_by_runner = index.dimensions(options.view, CoverageDimension::Runner)?;
2765    let coverage_by_tests = if options.kind.is_none() && options.runner.is_none() {
2766        index
2767            .dimensions(options.view, CoverageDimension::Tests)?
2768            .into_iter()
2769            .next()
2770            .filter(|tests| tests.tests + tests.setups > 0)
2771            .map(|tests| tests.summary)
2772    } else {
2773        None
2774    };
2775    let other_e2e_kinds = coverage_by_kind
2776        .iter()
2777        .filter(|dimension| dimension.tests > 0)
2778        .filter_map(|dimension| dimension.kind.clone())
2779        .filter(|kind| kind != "e2e")
2780        .collect::<Vec<_>>();
2781    let e2e_observed = coverage_by_kind
2782        .iter()
2783        .any(|dimension| dimension.tests > 0 && dimension.kind.as_deref() == Some("e2e"));
2784    let e2e_gap_context = if options.kind.is_none()
2785        && options.runner.is_none()
2786        && e2e_observed
2787        && !other_e2e_kinds.is_empty()
2788    {
2789        let mut covered_elsewhere = IndexedGapDimensions {
2790            lines: 0,
2791            statements: 0,
2792            functions: 0,
2793            branches: 0,
2794            mcdc_conditions: 0,
2795        };
2796        let mut uncovered_everywhere = covered_elsewhere.clone();
2797        for gap in index.file_gaps(options.view, Some("e2e"), None)? {
2798            covered_elsewhere.lines += gap.covered_by_other_tests.lines;
2799            covered_elsewhere.statements += gap.covered_by_other_tests.statements;
2800            covered_elsewhere.functions += gap.covered_by_other_tests.functions;
2801            covered_elsewhere.branches += gap.covered_by_other_tests.branches;
2802            covered_elsewhere.mcdc_conditions += gap.covered_by_other_tests.mcdc_conditions;
2803            uncovered_everywhere.lines += gap.uncovered_everywhere.lines;
2804            uncovered_everywhere.statements += gap.uncovered_everywhere.statements;
2805            uncovered_everywhere.functions += gap.uncovered_everywhere.functions;
2806            uncovered_everywhere.branches += gap.uncovered_everywhere.branches;
2807            uncovered_everywhere.mcdc_conditions += gap.uncovered_everywhere.mcdc_conditions;
2808        }
2809        Some(CoverageKindGapContext {
2810            kind: "e2e".into(),
2811            other_kinds: other_e2e_kinds,
2812            covered_elsewhere,
2813            uncovered_everywhere,
2814        })
2815    } else {
2816        None
2817    };
2818    let filters = CoverageQueryFilters {
2819        outcome: match options.view {
2820            CoverageViewId::All => "all",
2821            CoverageViewId::Passed => "passed",
2822            CoverageViewId::Failed => "failed",
2823        }
2824        .into(),
2825        kind: options.kind.map(str::to_owned),
2826        runner: options.runner.map(str::to_owned),
2827    };
2828    let mut measurement = projection.measurement.clone();
2829    if transport_blockers > 0 {
2830        measurement.complete = false;
2831        measurement.limitations = measurement.limitations.saturating_add(transport_blockers);
2832        measurement.blocking = measurement.blocking.saturating_add(transport_blockers);
2833    }
2834    let structurally_complete = projection.summary.coverage_complete && measurement.complete;
2835    let complete = options.view == CoverageViewId::Passed
2836        && options.valid
2837        && !options.stale
2838        && structurally_complete;
2839    Ok(CoverageSummaryData {
2840        test_kind_sources,
2841        assertion_coverage: None,
2842        run: options.run.into(),
2843        command: Vec::new(),
2844        hints: Vec::new(),
2845        workspace: None,
2846        filters,
2847        model: index.model()?,
2848        generated_at: projection.generated_at,
2849        valid: options.valid,
2850        test_exit_code: options.test_exit_code,
2851        stale: options.stale,
2852        stale_reasons: options.stale_reasons,
2853        structurally_complete,
2854        complete,
2855        coverage: projection.summary,
2856        measurement,
2857        coverage_by_kind,
2858        coverage_by_tests,
2859        e2e_gap_context,
2860        coverage_by_runner,
2861        attribution: projection.attribution,
2862        test_attribution: {
2863            let mut counts = TestAttributionCounts::default();
2864            for test in index
2865                .test_summaries(options.view)?
2866                .iter()
2867                .filter(|test| test.role == "test")
2868            {
2869                match test.attribution.as_str() {
2870                    crate::coverage_report::ATTRIBUTION_RUN_WIDE => counts.run_wide += 1,
2871                    crate::coverage_report::ATTRIBUTION_PARTIAL => counts.partial += 1,
2872                    _ => counts.exact += 1,
2873                }
2874            }
2875            counts
2876        },
2877        transport: projection.transport,
2878        diagnostics,
2879        confidence: (options.kind.is_none() && options.runner.is_none())
2880            .then_some(projection.confidence),
2881        files_with_gaps: projection.files_with_gaps,
2882        files_with_coverage_gaps: projection.files_with_coverage_gaps,
2883        files_with_measurement_limitations: projection.measurement.files,
2884        tests: projection.tests,
2885        setups: projection.setups,
2886        test_outcomes: projection.test_outcomes,
2887        unclassified_source: match &projection.source_scope {
2888            Some(scope) if scope.ambiguous > 0 => {
2889                unclassified_source(&scope.roots, &index.scope_entries(options.view)?)
2890            }
2891            _ => None,
2892        },
2893        source_scope: projection.source_scope,
2894    })
2895}
2896
2897#[derive(Debug, Clone, PartialEq)]
2898pub enum CoverageDimensionQueryData {
2899    Kinds(CoverageKindsData),
2900    Runners(CoverageRunnersData),
2901}
2902
2903/// Coverage of one directory: what its files have to cover, how much of it
2904/// is covered, and how much of it each kind of test covers.
2905#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2906#[serde(rename_all = "camelCase")]
2907pub struct CoverageArea {
2908    pub directory: String,
2909    pub files: usize,
2910    pub totals: IndexedGapDimensions,
2911    pub covered: IndexedGapDimensions,
2912    pub by_kind: Vec<CoverageAreaKind>,
2913    /// What the tests of every kind cover together. The rest of `covered`
2914    /// ran while no test was running. Absent where the query names a kind or
2915    /// a runner, and for a run stored before this was recorded.
2916    #[serde(skip_serializing_if = "Option::is_none")]
2917    pub covered_by_tests: Option<IndexedGapDimensions>,
2918}
2919
2920#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2921#[serde(rename_all = "camelCase")]
2922pub struct CoverageAreaKind {
2923    pub kind: String,
2924    pub covered: IndexedGapDimensions,
2925}
2926
2927#[derive(Debug, Clone, PartialEq, Serialize)]
2928#[serde(rename_all = "camelCase")]
2929pub struct CoverageAreasData {
2930    pub run: String,
2931    pub filters: CoverageQueryFilters,
2932    pub metric: MinimizeMetric,
2933    pub depth: usize,
2934    /// The kinds each area is split by, in the order of `byKind`. Empty when
2935    /// the query already names one kind or runner.
2936    pub kinds: Vec<String>,
2937    pub areas: Vec<CoverageArea>,
2938}
2939
2940#[derive(Debug, Clone, Copy)]
2941pub struct CoverageAreasQueryOptions<'a> {
2942    pub run: &'a str,
2943    pub view: CoverageViewId,
2944    pub metric: MinimizeMetric,
2945    pub kind: Option<&'a str>,
2946    pub runner: Option<&'a str>,
2947    pub depth: usize,
2948    pub offset: usize,
2949    pub limit: usize,
2950}
2951
2952/// The directory a file is counted under: its first `depth` folders, or all
2953/// of them when it has fewer. A file at the top of the project is in `.`.
2954fn area_directory(file: &str, depth: usize) -> String {
2955    let mut folders = file.split('/').collect::<Vec<_>>();
2956    folders.pop();
2957    if folders.is_empty() {
2958        ".".into()
2959    } else {
2960        folders[..folders.len().min(depth)].join("/")
2961    }
2962}
2963
2964fn add_dimensions(sum: &mut IndexedGapDimensions, value: &IndexedGapDimensions) {
2965    sum.lines += value.lines;
2966    sum.statements += value.statements;
2967    sum.functions += value.functions;
2968    sum.branches += value.branches;
2969    sum.mcdc_conditions += value.mcdc_conditions;
2970}
2971
2972const NO_DIMENSIONS: IndexedGapDimensions = IndexedGapDimensions {
2973    lines: 0,
2974    statements: 0,
2975    functions: 0,
2976    branches: 0,
2977    mcdc_conditions: 0,
2978};
2979
2980/// One metric of a file's or a directory's counts; `all` reads as lines.
2981pub fn dimension_for_metric(dimensions: &IndexedGapDimensions, metric: MinimizeMetric) -> usize {
2982    match metric {
2983        MinimizeMetric::All | MinimizeMetric::Lines => dimensions.lines,
2984        MinimizeMetric::Statements => dimensions.statements,
2985        MinimizeMetric::Functions => dimensions.functions,
2986        MinimizeMetric::Branches => dimensions.branches,
2987        MinimizeMetric::Mcdc => dimensions.mcdc_conditions,
2988    }
2989}
2990
2991/// Coverage by directory, split by test kind. "Where is the coverage" was the
2992/// first question after a full run, and answering it took an LCOV export and
2993/// a script: listings named files only and carried no totals.
2994pub fn coverage_areas_query(
2995    index: &CoverageIndex<'_>,
2996    options: CoverageAreasQueryOptions<'_>,
2997) -> Result<(CoverageAreasData, AgentPagination), QueryError> {
2998    if options.limit == 0 || options.depth == 0 {
2999        return Err(QueryError::InvalidPagination);
3000    }
3001    let files = index.file_gaps(options.view, options.kind, options.runner)?;
3002    if (options.kind.is_some() || options.runner.is_some()) && files.is_empty() {
3003        return Err(QueryError::TestFilterEmpty {
3004            kind: options.kind.map(str::to_owned),
3005            runner: options.runner.map(str::to_owned),
3006        });
3007    }
3008    let kinds = if options.kind.is_none() && options.runner.is_none() {
3009        index
3010            .dimensions(options.view, CoverageDimension::Kind)?
3011            .into_iter()
3012            .filter(|kind| kind.tests > 0 && kind.attributed > 0)
3013            .filter_map(|kind| kind.kind)
3014            .collect::<Vec<_>>()
3015    } else {
3016        Vec::new()
3017    };
3018    let mut areas = BTreeMap::<String, CoverageArea>::new();
3019    for file in &files {
3020        let directory = area_directory(&file.file, options.depth);
3021        let area = areas
3022            .entry(directory.clone())
3023            .or_insert_with(|| CoverageArea {
3024                directory,
3025                files: 0,
3026                totals: NO_DIMENSIONS,
3027                covered: NO_DIMENSIONS,
3028                by_kind: kinds
3029                    .iter()
3030                    .map(|kind| CoverageAreaKind {
3031                        kind: kind.clone(),
3032                        covered: NO_DIMENSIONS,
3033                    })
3034                    .collect(),
3035                covered_by_tests: None,
3036            });
3037        area.files += 1;
3038        add_dimensions(&mut area.totals, &file.totals);
3039        add_dimensions(&mut area.covered, &file.covered);
3040    }
3041    for (position, kind) in kinds.iter().enumerate() {
3042        for file in index.file_gaps(options.view, Some(kind), None)? {
3043            if let Some(area) = areas.get_mut(&area_directory(&file.file, options.depth)) {
3044                add_dimensions(&mut area.by_kind[position].covered, &file.covered);
3045            }
3046        }
3047    }
3048    if !kinds.is_empty() {
3049        let tested = index.file_gaps(options.view, Some(crate::coverage_index::ALL_TESTS), None)?;
3050        if !tested.is_empty() {
3051            for area in areas.values_mut() {
3052                area.covered_by_tests = Some(NO_DIMENSIONS);
3053            }
3054            for file in tested {
3055                if let Some(sum) = areas
3056                    .get_mut(&area_directory(&file.file, options.depth))
3057                    .and_then(|area| area.covered_by_tests.as_mut())
3058                {
3059                    add_dimensions(sum, &file.covered);
3060                }
3061            }
3062        }
3063    }
3064    let mut areas = areas.into_values().collect::<Vec<_>>();
3065    areas.sort_by(|left, right| {
3066        dimension_for_metric(&right.totals, options.metric)
3067            .cmp(&dimension_for_metric(&left.totals, options.metric))
3068            .then_with(|| left.directory.cmp(&right.directory))
3069    });
3070    let total = areas.len();
3071    let page = areas
3072        .into_iter()
3073        .skip(options.offset)
3074        .take(options.limit)
3075        .collect::<Vec<_>>();
3076    let returned = page.len();
3077    Ok((
3078        CoverageAreasData {
3079            run: options.run.into(),
3080            filters: CoverageQueryFilters {
3081                outcome: match options.view {
3082                    CoverageViewId::All => "all",
3083                    CoverageViewId::Passed => "passed",
3084                    CoverageViewId::Failed => "failed",
3085                }
3086                .into(),
3087                kind: options.kind.map(str::to_owned),
3088                runner: options.runner.map(str::to_owned),
3089            },
3090            metric: options.metric,
3091            depth: options.depth,
3092            kinds,
3093            areas: page,
3094        },
3095        pagination(options.offset, options.limit, returned, total),
3096    ))
3097}
3098
3099/// A test that made assertions and has no line, hit or decision to its name.
3100#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3101#[serde(rename_all = "camelCase")]
3102pub struct TestWithoutEvidence {
3103    pub id: String,
3104    pub name: String,
3105    #[serde(skip_serializing_if = "Option::is_none")]
3106    pub file: Option<String>,
3107    pub kind: String,
3108    pub runner: String,
3109    pub outcome: String,
3110    pub assertions: usize,
3111}
3112
3113#[derive(Debug, Clone, PartialEq, Serialize)]
3114#[serde(rename_all = "camelCase")]
3115pub struct TestsWithoutEvidenceData {
3116    pub run: String,
3117    pub filters: CoverageQueryFilters,
3118    pub tests: Vec<TestWithoutEvidence>,
3119}
3120
3121#[derive(Debug, Clone, Copy)]
3122pub struct TestsWithoutEvidenceOptions<'a> {
3123    pub run: &'a str,
3124    pub view: CoverageViewId,
3125    pub kind: Option<&'a str>,
3126    pub runner: Option<&'a str>,
3127    pub offset: usize,
3128    pub limit: usize,
3129}
3130
3131/// Every test the run's summary counts under "made assertions, but Supercov
3132/// received no source-coverage evidence". The summary names one of them; 63
3133/// tests behind one example could not be followed up.
3134pub fn tests_without_evidence_query(
3135    index: &CoverageIndex<'_>,
3136    options: TestsWithoutEvidenceOptions<'_>,
3137) -> Result<(TestsWithoutEvidenceData, AgentPagination), QueryError> {
3138    if options.limit == 0 {
3139        return Err(QueryError::InvalidPagination);
3140    }
3141    let mut assertions = BTreeMap::<String, usize>::new();
3142    for phase in index.phase_summaries(options.view)? {
3143        *assertions.entry(phase.test).or_insert(0) += 1;
3144    }
3145    let mut tests = index
3146        .test_details(options.view)?
3147        .into_iter()
3148        .filter(|test| {
3149            test.summary.role == "test"
3150                && test.lines.is_empty()
3151                && test.hits.is_empty()
3152                && test.decisions.is_empty()
3153                && options
3154                    .kind
3155                    .is_none_or(|kind| test.summary.provenance.kind == kind)
3156                && options
3157                    .runner
3158                    .is_none_or(|runner| test.summary.provenance.runner == runner)
3159        })
3160        .filter_map(|test| {
3161            let assertions = *assertions.get(&test.summary.id)?;
3162            Some(TestWithoutEvidence {
3163                id: test.summary.id,
3164                name: test.summary.name,
3165                file: test.summary.file,
3166                kind: test.summary.provenance.kind,
3167                runner: test.summary.provenance.runner,
3168                outcome: test.summary.outcome,
3169                assertions,
3170            })
3171        })
3172        .collect::<Vec<_>>();
3173    tests.sort_by(|left, right| {
3174        (&left.file, &left.name, &left.id).cmp(&(&right.file, &right.name, &right.id))
3175    });
3176    let total = tests.len();
3177    let page = tests
3178        .into_iter()
3179        .skip(options.offset)
3180        .take(options.limit)
3181        .collect::<Vec<_>>();
3182    let returned = page.len();
3183    Ok((
3184        TestsWithoutEvidenceData {
3185            run: options.run.into(),
3186            filters: CoverageQueryFilters {
3187                outcome: match options.view {
3188                    CoverageViewId::All => "all",
3189                    CoverageViewId::Passed => "passed",
3190                    CoverageViewId::Failed => "failed",
3191                }
3192                .into(),
3193                kind: options.kind.map(str::to_owned),
3194                runner: options.runner.map(str::to_owned),
3195            },
3196            tests: page,
3197        },
3198        pagination(options.offset, options.limit, returned, total),
3199    ))
3200}
3201
3202#[derive(Debug, Clone, PartialEq)]
3203pub enum CoverageFileQueryData {
3204    Files(CoverageFilesData),
3205    Gaps(CoverageGapsData),
3206}
3207
3208#[derive(Debug, Clone, PartialEq)]
3209pub struct CoverageFileQueryResult {
3210    pub data: CoverageFileQueryData,
3211    pub pagination: AgentPagination,
3212}
3213
3214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3215#[serde(rename_all = "lowercase")]
3216pub enum DecisionSort {
3217    Location,
3218    Missing,
3219}
3220
3221#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3222#[serde(rename_all = "camelCase")]
3223pub struct DecisionGapTotals {
3224    pub decisions: usize,
3225    pub decisions_with_missing_conditions: usize,
3226    pub conditions: usize,
3227    pub missing_conditions: usize,
3228}
3229
3230#[derive(Debug, Clone, PartialEq, Serialize)]
3231#[serde(rename_all = "camelCase")]
3232pub struct CoverageFileDecisionsData {
3233    pub run: String,
3234    pub filters: CoverageQueryFilters,
3235    pub file: String,
3236    pub group: String,
3237    pub sort: DecisionSort,
3238    pub totals: DecisionGapTotals,
3239    pub decisions: Vec<IndexedDecisionGap>,
3240}
3241
3242#[derive(Debug, Clone, Copy)]
3243pub struct CoverageFileDecisionsOptions<'a> {
3244    pub run: &'a str,
3245    pub view: CoverageViewId,
3246    pub kind: Option<&'a str>,
3247    pub runner: Option<&'a str>,
3248    pub file: &'a str,
3249    pub sort: DecisionSort,
3250    pub offset: usize,
3251    pub limit: usize,
3252}
3253
3254#[derive(Debug, Clone)]
3255pub struct CoverageDimensionQueryOptions<'a> {
3256    pub run: &'a str,
3257    pub view: CoverageViewId,
3258    pub dimension: CoverageDimension,
3259    pub filters: CoverageQueryFilters,
3260    pub offset: usize,
3261    pub limit: usize,
3262}
3263
3264pub fn coverage_dimension_query(
3265    index: &CoverageIndex<'_>,
3266    options: CoverageDimensionQueryOptions<'_>,
3267) -> Result<(CoverageDimensionQueryData, AgentPagination), QueryError> {
3268    let CoverageDimensionQueryOptions {
3269        run,
3270        view,
3271        dimension,
3272        filters,
3273        offset,
3274        limit,
3275    } = options;
3276    if limit == 0 {
3277        return Err(QueryError::InvalidPagination);
3278    }
3279    let values = index.dimensions(view, dimension)?;
3280    let total = values.len();
3281    let selected = values
3282        .into_iter()
3283        .skip(offset)
3284        .take(limit)
3285        .collect::<Vec<_>>();
3286    let returned = selected.len();
3287    let data = match dimension {
3288        // The tests together are one entry of the summary, never a listing.
3289        CoverageDimension::Kind | CoverageDimension::Tests => {
3290            CoverageDimensionQueryData::Kinds(CoverageKindsData {
3291                run: run.into(),
3292                filters,
3293                kinds: selected,
3294            })
3295        }
3296        CoverageDimension::Runner => CoverageDimensionQueryData::Runners(CoverageRunnersData {
3297            run: run.into(),
3298            filters,
3299            runners: selected,
3300        }),
3301    };
3302    Ok((data, pagination(offset, limit, returned, total)))
3303}
3304
3305#[derive(Debug, Clone, Copy)]
3306pub struct CoverageFileQueryOptions<'a> {
3307    pub run: &'a str,
3308    pub view: CoverageViewId,
3309    pub metric: MinimizeMetric,
3310    pub gaps_only: bool,
3311    pub kind: Option<&'a str>,
3312    pub runner: Option<&'a str>,
3313    pub offset: usize,
3314    pub limit: usize,
3315}
3316
3317fn gap_metric_value(gap: &IndexedFileGap, metric: MinimizeMetric) -> usize {
3318    match metric {
3319        MinimizeMetric::All => gap.score,
3320        MinimizeMetric::Lines => gap.uncovered_lines,
3321        MinimizeMetric::Statements => gap.uncovered_statements,
3322        MinimizeMetric::Functions => gap.uncovered_functions,
3323        MinimizeMetric::Branches => gap.missing_branches,
3324        MinimizeMetric::Mcdc => gap.missing_mcdc_conditions,
3325    }
3326}
3327
3328fn has_gap_for_metric(gap: &IndexedFileGap, metric: MinimizeMetric) -> bool {
3329    match metric {
3330        MinimizeMetric::All => {
3331            gap.uncovered_lines > 0
3332                || gap.uncovered_statements > 0
3333                || gap.uncovered_functions > 0
3334                || gap.missing_branches > 0
3335                || gap.missing_mcdc_conditions > 0
3336        }
3337        _ => gap_metric_value(gap, metric) > 0,
3338    }
3339}
3340
3341pub fn coverage_file_decisions_query(
3342    index: &CoverageIndex<'_>,
3343    options: CoverageFileDecisionsOptions<'_>,
3344) -> Result<(CoverageFileDecisionsData, AgentPagination), QueryError> {
3345    if options.limit == 0 {
3346        return Err(QueryError::InvalidPagination);
3347    }
3348    let all = index.decision_gaps(options.view, options.kind, options.runner, options.file)?;
3349    if (options.kind.is_some() || options.runner.is_some()) && all.is_empty() {
3350        // A file with no decisions is valid, so consult the file projection to
3351        // distinguish it from a nonexistent test provenance projection.
3352        if index
3353            .file_gaps(options.view, options.kind, options.runner)?
3354            .is_empty()
3355        {
3356            return Err(QueryError::TestFilterEmpty {
3357                kind: options.kind.map(str::to_owned),
3358                runner: options.runner.map(str::to_owned),
3359            });
3360        }
3361    }
3362    let totals = DecisionGapTotals {
3363        decisions: all.len(),
3364        decisions_with_missing_conditions: all
3365            .iter()
3366            .filter(|decision| decision.missing_conditions > 0)
3367            .count(),
3368        conditions: all.iter().map(|decision| decision.conditions).sum(),
3369        missing_conditions: all.iter().map(|decision| decision.missing_conditions).sum(),
3370    };
3371    let mut missing = all
3372        .into_iter()
3373        .filter(|decision| decision.missing_conditions > 0)
3374        .collect::<Vec<_>>();
3375    missing.sort_by(|left, right| match options.sort {
3376        DecisionSort::Missing => right
3377            .missing_conditions
3378            .cmp(&left.missing_conditions)
3379            .then_with(|| left.line.cmp(&right.line))
3380            .then_with(|| left.column.cmp(&right.column)),
3381        DecisionSort::Location => left
3382            .line
3383            .cmp(&right.line)
3384            .then_with(|| left.column.cmp(&right.column))
3385            .then_with(|| left.id.cmp(&right.id)),
3386    });
3387    let total = missing.len();
3388    let rows = missing
3389        .into_iter()
3390        .skip(options.offset)
3391        .take(options.limit)
3392        .collect::<Vec<_>>();
3393    let returned = rows.len();
3394    let filters = CoverageQueryFilters {
3395        outcome: match options.view {
3396            CoverageViewId::All => "all",
3397            CoverageViewId::Passed => "passed",
3398            CoverageViewId::Failed => "failed",
3399        }
3400        .into(),
3401        kind: options.kind.map(str::to_owned),
3402        runner: options.runner.map(str::to_owned),
3403    };
3404    Ok((
3405        CoverageFileDecisionsData {
3406            run: options.run.into(),
3407            filters,
3408            file: options.file.into(),
3409            group: "decision".into(),
3410            sort: options.sort,
3411            totals,
3412            decisions: rows,
3413        },
3414        pagination(options.offset, options.limit, returned, total),
3415    ))
3416}
3417
3418pub fn coverage_file_query(
3419    index: &CoverageIndex<'_>,
3420    options: CoverageFileQueryOptions<'_>,
3421) -> Result<CoverageFileQueryResult, QueryError> {
3422    let CoverageFileQueryOptions {
3423        run,
3424        view,
3425        metric,
3426        gaps_only,
3427        kind,
3428        runner,
3429        offset,
3430        limit,
3431    } = options;
3432    if limit == 0 {
3433        return Err(QueryError::InvalidPagination);
3434    }
3435    let mut files = index.file_gaps(view, kind, runner)?;
3436    if (kind.is_some() || runner.is_some()) && files.is_empty() {
3437        return Err(QueryError::TestFilterEmpty {
3438            kind: kind.map(str::to_owned),
3439            runner: runner.map(str::to_owned),
3440        });
3441    }
3442    if gaps_only {
3443        files.retain(|gap| has_gap_for_metric(gap, metric) || gap.measurement_limitations > 0);
3444    }
3445    files.sort_by(|left, right| {
3446        gap_metric_value(right, metric)
3447            .cmp(&gap_metric_value(left, metric))
3448            .then_with(|| {
3449                right
3450                    .measurement_limitations
3451                    .cmp(&left.measurement_limitations)
3452            })
3453            .then_with(|| left.file.cmp(&right.file))
3454    });
3455    let total = files.len();
3456    let page = files
3457        .into_iter()
3458        .skip(offset)
3459        .take(limit)
3460        .collect::<Vec<_>>();
3461    let page_info = pagination(offset, limit, page.len(), total);
3462    let filters = CoverageQueryFilters {
3463        outcome: match view {
3464            CoverageViewId::All => "all",
3465            CoverageViewId::Passed => "passed",
3466            CoverageViewId::Failed => "failed",
3467        }
3468        .into(),
3469        kind: kind.map(str::to_owned),
3470        runner: runner.map(str::to_owned),
3471    };
3472    let data = if gaps_only {
3473        CoverageFileQueryData::Gaps(CoverageGapsData {
3474            run: run.into(),
3475            filters,
3476            metric,
3477            gaps: page,
3478        })
3479    } else {
3480        CoverageFileQueryData::Files(CoverageFilesData {
3481            run: run.into(),
3482            filters,
3483            metric,
3484            files: page,
3485        })
3486    };
3487    Ok(CoverageFileQueryResult {
3488        data,
3489        pagination: page_info,
3490    })
3491}
3492
3493#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3494enum ObligationMetric {
3495    Lines,
3496    Statements,
3497    Functions,
3498    Branches,
3499    Mcdc,
3500}
3501
3502impl ObligationMetric {
3503    fn selected(self, metric: MinimizeMetric) -> bool {
3504        metric == MinimizeMetric::All
3505            || matches!(
3506                (self, metric),
3507                (Self::Lines, MinimizeMetric::Lines)
3508                    | (Self::Statements, MinimizeMetric::Statements)
3509                    | (Self::Functions, MinimizeMetric::Functions)
3510                    | (Self::Branches, MinimizeMetric::Branches)
3511                    | (Self::Mcdc, MinimizeMetric::Mcdc)
3512            )
3513    }
3514
3515    fn public(self) -> MinimizeMetric {
3516        match self {
3517            Self::Lines => MinimizeMetric::Lines,
3518            Self::Statements => MinimizeMetric::Statements,
3519            Self::Functions => MinimizeMetric::Functions,
3520            Self::Branches => MinimizeMetric::Branches,
3521            Self::Mcdc => MinimizeMetric::Mcdc,
3522        }
3523    }
3524}
3525
3526#[derive(Clone)]
3527struct Obligation {
3528    id: String,
3529    metric: ObligationMetric,
3530    /// Any one complete option satisfies this obligation.
3531    options: Vec<Vec<String>>,
3532}
3533
3534struct ObligationModel {
3535    obligations: Vec<Obligation>,
3536    setup_by_file: BTreeMap<String, Vec<String>>,
3537    tests_by_file: BTreeMap<String, Vec<String>>,
3538    background: Vec<String>,
3539}
3540
3541impl ObligationModel {
3542    fn expand(&self, selected: &BTreeSet<String>) -> BTreeSet<String> {
3543        let mut expanded = selected.clone();
3544        expanded.extend(self.background.iter().cloned());
3545        for (file, setup_ids) in &self.setup_by_file {
3546            if self
3547                .tests_by_file
3548                .get(file)
3549                .is_some_and(|tests| tests.iter().any(|id| selected.contains(id)))
3550            {
3551                expanded.extend(setup_ids.iter().cloned());
3552            }
3553        }
3554        expanded
3555    }
3556}
3557
3558fn deduplicate_options(options: impl IntoIterator<Item = Vec<String>>) -> Vec<Vec<String>> {
3559    let mut unique = BTreeMap::<String, Vec<String>>::new();
3560    for mut option in options {
3561        option.sort();
3562        option.dedup();
3563        let key = option.join("\0");
3564        unique.entry(key).or_insert(option);
3565    }
3566    unique.into_values().collect()
3567}
3568
3569fn evidence_choices(
3570    ids: &[Id],
3571    tests: &HashMap<&str, &crate::coverage_report::TestCoverageResult>,
3572    candidates: &BTreeSet<String>,
3573    tests_by_file: &BTreeMap<String, Vec<String>>,
3574) -> Vec<Vec<String>> {
3575    let mut choices = Vec::new();
3576    for id in ids {
3577        let Some(test) = tests.get(id.as_str()) else {
3578            continue;
3579        };
3580        if test.role == "background" {
3581            choices.push(Vec::new());
3582        } else if test.role == "setup" {
3583            if let Some(file) = &test.file {
3584                choices.extend(
3585                    tests_by_file
3586                        .get(file)
3587                        .into_iter()
3588                        .flatten()
3589                        .map(|candidate| vec![candidate.clone()]),
3590                );
3591            }
3592        } else if candidates.contains(id.as_str()) {
3593            choices.push(vec![id.to_string()]);
3594        }
3595    }
3596    deduplicate_options(choices)
3597}
3598
3599fn build_obligations(view: &CoverageView, candidates: &BTreeSet<String>) -> ObligationModel {
3600    let tests = view
3601        .tests
3602        .iter()
3603        .map(|test| (test.id.as_str(), test))
3604        .collect::<HashMap<_, _>>();
3605    let mut tests_by_file = BTreeMap::<String, Vec<String>>::new();
3606    let mut setup_by_file = BTreeMap::<String, Vec<String>>::new();
3607    let mut background = Vec::new();
3608    for test in &view.tests {
3609        match test.role.as_str() {
3610            "test" if candidates.contains(&test.id) => {
3611                if let Some(file) = &test.file {
3612                    tests_by_file
3613                        .entry(file.clone())
3614                        .or_default()
3615                        .push(test.id.clone());
3616                }
3617            }
3618            "setup" => {
3619                if let Some(file) = &test.file {
3620                    setup_by_file
3621                        .entry(file.clone())
3622                        .or_default()
3623                        .push(test.id.clone());
3624                }
3625            }
3626            "background" => background.push(test.id.clone()),
3627            _ => {}
3628        }
3629    }
3630    let choices = |ids: &[Id]| evidence_choices(ids, &tests, candidates, &tests_by_file);
3631    let mut obligations = Vec::new();
3632    let mut unique_lines = BTreeMap::new();
3633    for line in &view.lines {
3634        unique_lines.insert((line.file.as_str(), line.line), line);
3635    }
3636    for ((file, line), result) in unique_lines {
3637        obligations.push(Obligation {
3638            id: format!("line:{file}:{line}"),
3639            metric: ObligationMetric::Lines,
3640            options: choices(&result.tests),
3641        });
3642    }
3643    for point in &view.points {
3644        let (kind, metric) = match point.meta.kind {
3645            crate::coverage_analysis::PointKind::Statement => {
3646                ("statement", ObligationMetric::Statements)
3647            }
3648            crate::coverage_analysis::PointKind::Function => {
3649                ("function", ObligationMetric::Functions)
3650            }
3651        };
3652        obligations.push(Obligation {
3653            id: format!("{kind}:{}", point.meta.id),
3654            metric,
3655            options: choices(&point.tests),
3656        });
3657    }
3658    for branch in &view.branches {
3659        for alternative in &branch.alternatives {
3660            obligations.push(Obligation {
3661                id: format!("branch:{}:{}", branch.meta.id, alternative.id),
3662                metric: ObligationMetric::Branches,
3663                options: choices(&alternative.tests),
3664            });
3665        }
3666    }
3667    for decision in &view.decisions {
3668        for condition in 0..decision.meta.conditions.len() {
3669            let mut options = Vec::new();
3670            for left in 0..decision.vector_observations.len() {
3671                for right in (left + 1)..decision.vector_observations.len() {
3672                    let first = &decision.vector_observations[left];
3673                    let second = &decision.vector_observations[right];
3674                    if !is_independence_pair(&first.vector, &second.vector, condition) {
3675                        continue;
3676                    }
3677                    for first_choice in choices(&first.tests) {
3678                        for second_choice in choices(&second.tests) {
3679                            let mut combined = first_choice.clone();
3680                            combined.extend(second_choice);
3681                            options.push(combined);
3682                        }
3683                    }
3684                }
3685            }
3686            obligations.push(Obligation {
3687                id: format!("mcdc:{}:{condition}", decision.meta.id),
3688                metric: ObligationMetric::Mcdc,
3689                options: deduplicate_options(options),
3690            });
3691        }
3692    }
3693    ObligationModel {
3694        obligations,
3695        setup_by_file,
3696        tests_by_file,
3697        background,
3698    }
3699}
3700
3701fn percentage(metric: ObligationMetric, summary: &CoverageSummary) -> f64 {
3702    match metric {
3703        ObligationMetric::Lines => summary.lines.percentage,
3704        ObligationMetric::Statements => summary.statements.percentage,
3705        ObligationMetric::Functions => summary.functions.percentage,
3706        ObligationMetric::Branches => summary.branches.percentage,
3707        ObligationMetric::Mcdc => summary.condition_coverage_pct,
3708    }
3709}
3710
3711fn obligation_satisfied(obligation: &Obligation, selected: &BTreeSet<String>) -> bool {
3712    obligation
3713        .options
3714        .iter()
3715        .any(|option| option.iter().all(|test| selected.contains(test)))
3716}
3717
3718struct Search<'a> {
3719    obligations: &'a [Obligation],
3720    skip_limits: BTreeMap<ObligationMetric, usize>,
3721    best: BTreeSet<String>,
3722    explored_states: usize,
3723    max_states: usize,
3724    seen: BTreeSet<String>,
3725    candidate_tests: usize,
3726    target: f64,
3727    metric: MinimizeMetric,
3728}
3729
3730impl Search<'_> {
3731    fn visit(
3732        &mut self,
3733        selected: BTreeSet<String>,
3734        skipped: BTreeSet<String>,
3735        skipped_by_metric: BTreeMap<ObligationMetric, usize>,
3736    ) -> Result<(), QueryError> {
3737        self.explored_states += 1;
3738        if self.explored_states > self.max_states {
3739            return Err(QueryError::ComplexityLimit {
3740                candidate_tests: self.candidate_tests,
3741                obligations: self.obligations.len(),
3742                explored_states: self.explored_states,
3743                max_states: self.max_states,
3744                target: self.target,
3745                metric: self.metric,
3746            });
3747        }
3748        if selected.len() >= self.best.len() {
3749            return Ok(());
3750        }
3751        let state_key = format!(
3752            "{}|{}",
3753            selected.iter().cloned().collect::<Vec<_>>().join(","),
3754            skipped.iter().cloned().collect::<Vec<_>>().join(",")
3755        );
3756        if !self.seen.insert(state_key) {
3757            return Ok(());
3758        }
3759        let mut unmet = self
3760            .obligations
3761            .iter()
3762            .filter(|obligation| {
3763                !skipped.contains(&obligation.id) && !obligation_satisfied(obligation, &selected)
3764            })
3765            .collect::<Vec<_>>();
3766        if unmet.is_empty() {
3767            self.best = selected;
3768            return Ok(());
3769        }
3770        unmet.sort_by(|left, right| {
3771            let feasible = |obligation: &Obligation| {
3772                obligation
3773                    .options
3774                    .iter()
3775                    .filter(|option| option.iter().any(|test| !selected.contains(test)))
3776                    .count()
3777            };
3778            feasible(left)
3779                .cmp(&feasible(right))
3780                .then_with(|| left.id.cmp(&right.id))
3781        });
3782        let obligation = unmet[0];
3783        let mut additions = deduplicate_options(obligation.options.iter().filter_map(|option| {
3784            let addition = option
3785                .iter()
3786                .filter(|test| !selected.contains(*test))
3787                .cloned()
3788                .collect::<Vec<_>>();
3789            (!addition.is_empty()).then_some(addition)
3790        }));
3791        additions.sort_by(|left, right| {
3792            left.len()
3793                .cmp(&right.len())
3794                .then_with(|| left.join("\0").cmp(&right.join("\0")))
3795        });
3796        for addition in additions {
3797            if selected.len() + addition.len() >= self.best.len() {
3798                continue;
3799            }
3800            let mut next = selected.clone();
3801            next.extend(addition);
3802            self.visit(next, skipped.clone(), skipped_by_metric.clone())?;
3803        }
3804        let skipped_count = skipped_by_metric
3805            .get(&obligation.metric)
3806            .copied()
3807            .unwrap_or(0);
3808        if skipped_count
3809            < self
3810                .skip_limits
3811                .get(&obligation.metric)
3812                .copied()
3813                .unwrap_or(0)
3814        {
3815            let mut next_skipped = skipped;
3816            next_skipped.insert(obligation.id.clone());
3817            let mut next_counts = skipped_by_metric;
3818            next_counts.insert(obligation.metric, skipped_count + 1);
3819            self.visit(selected, next_skipped, next_counts)?;
3820        }
3821        Ok(())
3822    }
3823}
3824
3825pub fn minimum_test_set(
3826    view: &CoverageView,
3827    target: f64,
3828    metric: MinimizeMetric,
3829    max_states: usize,
3830) -> Result<MinimumTestSetResult, QueryError> {
3831    use crate::coverage_report::coverage_is_complete;
3832    if !target.is_finite() || !(0.0..=100.0).contains(&target) {
3833        return Err(QueryError::InvalidTarget(target));
3834    }
3835    if view.tests.iter().any(|test| {
3836        test.role == "background" && (!test.hits.is_empty() || !test.decisions.is_empty())
3837    }) {
3838        return Err(QueryError::UnattributedEvidence);
3839    }
3840    // And a test whose own coverage nothing recorded cannot be minimized
3841    // around either. Its empty hit set would read as a test that adds nothing
3842    // to any set, so the smallest set would drop it -- and what it covers is
3843    // precisely what nothing knows. The check above catches this whenever the
3844    // run-wide record holds anything, which is nearly always; this catches the
3845    // case where those tests happened to reach nothing measured.
3846    if view
3847        .tests
3848        .iter()
3849        .any(|test| test.role == "test" && !coverage_is_complete(&test.attribution))
3850    {
3851        return Err(QueryError::UnattributedEvidence);
3852    }
3853    let candidate_tests = view
3854        .tests
3855        .iter()
3856        .filter(|test| test.role == "test")
3857        .map(|test| test.id.clone())
3858        .collect::<BTreeSet<_>>();
3859    let model = build_obligations(view, &candidate_tests);
3860    let obligations = model
3861        .obligations
3862        .iter()
3863        .filter(|obligation| obligation.metric.selected(metric))
3864        .cloned()
3865        .collect::<Vec<_>>();
3866    let mut totals = BTreeMap::<ObligationMetric, usize>::new();
3867    for obligation in &obligations {
3868        *totals.entry(obligation.metric).or_default() += 1;
3869    }
3870    let metrics = [
3871        ObligationMetric::Lines,
3872        ObligationMetric::Statements,
3873        ObligationMetric::Functions,
3874        ObligationMetric::Branches,
3875        ObligationMetric::Mcdc,
3876    ]
3877    .into_iter()
3878    .filter(|candidate| candidate.selected(metric))
3879    .collect::<Vec<_>>();
3880    let skip_limits = metrics
3881        .iter()
3882        .map(|selected_metric| {
3883            let total = totals.get(selected_metric).copied().unwrap_or(0);
3884            let required = ((total as f64 * target) / 100.0).ceil() as usize;
3885            (*selected_metric, total.saturating_sub(required))
3886        })
3887        .collect::<BTreeMap<_, _>>();
3888    let full_expanded = model.expand(&candidate_tests);
3889    let full_summary = coverage_summary_for_tests(view, &full_expanded)?;
3890    for selected_metric in &metrics {
3891        let reachable = percentage(*selected_metric, &full_summary);
3892        if reachable + 1e-9 < target {
3893            return Err(QueryError::TargetUnreachable {
3894                metric: selected_metric.public(),
3895                target,
3896                reachable,
3897            });
3898        }
3899    }
3900    let mut search = Search {
3901        obligations: &obligations,
3902        skip_limits,
3903        best: candidate_tests.clone(),
3904        explored_states: 0,
3905        max_states,
3906        seen: BTreeSet::new(),
3907        candidate_tests: candidate_tests.len(),
3908        target,
3909        metric,
3910    };
3911    search.visit(BTreeSet::new(), BTreeSet::new(), BTreeMap::new())?;
3912    let expanded = model.expand(&search.best);
3913    let summary = coverage_summary_for_tests(view, &expanded)?;
3914    Ok(MinimumTestSetResult {
3915        optimal: true,
3916        target,
3917        metric,
3918        selected: search.best.into_iter().collect(),
3919        expanded: expanded.into_iter().collect(),
3920        summary,
3921        explored_states: search.explored_states,
3922    })
3923}
3924
3925#[cfg(test)]
3926mod tests {
3927    use crate::{
3928        coverage_analysis::McdcVector,
3929        coverage_report::{
3930            CoverageManifest, CoverageReport, CoverageReportRequest, DecisionMeta, ExitCodeInput,
3931            RawTestResult, RuntimeSnapshot, TestProvenance, analyze_coverage_results,
3932        },
3933    };
3934
3935    use super::*;
3936
3937    fn scope_entry(
3938        file: &str,
3939        status: &str,
3940        reason: &str,
3941        package: Option<&str>,
3942    ) -> IndexedScopeEntry {
3943        IndexedScopeEntry {
3944            file: file.into(),
3945            status: status.into(),
3946            reason: reason.into(),
3947            package_root: package.map(str::to_owned),
3948            measurement_limitations: 0,
3949            limitation_kinds: Vec::new(),
3950        }
3951    }
3952
3953    #[test]
3954    fn a_scope_is_grouped_by_place_and_names_the_roots_that_settle_it() {
3955        let unclassified = "unclassified first-party source";
3956        let entries = [
3957            scope_entry("components/a.tsx", "ambiguous", unclassified, None),
3958            scope_entry("components/ui/b.tsx", "ambiguous", unclassified, None),
3959            scope_entry("proxy.ts", "ambiguous", unclassified, None),
3960            scope_entry(
3961                "app/page.tsx",
3962                "included",
3963                "discovered package source root",
3964                None,
3965            ),
3966            scope_entry(
3967                "tests/a.test.ts",
3968                "excluded",
3969                "test or fixture source",
3970                None,
3971            ),
3972            scope_entry(
3973                "next.config.ts",
3974                "excluded",
3975                "build/test/tool configuration",
3976                None,
3977            ),
3978            scope_entry(
3979                "packages/ui/extra/c.ts",
3980                "ambiguous",
3981                unclassified,
3982                Some("packages/ui"),
3983            ),
3984            scope_entry(
3985                "packages/ui/src/d.ts",
3986                "included",
3987                "discovered package source root",
3988                Some("packages/ui"),
3989            ),
3990        ];
3991        let groups = scope_groups(&entries)
3992            .into_iter()
3993            .map(|group| (group.files, group.status, group.directory))
3994            .collect::<Vec<_>>();
3995        assert_eq!(
3996            groups,
3997            [
3998                (2, "ambiguous".to_owned(), "components".to_owned()),
3999                (1, "ambiguous".into(), ".".into()),
4000                (1, "ambiguous".into(), "packages/ui/extra".into()),
4001                (1, "included".into(), "app".into()),
4002                (1, "included".into(), "packages/ui/src".into()),
4003                (1, "excluded".into(), ".".into()),
4004                (1, "excluded".into(), "tests".into()),
4005            ]
4006        );
4007        // The roots discovery found, then each place: a directory by name,
4008        // and a file at the top by its own, never `.`.
4009        let hint =
4010            unclassified_source(&["app".into(), "packages/ui/src".into()], &entries).unwrap();
4011        assert_eq!(
4012            hint.roots,
4013            [
4014                "app",
4015                "packages/ui/src",
4016                "components",
4017                "packages/ui/extra",
4018                "proxy.ts"
4019            ]
4020        );
4021        assert_eq!(hint.directories[0].directory, "components");
4022        assert_eq!(hint.directories[0].files, 2);
4023        assert_eq!(unclassified_source(&[], &entries[3..6]), None);
4024    }
4025
4026    #[test]
4027    fn a_file_is_counted_under_its_first_folders() {
4028        assert_eq!(area_directory("app/api/cron/route.ts", 1), "app");
4029        assert_eq!(area_directory("app/api/cron/route.ts", 2), "app/api");
4030        assert_eq!(area_directory("app/page.tsx", 2), "app");
4031        assert_eq!(area_directory("proxy.ts", 2), ".");
4032    }
4033
4034    #[test]
4035    fn all_metric_keeps_statement_only_files_in_the_gap_set() {
4036        let gap = IndexedFileGap {
4037            view: CoverageViewId::All,
4038            file: "src/statement.js".into(),
4039            uncovered_lines: 0,
4040            uncovered_statements: 1,
4041            uncovered_functions: 0,
4042            missing_branches: 0,
4043            missing_mcdc_conditions: 0,
4044            measurement_limitations: 0,
4045            limitation_kinds: Vec::new(),
4046            covered_by_other_tests: crate::coverage_index::IndexedGapDimensions {
4047                lines: 0,
4048                statements: 0,
4049                functions: 0,
4050                branches: 0,
4051                mcdc_conditions: 0,
4052            },
4053            uncovered_everywhere: crate::coverage_index::IndexedGapDimensions {
4054                lines: 0,
4055                statements: 1,
4056                functions: 0,
4057                branches: 0,
4058                mcdc_conditions: 0,
4059            },
4060            totals: crate::coverage_index::IndexedGapDimensions {
4061                lines: 1,
4062                statements: 2,
4063                functions: 0,
4064                branches: 0,
4065                mcdc_conditions: 0,
4066            },
4067            covered: crate::coverage_index::IndexedGapDimensions {
4068                lines: 1,
4069                statements: 1,
4070                functions: 0,
4071                branches: 0,
4072                mcdc_conditions: 0,
4073            },
4074            score: 0,
4075        };
4076        assert!(has_gap_for_metric(&gap, MinimizeMetric::All));
4077        assert!(has_gap_for_metric(&gap, MinimizeMetric::Statements));
4078        assert!(!has_gap_for_metric(&gap, MinimizeMetric::Lines));
4079    }
4080
4081    fn result(id: &str, vector: McdcVector) -> RawTestResult {
4082        RawTestResult {
4083            test_id: Some(id.into()),
4084            scope: None,
4085            test: id.into(),
4086            test_file: Some("tests/permission.test.js".into()),
4087            title: None,
4088            retry: Some(0),
4089            status: Some("passed".into()),
4090            expected_status: None,
4091            flaky: false,
4092            provenance: TestProvenance {
4093                runner: "node:test".into(),
4094                kind: "unit".into(),
4095                project: None,
4096                source: "runner-default".into(),
4097            },
4098            role: "test".into(),
4099            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
4100            phases: Vec::new(),
4101            runtime: vec![RuntimeSnapshot {
4102                decisions: vec![crate::coverage_report::DecisionSnapshot {
4103                    meta: decision(),
4104                    vectors: vec![vector],
4105                }],
4106                hits: Vec::new(),
4107                events: Vec::new(),
4108                logicals: Vec::new(),
4109                phase_id: None,
4110            }],
4111            browser: Vec::new(),
4112            server: Vec::new(),
4113        }
4114    }
4115
4116    fn decision() -> DecisionMeta {
4117        DecisionMeta {
4118            id: "decision".into(),
4119            file: "src/permission.js".into(),
4120            line: 1,
4121            column: 1,
4122            source: "admin || owner".into(),
4123            conditions: vec!["admin".into(), "owner".into()],
4124            kind: "if".into(),
4125        }
4126    }
4127
4128    fn report(mut results: Vec<RawTestResult>) -> CoverageReport {
4129        analyze_coverage_results(&CoverageReportRequest {
4130            run_id: "run".into(),
4131            manifest: CoverageManifest {
4132                unmeasured: Vec::new(),
4133                decisions: vec![decision()],
4134                points: Vec::new(),
4135                branches: Vec::new(),
4136                limitations: Vec::new(),
4137                scope: None,
4138                assertion_sites: Vec::new(),
4139            },
4140            raw_results: std::mem::take(&mut results),
4141            generated_at: "time".into(),
4142            coverage_model: None,
4143            integrity: None,
4144            test_exit_code: ExitCodeInput::Present(Some(0)),
4145        })
4146        .unwrap()
4147    }
4148
4149    #[test]
4150    fn recomputes_mcdc_witnesses_and_removes_a_redundant_vector() {
4151        let report = report(vec![
4152            result(
4153                "admin",
4154                McdcVector {
4155                    values: vec![Some(true), None],
4156                    outcome: true,
4157                },
4158            ),
4159            result(
4160                "owner",
4161                McdcVector {
4162                    values: vec![Some(false), Some(true)],
4163                    outcome: true,
4164                },
4165            ),
4166            result(
4167                "both",
4168                McdcVector {
4169                    values: vec![Some(true), None],
4170                    outcome: true,
4171                },
4172            ),
4173            result(
4174                "neither",
4175                McdcVector {
4176                    values: vec![Some(false), Some(false)],
4177                    outcome: false,
4178                },
4179            ),
4180        ]);
4181        let minimized = minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 5_000).unwrap();
4182        assert_eq!(minimized.selected.len(), 3);
4183        assert!(minimized.selected.contains(&"owner".into()));
4184        assert!(minimized.selected.contains(&"neither".into()));
4185        assert_eq!(minimized.summary.condition_coverage_pct, 100.0);
4186    }
4187
4188    #[test]
4189    fn a_test_that_belongs_to_no_test_is_not_minimized_away() {
4190        // The first guard catches this whenever the run-wide record holds
4191        // anything, which is nearly always -- so it is the rare run, where
4192        // those tests happened to reach nothing measured, that reaches this
4193        // one. Without it the smallest set would quietly drop a test whose
4194        // coverage is unknown rather than absent, and report the drop as a
4195        // saving.
4196        let mut parallel = result(
4197            "parallel",
4198            McdcVector {
4199                values: vec![Some(false), Some(false)],
4200                outcome: false,
4201            },
4202        );
4203        parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
4204        assert!(matches!(
4205            minimum_test_set(
4206                &report(vec![parallel]).view,
4207                100.0,
4208                MinimizeMetric::Mcdc,
4209                5_000,
4210            ),
4211            Err(QueryError::UnattributedEvidence)
4212        ));
4213    }
4214
4215    #[test]
4216    fn the_summary_counts_each_test_under_the_attribution_it_carries() {
4217        // These three numbers are what the `Attribution` line reads out, and
4218        // nothing -- here or in the integration scripts -- had ever checked
4219        // that a test lands in the bucket it was written with.
4220        let vector = || McdcVector {
4221            values: vec![Some(false), Some(false)],
4222            outcome: false,
4223        };
4224        let mut serial = result("serial", vector());
4225        serial.attribution = crate::coverage_report::ATTRIBUTION_EXACT.into();
4226        let mut parallel = result("parallel", vector());
4227        parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
4228        let mut ruby = result("ruby", vector());
4229        ruby.attribution = crate::coverage_report::ATTRIBUTION_PARTIAL.into();
4230        let mut also_parallel = result("also-parallel", vector());
4231        also_parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
4232        let report = report(vec![serial, parallel, ruby, also_parallel]);
4233
4234        let nonce = std::time::SystemTime::now()
4235            .duration_since(std::time::UNIX_EPOCH)
4236            .unwrap()
4237            .as_nanos();
4238        let root = std::env::temp_dir().join(format!(
4239            "supercov-attribution-counts-{}-{nonce}",
4240            std::process::id()
4241        ));
4242        std::fs::create_dir(&root).unwrap();
4243        let path = root.join("query-index.v1.bin");
4244        let identity = crate::query_index::QueryIndexIdentity {
4245            evidence_sha256: [1; 32],
4246            evidence_bytes: 100,
4247            analysis_sha256: [2; 32],
4248            producer_sha256: [3; 32],
4249            archive_schema_version: 2,
4250        };
4251        crate::query_index::write_query_index(
4252            &crate::coverage_index::coverage_index_sections(&report).unwrap(),
4253            &identity,
4254            &path,
4255        )
4256        .unwrap();
4257        let container = crate::query_index::QueryIndex::open(&path, &identity).unwrap();
4258        let index = crate::coverage_index::CoverageIndex::new(&container).unwrap();
4259
4260        let summary = coverage_summary_query(
4261            &index,
4262            CoverageSummaryQueryOptions {
4263                run: "run",
4264                view: CoverageViewId::All,
4265                kind: None,
4266                runner: None,
4267                valid: true,
4268                test_exit_code: Some(0),
4269                stale: false,
4270                stale_reasons: Vec::new(),
4271            },
4272        )
4273        .unwrap();
4274        assert_eq!(summary.test_attribution.exact, 1);
4275        assert_eq!(summary.test_attribution.run_wide, 2);
4276        assert_eq!(summary.test_attribution.partial, 1);
4277        assert_eq!(summary.test_attribution.total(), 4);
4278        std::fs::remove_dir_all(&root).ok();
4279    }
4280
4281    #[test]
4282    fn refuses_background_evidence() {
4283        let mut aggregate = result(
4284            "aggregate",
4285            McdcVector {
4286                values: vec![Some(false), Some(false)],
4287                outcome: false,
4288            },
4289        );
4290        aggregate.role = "background".into();
4291        assert!(matches!(
4292            minimum_test_set(
4293                &report(vec![aggregate]).view,
4294                100.0,
4295                MinimizeMetric::Mcdc,
4296                5_000,
4297            ),
4298            Err(QueryError::UnattributedEvidence)
4299        ));
4300    }
4301
4302    #[test]
4303    fn bounds_the_exact_search() {
4304        let report = report(vec![
4305            result(
4306                "admin",
4307                McdcVector {
4308                    values: vec![Some(true), None],
4309                    outcome: true,
4310                },
4311            ),
4312            result(
4313                "owner",
4314                McdcVector {
4315                    values: vec![Some(false), Some(true)],
4316                    outcome: true,
4317                },
4318            ),
4319            result(
4320                "neither",
4321                McdcVector {
4322                    values: vec![Some(false), Some(false)],
4323                    outcome: false,
4324                },
4325            ),
4326        ]);
4327        assert!(matches!(
4328            minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 1),
4329            Err(QueryError::ComplexityLimit { .. })
4330        ));
4331    }
4332}