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