Skip to main content

supercov_engine/
coverage_report.rs

1//! Language-neutral reconstruction of coverage views from frozen obligations
2//! and per-attempt evidence.
3//!
4//! This module deliberately knows nothing about JavaScript or any test runner.
5//! Language frontends provide the manifest and normalized evidence records;
6//! Rust owns merging, attempt outcomes, attribution confidence, filtering and
7//! every structural coverage verdict.
8
9use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
10use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use supercov_contracts::{COVERAGE_MODEL_SCHEMA_VERSION, FrontendRunDeclaration};
15
16use crate::coverage_analysis::{
17    AnalysisError, BranchCoverage, CoverageCoreInput, CoverageSummary, DecisionCoverage,
18    McdcVector, PointCoverage, PointKind, analyze_core, find_witnesses_for_conditions,
19};
20use crate::evidence_archive::{EvidenceArchiveEntry, read_archive};
21use crate::interned::{FastMap, Id, Interner};
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase", deny_unknown_fields)]
25pub struct DecisionMeta {
26    pub id: String,
27    pub file: String,
28    pub line: usize,
29    pub column: usize,
30    pub source: String,
31    pub conditions: Vec<String>,
32    pub kind: String,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37pub struct PointMeta {
38    pub id: String,
39    pub kind: PointKind,
40    pub file: String,
41    pub line: usize,
42    pub column: usize,
43    pub source: String,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub label: Option<String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50pub struct BranchAlternativeMeta {
51    pub id: String,
52    pub label: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase", deny_unknown_fields)]
57pub struct BranchMeta {
58    pub id: String,
59    pub kind: String,
60    pub file: String,
61    pub line: usize,
62    pub column: usize,
63    pub source: String,
64    pub alternatives: Vec<BranchAlternativeMeta>,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct CoverageManifest {
70    pub decisions: Vec<DecisionMeta>,
71    pub points: Vec<PointMeta>,
72    pub branches: Vec<BranchMeta>,
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub limitations: Vec<Value>,
75    /// Obligation IDs the frontend declined to measure exactly. They stay in
76    /// the manifest so the report can say what was not measured, but they must
77    /// never be counted as uncovered.
78    #[serde(default, skip_serializing_if = "Vec::is_empty")]
79    pub unmeasured: Vec<String>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub scope: Option<Value>,
82    /// Statements in test code that carry an assertion marker. Test code is
83    /// not measured, so these are no obligation; they say where a passing
84    /// assertion stood.
85    #[serde(default, skip_serializing_if = "Vec::is_empty")]
86    pub assertion_sites: Vec<PointMeta>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase", deny_unknown_fields)]
91pub struct DecisionSnapshot {
92    pub meta: DecisionMeta,
93    pub vectors: Vec<McdcVector>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct RuntimeEvent {
99    #[serde(rename = "type")]
100    pub event_type: String,
101    pub id: String,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub vector: Option<McdcVector>,
104    pub timestamp_ms: i64,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub phase_id: Option<String>,
107    /// Legacy test-statement location retained for reading historical archives.
108    /// The assertion-map workflow does not emit test-statement markers.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub statement_id: Option<String>,
111    pub environment: String,
112}
113
114/// One observed (side selected, result truthy) pair of a value-position logical expression.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117pub struct LogicalVector {
118    /// The right operand was evaluated (the left did not short-circuit).
119    pub right: bool,
120    /// The selected result was truthy.
121    pub truthy: bool,
122}
123
124/// Distinct outcomes of one `logical-value` branch within a test; gives per-operand outcomes
125/// once combined with the operator recorded in the manifest.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase", deny_unknown_fields)]
128pub struct LogicalSnapshot {
129    pub id: String,
130    pub vectors: Vec<LogicalVector>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
134#[serde(rename_all = "camelCase", deny_unknown_fields)]
135pub struct RuntimeSnapshot {
136    #[serde(default)]
137    pub decisions: Vec<DecisionSnapshot>,
138    #[serde(default)]
139    pub hits: Vec<String>,
140    #[serde(default)]
141    pub events: Vec<RuntimeEvent>,
142    #[serde(default, skip_serializing_if = "Vec::is_empty")]
143    pub logicals: Vec<LogicalSnapshot>,
144    /// Every hit and decision vector of this snapshot is an explicit event of
145    /// this phase. A frontend that observes a whole phase at once -- Python
146    /// reads a phase's slot -- names the phase here instead of writing an
147    /// event per observation, which on a 3,900-test run was three quarters
148    /// of the evidence. The analysis reads it exactly as those events.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub phase_id: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase", deny_unknown_fields)]
155pub struct ExecutionScope {
156    pub version: usize,
157    pub run_id: String,
158    pub worker_id: String,
159    pub test_id: String,
160    pub test_key: String,
161    pub retry: usize,
162    pub attempt_id: String,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase", deny_unknown_fields)]
167pub struct ServerRecord {
168    #[serde(rename = "type")]
169    pub record_type: String,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub meta: Option<DecisionMeta>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub vector: Option<McdcVector>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub id: Option<String>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub timestamp_ms: Option<i64>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub phase_id: Option<String>,
180    /// The producing test statement, under this record's exact execution scope.
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub statement_id: Option<String>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub scope: Option<ExecutionScope>,
185    /// Made while the server handled a request that named no test -- a
186    /// webhook a mock server sends -- numbered per request in its process.
187    /// Only such work is credited by time: start-up and a test process's own
188    /// module loading are not a request.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub request: Option<u64>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase", deny_unknown_fields)]
195pub struct CoveragePhase {
196    pub id: String,
197    pub kind: String,
198    pub operation: String,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub source: Option<String>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub caused_by_phase_id: Option<String>,
203    pub started_at_ms: i64,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub ended_at_ms: Option<i64>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub status: Option<String>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub error: Option<String>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase", deny_unknown_fields)]
214pub struct TestProvenance {
215    pub runner: String,
216    pub kind: String,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub project: Option<String>,
219    pub source: String,
220}
221
222impl Default for TestProvenance {
223    fn default() -> Self {
224        Self {
225            runner: "unknown".into(),
226            kind: "unknown".into(),
227            project: None,
228            source: "unknown".into(),
229        }
230    }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase", deny_unknown_fields)]
235pub struct RawTestResult {
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub test_id: Option<String>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub scope: Option<ExecutionScope>,
240    pub test: String,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub test_file: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub title: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub retry: Option<usize>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub status: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub expected_status: Option<String>,
251    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
252    pub flaky: bool,
253    #[serde(default)]
254    pub provenance: TestProvenance,
255    #[serde(default = "default_test_role")]
256    pub role: String,
257    /// How exactly this test's coverage could be credited to it.
258    ///
259    /// `exact` is the ordinary case and the default, so a frontend that has
260    /// not been taught the difference keeps its behaviour. `run-wide` says the
261    /// test ran and reached code, and that nothing can say which code: a Go
262    /// test that called `t.Parallel()` stores into the same probe array as the
263    /// tests running beside it.
264    ///
265    /// The distinction is the whole point of the field. A test that reached
266    /// nothing and a test whose reach cannot be narrowed both have an empty
267    /// hit set, and reporting the second as the first is a wrong number
268    /// wearing the shape of a right one.
269    #[serde(default = "default_attribution")]
270    pub attribution: String,
271    #[serde(default)]
272    pub phases: Vec<CoveragePhase>,
273    #[serde(default)]
274    pub runtime: Vec<RuntimeSnapshot>,
275    #[serde(default)]
276    pub browser: Vec<RuntimeSnapshot>,
277    #[serde(default)]
278    pub server: Vec<ServerRecord>,
279}
280
281fn default_test_role() -> String {
282    "test".into()
283}
284
285fn default_attribution() -> String {
286    ATTRIBUTION_EXACT.into()
287}
288
289/// This test's coverage is its own: what it reached was credited to it.
290pub const ATTRIBUTION_EXACT: &str = "exact";
291
292/// This test ran, and what it reached was recorded against the run rather than
293/// against it. Its own reach is unknown, and bounded above by the run's.
294pub const ATTRIBUTION_RUN_WIDE: &str = "run-wide";
295
296/// What this test is recorded as reaching is really its own, and is not all of
297/// it: a lower bound, with the run's coverage as the upper one.
298///
299/// Ruby records a line against the first test that reaches it and never again,
300/// which is what makes its collection cheap. Every later test that runs the
301/// same line is recorded as having reached nothing there -- so a test's hits
302/// prove what it ran and its silence proves nothing.
303pub const ATTRIBUTION_PARTIAL: &str = "partial";
304
305/// Exact, and credited besides with server work that no request named: work
306/// an app server did while this test, and no other, was running -- a webhook
307/// a mock server sends, a job the test set off. Credited by the time it ran.
308///
309/// What the test is recorded as reaching is still all its own work, so its
310/// silence still means it did not run the code; the credited work may in
311/// rare cases be another test's, finished late, which adds to this test's
312/// reach and never hides any of it.
313pub const ATTRIBUTION_BY_TIME: &str = "by-time";
314
315/// Server work for a request that named no test -- a webhook a mock server
316/// sends -- done while one test, and only one, was running is that test's,
317/// credited by the time it ran. Where tests ran at once, or none was
318/// running (start-up, between tests), it stays no test's, and is returned.
319///
320/// A test's time is from its first phase to the end of its last: what it did
321/// is known to fall inside, and work after it is left alone rather than
322/// guessed at.
323fn credit_by_time(
324    raw_results: &mut [RawTestResult],
325    processes: Vec<Vec<ServerRecord>>,
326) -> Vec<ServerRecord> {
327    let windows = raw_results
328        .iter()
329        .enumerate()
330        .filter(|(_, raw)| raw.role != "background")
331        .filter_map(|(index, raw)| {
332            let start = raw.phases.iter().map(|phase| phase.started_at_ms).min()?;
333            let end = raw
334                .phases
335                .iter()
336                .map(|phase| phase.ended_at_ms.unwrap_or(phase.started_at_ms))
337                .max()?;
338            let attempt = raw.scope.as_ref().map(|scope| scope.attempt_id.clone());
339            Some((index, start, end, attempt))
340        })
341        .collect::<Vec<_>>();
342    let mut work = Vec::new();
343    for records in processes {
344        // The attempts this process served a request naming, and among those
345        // alone, where it served any: with a server per worker, the worker's
346        // test, whatever the other workers ran meanwhile. A process that
347        // served none -- one only an unmeasured mock calls -- is judged
348        // against every test running.
349        let served = records
350            .iter()
351            .filter(|record| record.record_type == "served")
352            .filter_map(|record| record.id.clone())
353            .collect::<std::collections::HashSet<_>>();
354        let candidates = windows
355            .iter()
356            .filter(|(_, _, _, attempt)| {
357                served.is_empty() || attempt.as_ref().is_some_and(|id| served.contains(id))
358            })
359            .collect::<Vec<_>>();
360        for record in records
361            .into_iter()
362            .filter(|record| record.record_type != "served")
363        {
364            let owner = record.request.and(record.timestamp_ms).and_then(|at| {
365                let mut running = candidates
366                    .iter()
367                    .filter(|(_, start, end, _)| *start <= at && at <= *end);
368                let only = running.next()?;
369                running.next().is_none().then_some(only.0)
370            });
371            work.push((owner, record));
372        }
373    }
374    let mut unattributed = Vec::new();
375    // A request's work appears once per request; what stays no test's is
376    // counted once.
377    let mut kept = std::collections::HashSet::new();
378    for (owner, record) in work {
379        match owner {
380            Some(index) => {
381                let raw = &mut raw_results[index];
382                raw.attribution = least_exact(&raw.attribution, ATTRIBUTION_BY_TIME);
383                raw.server.push(record);
384            }
385            None => {
386                let mut record = record;
387                record.request = None;
388                if kept.insert(serde_json::to_vec(&record).unwrap_or_default()) {
389                    unattributed.push(record);
390                }
391            }
392        }
393    }
394    unattributed
395}
396
397/// Of two attributions for one test, the one that claims less.
398fn least_exact(left: &str, right: &str) -> String {
399    let rank = |attribution: &str| match attribution {
400        ATTRIBUTION_RUN_WIDE => 3,
401        ATTRIBUTION_PARTIAL => 2,
402        ATTRIBUTION_BY_TIME => 1,
403        _ => 0,
404    };
405    if rank(right) > rank(left) {
406        right.to_owned()
407    } else {
408        left.to_owned()
409    }
410}
411
412/// Whether what this test is recorded as reaching is its own.
413///
414/// True for `exact` and for `partial`: both record real coverage of this
415/// test's, and a union over them is a union of things that happened. False
416/// only where the coverage went to the run instead, and counting the test in a
417/// percentage would divide by a test that contributes nothing.
418pub fn coverage_is_its_own(attribution: &str) -> bool {
419    attribution != ATTRIBUTION_RUN_WIDE
420}
421
422/// Whether this test's silence means it did not run the code.
423///
424/// Only `exact` earns that. Under `partial` a test that is recorded as
425/// reaching nothing may have run the same lines as the test that was credited
426/// with them, and under `run-wide` nothing was recorded at all -- so reading
427/// an absence as proof is how a change to code a test exercised comes back as
428/// "this test is unaffected".
429pub fn coverage_is_complete(attribution: &str) -> bool {
430    attribution == ATTRIBUTION_EXACT || attribution == ATTRIBUTION_BY_TIME || attribution.is_empty()
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
434#[serde(rename_all = "camelCase")]
435pub struct TestAttempt {
436    pub retry: usize,
437    pub status: String,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub expected_status: Option<String>,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
443#[serde(rename_all = "camelCase")]
444pub struct CoverageConfidence {
445    pub level: String,
446    pub setup_only: bool,
447    pub background_only: bool,
448    pub asserted: bool,
449    pub tests: Vec<Id>,
450    pub asserted_tests: Vec<Id>,
451    pub runners: Vec<String>,
452    pub kinds: Vec<String>,
453    pub e2e: bool,
454}
455
456#[derive(Debug, Clone, PartialEq, Serialize)]
457#[serde(rename_all = "camelCase")]
458pub struct VectorObservation {
459    pub vector: McdcVector,
460    pub tests: Vec<Id>,
461    #[serde(skip_serializing_if = "Vec::is_empty")]
462    pub phases: Vec<Id>,
463    #[serde(skip_serializing_if = "Vec::is_empty")]
464    pub explicit_phases: Vec<Id>,
465    pub confidence: CoverageConfidence,
466}
467
468#[derive(Debug, Clone, PartialEq, Serialize)]
469#[serde(rename_all = "camelCase")]
470pub struct ConditionResult {
471    pub index: usize,
472    pub source: String,
473    pub covered: bool,
474    pub assertion_covered: bool,
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub witness: Option<[McdcVector; 2]>,
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub witness_tests: Option<[Vec<Id>; 2]>,
479}
480
481#[derive(Debug, Clone, PartialEq, Serialize)]
482#[serde(rename_all = "camelCase")]
483pub struct DecisionResult {
484    pub meta: DecisionMeta,
485    pub executed: bool,
486    pub covered: bool,
487    pub vectors: Vec<McdcVector>,
488    pub vector_observations: Vec<VectorObservation>,
489    pub conditions: Vec<ConditionResult>,
490    pub tests: Vec<Id>,
491    pub confidence: CoverageConfidence,
492}
493
494#[derive(Debug, Clone, PartialEq, Serialize)]
495#[serde(rename_all = "camelCase")]
496pub struct PointResult {
497    pub meta: PointMeta,
498    pub covered: bool,
499    #[serde(skip)]
500    pub measured: bool,
501    pub tests: Vec<Id>,
502    pub phases: Vec<Id>,
503    pub confidence: CoverageConfidence,
504}
505
506#[derive(Debug, Clone, PartialEq, Serialize)]
507#[serde(rename_all = "camelCase")]
508pub struct AlternativeResult {
509    pub id: String,
510    pub label: String,
511    pub covered: bool,
512    pub tests: Vec<Id>,
513    pub phases: Vec<Id>,
514    pub confidence: CoverageConfidence,
515}
516
517#[derive(Debug, Clone, PartialEq, Serialize)]
518#[serde(rename_all = "camelCase")]
519pub struct BranchResult {
520    pub meta: BranchMeta,
521    pub covered: bool,
522    pub alternatives: Vec<AlternativeResult>,
523}
524
525#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
526pub struct SourceLine {
527    pub file: Id,
528    pub line: usize,
529}
530
531/// One line's share of the points that sit on it, while the view is built.
532#[derive(Debug, Default)]
533struct LineAggregate {
534    covered: bool,
535    measured: bool,
536    /// Numbers of the relation each is drawn from, in no order.
537    tests: Vec<u32>,
538    phases: Vec<u32>,
539    explicit_phases: Vec<u32>,
540}
541
542#[derive(Debug, Clone, PartialEq, Serialize)]
543#[serde(rename_all = "camelCase")]
544pub struct LineResult {
545    pub file: String,
546    pub line: usize,
547    pub covered: bool,
548    /// False when every obligation on this line was declined, which keeps the
549    /// line addressable while leaving it out of the covered/total counts. Not
550    /// serialized: it decides a total, it is not a fact about the line worth
551    /// publishing, and the limitation records already say why.
552    #[serde(skip)]
553    pub measured: bool,
554    pub tests: Vec<Id>,
555    pub runners: Vec<String>,
556    pub kinds: Vec<String>,
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub exclusive_kind: Option<String>,
559    pub phases: Vec<Id>,
560    pub confidence: CoverageConfidence,
561}
562
563#[derive(Debug, Clone, PartialEq, Serialize)]
564#[serde(rename_all = "camelCase")]
565pub struct TestDecisionResult {
566    pub id: String,
567    pub vectors: Vec<McdcVector>,
568}
569
570#[derive(Debug, Clone, PartialEq, Serialize)]
571#[serde(rename_all = "camelCase")]
572pub struct TestCoverageResult {
573    pub id: String,
574    pub name: String,
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub file: Option<String>,
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub title: Option<String>,
579    pub retries: Vec<usize>,
580    pub attempts: Vec<TestAttempt>,
581    pub outcome: String,
582    pub provenance: TestProvenance,
583    pub role: String,
584    /// `exact`, or `run-wide` when the test ran but nothing can say what it
585    /// reached. An empty `hits` means "reached nothing" only under `exact`.
586    pub attribution: String,
587    pub hits: Vec<Id>,
588    pub decisions: Vec<TestDecisionResult>,
589    pub lines: Vec<SourceLine>,
590}
591
592#[derive(Debug, Clone, PartialEq, Serialize)]
593#[serde(rename_all = "camelCase")]
594pub struct TestFileResult {
595    pub file: String,
596    pub tests: Vec<String>,
597    pub runners: Vec<String>,
598    pub kinds: Vec<String>,
599    pub lines: Vec<SourceLine>,
600}
601
602#[derive(Debug, Clone, PartialEq, Serialize)]
603#[serde(rename_all = "camelCase")]
604pub struct PhaseResult {
605    #[serde(flatten)]
606    pub phase: CoveragePhase,
607    pub test: Id,
608    pub hits: Vec<Id>,
609    pub decisions: Vec<TestDecisionResult>,
610    pub lines: Vec<SourceLine>,
611    pub browser_events: usize,
612    pub server_events: usize,
613    pub explicit_events: usize,
614    pub inferred_events: usize,
615    pub explicit_browser_events: usize,
616    pub inferred_browser_events: usize,
617    pub explicit_server_events: usize,
618    pub inferred_server_events: usize,
619}
620
621#[derive(Debug, Clone, PartialEq, Serialize)]
622#[serde(rename_all = "camelCase")]
623pub struct DimensionCoverage {
624    #[serde(skip_serializing_if = "Option::is_none")]
625    pub kind: Option<String>,
626    #[serde(skip_serializing_if = "Option::is_none")]
627    pub runner: Option<String>,
628    pub tests: usize,
629    pub setups: usize,
630    /// How many of `tests` have coverage of their own. The summary below is
631    /// computed over exactly these: a test whose reach nothing can narrow
632    /// contributes no hits, and counting it would report a suite that covers
633    /// everything as covering nothing.
634    pub attributed: usize,
635    pub summary: CoverageSummary,
636}
637
638#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
639#[serde(rename_all = "camelCase")]
640pub struct CoverageModel {
641    pub language: String,
642    pub name: String,
643    pub completeness_meaning: String,
644    pub measured: Vec<String>,
645    pub not_measured: Vec<String>,
646}
647
648#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
649#[serde(rename_all = "camelCase", deny_unknown_fields)]
650pub struct CoverageModelDeclaration {
651    pub language: String,
652    pub variant: String,
653    pub name: String,
654    pub completeness_meaning: String,
655    pub measured: Vec<String>,
656    pub not_measured: Vec<String>,
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
660#[serde(rename_all = "camelCase", deny_unknown_fields)]
661pub struct PersistedCoverageModel {
662    pub schema_version: u32,
663    pub language: String,
664    pub variant: String,
665    pub name: String,
666    pub completeness_meaning: String,
667    pub measured: Vec<String>,
668    pub not_measured: Vec<String>,
669}
670
671impl PersistedCoverageModel {
672    pub fn from_declaration(value: &CoverageModelDeclaration) -> Result<Self, &'static str> {
673        let persisted = Self {
674            schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
675            language: value.language.clone(),
676            variant: value.variant.clone(),
677            name: value.name.clone(),
678            completeness_meaning: value.completeness_meaning.clone(),
679            measured: value.measured.clone(),
680            not_measured: value.not_measured.clone(),
681        };
682        persisted.clone().into_declaration()?;
683        Ok(persisted)
684    }
685
686    fn into_declaration(self) -> Result<CoverageModelDeclaration, &'static str> {
687        if self.schema_version != COVERAGE_MODEL_SCHEMA_VERSION {
688            return Err("unsupported coverage model schema");
689        }
690        let valid_identifier = |value: &str| {
691            (1..=supercov_contracts::COVERAGE_MODEL_MAX_IDENTIFIER_BYTES).contains(&value.len())
692                && value.as_bytes()[0].is_ascii_lowercase()
693                && value.bytes().all(|byte| {
694                    byte.is_ascii_lowercase()
695                        || byte.is_ascii_digit()
696                        || matches!(byte, b'.' | b'+' | b'-')
697                })
698        };
699        let valid_description = |value: &str| {
700            !value.is_empty()
701                && value.trim().len() == value.len()
702                && value.len() <= supercov_contracts::COVERAGE_MODEL_MAX_DESCRIPTION_BYTES
703                && !value.chars().any(char::is_control)
704        };
705        if !valid_identifier(&self.language)
706            || !valid_identifier(&self.variant)
707            || !valid_description(&self.name)
708            || !valid_description(&self.completeness_meaning)
709            || self.measured.is_empty()
710            || self.measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
711            || self.not_measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
712            || self
713                .measured
714                .iter()
715                .chain(&self.not_measured)
716                .any(|item| !valid_description(item))
717        {
718            return Err("invalid coverage model declaration");
719        }
720        let measured = self.measured.iter().collect::<BTreeSet<_>>();
721        let not_measured = self.not_measured.iter().collect::<BTreeSet<_>>();
722        if measured.len() != self.measured.len()
723            || not_measured.len() != self.not_measured.len()
724            || !measured.is_disjoint(&not_measured)
725        {
726            return Err("invalid coverage model surface partition");
727        }
728        Ok(CoverageModelDeclaration {
729            language: self.language,
730            variant: self.variant,
731            name: self.name,
732            completeness_meaning: self.completeness_meaning,
733            measured: self.measured,
734            not_measured: self.not_measured,
735        })
736    }
737}
738
739#[derive(Debug, Clone, PartialEq, Serialize)]
740#[serde(rename_all = "camelCase")]
741pub struct CoverageView {
742    pub generated_at: String,
743    pub variant: String,
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub scope: Option<Value>,
746    pub model: CoverageModel,
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub integrity: Option<Value>,
749    pub limitations: Vec<Value>,
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub transport: Option<TransportStats>,
752    pub summary: CoverageSummary,
753    pub coverage_by_kind: Vec<DimensionCoverage>,
754    pub coverage_by_runner: Vec<DimensionCoverage>,
755    pub decisions: Vec<DecisionResult>,
756    pub points: Vec<PointResult>,
757    pub branches: Vec<BranchResult>,
758    pub tests: Vec<TestCoverageResult>,
759    pub test_files: Vec<TestFileResult>,
760    pub phases: Vec<PhaseResult>,
761    pub lines: Vec<LineResult>,
762}
763
764#[derive(Debug, Clone, PartialEq, Serialize)]
765#[serde(rename_all = "camelCase")]
766pub struct CoverageFilters {
767    pub passed: CoverageView,
768    pub failed: CoverageView,
769}
770
771#[derive(Debug, Clone, PartialEq, Serialize)]
772#[serde(rename_all = "camelCase")]
773pub struct CoverageReport {
774    #[serde(flatten)]
775    pub view: CoverageView,
776    #[serde(skip_serializing_if = "Option::is_none")]
777    pub execution: Option<ExecutionResult>,
778    pub filters: CoverageFilters,
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
782#[serde(rename_all = "camelCase")]
783pub struct ExecutionResult {
784    pub test_exit_code: Option<i32>,
785    pub valid: bool,
786}
787
788#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
789#[serde(rename_all = "camelCase")]
790pub struct TransportStats {
791    pub processes: usize,
792    pub child_launches: usize,
793    pub remote_launches: usize,
794    pub workspace_capabilities: usize,
795    /// Processes that ran the workspace without Supercov's settings -- in a
796    /// container or VM it was mounted into -- and set themselves up from it.
797    /// Their coverage counts for the run and is credited to no test.
798    pub guest_processes: usize,
799    pub scoped_server_records: usize,
800    pub background_server_records: usize,
801    pub corrupt_records: usize,
802    pub corrupt_files: usize,
803}
804
805#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
806#[serde(untagged)]
807enum ExecutionSafeArgument {
808    Text(String),
809    Digest(ExecutionArgumentDigest),
810}
811
812#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
813#[serde(rename_all = "camelCase", deny_unknown_fields)]
814struct ExecutionArgumentDigest {
815    bytes: usize,
816    sha256: String,
817}
818
819#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
820#[serde(rename_all = "camelCase", deny_unknown_fields)]
821struct ExecutionCommandSummary {
822    #[serde(default)]
823    executable: Option<ExecutionSafeArgument>,
824    arguments: Vec<ExecutionSafeArgument>,
825    argument_count: usize,
826}
827
828#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
829#[serde(
830    tag = "event",
831    rename_all = "kebab-case",
832    rename_all_fields = "camelCase",
833    deny_unknown_fields
834)]
835enum ExecutionTraceEvent {
836    Process {
837        at: String,
838        pid: u32,
839        ppid: u32,
840        cwd: String,
841        command: ExecutionCommandSummary,
842        #[serde(default)]
843        entrypoint: Option<String>,
844    },
845    ChildLaunch {
846        at: String,
847        pid: u32,
848        ppid: u32,
849        method: String,
850        command: ExecutionSafeArgument,
851    },
852    RemoteLaunch {
853        at: String,
854        pid: u32,
855        ppid: u32,
856        command: ExecutionCommandSummary,
857        guest_root: String,
858    },
859    WorkspaceCapability {
860        at: String,
861        pid: u32,
862        ppid: u32,
863        host_root: String,
864        guest_root: String,
865        cache_identities: Vec<String>,
866    },
867    GuestProcess {
868        at: String,
869        pid: u32,
870        ppid: u32,
871        guest_root: String,
872    },
873}
874
875impl ExecutionTraceEvent {
876    fn kind(&self) -> &'static str {
877        match self {
878            Self::Process { .. } => "process",
879            Self::ChildLaunch { .. } => "child-launch",
880            Self::RemoteLaunch { .. } => "remote-launch",
881            Self::WorkspaceCapability { .. } => "workspace-capability",
882            Self::GuestProcess { .. } => "guest-process",
883        }
884    }
885}
886
887#[derive(Debug, Clone, PartialEq, Deserialize)]
888#[serde(rename_all = "camelCase", deny_unknown_fields)]
889pub struct CoverageReportRequest {
890    pub run_id: String,
891    pub manifest: CoverageManifest,
892    pub raw_results: Vec<RawTestResult>,
893    pub generated_at: String,
894    #[serde(default, skip_serializing_if = "Option::is_none")]
895    pub coverage_model: Option<CoverageModelDeclaration>,
896    #[serde(skip_serializing_if = "Option::is_none")]
897    pub integrity: Option<Value>,
898    #[serde(default, deserialize_with = "deserialize_exit_code")]
899    pub test_exit_code: ExitCodeInput,
900}
901
902#[derive(Debug, Clone, PartialEq, Deserialize)]
903#[serde(rename_all = "camelCase", deny_unknown_fields)]
904pub struct ArchiveReportRequest {
905    pub archive_path: PathBuf,
906    pub run_id: String,
907    pub generated_at: String,
908    #[serde(skip_serializing_if = "Option::is_none")]
909    pub integrity: Option<Value>,
910    #[serde(default, deserialize_with = "deserialize_exit_code")]
911    pub test_exit_code: ExitCodeInput,
912}
913
914#[derive(Debug, Clone, PartialEq, Eq, Default)]
915pub enum ExitCodeInput {
916    #[default]
917    Missing,
918    Present(Option<i32>),
919}
920
921fn deserialize_exit_code<'de, D>(deserializer: D) -> Result<ExitCodeInput, D::Error>
922where
923    D: serde::Deserializer<'de>,
924{
925    Option::<i32>::deserialize(deserializer).map(ExitCodeInput::Present)
926}
927
928#[derive(Debug, Clone, PartialEq, Eq)]
929pub enum ReportError {
930    Analysis(AnalysisError),
931    DecisionAnalysis {
932        decision_id: String,
933        error: AnalysisError,
934    },
935    InvalidEvent(String),
936    InvalidServerRecord(String),
937    InvalidArchive(String),
938    MissingManifest,
939    InvalidJson {
940        path: String,
941        reason: String,
942    },
943    ScopeMismatch {
944        expected: String,
945        actual: String,
946    },
947    NoEvidence(String),
948}
949
950impl From<AnalysisError> for ReportError {
951    fn from(value: AnalysisError) -> Self {
952        Self::Analysis(value)
953    }
954}
955
956#[derive(Clone, Default)]
957struct OrderedVectors {
958    values: Vec<McdcVector>,
959    indexes: HashMap<String, usize>,
960}
961
962impl OrderedVectors {
963    fn insert(&mut self, vector: &McdcVector) -> usize {
964        let key = vector_key(vector);
965        if let Some(index) = self.indexes.get(&key) {
966            return *index;
967        }
968        let index = self.values.len();
969        self.values.push(vector.clone());
970        self.indexes.insert(key, index);
971        index
972    }
973}
974
975#[derive(Clone)]
976struct MutableObservation {
977    vector: McdcVector,
978    /// Numbers of the view's test, phase and explicit phase relations, as
979    /// they arrive: sorted and made unique when the view is written.
980    tests: Vec<u32>,
981    phases: Vec<u32>,
982    explicit_phases: Vec<u32>,
983}
984
985#[derive(Clone)]
986struct MutableTest {
987    id: String,
988    name: String,
989    file: Option<String>,
990    title: Option<String>,
991    retries: BTreeSet<usize>,
992    attempts: BTreeMap<usize, TestAttempt>,
993    unstarted: bool,
994    runner_reported_flaky: bool,
995    provenance: TestProvenance,
996    role: String,
997    attribution: String,
998    /// Hit numbers as they arrive, repeats included: sorted and made unique
999    /// once, when the view is written.
1000    hits: Vec<usize>,
1001    decisions: BTreeMap<String, OrderedVectors>,
1002}
1003
1004#[derive(Clone)]
1005struct MutablePhase {
1006    phase: CoveragePhase,
1007    test: Id,
1008    hits: Vec<usize>,
1009    decisions: BTreeMap<String, OrderedVectors>,
1010    browser_events: usize,
1011    server_events: usize,
1012    explicit_events: usize,
1013    inferred_events: usize,
1014    explicit_browser_events: usize,
1015    inferred_browser_events: usize,
1016    explicit_server_events: usize,
1017    inferred_server_events: usize,
1018}
1019
1020fn vector_key(vector: &McdcVector) -> String {
1021    let mut key = String::with_capacity(vector.values.len() + 2);
1022    for value in &vector.values {
1023        key.push(match value {
1024            None => '-',
1025            Some(false) => 'F',
1026            Some(true) => 'T',
1027        });
1028    }
1029    key.push(':');
1030    key.push(if vector.outcome { 'T' } else { 'F' });
1031    key
1032}
1033
1034fn sorted<T: Clone + Ord>(values: &BTreeSet<T>) -> Vec<T> {
1035    values.iter().cloned().collect()
1036}
1037
1038fn record_attempt(test: &mut MutableTest, raw: &RawTestResult) {
1039    let (Some(retry), Some(raw_status)) = (raw.retry, raw.status.as_ref()) else {
1040        return;
1041    };
1042    let previous = test.attempts.get(&retry);
1043    // A later record does not pass an attempt an earlier one failed: the same
1044    // tests run twice under one identity fail if either execution did.
1045    let status =
1046        if raw_status == "unknown" || previous.is_some_and(|attempt| attempt.status == "failed") {
1047            previous.map_or_else(|| raw_status.clone(), |attempt| attempt.status.clone())
1048        } else {
1049            raw_status.clone()
1050        };
1051    let expected_status = raw
1052        .expected_status
1053        .clone()
1054        .or_else(|| previous.and_then(|attempt| attempt.expected_status.clone()));
1055    test.attempts.insert(
1056        retry,
1057        TestAttempt {
1058            retry,
1059            status,
1060            expected_status,
1061        },
1062    );
1063}
1064
1065fn effective_attempt_status(attempt: &TestAttempt) -> &str {
1066    if attempt.expected_status.as_deref() == Some("failed") {
1067        match attempt.status.as_str() {
1068            "failed" => "passed",
1069            "passed" => "failed",
1070            status => status,
1071        }
1072    } else {
1073        attempt.status.as_str()
1074    }
1075}
1076
1077fn test_outcome(test: &MutableTest) -> String {
1078    let Some(terminal) = test.attempts.values().next_back() else {
1079        return if test.unstarted {
1080            "unstarted".into()
1081        } else {
1082            "unknown".into()
1083        };
1084    };
1085    let terminal_status = effective_attempt_status(terminal);
1086    if terminal_status == "passed"
1087        && (test.runner_reported_flaky
1088            || test
1089                .attempts
1090                .values()
1091                .take(test.attempts.len().saturating_sub(1))
1092                .any(|attempt| effective_attempt_status(attempt) != "passed"))
1093    {
1094        "flaky".into()
1095    } else {
1096        terminal_status.into()
1097    }
1098}
1099
1100fn raw_test_id(raw: &RawTestResult) -> &str {
1101    raw.test_id.as_deref().unwrap_or(&raw.test)
1102}
1103
1104/// Borrowed rather than copied: a view only reads its results, and copying
1105/// every result of a 3,900-test run to build the passing view cost a sixth of
1106/// the analysis.
1107pub fn passing_coverage_results(raw_results: &[RawTestResult]) -> Vec<&RawTestResult> {
1108    let mut attempts: BTreeMap<(String, usize), (BTreeSet<String>, bool)> = BTreeMap::new();
1109    for raw in raw_results {
1110        let Some(retry) = raw.retry else {
1111            continue;
1112        };
1113        let entry = attempts
1114            .entry((raw_test_id(raw).into(), retry))
1115            .or_default();
1116        if let Some(status) = &raw.status {
1117            entry.0.insert(status.clone());
1118        }
1119        entry.1 |= raw.expected_status.as_deref() == Some("failed");
1120    }
1121    let mut terminal_retries = BTreeMap::<String, usize>::new();
1122    for (test, retry) in attempts.keys() {
1123        terminal_retries
1124            .entry(test.clone())
1125            .and_modify(|value| *value = (*value).max(*retry))
1126            .or_insert(*retry);
1127    }
1128    let accepted = terminal_retries
1129        .into_iter()
1130        .filter_map(|(test, retry)| {
1131            let (statuses, expected_failure) = attempts.get(&(test.clone(), retry))?;
1132            (statuses.contains("passed") && !expected_failure).then_some((test, retry))
1133        })
1134        .collect::<BTreeSet<_>>();
1135    raw_results
1136        .iter()
1137        .filter(|raw| {
1138            raw.retry
1139                .is_some_and(|retry| accepted.contains(&(raw_test_id(raw).into(), retry)))
1140        })
1141        .collect()
1142}
1143
1144pub fn failed_coverage_results(raw_results: &[RawTestResult]) -> Vec<&RawTestResult> {
1145    let mut attempts = BTreeMap::<(String, usize), Vec<&RawTestResult>>::new();
1146    for raw in raw_results {
1147        if let Some(retry) = raw.retry {
1148            attempts
1149                .entry((raw_test_id(raw).to_owned(), retry))
1150                .or_default()
1151                .push(raw);
1152        }
1153    }
1154    let failed = attempts
1155        .into_iter()
1156        .filter_map(|(identity, records)| {
1157            // Runner reporters carry expected-status semantics while hook
1158            // evidence records often do not. Treat reporter records as the
1159            // authority for the attempt outcome, then retain every companion
1160            // evidence record only after the attempt is classified.
1161            let authoritative = records
1162                .iter()
1163                .copied()
1164                .filter(|raw| raw.expected_status.is_some())
1165                .collect::<Vec<_>>();
1166            let authoritative = if authoritative.is_empty() {
1167                records
1168            } else {
1169                authoritative
1170            };
1171            authoritative
1172                .iter()
1173                .any(
1174                    |raw| match (raw.status.as_deref(), raw.expected_status.as_deref()) {
1175                        (Some("failed"), Some("failed")) => false,
1176                        (Some("passed"), Some("failed")) => true,
1177                        (Some("failed"), _) => true,
1178                        _ => false,
1179                    },
1180                )
1181                .then_some(identity)
1182        })
1183        .collect::<BTreeSet<_>>();
1184    raw_results
1185        .iter()
1186        .filter(|raw| {
1187            raw.retry
1188                .is_some_and(|retry| failed.contains(&(raw_test_id(raw).into(), retry)))
1189        })
1190        .collect()
1191}
1192
1193pub(crate) fn javascript_coverage_model() -> CoverageModelDeclaration {
1194    CoverageModelDeclaration {
1195        language: "javascript".into(),
1196        variant: "masking-short-circuit".into(),
1197        name: "coverage-completeness-v2".into(),
1198        completeness_meaning: "Every obligation in the measured model was observed by at least one existing test; test assertions and product correctness are separate assumptions.".into(),
1199        measured: [
1200            "executable source lines",
1201            "executable statements",
1202            "function entries",
1203            "true and false outcomes of if, ternary, while, do/while, and classic for decisions",
1204            "true and false outcomes of every atomic condition in those decisions",
1205            "masking MC/DC independence for every atomic condition in those decisions",
1206            "short-circuit and right-evaluated selections for &&, ||, and ?? value expressions, including JSX",
1207            "short-circuit and evaluated alternatives for logical assignments and optional chains",
1208            "provided and default-evaluated parameter and destructuring values",
1209            "try success and catch entry",
1210            "zero and entered for-in/for-of loops",
1211            "entered switch cases, defaults, and implicit no-match alternatives",
1212        ]
1213        .into_iter()
1214        .map(str::to_owned)
1215        .collect(),
1216        not_measured: [
1217            "all input values or semantic input partitions",
1218            "all execution paths or ordering/concurrency interleavings",
1219            "destructuring defaults in classic for initializers (reported as blockers when discovered)",
1220            "the internal statements and decisions of runtime-generated eval/Function source",
1221            "mutation score or assertion fault-detection strength",
1222        ]
1223        .into_iter()
1224        .map(str::to_owned)
1225        .collect(),
1226    }
1227}
1228
1229/// One file's counts, derived by the same function that produced the run's
1230/// totals.
1231///
1232/// Every feature that judges a run -- a threshold gate, a changed-line check,
1233/// an export, an HTML report -- has to agree with the summary it sits beside.
1234/// Re-deriving a file's denominator independently is how a gate and a report
1235/// come to disagree about the same run, so this filters the same results and
1236/// calls the same core.
1237pub fn coverage_summary_for_file(
1238    view: &CoverageView,
1239    file: &str,
1240) -> Result<CoverageSummary, ReportError> {
1241    let decisions = view
1242        .decisions
1243        .iter()
1244        .filter(|decision| decision.meta.file == file)
1245        .cloned()
1246        .collect::<Vec<_>>();
1247    let points = view
1248        .points
1249        .iter()
1250        .filter(|point| point.meta.file == file)
1251        .cloned()
1252        .collect::<Vec<_>>();
1253    let branches = view
1254        .branches
1255        .iter()
1256        .filter(|branch| branch.meta.file == file)
1257        .cloned()
1258        .collect::<Vec<_>>();
1259    let lines = view
1260        .lines
1261        .iter()
1262        .filter(|line| line.file == file)
1263        .cloned()
1264        .collect::<Vec<_>>();
1265    summary_for_results(&decisions, &points, &branches, &lines, None)
1266}
1267
1268/// The entries whose coverage a kind's row counts: every test of the view,
1269/// and the setup scopes of a kind that has tests. A kind's row counts both.
1270/// Leaving a test file's own setup out of this put one line in the `unit` row
1271/// and in `no test`, and the rows added up to more than the run's total. A
1272/// setup scope of a kind with no test has no row: a server started there is
1273/// in the total only, and that is what `no test` is for.
1274pub(crate) fn entries_in_kind_rows(view: &CoverageView) -> BTreeSet<String> {
1275    let with_tests = view
1276        .tests
1277        .iter()
1278        .filter(|test| test.role == "test")
1279        .map(|test| test.provenance.kind.as_str())
1280        .collect::<BTreeSet<_>>();
1281    view.tests
1282        .iter()
1283        .filter(|test| {
1284            coverage_is_its_own(&test.attribution)
1285                && (test.role == "test"
1286                    || (test.role == "setup" && with_tests.contains(test.provenance.kind.as_str())))
1287        })
1288        .map(|test| test.id.clone())
1289        .collect()
1290}
1291
1292/// What the tests of a view cover between them, leaving out what ran while
1293/// no test was running: start-up, module loading, work between tests.
1294pub(crate) fn summary_for_tests(view: &CoverageView) -> Result<CoverageSummary, ReportError> {
1295    let selected = entries_in_kind_rows(view);
1296    summary_for_results(
1297        &view.decisions,
1298        &view.points,
1299        &view.branches,
1300        &view.lines,
1301        Some(&selected),
1302    )
1303}
1304
1305fn summary_for_results(
1306    decisions: &[DecisionResult],
1307    points: &[PointResult],
1308    branches: &[BranchResult],
1309    lines: &[LineResult],
1310    test_ids: Option<&BTreeSet<String>>,
1311) -> Result<CoverageSummary, ReportError> {
1312    // Hashed once: every obligation asks it about each of its tests.
1313    let test_ids = test_ids.map(|ids| {
1314        ids.iter()
1315            .map(String::as_str)
1316            .collect::<crate::interned::FastSet<&str>>()
1317    });
1318    let includes = |tests: &[Id], covered: bool| {
1319        test_ids.as_ref().map_or(covered, |selected| {
1320            tests.iter().any(|test| selected.contains(test.as_str()))
1321        })
1322    };
1323    let input = CoverageCoreInput {
1324        decisions: decisions
1325            .iter()
1326            .map(|decision| DecisionCoverage {
1327                condition_count: decision.meta.conditions.len(),
1328                vectors: decision
1329                    .vector_observations
1330                    .iter()
1331                    .filter(|observation| includes(&observation.tests, true))
1332                    .map(|observation| observation.vector.clone())
1333                    .collect(),
1334            })
1335            .collect(),
1336        points: points
1337            .iter()
1338            .map(|point| PointCoverage {
1339                kind: point.meta.kind.clone(),
1340                covered: includes(&point.tests, point.covered),
1341            })
1342            .collect(),
1343        branches: branches
1344            .iter()
1345            .map(|branch| BranchCoverage {
1346                kind: branch.meta.kind.clone(),
1347                alternatives: branch
1348                    .alternatives
1349                    .iter()
1350                    .map(|alternative| includes(&alternative.tests, alternative.covered))
1351                    .collect(),
1352            })
1353            .collect(),
1354        // A line every frontend declined carries no coverage question, so it
1355        // is absent from the total rather than counted as uncovered.
1356        lines: lines
1357            .iter()
1358            .filter(|line| line.measured)
1359            .map(|line| includes(&line.tests, line.covered))
1360            .collect(),
1361    };
1362    Ok(analyze_core(&input)?.summary)
1363}
1364
1365/// Recompute every structural coverage metric for an arbitrary set of test,
1366/// setup, and background evidence identities. MC/DC witnesses are rebuilt
1367/// from the selected observations; existing aggregate verdicts are never
1368/// reused.
1369pub fn coverage_summary_for_tests(
1370    view: &CoverageView,
1371    test_ids: &BTreeSet<String>,
1372) -> Result<CoverageSummary, ReportError> {
1373    summary_for_results(
1374        &view.decisions,
1375        &view.points,
1376        &view.branches,
1377        &view.lines,
1378        Some(test_ids),
1379    )
1380}
1381
1382/// Whether a manifest limitation blocks measurement of the denominator, as
1383/// opposed to declaring a boundary of it. Absent means blocking.
1384pub fn blocking_limitation(limitation: &Value) -> bool {
1385    limitation
1386        .get("blocking")
1387        .and_then(Value::as_bool)
1388        .unwrap_or(true)
1389}
1390
1391fn hash_slot<'m, V: Default, S: std::hash::BuildHasher>(
1392    map: &'m mut HashMap<String, V, S>,
1393    key: &str,
1394) -> &'m mut V {
1395    if !map.contains_key(key) {
1396        map.insert(key.to_owned(), V::default());
1397    }
1398    map.get_mut(key).expect("slot was inserted")
1399}
1400
1401fn tree_slot<'m, V: Default>(map: &'m mut BTreeMap<String, V>, key: &str) -> &'m mut V {
1402    if !map.contains_key(key) {
1403        map.insert(key.to_owned(), V::default());
1404    }
1405    map.get_mut(key).expect("slot was inserted")
1406}
1407
1408/// The phase an event belongs to: its own, or the one it falls in by time.
1409fn correlate_event<'a>(
1410    event: &'a RuntimeEvent,
1411    ordered_phases: &'a [CoveragePhase],
1412) -> Option<&'a str> {
1413    event
1414        .phase_id
1415        .as_deref()
1416        .or_else(|| phase_at(ordered_phases, event.timestamp_ms))
1417}
1418
1419/// The phase an event with none of its own falls in: the last to start at or
1420/// before it. Times are whole milliseconds, and one phase often ends in the
1421/// millisecond the next begins: `page.goto` resolves and the test's `expect`
1422/// starts. What a page ran while it loaded, stamped with that millisecond,
1423/// went to the `expect`, and now and then all of it did. Work a phase waited
1424/// for comes before the phase ends, so in that millisecond it is the ending
1425/// phase's.
1426fn phase_at(ordered_phases: &[CoveragePhase], timestamp_ms: i64) -> Option<&str> {
1427    let started = ordered_phases
1428        .iter()
1429        .take_while(|phase| phase.started_at_ms <= timestamp_ms)
1430        .count();
1431    let last = ordered_phases[..started].last()?;
1432    let handed_over = last.started_at_ms == timestamp_ms
1433        && started >= 2
1434        && ordered_phases[started - 2].ended_at_ms == Some(timestamp_ms)
1435        && ordered_phases[started - 2].started_at_ms < timestamp_ms;
1436    Some(
1437        if handed_over {
1438            &ordered_phases[started - 2]
1439        } else {
1440            last
1441        }
1442        .id
1443        .as_str(),
1444    )
1445}
1446
1447/// One lookup per hit while ingesting evidence. All hit relations share these
1448/// dense numbers, avoiding repeated string hashing and tree comparisons. Even
1449/// IDs absent from the manifest are retained; output restores lexical ordering.
1450#[derive(Default)]
1451struct HitIds {
1452    names: Vec<Id>,
1453    numbers: FastMap<String, usize>,
1454}
1455
1456impl HitIds {
1457    fn number(&mut self, name: &str, ids: &mut Interner) -> usize {
1458        if let Some(number) = self.numbers.get(name) {
1459            return *number;
1460        }
1461        let number = self.names.len();
1462        self.names.push(ids.id(name));
1463        self.numbers.insert(name.to_owned(), number);
1464        number
1465    }
1466
1467    /// Each hit number's position in id order: hits sort by these numbers
1468    /// instead of by comparing their text, per test and per phase.
1469    fn ranks(&self) -> Vec<u32> {
1470        let mut order = (0..self.names.len()).collect::<Vec<_>>();
1471        order.sort_unstable_by(|left, right| self.names[*left].cmp(&self.names[*right]));
1472        let mut rank = vec![0_u32; self.names.len()];
1473        for (position, number) in order.into_iter().enumerate() {
1474            rank[number] = position as u32;
1475        }
1476        rank
1477    }
1478
1479    /// `sorted`, by precomputed rank.
1480    fn sorted_by(&self, mut numbers: Vec<usize>, rank: &[u32]) -> Vec<Id> {
1481        numbers.sort_unstable_by_key(|number| rank[*number]);
1482        numbers.dedup();
1483        numbers
1484            .into_iter()
1485            .map(|number| self.names[number].clone())
1486            .collect()
1487    }
1488
1489    #[cfg(test)]
1490    fn sorted(&self, numbers: BTreeSet<usize>) -> Vec<Id> {
1491        let mut names = numbers
1492            .into_iter()
1493            .map(|n| self.names[n].clone())
1494            .collect::<Vec<_>>();
1495        names.sort_unstable();
1496        names
1497    }
1498}
1499
1500/// Which tests or phases reached each obligation. Both sides are numbered
1501/// while ingesting evidence, then restored to sorted ID sets for the report.
1502#[derive(Default)]
1503struct References {
1504    names: Vec<Id>,
1505    numbers: FastMap<Id, u32>,
1506    last: Option<u32>,
1507    by_obligation: Vec<Vec<u32>>,
1508}
1509
1510impl References {
1511    /// The number standing for a test or phase id. The same id usually
1512    /// arrives many times in a row, so the last one is checked first.
1513    fn number(&mut self, name: &str, ids: &mut Interner) -> u32 {
1514        if let Some(number) = self.last
1515            && self.names[number as usize] == name
1516        {
1517            return number;
1518        }
1519        let number = match self.numbers.get(name) {
1520            Some(number) => *number,
1521            None => {
1522                let number = self.names.len() as u32;
1523                let id = ids.id(name);
1524                self.names.push(id.clone());
1525                self.numbers.insert(id, number);
1526                number
1527            }
1528        };
1529        self.last = Some(number);
1530        number
1531    }
1532
1533    fn add(&mut self, obligation: usize, name: &str, ids: &mut Interner) {
1534        let number = self.number(name, ids);
1535        if self.by_obligation.len() <= obligation {
1536            self.by_obligation.resize_with(obligation + 1, Vec::new);
1537        }
1538        let reached = &mut self.by_obligation[obligation];
1539        if reached.last() != Some(&number) {
1540            reached.push(number);
1541        }
1542    }
1543
1544    fn into_relation(self, hits: &HitIds) -> Relation {
1545        let mut order = (0..self.names.len()).collect::<Vec<_>>();
1546        order.sort_unstable_by(|left, right| self.names[*left].cmp(&self.names[*right]));
1547        let mut rank = vec![0_u32; self.names.len()];
1548        for (position, number) in order.into_iter().enumerate() {
1549            rank[number] = position as u32;
1550        }
1551        let mut relation = Relation {
1552            names: self.names,
1553            rank,
1554            by_hit: HashMap::new(),
1555            none: Reached::default(),
1556        };
1557        for (obligation, reached) in self.by_obligation.into_iter().enumerate() {
1558            if !reached.is_empty() {
1559                let reached = relation.reached(reached);
1560                relation
1561                    .by_hit
1562                    .insert(hits.names[obligation].to_string(), reached);
1563            }
1564        }
1565        relation
1566    }
1567}
1568
1569/// A `References` restored for the report: what reached each obligation,
1570/// and the table its numbers index.
1571struct Relation {
1572    names: Vec<Id>,
1573    /// Each number's position in id order.
1574    rank: Vec<u32>,
1575    by_hit: HashMap<String, Reached>,
1576    none: Reached,
1577}
1578
1579impl Relation {
1580    /// What reached obligation `id`: nothing, for one no test reached.
1581    fn of(&self, id: &str) -> &Reached {
1582        self.by_hit.get(id).unwrap_or(&self.none)
1583    }
1584
1585    /// Numbers of this relation as the ids they stand for, in id order and
1586    /// once each: the set the report writes.
1587    fn reached(&self, mut numbers: Vec<u32>) -> Reached {
1588        numbers.sort_unstable_by_key(|number| self.rank[*number as usize]);
1589        numbers.dedup();
1590        Reached {
1591            names: numbers
1592                .iter()
1593                .map(|number| self.names[*number as usize].clone())
1594                .collect(),
1595            numbers,
1596        }
1597    }
1598}
1599
1600/// Which tests or phases reached one obligation: their ids in id order, and
1601/// beside each the number its relation gave it.
1602#[derive(Default)]
1603struct Reached {
1604    names: Vec<Id>,
1605    numbers: Vec<u32>,
1606}
1607
1608/// What confidence reads of one test.
1609struct TestTraits<'t> {
1610    setup: bool,
1611    background: bool,
1612    runner: &'t str,
1613    kind: &'t str,
1614}
1615
1616impl<'t> TestTraits<'t> {
1617    fn of(test: &'t MutableTest) -> Self {
1618        Self {
1619            setup: test.role == "setup",
1620            background: test.role == "background",
1621            runner: &test.provenance.runner,
1622            kind: &test.provenance.kind,
1623        }
1624    }
1625}
1626
1627/// How a point, alternative, line or vector was reached, from the tests
1628/// and phases that reached it: read from per-number tables, since a run
1629/// mentions its tests millions of times and a lookup per mention hashed an
1630/// id each time.
1631fn confidence_from(
1632    tests: &Reached,
1633    phases: &Reached,
1634    explicit: &Reached,
1635    test_traits: &[Option<TestTraits<'_>>],
1636    phase_kinds: &[Option<&str>],
1637    explicit_kinds: &[Option<&str>],
1638) -> CoverageConfidence {
1639    let mut has_test = false;
1640    let mut setup_roles = true;
1641    let mut background_roles = true;
1642    let mut runners = BTreeSet::new();
1643    let mut kinds = BTreeSet::new();
1644    for number in &tests.numbers {
1645        if let Some(test) = &test_traits[*number as usize] {
1646            has_test = true;
1647            setup_roles &= test.setup;
1648            background_roles &= test.background;
1649            runners.insert(test.runner);
1650            kinds.insert(test.kind);
1651        }
1652    }
1653    let mut has_phase = false;
1654    let mut setup_phases = true;
1655    let mut background_phases = true;
1656    for number in &phases.numbers {
1657        if let Some(kind) = phase_kinds[*number as usize] {
1658            has_phase = true;
1659            setup_phases &= kind == "setup";
1660            background_phases &= kind == "background";
1661        }
1662    }
1663    let has_action = explicit
1664        .numbers
1665        .iter()
1666        .any(|number| explicit_kinds[*number as usize] == Some("action"));
1667    let level = if tests.names.is_empty() {
1668        "unexecuted"
1669    } else if has_action {
1670        "action"
1671    } else {
1672        "executed"
1673    };
1674    CoverageConfidence {
1675        level: level.into(),
1676        setup_only: if has_phase {
1677            setup_phases
1678        } else {
1679            has_test && setup_roles
1680        },
1681        background_only: if has_phase {
1682            background_phases
1683        } else {
1684            has_test && background_roles
1685        },
1686        asserted: false,
1687        tests: tests.names.clone(),
1688        asserted_tests: vec![],
1689        runners: runners.into_iter().map(str::to_owned).collect(),
1690        e2e: kinds.contains("e2e"),
1691        kinds: kinds.into_iter().map(str::to_owned).collect(),
1692    }
1693}
1694
1695pub fn create_coverage_view(
1696    manifest: &CoverageManifest,
1697    raw_results: &[RawTestResult],
1698    generated_at: &str,
1699) -> Result<CoverageView, ReportError> {
1700    create_coverage_view_with_model(
1701        manifest,
1702        &raw_results.iter().collect::<Vec<_>>(),
1703        generated_at,
1704        &javascript_coverage_model(),
1705        &mut Interner::default(),
1706    )
1707}
1708
1709fn create_coverage_view_with_model(
1710    manifest: &CoverageManifest,
1711    raw_results: &[&RawTestResult],
1712    generated_at: &str,
1713    coverage_model: &CoverageModelDeclaration,
1714    ids: &mut Interner,
1715) -> Result<CoverageView, ReportError> {
1716    let mut decision_metadata = manifest.decisions.clone();
1717    let decision_indexes = decision_metadata
1718        .iter()
1719        .enumerate()
1720        .map(|(index, meta)| (meta.id.clone(), index))
1721        .collect::<HashMap<_, _>>();
1722    let manifest_files = manifest
1723        .decisions
1724        .iter()
1725        .map(|meta| meta.file.clone())
1726        .chain(manifest.points.iter().map(|meta| meta.file.clone()))
1727        .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
1728        .chain(
1729            manifest
1730                .scope
1731                .as_ref()
1732                .and_then(|scope| scope.get("entries"))
1733                .and_then(Value::as_array)
1734                .into_iter()
1735                .flatten()
1736                .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
1737                .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
1738        )
1739        .collect::<BTreeSet<_>>();
1740    let mut vectors_by_decision = FastMap::<String, Vec<MutableObservation>>::default();
1741    let mut vector_indexes = FastMap::<String, FastMap<String, usize>>::default();
1742    // Numbers of `tests_by_hit`'s tests, as they arrive.
1743    let mut tests_by_decision = FastMap::<String, Vec<u32>>::default();
1744    let mut tests_by_hit = References::default();
1745    let mut hit_ids = HitIds::default();
1746    let mut tests_by_id = FastMap::<Id, MutableTest>::default();
1747    let mut test_order = Vec::<Id>::new();
1748    let mut phases_by_id = FastMap::<Id, MutablePhase>::default();
1749    let mut phases_by_hit = References::default();
1750    let mut explicit_phases_by_hit = References::default();
1751
1752    for raw in raw_results {
1753        let id = ids.id(raw_test_id(raw));
1754        if !tests_by_id.contains_key(&id) {
1755            test_order.push(id.clone());
1756            tests_by_id.insert(
1757                id.clone(),
1758                MutableTest {
1759                    id: id.to_string(),
1760                    name: raw.test.clone(),
1761                    file: raw.test_file.clone(),
1762                    title: raw.title.clone(),
1763                    retries: raw.retry.into_iter().collect(),
1764                    attempts: BTreeMap::new(),
1765                    unstarted: raw.status.as_deref() == Some("unstarted"),
1766                    runner_reported_flaky: raw.flaky,
1767                    provenance: raw.provenance.clone(),
1768                    role: raw.role.clone(),
1769                    attribution: raw.attribution.clone(),
1770                    hits: Vec::new(),
1771                    decisions: BTreeMap::new(),
1772                },
1773            );
1774        }
1775        let test = tests_by_id.get_mut(&id).expect("test was inserted");
1776        // A test's records can disagree about how exactly its coverage is its
1777        // own, and the least exact one is true of the whole: Playwright's
1778        // outcome record says nothing about coverage and often arrives first,
1779        // before the record that says a worker could not be read.
1780        test.attribution = least_exact(&test.attribution, &raw.attribution);
1781        test.unstarted |= raw.status.as_deref() == Some("unstarted");
1782        if let Some(retry) = raw.retry {
1783            test.retries.insert(retry);
1784        }
1785        test.runner_reported_flaky |= raw.flaky;
1786        record_attempt(test, raw);
1787
1788        let mut ordered_phases = raw.phases.clone();
1789        ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1790        for phase in &ordered_phases {
1791            phases_by_id.insert(
1792                ids.id(&phase.id),
1793                MutablePhase {
1794                    phase: phase.clone(),
1795                    test: id.clone(),
1796                    hits: Vec::new(),
1797                    decisions: BTreeMap::new(),
1798                    browser_events: 0,
1799                    server_events: 0,
1800                    explicit_events: 0,
1801                    inferred_events: 0,
1802                    explicit_browser_events: 0,
1803                    inferred_browser_events: 0,
1804                    explicit_server_events: 0,
1805                    inferred_server_events: 0,
1806                },
1807            );
1808        }
1809
1810        let correlate = |event: &RuntimeEvent| {
1811            event
1812                .phase_id
1813                .clone()
1814                .or_else(|| phase_at(&ordered_phases, event.timestamp_ms).map(str::to_owned))
1815        };
1816
1817        let snapshots = raw
1818            .runtime
1819            .iter()
1820            .map(|snapshot| (snapshot, false))
1821            .chain(raw.browser.iter().map(|snapshot| (snapshot, true)));
1822        for (snapshot, browser) in snapshots {
1823            for decision in &snapshot.decisions {
1824                let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1825                    if manifest_files.contains(&decision.meta.file) {
1826                        return Err(ReportError::InvalidServerRecord(format!(
1827                            "decision {} is absent from the frozen manifest",
1828                            decision.meta.id
1829                        )));
1830                    }
1831                    continue;
1832                };
1833                if decision_metadata[index] != decision.meta {
1834                    return Err(ReportError::InvalidServerRecord(format!(
1835                        "decision {} metadata differs from the frozen manifest",
1836                        decision.meta.id
1837                    )));
1838                }
1839                for vector in &decision.vectors {
1840                    let key = vector_key(vector);
1841                    let indexes = hash_slot(&mut vector_indexes, &decision.meta.id);
1842                    let observations = hash_slot(&mut vectors_by_decision, &decision.meta.id);
1843                    let observation_index = *indexes.entry(key).or_insert_with(|| {
1844                        observations.push(MutableObservation {
1845                            vector: vector.clone(),
1846                            tests: Vec::new(),
1847                            phases: Vec::new(),
1848                            explicit_phases: Vec::new(),
1849                        });
1850                        observations.len() - 1
1851                    });
1852                    observations[observation_index]
1853                        .tests
1854                        .push(tests_by_hit.number(&id, ids));
1855                    tree_slot(
1856                        &mut tests_by_id.get_mut(&id).expect("registered test").decisions,
1857                        &decision.meta.id,
1858                    )
1859                    .insert(vector);
1860                }
1861                if !decision.vectors.is_empty() {
1862                    hash_slot(&mut tests_by_decision, &decision.meta.id)
1863                        .push(tests_by_hit.number(&id, ids));
1864                }
1865            }
1866            let test_hits = &mut tests_by_id.get_mut(&id).expect("registered test").hits;
1867            for hit in &snapshot.hits {
1868                let hit = hit_ids.number(hit, ids);
1869                tests_by_hit.add(hit, &id, ids);
1870                test_hits.push(hit);
1871            }
1872            for event in &snapshot.events {
1873                let explicit = event.phase_id.is_some();
1874                let Some(phase_id) = correlate_event(event, &ordered_phases) else {
1875                    continue;
1876                };
1877                let Some(phase) = phases_by_id.get_mut(phase_id) else {
1878                    continue;
1879                };
1880                if event.environment == "browser" {
1881                    phase.browser_events += 1;
1882                    if explicit {
1883                        phase.explicit_browser_events += 1;
1884                    } else {
1885                        phase.inferred_browser_events += 1;
1886                    }
1887                } else {
1888                    phase.server_events += 1;
1889                    if explicit {
1890                        phase.explicit_server_events += 1;
1891                    } else {
1892                        phase.inferred_server_events += 1;
1893                    }
1894                }
1895                if explicit {
1896                    phase.explicit_events += 1;
1897                } else {
1898                    phase.inferred_events += 1;
1899                }
1900                if event.event_type == "hit" {
1901                    let hit = hit_ids.number(&event.id, ids);
1902                    phase.hits.push(hit);
1903                    phases_by_hit.add(hit, phase_id, ids);
1904                    if explicit {
1905                        explicit_phases_by_hit.add(hit, phase_id, ids);
1906                    }
1907                } else if event.event_type == "decision" {
1908                    let vector = event
1909                        .vector
1910                        .as_ref()
1911                        .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1912                    tree_slot(&mut phase.decisions, &event.id).insert(vector);
1913                    if let Some(index) = vector_indexes
1914                        .get(&event.id)
1915                        .and_then(|indexes| indexes.get(&vector_key(vector)))
1916                        .copied()
1917                        && let Some(observation) = vectors_by_decision
1918                            .get_mut(&event.id)
1919                            .and_then(|observations| observations.get_mut(index))
1920                    {
1921                        observation.phases.push(phases_by_hit.number(phase_id, ids));
1922                        if explicit {
1923                            observation
1924                                .explicit_phases
1925                                .push(explicit_phases_by_hit.number(phase_id, ids));
1926                        }
1927                    }
1928                } else {
1929                    return Err(ReportError::InvalidEvent(event.event_type.clone()));
1930                }
1931            }
1932            // A snapshot that names its phase: each hit and each decision
1933            // vector is an explicit event of it, counted and related exactly
1934            // as the event the frontend no longer writes would have been.
1935            if let Some(phase_id) = snapshot.phase_id.as_deref()
1936                && let Some(phase) = phases_by_id.get_mut(phase_id)
1937            {
1938                let observed = snapshot.hits.len()
1939                    + snapshot
1940                        .decisions
1941                        .iter()
1942                        .map(|decision| decision.vectors.len())
1943                        .sum::<usize>();
1944                if browser {
1945                    phase.browser_events += observed;
1946                    phase.explicit_browser_events += observed;
1947                } else {
1948                    phase.server_events += observed;
1949                    phase.explicit_server_events += observed;
1950                }
1951                phase.explicit_events += observed;
1952                for hit in &snapshot.hits {
1953                    let hit = hit_ids.number(hit, ids);
1954                    phase.hits.push(hit);
1955                    phases_by_hit.add(hit, phase_id, ids);
1956                    explicit_phases_by_hit.add(hit, phase_id, ids);
1957                }
1958                for decision in &snapshot.decisions {
1959                    let id = &decision.meta.id;
1960                    for vector in &decision.vectors {
1961                        tree_slot(&mut phase.decisions, id).insert(vector);
1962                        if let Some(index) = vector_indexes
1963                            .get(id)
1964                            .and_then(|indexes| indexes.get(&vector_key(vector)))
1965                            .copied()
1966                            && let Some(observation) = vectors_by_decision
1967                                .get_mut(id)
1968                                .and_then(|observations| observations.get_mut(index))
1969                        {
1970                            observation.phases.push(phases_by_hit.number(phase_id, ids));
1971                            observation
1972                                .explicit_phases
1973                                .push(explicit_phases_by_hit.number(phase_id, ids));
1974                        }
1975                    }
1976                }
1977            }
1978        }
1979
1980        for record in &raw.server {
1981            let (record_id, decision) = if record.record_type == "decision" {
1982                let meta = record
1983                    .meta
1984                    .as_ref()
1985                    .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1986                let vector = record
1987                    .vector
1988                    .as_ref()
1989                    .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1990                let Some(index) = decision_indexes.get(&meta.id).copied() else {
1991                    if manifest_files.contains(&meta.file) {
1992                        return Err(ReportError::InvalidServerRecord(format!(
1993                            "decision {} is absent from the frozen manifest",
1994                            meta.id
1995                        )));
1996                    }
1997                    continue;
1998                };
1999                if decision_metadata[index] != *meta {
2000                    return Err(ReportError::InvalidServerRecord(format!(
2001                        "decision {} metadata differs from the frozen manifest",
2002                        meta.id
2003                    )));
2004                }
2005                let key = vector_key(vector);
2006                let indexes = vector_indexes.entry(meta.id.clone()).or_default();
2007                let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
2008                let index = *indexes.entry(key).or_insert_with(|| {
2009                    observations.push(MutableObservation {
2010                        vector: vector.clone(),
2011                        tests: Vec::new(),
2012                        phases: Vec::new(),
2013                        explicit_phases: Vec::new(),
2014                    });
2015                    observations.len() - 1
2016                });
2017                observations[index]
2018                    .tests
2019                    .push(tests_by_hit.number(&id, ids));
2020                tests_by_id
2021                    .get_mut(&id)
2022                    .expect("registered test")
2023                    .decisions
2024                    .entry(meta.id.clone())
2025                    .or_default()
2026                    .insert(vector);
2027                hash_slot(&mut tests_by_decision, &meta.id).push(tests_by_hit.number(&id, ids));
2028                (meta.id.clone(), Some(vector.clone()))
2029            } else if record.record_type == "hit" {
2030                let hit = record
2031                    .id
2032                    .as_ref()
2033                    .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
2034                let number = hit_ids.number(hit, ids);
2035                tests_by_hit.add(number, &id, ids);
2036                tests_by_id
2037                    .get_mut(&id)
2038                    .expect("registered test")
2039                    .hits
2040                    .push(number);
2041                (hit.clone(), None)
2042            } else {
2043                return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
2044            };
2045            let Some(timestamp_ms) = record.timestamp_ms else {
2046                continue;
2047            };
2048            let event = RuntimeEvent {
2049                event_type: record.record_type.clone(),
2050                id: record_id,
2051                vector: decision,
2052                timestamp_ms,
2053                statement_id: record.statement_id.clone(),
2054                phase_id: record.phase_id.clone(),
2055                environment: "server".into(),
2056            };
2057            let explicit = event.phase_id.is_some();
2058            let phase_id = correlate(&event);
2059            let Some(phase_id) = phase_id else { continue };
2060            let Some(phase) = phases_by_id.get_mut(phase_id.as_str()) else {
2061                continue;
2062            };
2063            phase.server_events += 1;
2064            if explicit {
2065                phase.explicit_events += 1;
2066                phase.explicit_server_events += 1;
2067            } else {
2068                phase.inferred_events += 1;
2069                phase.inferred_server_events += 1;
2070            }
2071            if event.event_type == "hit" {
2072                let hit = hit_ids.number(&event.id, ids);
2073                phase.hits.push(hit);
2074                phases_by_hit.add(hit, &phase_id, ids);
2075                if explicit {
2076                    explicit_phases_by_hit.add(hit, &phase_id, ids);
2077                }
2078            } else if let Some(vector) = &event.vector {
2079                phase
2080                    .decisions
2081                    .entry(event.id.clone())
2082                    .or_default()
2083                    .insert(vector);
2084                if let Some(index) = vector_indexes
2085                    .get(&event.id)
2086                    .and_then(|indexes| indexes.get(&vector_key(vector)))
2087                    .copied()
2088                    && let Some(observation) = vectors_by_decision
2089                        .get_mut(&event.id)
2090                        .and_then(|observations| observations.get_mut(index))
2091                {
2092                    observation
2093                        .phases
2094                        .push(phases_by_hit.number(&phase_id, ids));
2095                    if explicit {
2096                        observation
2097                            .explicit_phases
2098                            .push(explicit_phases_by_hit.number(&phase_id, ids));
2099                    }
2100                }
2101            }
2102        }
2103    }
2104
2105    // Legacy confidence fields remain readable for archive compatibility.
2106    // Execution inside/before a passing assertion does not establish that
2107    // an assertion checks a statement. Only the agent-authored map awards it.
2108
2109    decision_metadata.sort_by(|left, right| {
2110        left.file
2111            .cmp(&right.file)
2112            .then(left.line.cmp(&right.line))
2113            .then(left.column.cmp(&right.column))
2114    });
2115    let tests_by_hit = tests_by_hit.into_relation(&hit_ids);
2116    let phases_by_hit = phases_by_hit.into_relation(&hit_ids);
2117    let explicit_phases_by_hit = explicit_phases_by_hit.into_relation(&hit_ids);
2118    // A frontend may decline one outcome of a branch (a block that can never
2119    // be entered, a rescue that can never be reached) or the whole branch;
2120    // a branch whose every outcome is declined is declined itself.
2121    let mut declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
2122    for branch in &manifest.branches {
2123        if !branch.alternatives.is_empty()
2124            && branch
2125                .alternatives
2126                .iter()
2127                .all(|alternative| declined.contains(&alternative.id))
2128        {
2129            declined.insert(&branch.id);
2130        }
2131    }
2132    // What confidence asks of each test and phase a relation numbers, looked
2133    // up once per number rather than once per mention: a run's points and
2134    // lines mention tests millions of times, and each lookup hashed an id.
2135    let test_traits = tests_by_hit
2136        .names
2137        .iter()
2138        .map(|name| tests_by_id.get(name).map(TestTraits::of))
2139        .collect::<Vec<_>>();
2140    let phase_kind = |relation: &Relation| {
2141        relation
2142            .names
2143            .iter()
2144            .map(|name| {
2145                phases_by_id
2146                    .get(name)
2147                    .map(|phase| phase.phase.kind.as_str())
2148            })
2149            .collect::<Vec<_>>()
2150    };
2151    let phase_kinds = phase_kind(&phases_by_hit);
2152    let explicit_kinds = phase_kind(&explicit_phases_by_hit);
2153    let confidence = |tests: &Reached, phases: &Reached, explicit: &Reached| {
2154        confidence_from(
2155            tests,
2156            phases,
2157            explicit,
2158            &test_traits,
2159            &phase_kinds,
2160            &explicit_kinds,
2161        )
2162    };
2163    // A decision's two outcomes are branch outcomes. Python, Ruby and Rust
2164    // declare them as a branch of their own, `<decision>:outcome`; for a
2165    // decision declared without one, the branch is made here from the
2166    // decision's vectors, so every language counts and lists outcomes the
2167    // same way and no decision is counted twice.
2168    let declared_branches = manifest
2169        .branches
2170        .iter()
2171        .map(|branch| branch.id.as_str())
2172        .collect::<HashSet<_>>();
2173    let mut outcome_branches = Vec::new();
2174    let mut decisions = Vec::with_capacity(decision_metadata.len());
2175    for meta in decision_metadata {
2176        let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
2177        let outcome_id = format!("{}:outcome", meta.id);
2178        if !declared_branches.contains(outcome_id.as_str()) {
2179            let alternatives = [true, false]
2180                .into_iter()
2181                .map(|outcome| {
2182                    let (mut tests, mut phases, mut explicit) =
2183                        (Vec::new(), Vec::new(), Vec::new());
2184                    for observation in mutable
2185                        .iter()
2186                        .filter(|observation| observation.vector.outcome == outcome)
2187                    {
2188                        tests.extend(&observation.tests);
2189                        phases.extend(&observation.phases);
2190                        explicit.extend(&observation.explicit_phases);
2191                    }
2192                    let tests = tests_by_hit.reached(tests);
2193                    let phases = phases_by_hit.reached(phases);
2194                    let explicit = explicit_phases_by_hit.reached(explicit);
2195                    AlternativeResult {
2196                        id: format!("{outcome_id}:{outcome}"),
2197                        label: outcome.to_string(),
2198                        covered: !tests.names.is_empty(),
2199                        confidence: confidence(&tests, &phases, &explicit),
2200                        tests: tests.names,
2201                        phases: phases.names,
2202                    }
2203                })
2204                .collect::<Vec<_>>();
2205            outcome_branches.push(BranchResult {
2206                covered: alternatives.iter().all(|alternative| alternative.covered),
2207                meta: BranchMeta {
2208                    id: outcome_id,
2209                    kind: meta.kind.clone(),
2210                    file: meta.file.clone(),
2211                    line: meta.line,
2212                    column: meta.column,
2213                    source: meta.source.clone(),
2214                    alternatives: alternatives
2215                        .iter()
2216                        .map(|alternative| BranchAlternativeMeta {
2217                            id: alternative.id.clone(),
2218                            label: alternative.label.clone(),
2219                        })
2220                        .collect(),
2221                },
2222                alternatives,
2223            });
2224        }
2225        let mut observations = Vec::with_capacity(mutable.len());
2226        let mut observed_phases = Vec::new();
2227        let mut observed_explicit = Vec::new();
2228        for observation in mutable {
2229            let tests = tests_by_hit.reached(observation.tests);
2230            let phases = phases_by_hit.reached(observation.phases);
2231            let explicit = explicit_phases_by_hit.reached(observation.explicit_phases);
2232            observed_phases.extend(&phases.numbers);
2233            observed_explicit.extend(&explicit.numbers);
2234            observations.push(VectorObservation {
2235                vector: observation.vector,
2236                confidence: confidence(&tests, &phases, &explicit),
2237                tests: tests.names,
2238                phases: phases.names,
2239                explicit_phases: explicit.names,
2240            });
2241        }
2242        let vectors = observations
2243            .iter()
2244            .map(|observation| observation.vector.clone())
2245            .collect::<Vec<_>>();
2246        let witnesses =
2247            find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
2248                ReportError::DecisionAnalysis {
2249                    decision_id: meta.id.clone(),
2250                    error,
2251                }
2252            })?;
2253        let mut conditions = Vec::with_capacity(meta.conditions.len());
2254        for (index, source) in meta.conditions.iter().enumerate() {
2255            let witness = witnesses[index].map(|witness| {
2256                [
2257                    vectors[witness.first].clone(),
2258                    vectors[witness.second].clone(),
2259                ]
2260            });
2261            let witness_tests = witnesses[index].map(|witness| {
2262                [
2263                    observations[witness.first].tests.clone(),
2264                    observations[witness.second].tests.clone(),
2265                ]
2266            });
2267            let assertion_covered = false;
2268            conditions.push(ConditionResult {
2269                index,
2270                source: source.clone(),
2271                covered: witness.is_some(),
2272                assertion_covered,
2273                witness,
2274                witness_tests,
2275            });
2276        }
2277        let decision_tests =
2278            tests_by_hit.reached(tests_by_decision.remove(&meta.id).unwrap_or_default());
2279        let decision_confidence = confidence(
2280            &decision_tests,
2281            &phases_by_hit.reached(observed_phases),
2282            &explicit_phases_by_hit.reached(observed_explicit),
2283        );
2284        decisions.push(DecisionResult {
2285            executed: !vectors.is_empty(),
2286            covered: conditions.iter().all(|condition| condition.covered),
2287            meta,
2288            vectors,
2289            vector_observations: observations,
2290            conditions,
2291            tests: decision_tests.names,
2292            confidence: decision_confidence,
2293        });
2294    }
2295
2296    let points = manifest
2297        .points
2298        .iter()
2299        .cloned()
2300        .map(|meta| {
2301            let tests = tests_by_hit.of(&meta.id);
2302            let phases = phases_by_hit.of(&meta.id);
2303            let explicit = explicit_phases_by_hit.of(&meta.id);
2304            PointResult {
2305                measured: !declined.contains(&meta.id),
2306                covered: tests_by_hit.by_hit.contains_key(&meta.id),
2307                confidence: confidence(tests, phases, explicit),
2308                meta,
2309                tests: tests.names.clone(),
2310                phases: phases.names.clone(),
2311            }
2312        })
2313        .collect::<Vec<_>>();
2314
2315    let branches = manifest
2316        .branches
2317        .iter()
2318        .cloned()
2319        .map(|mut meta| {
2320            // A declined outcome leaves the denominator; the others stay.
2321            meta.alternatives
2322                .retain(|alternative| !declined.contains(&alternative.id));
2323            let alternatives = meta
2324                .alternatives
2325                .iter()
2326                .map(|alternative| {
2327                    let tests = tests_by_hit.of(&alternative.id);
2328                    let phases = phases_by_hit.of(&alternative.id);
2329                    let explicit = explicit_phases_by_hit.of(&alternative.id);
2330                    AlternativeResult {
2331                        id: alternative.id.clone(),
2332                        label: alternative.label.clone(),
2333                        covered: tests_by_hit.by_hit.contains_key(&alternative.id),
2334                        tests: tests.names.clone(),
2335                        phases: phases.names.clone(),
2336                        confidence: confidence(tests, phases, explicit),
2337                    }
2338                })
2339                .collect::<Vec<_>>();
2340            BranchResult {
2341                covered: alternatives.iter().all(|alternative| alternative.covered),
2342                meta,
2343                alternatives,
2344            }
2345        })
2346        .chain(outcome_branches)
2347        .collect::<Vec<_>>();
2348
2349    // Obligations the frontend declined to measure leave the covered/uncovered
2350    // denominator entirely. Counting them as uncovered would report a
2351    // measurement gap as a coverage gap — a wrong number, and wrong numbers get
2352    // trusted. They are reported separately instead, alongside the share of
2353    // obligations that were measured exactly.
2354    // Lines are folded from every point, declined ones included, so a file
2355    // Supercov could not measure still has addressable lines to report a
2356    // limitation against. Only what was measured decides the line's state and
2357    // whether it counts. A line gathers its points' test and phase numbers,
2358    // which sort and deduplicate as numbers.
2359    let mut line_aggregates = BTreeMap::<SourceLine, LineAggregate>::new();
2360    for point in &points {
2361        let aggregate = line_aggregates
2362            .entry(SourceLine {
2363                file: ids.id(&point.meta.file),
2364                line: point.meta.line,
2365            })
2366            .or_default();
2367        if declined.contains(&point.meta.id) {
2368            continue;
2369        }
2370        aggregate.measured = true;
2371        aggregate.covered |= point.covered;
2372        aggregate
2373            .tests
2374            .extend(&tests_by_hit.of(&point.meta.id).numbers);
2375        aggregate
2376            .phases
2377            .extend(&phases_by_hit.of(&point.meta.id).numbers);
2378        aggregate
2379            .explicit_phases
2380            .extend(&explicit_phases_by_hit.of(&point.meta.id).numbers);
2381    }
2382    let lines = line_aggregates
2383        .into_iter()
2384        .map(|(location, aggregate)| {
2385            let LineAggregate {
2386                covered,
2387                measured,
2388                tests,
2389                phases,
2390                explicit_phases,
2391            } = aggregate;
2392            let tests = tests_by_hit.reached(tests);
2393            let phases = phases_by_hit.reached(phases);
2394            let explicit = explicit_phases_by_hit.reached(explicit_phases);
2395            let traits = tests
2396                .numbers
2397                .iter()
2398                .filter_map(|number| test_traits[*number as usize].as_ref())
2399                .collect::<Vec<_>>();
2400            let runners = traits
2401                .iter()
2402                .map(|traits| traits.runner)
2403                .collect::<BTreeSet<_>>();
2404            let kinds = traits
2405                .iter()
2406                .map(|traits| traits.kind)
2407                .collect::<BTreeSet<_>>();
2408            LineResult {
2409                file: location.file.to_string(),
2410                line: location.line,
2411                covered,
2412                measured,
2413                confidence: confidence(&tests, &phases, &explicit),
2414                tests: tests.names,
2415                runners: runners.iter().map(|runner| (*runner).to_owned()).collect(),
2416                exclusive_kind: (kinds.len() == 1).then(|| (*kinds.first().unwrap()).to_owned()),
2417                phases: phases.names,
2418                kinds: kinds.iter().map(|kind| (*kind).to_owned()).collect(),
2419            }
2420        })
2421        .collect::<Vec<_>>();
2422    drop(test_traits);
2423
2424    // Where each hit sits, by hit number, as (file rank, line): a test's or
2425    // phase's lines sort and deduplicate as numbers, in the order the
2426    // `SourceLine`s they stand for sort in.
2427    let hit_ranks = hit_ids.ranks();
2428    let mut point_files = manifest
2429        .points
2430        .iter()
2431        .map(|point| ids.id(&point.file))
2432        .collect::<Vec<_>>();
2433    point_files.sort_unstable();
2434    point_files.dedup();
2435    let mut hit_locations = vec![None; hit_ids.names.len()];
2436    for point in &manifest.points {
2437        if let Some(number) = hit_ids.numbers.get(point.id.as_str()) {
2438            let file = point_files
2439                .binary_search_by(|file| file.as_str().cmp(&point.file))
2440                .expect("every point file is listed") as u32;
2441            hit_locations[*number] = Some((file, point.line));
2442        }
2443    }
2444    let lines_of = |hits: &[usize]| {
2445        let mut located = hits
2446            .iter()
2447            .filter_map(|hit| hit_locations[*hit])
2448            .collect::<Vec<_>>();
2449        located.sort_unstable();
2450        located.dedup();
2451        located
2452            .into_iter()
2453            .map(|(file, line)| SourceLine {
2454                file: point_files[file as usize].clone(),
2455                line,
2456            })
2457            .collect::<Vec<_>>()
2458    };
2459    test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
2460    let tests = test_order
2461        .into_iter()
2462        .map(|id| {
2463            let test = tests_by_id.remove(&id).expect("test order references test");
2464            let outcome = test_outcome(&test);
2465            let lines = lines_of(&test.hits);
2466            let hits = hit_ids.sorted_by(test.hits, &hit_ranks);
2467            TestCoverageResult {
2468                id: test.id,
2469                name: test.name,
2470                file: test.file,
2471                title: test.title,
2472                retries: test.retries.into_iter().collect(),
2473                attempts: test.attempts.into_values().collect(),
2474                outcome,
2475                provenance: test.provenance,
2476                role: test.role,
2477                attribution: test.attribution,
2478                hits,
2479                decisions: test
2480                    .decisions
2481                    .into_iter()
2482                    .map(|(id, vectors)| TestDecisionResult {
2483                        id,
2484                        vectors: vectors.values,
2485                    })
2486                    .collect(),
2487                lines,
2488            }
2489        })
2490        .collect::<Vec<_>>();
2491
2492    let mut test_files = BTreeMap::<
2493        String,
2494        (
2495            BTreeSet<String>,
2496            BTreeSet<String>,
2497            BTreeSet<String>,
2498            BTreeSet<SourceLine>,
2499        ),
2500    >::new();
2501    for test in &tests {
2502        let aggregate = test_files
2503            .entry(
2504                test.file
2505                    .clone()
2506                    .unwrap_or_else(|| "(unknown test file)".into()),
2507            )
2508            .or_default();
2509        aggregate.0.insert(test.id.clone());
2510        aggregate.1.insert(test.provenance.runner.clone());
2511        aggregate.2.insert(test.provenance.kind.clone());
2512        aggregate.3.extend(test.lines.iter().cloned());
2513    }
2514    let test_files = test_files
2515        .into_iter()
2516        .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
2517            file,
2518            tests: sorted(&tests),
2519            runners: sorted(&runners),
2520            kinds: sorted(&kinds),
2521            lines: lines.into_iter().collect(),
2522        })
2523        .collect::<Vec<_>>();
2524
2525    let mut phases = phases_by_id.into_values().collect::<Vec<_>>();
2526    phases.sort_by(|left, right| {
2527        left.phase
2528            .started_at_ms
2529            .cmp(&right.phase.started_at_ms)
2530            .then(left.phase.id.cmp(&right.phase.id))
2531    });
2532    let phases = phases
2533        .into_iter()
2534        .map(|phase| {
2535            let lines = lines_of(&phase.hits);
2536            let hits = hit_ids.sorted_by(phase.hits, &hit_ranks);
2537            PhaseResult {
2538                phase: phase.phase,
2539                test: phase.test,
2540                hits,
2541                decisions: phase
2542                    .decisions
2543                    .into_iter()
2544                    .map(|(id, vectors)| TestDecisionResult {
2545                        id,
2546                        vectors: vectors.values,
2547                    })
2548                    .collect(),
2549                lines,
2550                browser_events: phase.browser_events,
2551                server_events: phase.server_events,
2552                explicit_events: phase.explicit_events,
2553                inferred_events: phase.inferred_events,
2554                explicit_browser_events: phase.explicit_browser_events,
2555                inferred_browser_events: phase.inferred_browser_events,
2556                explicit_server_events: phase.explicit_server_events,
2557                inferred_server_events: phase.inferred_server_events,
2558            }
2559        })
2560        .collect::<Vec<_>>();
2561
2562    let total_obligations = decisions.len() + points.len() + branches.len();
2563    let (decisions, points, branches) = if declined.is_empty() {
2564        (decisions, points, branches)
2565    } else {
2566        (
2567            decisions
2568                .into_iter()
2569                .filter(|result| !declined.contains(&result.meta.id))
2570                .collect::<Vec<_>>(),
2571            points
2572                .into_iter()
2573                .filter(|result| !declined.contains(&result.meta.id))
2574                .collect::<Vec<_>>(),
2575            branches
2576                .into_iter()
2577                .filter(|result| !declined.contains(&result.meta.id))
2578                .collect::<Vec<_>>(),
2579        )
2580    };
2581    let measured_obligations = decisions.len() + points.len() + branches.len();
2582    let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
2583    if total_obligations > measured_obligations {
2584        summary.unmeasured_obligations = Some(total_obligations - measured_obligations);
2585        summary.exact_fraction_pct = Some(if total_obligations == 0 {
2586            100.0
2587        } else {
2588            (measured_obligations as f64) * 100.0 / (total_obligations as f64)
2589        });
2590    }
2591    // A limitation that says `"blocking": false` is a declared boundary of
2592    // the denominator -- a macro the compiler expands, a const context no
2593    // probe can run in -- not a failure to measure what is inside it. Only a
2594    // blocking one makes the run incomplete. A limitation that says nothing
2595    // is blocking, so a frontend that has not been taught the difference
2596    // keeps its behaviour.
2597    if manifest.limitations.iter().any(blocking_limitation) {
2598        summary.coverage_complete = false;
2599        summary.completeness_blocked = Some(true);
2600    }
2601
2602    let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
2603        let values = tests
2604            .iter()
2605            .map(|test| {
2606                if field == "kind" {
2607                    test.provenance.kind.clone()
2608                } else {
2609                    test.provenance.runner.clone()
2610                }
2611            })
2612            .collect::<BTreeSet<_>>();
2613        values
2614            .into_iter()
2615            .map(|value| {
2616                let in_dimension = |test: &&TestCoverageResult| {
2617                    if field == "kind" {
2618                        test.provenance.kind == value
2619                    } else {
2620                        test.provenance.runner == value
2621                    }
2622                };
2623                // Coverage is summarised over the tests that have coverage of
2624                // their own. A Go test that called `t.Parallel()` reached real
2625                // code and contributes no hits, because nothing can say which
2626                // code -- so counting it here reported a package covered
2627                // entirely by parallel tests as covered 0%, which is a wrong
2628                // number rather than a missing one.
2629                let selected = tests
2630                    .iter()
2631                    .filter(in_dimension)
2632                    .filter(|test| coverage_is_its_own(&test.attribution))
2633                    .map(|test| test.id.clone())
2634                    .collect::<BTreeSet<_>>();
2635                let counted = tests
2636                    .iter()
2637                    .filter(in_dimension)
2638                    .map(|test| test.id.clone())
2639                    .collect::<BTreeSet<_>>();
2640                Ok(DimensionCoverage {
2641                    kind: (field == "kind").then(|| value.clone()),
2642                    runner: (field == "runner").then(|| value.clone()),
2643                    // Counted whether or not anything could be attributed to
2644                    // them: they ran.
2645                    tests: tests
2646                        .iter()
2647                        .filter(|test| counted.contains(&test.id) && test.role == "test")
2648                        .count(),
2649                    setups: tests
2650                        .iter()
2651                        .filter(|test| counted.contains(&test.id) && test.role == "setup")
2652                        .count(),
2653                    attributed: tests
2654                        .iter()
2655                        .filter(|test| selected.contains(&test.id) && test.role == "test")
2656                        .count(),
2657                    summary: summary_for_results(
2658                        &decisions,
2659                        &points,
2660                        &branches,
2661                        &lines,
2662                        Some(&selected),
2663                    )?,
2664                })
2665            })
2666            .collect()
2667    };
2668
2669    Ok(CoverageView {
2670        generated_at: generated_at.into(),
2671        variant: coverage_model.variant.clone(),
2672        scope: manifest.scope.clone(),
2673        model: CoverageModel {
2674            language: coverage_model.language.clone(),
2675            name: coverage_model.name.clone(),
2676            completeness_meaning: coverage_model.completeness_meaning.clone(),
2677            measured: coverage_model.measured.clone(),
2678            not_measured: coverage_model.not_measured.clone(),
2679        },
2680        integrity: None,
2681        limitations: manifest.limitations.clone(),
2682        transport: None,
2683        summary,
2684        coverage_by_kind: dimension_coverage("kind")?,
2685        coverage_by_runner: dimension_coverage("runner")?,
2686        decisions,
2687        points,
2688        branches,
2689        tests,
2690        test_files,
2691        phases,
2692        lines,
2693    })
2694}
2695
2696pub fn analyze_coverage_results(
2697    request: &CoverageReportRequest,
2698) -> Result<CoverageReport, ReportError> {
2699    if let Some(scope) = request
2700        .raw_results
2701        .iter()
2702        .filter_map(|raw| raw.scope.as_ref())
2703        .find(|scope| scope.run_id != request.run_id)
2704    {
2705        return Err(ReportError::ScopeMismatch {
2706            expected: request.run_id.clone(),
2707            actual: scope.run_id.clone(),
2708        });
2709    }
2710    if request.raw_results.is_empty() {
2711        return Err(ReportError::NoEvidence(request.run_id.clone()));
2712    }
2713    let default_model = javascript_coverage_model();
2714    let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
2715    PersistedCoverageModel::from_declaration(coverage_model).map_err(|reason| {
2716        ReportError::InvalidJson {
2717            path: "coverage-model.json".into(),
2718            reason: reason.into(),
2719        }
2720    })?;
2721    // The three views are built side by side: each reads the same evidence
2722    // and nothing the others write. Each holds one copy of each id it names.
2723    // They do not share copies: two threads cloning the same ids contend on
2724    // their counts, which made each view twice as slow as building it alone.
2725    let all = request.raw_results.iter().collect::<Vec<_>>();
2726    let passing = passing_coverage_results(&request.raw_results);
2727    let failing = failed_coverage_results(&request.raw_results);
2728    let build = |results: &[&RawTestResult]| {
2729        create_coverage_view_with_model(
2730            &request.manifest,
2731            results,
2732            &request.generated_at,
2733            coverage_model,
2734            &mut Interner::default(),
2735        )
2736    };
2737    let (view, passed, failed) = std::thread::scope(|scope| {
2738        let passed = scope.spawn(|| build(&passing));
2739        let failed = scope.spawn(|| build(&failing));
2740        let view = build(&all);
2741        (
2742            view,
2743            passed
2744                .join()
2745                .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
2746            failed
2747                .join()
2748                .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
2749        )
2750    });
2751    let (view, passed, failed) = (view?, passed?, failed?);
2752    let execution = match request.test_exit_code {
2753        ExitCodeInput::Missing => None,
2754        ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
2755            valid: test_exit_code == Some(0),
2756            test_exit_code,
2757        }),
2758    };
2759    let mut view = view;
2760    let mut passed = passed;
2761    let mut failed = failed;
2762    if let Some(integrity) = &request.integrity {
2763        view.integrity = Some(integrity.clone());
2764        passed.integrity = Some(integrity.clone());
2765        failed.integrity = Some(integrity.clone());
2766    }
2767    Ok(CoverageReport {
2768        view,
2769        execution,
2770        filters: CoverageFilters { passed, failed },
2771    })
2772}
2773
2774fn parse_entry<T: for<'de> Deserialize<'de>>(
2775    entry: &EvidenceArchiveEntry,
2776) -> Result<T, ReportError> {
2777    serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
2778        path: entry.path.clone(),
2779        reason: error.to_string(),
2780    })
2781}
2782
2783fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
2784    entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2785) -> Result<Vec<T>, ReportError> {
2786    let mut records = Vec::new();
2787    for entry in entries {
2788        let Some(contents) = entry.contents.strip_suffix(b"\n") else {
2789            return Err(ReportError::InvalidJson {
2790                path: entry.path.clone(),
2791                reason: "recognized JSONL evidence must end with a newline".into(),
2792            });
2793        };
2794        if contents.is_empty() {
2795            return Err(ReportError::InvalidJson {
2796                path: entry.path.clone(),
2797                reason: "recognized JSONL evidence must contain at least one record".into(),
2798            });
2799        }
2800        for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
2801            if line.is_empty() {
2802                return Err(ReportError::InvalidJson {
2803                    path: entry.path.clone(),
2804                    reason: format!("blank JSONL record at line {}", index + 1),
2805                });
2806            }
2807            records.push(serde_json::from_slice(line).map_err(|error| {
2808                ReportError::InvalidJson {
2809                    path: entry.path.clone(),
2810                    reason: format!("invalid JSONL record at line {}: {error}", index + 1),
2811                }
2812            })?);
2813        }
2814    }
2815    Ok(records)
2816}
2817
2818/// Server evidence is appended by application processes Supercov does not
2819/// control — including pool VMs restored from one snapshot, whose clones can
2820/// tear a shared shard. A torn line is one lost record, not a lost run: it is
2821/// skipped and counted, and the report says so through the existing
2822/// CORRUPT_EVIDENCE_RECORDS diagnostic and the blocking-limitation total.
2823struct TolerantJsonLines<T> {
2824    records: Vec<T>,
2825    corrupt_records: usize,
2826    corrupt_files: usize,
2827}
2828
2829fn parse_server_json_lines<'a, T: for<'de> Deserialize<'de>>(
2830    entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2831) -> TolerantJsonLines<T> {
2832    let mut parsed = TolerantJsonLines {
2833        records: Vec::new(),
2834        corrupt_records: 0,
2835        corrupt_files: 0,
2836    };
2837    for entry in entries {
2838        let mut corrupt_here = 0;
2839        for line in entry.contents.split(|byte| *byte == b'\n') {
2840            if line.is_empty() {
2841                continue;
2842            }
2843            match serde_json::from_slice(line) {
2844                Ok(record) => parsed.records.push(record),
2845                Err(_) => corrupt_here += 1,
2846            }
2847        }
2848        if corrupt_here > 0 {
2849            parsed.corrupt_records += corrupt_here;
2850            parsed.corrupt_files += 1;
2851        }
2852    }
2853    parsed
2854}
2855
2856fn is_mcdc_result(path: &str) -> bool {
2857    path == "mcdc.json" || path.ends_with("/mcdc.json")
2858}
2859
2860fn is_mcdc_journal(path: &str) -> bool {
2861    path == "mcdc.jsonl" || path.ends_with(".mcdc.jsonl")
2862}
2863
2864pub fn analyze_coverage_archive(
2865    request: &ArchiveReportRequest,
2866) -> Result<CoverageReport, ReportError> {
2867    let entries = read_archive(Path::new(&request.archive_path))
2868        .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2869    let manifest = entries
2870        .iter()
2871        .find(|entry| entry.path == "manifest.json")
2872        .ok_or(ReportError::MissingManifest)
2873        .and_then(parse_entry::<CoverageManifest>)?;
2874    let mut raw_results = entries
2875        .iter()
2876        .filter(|entry| is_mcdc_result(&entry.path))
2877        .map(parse_entry::<RawTestResult>)
2878        .collect::<Result<Vec<_>, _>>()?;
2879    let journal_results = parse_json_lines::<RawTestResult>(
2880        entries.iter().filter(|entry| is_mcdc_journal(&entry.path)),
2881    )?;
2882    raw_results.extend(journal_results);
2883
2884    let scoped = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2885        entry.path.starts_with("server/")
2886            && !entry.path.starts_with("server/background/")
2887            && entry.path.ends_with(".jsonl")
2888    }));
2889    let scoped_records = scoped.records;
2890    // Each server record joins the first result with its scope, once. A
2891    // linear search per record and a linear duplicate check per result made
2892    // this quadratic: supergateway's e2e servers spent most of a 20 s report
2893    // preparation, and of publication, comparing scopes.
2894    let mut by_scope = std::collections::HashMap::<&ExecutionScope, usize>::new();
2895    for (index, raw) in raw_results.iter().enumerate() {
2896        if let Some(scope) = &raw.scope {
2897            by_scope.entry(scope).or_insert(index);
2898        }
2899    }
2900    let by_scope: std::collections::HashMap<ExecutionScope, usize> = by_scope
2901        .into_iter()
2902        .map(|(scope, index)| (scope.clone(), index))
2903        .collect();
2904    let mut seen = std::collections::HashMap::<usize, std::collections::HashSet<Vec<u8>>>::new();
2905    for record in &scoped_records {
2906        let Some(scope) = &record.scope else { continue };
2907        let Some(&index) = by_scope.get(scope) else {
2908            continue;
2909        };
2910        let raw = &mut raw_results[index];
2911        let known = seen.entry(index).or_insert_with(|| {
2912            raw.server
2913                .iter()
2914                .filter_map(|existing| serde_json::to_vec(existing).ok())
2915                .collect()
2916        });
2917        let Ok(key) = serde_json::to_vec(record) else {
2918            if !raw.server.contains(record) {
2919                raw.server.push(record.clone());
2920            }
2921            continue;
2922        };
2923        if known.insert(key) {
2924            raw.server.push(record.clone());
2925        }
2926    }
2927
2928    // Background evidence by the process that wrote it: `<writer>-<n>.jsonl`.
2929    let mut writers = BTreeMap::<&str, Vec<&EvidenceArchiveEntry>>::new();
2930    for entry in entries.iter().filter(|entry| {
2931        entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2932    }) {
2933        let name = &entry.path["server/background/".len()..entry.path.len() - ".jsonl".len()];
2934        let writer = name
2935            .rsplit_once('-')
2936            .filter(|(_, sequence)| sequence.chars().all(|c| c.is_ascii_digit()))
2937            .map_or(name, |(writer, _)| writer);
2938        writers.entry(writer).or_default().push(entry);
2939    }
2940    let mut background = TolerantJsonLines::<ServerRecord> {
2941        records: Vec::new(),
2942        corrupt_records: 0,
2943        corrupt_files: 0,
2944    };
2945    let mut processes = Vec::new();
2946    for files in writers.into_values() {
2947        let parsed = parse_server_json_lines::<ServerRecord>(files.into_iter());
2948        background.corrupt_records += parsed.corrupt_records;
2949        background.corrupt_files += parsed.corrupt_files;
2950        processes.push(parsed.records);
2951    }
2952    // The run declares its background runner whenever a process wrote
2953    // background evidence, so the runner stays though everything in it was
2954    // credited to a test or was only a note of what a server served.
2955    let had_background = !processes.is_empty();
2956    let background_records = credit_by_time(&mut raw_results, processes);
2957    background.records.clone_from(&background_records);
2958    if had_background {
2959        raw_results.push(RawTestResult {
2960            test_id: Some(format!("background:{}", request.run_id)),
2961            scope: None,
2962            test: "Background / unattributed".into(),
2963            test_file: None,
2964            title: Some("Background / unattributed".into()),
2965            retry: None,
2966            status: Some("unknown".into()),
2967            expected_status: None,
2968            flaky: false,
2969            provenance: TestProvenance {
2970                runner: "background".into(),
2971                kind: "background".into(),
2972                project: None,
2973                source: "explicit".into(),
2974            },
2975            role: "background".into(),
2976            attribution: ATTRIBUTION_EXACT.into(),
2977            phases: vec![],
2978            runtime: vec![],
2979            browser: vec![],
2980            server: background_records.clone(),
2981        });
2982    }
2983
2984    // Execution traces are appended by the launch observer inside application
2985    // processes, so they share the clone hazard of server evidence.
2986    let execution =
2987        parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2988            entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2989        }));
2990    let execution_events = execution.records;
2991    let count_event = |name: &str| {
2992        execution_events
2993            .iter()
2994            .filter(|event| event.kind() == name)
2995            .count()
2996    };
2997    let transport = TransportStats {
2998        processes: count_event("process"),
2999        child_launches: count_event("child-launch"),
3000        remote_launches: count_event("remote-launch"),
3001        workspace_capabilities: count_event("workspace-capability"),
3002        guest_processes: count_event("guest-process"),
3003        scoped_server_records: scoped_records.len(),
3004        background_server_records: background_records.len(),
3005        corrupt_records: scoped.corrupt_records
3006            + background.corrupt_records
3007            + execution.corrupt_records,
3008        corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
3009    };
3010    let frontend = entries
3011        .iter()
3012        .find(|entry| entry.path == "frontend.json")
3013        .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
3014        .and_then(parse_entry::<FrontendRunDeclaration>)?;
3015    let persisted = entries
3016        .iter()
3017        .find(|entry| entry.path == "coverage-model.json")
3018        .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
3019        .and_then(parse_entry::<PersistedCoverageModel>)?;
3020    let coverage_model =
3021        persisted
3022            .into_declaration()
3023            .map_err(|reason| ReportError::InvalidJson {
3024                path: "coverage-model.json".into(),
3025                reason: reason.into(),
3026            })?;
3027    if frontend.language != coverage_model.language {
3028        return Err(ReportError::InvalidArchive(format!(
3029            "frontend language {} differs from coverage model language {}",
3030            frontend.language, coverage_model.language
3031        )));
3032    }
3033    separate_repeated_executions(&mut raw_results);
3034    release_leaked_phases(&mut raw_results);
3035    let normalized = CoverageReportRequest {
3036        run_id: request.run_id.clone(),
3037        manifest,
3038        raw_results,
3039        generated_at: request.generated_at.clone(),
3040        coverage_model: Some(coverage_model),
3041        integrity: request.integrity.clone(),
3042        test_exit_code: request.test_exit_code.clone(),
3043    };
3044    // Everything is parsed out of the archive's bytes -- 385 MB on a
3045    // 3,900-test run -- and nothing reads them again, so they need not stay
3046    // resident while the views are built beside what was parsed from them.
3047    drop(entries);
3048    analyze_frontend_request(&frontend, &normalized, transport)
3049}
3050
3051/// Give a test that ran again under the same identity phases of its own.
3052///
3053/// A runner's attempt counter lives in its process, so a command that runs the
3054/// same tests twice -- ms runs Jest once in Node's environment and once in the
3055/// edge runtime's -- mints the same attempt, and the same phase ids, twice.
3056/// Every such run was refused whole ("duplicate frontend phase ID"). A record
3057/// that declares a phase an earlier record already declared is another
3058/// execution: its phases, and its own references to them, are renamed apart.
3059/// Both executions stay one attempt, whose outcome folds as any attempt's does.
3060fn separate_repeated_executions(raw_results: &mut [RawTestResult]) {
3061    let mut declared = BTreeSet::<String>::new();
3062    let mut executions = BTreeMap::<String, usize>::new();
3063    for raw in raw_results.iter_mut() {
3064        let repeated = raw.phases.iter().any(|phase| declared.contains(&phase.id));
3065        if !repeated {
3066            declared.extend(raw.phases.iter().map(|phase| phase.id.clone()));
3067            continue;
3068        }
3069        let mut renamed = BTreeMap::<String, String>::new();
3070        for phase in &raw.phases {
3071            let count = executions.entry(phase.id.clone()).or_insert(1);
3072            let fresh = loop {
3073                *count += 1;
3074                let candidate = format!("{}~{}", phase.id, count);
3075                if !declared.contains(&candidate) {
3076                    break candidate;
3077                }
3078            };
3079            renamed.insert(phase.id.clone(), fresh);
3080        }
3081        let rename = |phase: &mut Option<String>| {
3082            if let Some(fresh) = phase.as_ref().and_then(|id| renamed.get(id)) {
3083                *phase = Some(fresh.clone());
3084            }
3085        };
3086        for phase in &mut raw.phases {
3087            phase.id = renamed[&phase.id].clone();
3088            rename(&mut phase.caused_by_phase_id);
3089        }
3090        for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
3091            rename(&mut snapshot.phase_id);
3092            for event in &mut snapshot.events {
3093                rename(&mut event.phase_id);
3094            }
3095        }
3096        for record in &mut raw.server {
3097            rename(&mut record.phase_id);
3098        }
3099        declared.extend(renamed.into_values());
3100    }
3101}
3102
3103/// Untag evidence that names another test's phase.
3104///
3105/// Work a test starts can finish after it: axios's HTTP/2 upload tests leave a
3106/// stream settling, and 2.0.1's runtime tagged what it ran with the ended
3107/// test's assertion phase, inside the next test's record. Three such events
3108/// in 2,201 records made the whole run unreadable ("unknown frontend phase
3109/// reference"). A phase another record of the same run declares is not
3110/// corruption: the event keeps its hit in the record that holds it and loses
3111/// the phase it cannot belong to. A phase no record declares is still refused.
3112fn release_leaked_phases(raw_results: &mut [RawTestResult]) {
3113    let declared = raw_results
3114        .iter()
3115        .flat_map(|raw| raw.phases.iter().map(|phase| phase.id.clone()))
3116        .collect::<BTreeSet<_>>();
3117    for raw in raw_results.iter_mut() {
3118        let own = raw
3119            .phases
3120            .iter()
3121            .map(|phase| phase.id.clone())
3122            .collect::<BTreeSet<_>>();
3123        let leaked = |phase: &mut Option<String>| {
3124            if phase
3125                .as_ref()
3126                .is_some_and(|id| !own.contains(id) && declared.contains(id))
3127            {
3128                *phase = None;
3129            }
3130        };
3131        for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
3132            leaked(&mut snapshot.phase_id);
3133            for event in &mut snapshot.events {
3134                leaked(&mut event.phase_id);
3135            }
3136        }
3137        for record in &mut raw.server {
3138            leaked(&mut record.phase_id);
3139        }
3140    }
3141}
3142
3143/// The analysis of a frontend run, from its request as an archive holds it:
3144/// what `analyze_coverage_archive` does once it has parsed the archive, and
3145/// what a run that still holds the request it archived calls instead of
3146/// reading the archive back.
3147pub fn analyze_frontend_request(
3148    frontend: &FrontendRunDeclaration,
3149    normalized: &CoverageReportRequest,
3150    transport: TransportStats,
3151) -> Result<CoverageReport, ReportError> {
3152    let mut report = crate::frontend_protocol::analyze_frontend_results(frontend, normalized)
3153        .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
3154    report.view.transport = Some(transport.clone());
3155    report.filters.passed.transport = Some(transport.clone());
3156    report.filters.failed.transport = Some(transport);
3157    Ok(report)
3158}
3159
3160impl TransportStats {
3161    /// An archive with no launch-observer or server evidence: every frontend
3162    /// but JavaScript's.
3163    pub fn none() -> Self {
3164        Self {
3165            processes: 0,
3166            child_launches: 0,
3167            remote_launches: 0,
3168            workspace_capabilities: 0,
3169            guest_processes: 0,
3170            scoped_server_records: 0,
3171            background_server_records: 0,
3172            corrupt_records: 0,
3173            corrupt_files: 0,
3174        }
3175    }
3176}
3177
3178#[cfg(test)]
3179mod tests {
3180    use std::{
3181        fs,
3182        sync::atomic::{AtomicU64, Ordering},
3183        time::SystemTime,
3184    };
3185
3186    use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
3187
3188    use super::*;
3189
3190    #[test]
3191    fn a_view_holds_one_copy_of_each_id_it_names() {
3192        // What made analysis fit in memory: a view names each test at every
3193        // point, line and confidence it reached, and must share one copy of
3194        // the name rather than hold one per mention. A change that copies ids
3195        // again fails here before it shows up as gigabytes. The full and the
3196        // passing view are built side by side, each with its own copy.
3197        let root = std::env::temp_dir().join(format!(
3198            "supercov-shared-ids-{}-{}",
3199            std::process::id(),
3200            std::time::SystemTime::now()
3201                .duration_since(std::time::UNIX_EPOCH)
3202                .unwrap()
3203                .as_nanos()
3204        ));
3205        let directory = crate::run_store::create_analyzable_test_run(&root, "shared");
3206        let run = crate::run_store::discover_runs(&root)
3207            .unwrap()
3208            .runs
3209            .remove(0);
3210        let report = crate::run_store::analyze_stored_run(&run).unwrap();
3211        let _ = directory;
3212        for view in [&report.view, &report.filters.passed] {
3213            let mentions = view
3214                .points
3215                .iter()
3216                .flat_map(|point| point.tests.iter().chain(&point.confidence.tests))
3217                .chain(view.lines.iter().flat_map(|line| &line.tests))
3218                .collect::<Vec<_>>();
3219            assert!(
3220                mentions.len() >= 2,
3221                "the fixture names its test in several places"
3222            );
3223            let first = mentions[0];
3224            for mention in &mentions {
3225                assert_eq!(*mention, first);
3226                assert!(
3227                    mention.same_allocation(first),
3228                    "every mention shares one copy"
3229                );
3230            }
3231        }
3232        fs::remove_dir_all(root).unwrap();
3233    }
3234
3235    #[test]
3236    fn references_numbered_as_they_arrive_come_out_as_the_sets_inserted_directly() {
3237        // Out of order, repeated, interleaved across obligations, and sharing
3238        // long prefixes, as test and phase ids do.
3239        let arrivals = [
3240            ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3241            ("py:statement:a", "h11/tests/test_x.py::test_b[2-50]"),
3242            ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3243            ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3244            ("py:statement:a", "h11/tests/test_x.py::test_b[10-50]"),
3245            ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3246            ("py:statement:c", "python-phase:9"),
3247            ("py:statement:c", "python-phase:10"),
3248            ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3249        ];
3250        let mut ids = Interner::default();
3251        let mut hits = HitIds::default();
3252        let mut numbered = References::default();
3253        // A hit known to another relation must not become covered here.
3254        hits.number("unused", &mut ids);
3255        let mut direct = HashMap::<String, BTreeSet<String>>::new();
3256        for (obligation, name) in arrivals {
3257            let hit = hits.number(obligation, &mut ids);
3258            numbered.add(hit, name, &mut ids);
3259            direct
3260                .entry(obligation.to_owned())
3261                .or_default()
3262                .insert(name.to_owned());
3263        }
3264        let numbered = numbered
3265            .into_relation(&hits)
3266            .by_hit
3267            .into_iter()
3268            .map(|(obligation, reached)| {
3269                let names = reached
3270                    .names
3271                    .into_iter()
3272                    .map(String::from)
3273                    .collect::<Vec<_>>();
3274                let mut sorted_names = names.clone();
3275                sorted_names.sort();
3276                sorted_names.dedup();
3277                assert_eq!(
3278                    names, sorted_names,
3279                    "a relation lists its ids once, in order"
3280                );
3281                (obligation, names.into_iter().collect())
3282            })
3283            .collect::<HashMap<_, BTreeSet<String>>>();
3284        assert_eq!(numbered, direct);
3285        assert_eq!(
3286            hits.sorted((0..hits.names.len()).collect()),
3287            [
3288                "py:statement:a",
3289                "py:statement:b",
3290                "py:statement:c",
3291                "unused"
3292            ]
3293            .map(Id::from)
3294        );
3295    }
3296
3297    static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
3298
3299    fn point(id: &str, line: usize) -> PointMeta {
3300        PointMeta {
3301            id: id.into(),
3302            kind: PointKind::Statement,
3303            file: "src/app.js".into(),
3304            line,
3305            column: 0,
3306            source: "work();".into(),
3307            label: None,
3308        }
3309    }
3310
3311    fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
3312        RawTestResult {
3313            test_id: Some(id.into()),
3314            scope: None,
3315            test: id.into(),
3316            test_file: Some("tests/app.test.js".into()),
3317            title: None,
3318            retry: Some(retry),
3319            status: Some(status.into()),
3320            expected_status: None,
3321            flaky: false,
3322            provenance: TestProvenance {
3323                runner: "node:test".into(),
3324                kind: "unit".into(),
3325                project: None,
3326                source: "runner-default".into(),
3327            },
3328            role: "test".into(),
3329            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
3330            phases: vec![],
3331            runtime: vec![RuntimeSnapshot {
3332                decisions: vec![],
3333                hits: hits.iter().map(|hit| (*hit).into()).collect(),
3334                events: vec![],
3335                logicals: vec![],
3336                phase_id: None,
3337            }],
3338            browser: vec![],
3339            server: vec![],
3340        }
3341    }
3342
3343    #[test]
3344    fn server_work_no_request_named_is_credited_to_the_only_test_running() {
3345        let phase = |start: i64, end: i64| CoveragePhase {
3346            id: format!("phase-{start}"),
3347            kind: "action".into(),
3348            operation: "get".into(),
3349            source: None,
3350            caused_by_phase_id: None,
3351            started_at_ms: start,
3352            ended_at_ms: Some(end),
3353            status: None,
3354            error: None,
3355        };
3356        let at = |timestamp: i64| ServerRecord {
3357            record_type: "hit".into(),
3358            meta: None,
3359            vector: None,
3360            id: Some(format!("hit-{timestamp}")),
3361            timestamp_ms: Some(timestamp),
3362            phase_id: None,
3363            statement_id: None,
3364            scope: None,
3365            request: Some(timestamp as u64),
3366        };
3367        // Module loading in a test process: no request, never credited.
3368        let mut loading = at(170);
3369        loading.request = None;
3370        let mut first = raw("checkout", 0, "passed", &[]);
3371        first.phases = vec![phase(100, 150), phase(160, 200)];
3372        let mut second = raw("refund", 0, "passed", &[]);
3373        second.phases = vec![phase(300, 400)];
3374        // A third that overlaps the second: they ran at once.
3375        let mut beside = raw("report", 0, "passed", &[]);
3376        beside.phases = vec![phase(350, 450)];
3377        let mut results = vec![first, second, beside];
3378        let left = credit_by_time(
3379            &mut results,
3380            vec![vec![at(50), at(155), loading, at(320), at(360), at(500)]],
3381        );
3382        // During the first test, between two of its phases: its own.
3383        assert_eq!(results[0].server.len(), 1);
3384        assert_eq!(results[0].attribution, ATTRIBUTION_BY_TIME);
3385        // While the second ran alone: its own.
3386        assert_eq!(results[1].server.len(), 1);
3387        assert_eq!(results[1].attribution, ATTRIBUTION_BY_TIME);
3388        // Before any test, while two ran at once, and after all: no test's.
3389        assert_eq!(results[2].server.len(), 0);
3390        assert_eq!(results[2].attribution, ATTRIBUTION_EXACT);
3391        let left = left
3392            .iter()
3393            .filter_map(|record| record.timestamp_ms)
3394            .collect::<Vec<_>>();
3395        assert_eq!(left, [50, 170, 360, 500]);
3396        // Still complete: the credit adds to the test's reach, never hides it.
3397        assert!(coverage_is_complete(ATTRIBUTION_BY_TIME));
3398        assert_eq!(
3399            least_exact(ATTRIBUTION_BY_TIME, ATTRIBUTION_PARTIAL),
3400            ATTRIBUTION_PARTIAL
3401        );
3402        assert_eq!(
3403            least_exact(ATTRIBUTION_EXACT, ATTRIBUTION_BY_TIME),
3404            ATTRIBUTION_BY_TIME
3405        );
3406    }
3407
3408    #[test]
3409    fn with_a_server_per_worker_its_requests_are_credited_among_its_own_tests() {
3410        let scoped = |id: &str, start: i64, end: i64| {
3411            let mut result = raw(id, 0, "passed", &[]);
3412            result.scope = Some(ExecutionScope {
3413                version: 1,
3414                run_id: "run".into(),
3415                worker_id: format!("worker-{id}"),
3416                test_id: id.into(),
3417                test_key: id.into(),
3418                retry: 0,
3419                attempt_id: format!("attempt-{id}"),
3420            });
3421            result.phases = vec![CoveragePhase {
3422                id: format!("phase-{id}"),
3423                kind: "action".into(),
3424                operation: "get".into(),
3425                source: None,
3426                caused_by_phase_id: None,
3427                started_at_ms: start,
3428                ended_at_ms: Some(end),
3429                status: None,
3430                error: None,
3431            }];
3432            result
3433        };
3434        let record = |kind: &str, id: &str, at: i64, request: Option<u64>| ServerRecord {
3435            record_type: kind.into(),
3436            meta: None,
3437            vector: None,
3438            id: Some(id.into()),
3439            timestamp_ms: Some(at),
3440            phase_id: None,
3441            statement_id: None,
3442            scope: None,
3443            request,
3444        };
3445        // Two workers' tests overlap from 150 to 200; each has its own server.
3446        let mut results = vec![scoped("first", 100, 200), scoped("second", 150, 300)];
3447        let first_server = vec![
3448            record("served", "attempt-first", 101, None),
3449            // During the overlap: the first worker's server, so its test.
3450            record("hit", "webhook", 170, Some(1)),
3451            // While only the second worker's test runs: not this server's.
3452            record("hit", "late", 250, Some(2)),
3453        ];
3454        let second_server = vec![record("served", "attempt-second", 151, None)];
3455        let left = credit_by_time(&mut results, vec![first_server, second_server]);
3456        assert_eq!(results[0].server.len(), 1, "{:?}", results[0].server);
3457        assert_eq!(results[0].server[0].id.as_deref(), Some("webhook"));
3458        assert!(results[1].server.is_empty());
3459        assert_eq!(
3460            left.iter().filter_map(|r| r.id.clone()).collect::<Vec<_>>(),
3461            ["late"],
3462            "served markers are not coverage"
3463        );
3464    }
3465
3466    #[test]
3467    fn declined_obligations_are_unmeasured_never_uncovered() {
3468        // Two statements, neither executed. One of them Supercov declined to
3469        // measure. The declined one must not appear as a coverage gap: it
3470        // leaves the denominator and is reported as unmeasured instead.
3471        let point = |id: &str, line: usize| PointMeta {
3472            id: id.into(),
3473            kind: PointKind::Statement,
3474            file: "src/app.js".into(),
3475            line,
3476            column: 0,
3477            source: "work();".into(),
3478            label: None,
3479        };
3480        let measured_only = CoverageManifest {
3481            decisions: vec![],
3482            points: vec![point("measured", 1)],
3483            branches: vec![],
3484            limitations: vec![],
3485            unmeasured: Vec::new(),
3486            scope: None,
3487            assertion_sites: Vec::new(),
3488        };
3489        let with_declined = CoverageManifest {
3490            decisions: vec![],
3491            points: vec![point("measured", 1), point("declined", 2)],
3492            branches: vec![],
3493            limitations: vec![],
3494            unmeasured: vec!["declined".into()],
3495            scope: None,
3496            assertion_sites: Vec::new(),
3497        };
3498
3499        let baseline =
3500            create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
3501        let view =
3502            create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
3503
3504        // The declined statement never inflates the uncovered count.
3505        assert_eq!(
3506            view.summary.statements.total, baseline.summary.statements.total,
3507            "a declined obligation stayed in the covered/uncovered denominator"
3508        );
3509        assert_eq!(view.summary.unmeasured_obligations, Some(1));
3510        assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
3511
3512        // Its line leaves the line total too, but not the report: the line is
3513        // still there to hang a limitation on, and it is neither covered nor
3514        // uncovered.
3515        assert_eq!(
3516            view.summary.lines.total, baseline.summary.lines.total,
3517            "a declined obligation kept its line in the line denominator"
3518        );
3519        let declined_line = view
3520            .lines
3521            .iter()
3522            .find(|line| line.line == 2)
3523            .expect("the declined line stays addressable");
3524        assert!(!declined_line.measured);
3525        assert!(!declined_line.covered);
3526
3527        // A fully measured run keeps its previous output exactly: the new
3528        // fields are absent, not zero, so existing consumers see no change.
3529        assert_eq!(baseline.summary.unmeasured_obligations, None);
3530        assert_eq!(baseline.summary.exact_fraction_pct, None);
3531        let encoded = serde_json::to_string(&baseline.summary).unwrap();
3532        assert!(
3533            !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
3534            "a fully measured summary must serialize unchanged: {encoded}"
3535        );
3536    }
3537
3538    #[test]
3539    fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
3540        // The subtle case: the statement ran, but Supercov could not measure
3541        // it, so its line has no coverage question to answer. Counting it as
3542        // covered would inflate the ratio with a line nothing verified.
3543        let point = |id: &str| PointMeta {
3544            id: id.into(),
3545            kind: PointKind::Statement,
3546            file: "src/app.js".into(),
3547            line: 1,
3548            column: 0,
3549            source: "work();".into(),
3550            label: None,
3551        };
3552        let manifest = CoverageManifest {
3553            decisions: vec![],
3554            points: vec![point("declined")],
3555            branches: vec![],
3556            limitations: vec![],
3557            unmeasured: vec!["declined".into()],
3558            scope: None,
3559            assertion_sites: Vec::new(),
3560        };
3561        let view = create_coverage_view(
3562            &manifest,
3563            &[raw("test", 0, "passed", &["declined"])],
3564            "time",
3565        )
3566        .unwrap();
3567
3568        assert_eq!(view.summary.lines.total, 0);
3569        assert_eq!(view.summary.lines.covered, 0);
3570        let line = view.lines.first().expect("the line stays addressable");
3571        assert!(!line.measured);
3572        assert!(!line.covered, "an unmeasured line is not covered either");
3573    }
3574
3575    #[test]
3576    fn report_retains_unexecuted_manifest_conditions() {
3577        let manifest = CoverageManifest {
3578            unmeasured: Vec::new(),
3579            decisions: vec![DecisionMeta {
3580                id: "decision".into(),
3581                file: "src/app.js".into(),
3582                line: 1,
3583                column: 0,
3584                source: "left && right".into(),
3585                conditions: vec!["left".into(), "right".into()],
3586                kind: "if".into(),
3587            }],
3588            points: vec![],
3589            branches: vec![],
3590            limitations: vec![],
3591            scope: None,
3592            assertion_sites: Vec::new(),
3593        };
3594        let view =
3595            create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
3596        assert_eq!(view.summary.conditions, 2);
3597        assert_eq!(view.summary.covered_conditions, 0);
3598        assert_eq!(view.decisions[0].conditions.len(), 2);
3599    }
3600
3601    #[test]
3602    fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
3603        let manifest = CoverageManifest {
3604            unmeasured: Vec::new(),
3605            decisions: vec![DecisionMeta {
3606                id: "application-decision".into(),
3607                file: "src/app.js".into(),
3608                line: 1,
3609                column: 0,
3610                source: "left && right".into(),
3611                conditions: vec!["left".into(), "right".into()],
3612                kind: "if".into(),
3613            }],
3614            points: vec![],
3615            branches: vec![],
3616            limitations: vec![],
3617            scope: None,
3618            assertion_sites: Vec::new(),
3619        };
3620        let mut attempt = raw("test", 0, "passed", &[]);
3621        attempt.runtime[0].decisions.push(DecisionSnapshot {
3622            meta: DecisionMeta {
3623                id: "synthetic-fixture".into(),
3624                file: "fixtures/generated.js".into(),
3625                line: 1,
3626                column: 0,
3627                source: "a && b && c".into(),
3628                conditions: vec!["a".into(), "b".into(), "c".into()],
3629                kind: "if".into(),
3630            },
3631            vectors: vec![McdcVector {
3632                values: vec![Some(true), Some(true), Some(true)],
3633                outcome: true,
3634            }],
3635        });
3636
3637        let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3638        assert_eq!(view.decisions.len(), 1);
3639        assert_eq!(view.decisions[0].meta.id, "application-decision");
3640        assert!(view.decisions[0].vectors.is_empty());
3641    }
3642
3643    #[test]
3644    fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
3645        let expected = DecisionMeta {
3646            id: "application-decision".into(),
3647            file: "src/app.js".into(),
3648            line: 1,
3649            column: 0,
3650            source: "left && right".into(),
3651            conditions: vec!["left".into(), "right".into()],
3652            kind: "if".into(),
3653        };
3654        let manifest = CoverageManifest {
3655            unmeasured: Vec::new(),
3656            decisions: vec![expected.clone()],
3657            points: vec![],
3658            branches: vec![],
3659            limitations: vec![],
3660            scope: Some(serde_json::json!({
3661                "entries": [{ "file": "src/empty.js", "status": "included" }]
3662            })),
3663            assertion_sites: Vec::new(),
3664        };
3665        let vector = McdcVector {
3666            values: vec![Some(true), Some(true)],
3667            outcome: true,
3668        };
3669
3670        let mut unknown = raw("test", 0, "passed", &[]);
3671        unknown.runtime[0].decisions.push(DecisionSnapshot {
3672            meta: DecisionMeta {
3673                id: "unknown-decision".into(),
3674                file: "src/empty.js".into(),
3675                ..expected.clone()
3676            },
3677            vectors: vec![vector.clone()],
3678        });
3679        assert!(matches!(
3680            create_coverage_view(&manifest, &[unknown], "time"),
3681            Err(ReportError::InvalidServerRecord(reason))
3682                if reason.contains("absent from the frozen manifest")
3683        ));
3684
3685        let mut changed = raw("test", 0, "passed", &[]);
3686        changed.runtime[0].decisions.push(DecisionSnapshot {
3687            meta: DecisionMeta {
3688                source: "left || right".into(),
3689                ..expected
3690            },
3691            vectors: vec![vector],
3692        });
3693        assert!(matches!(
3694            create_coverage_view(&manifest, &[changed], "time"),
3695            Err(ReportError::InvalidServerRecord(reason))
3696                if reason.contains("differs from the frozen manifest")
3697        ));
3698    }
3699
3700    #[test]
3701    fn neither_timestamp_nor_explicit_phase_links_award_assertion_credit() {
3702        let manifest = CoverageManifest {
3703            unmeasured: Vec::new(),
3704            decisions: vec![],
3705            points: vec![point("hit", 1)],
3706            branches: vec![],
3707            limitations: vec![],
3708            scope: None,
3709            assertion_sites: Vec::new(),
3710        };
3711        let phase = CoveragePhase {
3712            id: "assertion".into(),
3713            kind: "assertion".into(),
3714            operation: "equal".into(),
3715            source: None,
3716            caused_by_phase_id: None,
3717            started_at_ms: 100,
3718            ended_at_ms: Some(120),
3719            status: Some("passed".into()),
3720            error: None,
3721        };
3722        let mut attempt = raw("test", 0, "passed", &["hit"]);
3723        attempt.phases.push(phase.clone());
3724        attempt.runtime[0].events.push(RuntimeEvent {
3725            event_type: "hit".into(),
3726            id: "hit".into(),
3727            vector: None,
3728            timestamp_ms: 110,
3729            phase_id: None,
3730            statement_id: None,
3731            environment: "server".into(),
3732        });
3733        let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
3734        assert_eq!(inferred.points[0].confidence.level, "executed");
3735        assert!(!inferred.points[0].confidence.asserted);
3736
3737        attempt.runtime[0].events[0].phase_id = Some(phase.id);
3738        let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3739        assert_eq!(explicit.points[0].confidence.level, "executed");
3740        assert!(!explicit.points[0].confidence.asserted);
3741    }
3742
3743    #[test]
3744    fn an_event_in_the_millisecond_one_phase_ended_and_the_next_began_is_the_first_ones() {
3745        let phase = |id: &str, started_at_ms: i64, ended_at_ms: i64| CoveragePhase {
3746            id: id.into(),
3747            kind: "action".into(),
3748            operation: id.into(),
3749            source: None,
3750            caused_by_phase_id: None,
3751            started_at_ms,
3752            ended_at_ms: Some(ended_at_ms),
3753            status: Some("passed".into()),
3754            error: None,
3755        };
3756        // `page.goto` resolved and the test's `expect` began in millisecond
3757        // 200. What the page ran while loading carried that time and no phase,
3758        // and all of it was the `expect`'s: the action that loaded the page
3759        // read as having run nothing.
3760        let handed_over = [phase("goto", 100, 200), phase("expect", 200, 214)];
3761        assert_eq!(phase_at(&handed_over, 199), Some("goto"));
3762        assert_eq!(phase_at(&handed_over, 200), Some("goto"));
3763        assert_eq!(phase_at(&handed_over, 201), Some("expect"));
3764        assert_eq!(phase_at(&handed_over, 99), None);
3765        // A phase that begins after a gap has its first millisecond to itself.
3766        let apart = [phase("goto", 100, 150), phase("click", 200, 214)];
3767        assert_eq!(phase_at(&apart, 200), Some("click"));
3768        // One inside another, still running, keeps what happens as it begins.
3769        let inside = [phase("step", 100, 300), phase("expect", 200, 220)];
3770        assert_eq!(phase_at(&inside, 200), Some("expect"));
3771        // Two that both lie within the millisecond: the later, as before.
3772        let instant = [phase("first", 200, 200), phase("second", 200, 210)];
3773        assert_eq!(phase_at(&instant, 200), Some("second"));
3774    }
3775
3776    #[test]
3777    fn verified_view_uses_only_the_terminal_successful_attempt() {
3778        let manifest = CoverageManifest {
3779            unmeasured: Vec::new(),
3780            decisions: vec![],
3781            points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
3782            branches: vec![],
3783            limitations: vec![],
3784            scope: None,
3785            assertion_sites: Vec::new(),
3786        };
3787        let failed = raw("flaky", 0, "failed", &["failed"]);
3788        let mut passed = raw("flaky", 1, "passed", &["passed"]);
3789        passed.flaky = true;
3790        let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
3791        expected.expected_status = Some("failed".into());
3792        let request = CoverageReportRequest {
3793            run_id: "run".into(),
3794            manifest,
3795            raw_results: vec![failed, passed, expected],
3796            generated_at: "time".into(),
3797            coverage_model: None,
3798            integrity: None,
3799            test_exit_code: ExitCodeInput::Missing,
3800        };
3801        let report = analyze_coverage_results(&request).unwrap();
3802        assert!(!report.filters.passed.points[0].covered);
3803        assert!(report.filters.passed.points[1].covered);
3804        assert!(!report.filters.passed.points[2].covered);
3805        assert!(report.filters.failed.points[0].covered);
3806        assert_eq!(
3807            report
3808                .view
3809                .tests
3810                .iter()
3811                .find(|test| test.name == "expected-failure")
3812                .unwrap()
3813                .outcome,
3814            "failed"
3815        );
3816        assert_eq!(report.view.tests[1].outcome, "flaky");
3817    }
3818
3819    #[test]
3820    fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
3821        let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
3822        expected.expected_status = Some("failed".into());
3823        let companion = raw("expected-failure", 0, "failed", &["expected"]);
3824        let request = CoverageReportRequest {
3825            run_id: "run".into(),
3826            manifest: CoverageManifest {
3827                unmeasured: Vec::new(),
3828                decisions: vec![],
3829                points: vec![point("expected", 1)],
3830                branches: vec![],
3831                limitations: vec![],
3832                scope: None,
3833                assertion_sites: Vec::new(),
3834            },
3835            raw_results: vec![companion, expected],
3836            generated_at: "time".into(),
3837            coverage_model: None,
3838            integrity: None,
3839            test_exit_code: ExitCodeInput::Missing,
3840        };
3841        let report = analyze_coverage_results(&request).unwrap();
3842        assert_eq!(report.view.tests[0].outcome, "passed");
3843        assert!(!report.filters.passed.points[0].covered);
3844        assert!(!report.filters.failed.points[0].covered);
3845    }
3846
3847    #[test]
3848    fn selected_but_unstarted_test_is_not_an_invented_attempt() {
3849        let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
3850        unstarted.scope = None;
3851        unstarted.retry = None;
3852        unstarted.runtime.clear();
3853        let request = CoverageReportRequest {
3854            run_id: "run".into(),
3855            manifest: CoverageManifest {
3856                unmeasured: Vec::new(),
3857                decisions: vec![],
3858                points: vec![],
3859                branches: vec![],
3860                limitations: vec![],
3861                scope: None,
3862                assertion_sites: Vec::new(),
3863            },
3864            raw_results: vec![unstarted],
3865            generated_at: "time".into(),
3866            coverage_model: None,
3867            integrity: None,
3868            test_exit_code: ExitCodeInput::Present(Some(100)),
3869        };
3870        let report = analyze_coverage_results(&request).unwrap();
3871        assert_eq!(report.view.tests[0].outcome, "unstarted");
3872        assert!(report.view.tests[0].attempts.is_empty());
3873        assert!(report.filters.passed.tests.is_empty());
3874        assert!(report.filters.failed.tests.is_empty());
3875    }
3876
3877    fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
3878        let nonce = SystemTime::now()
3879            .duration_since(SystemTime::UNIX_EPOCH)
3880            .unwrap()
3881            .as_nanos();
3882        let root = std::env::temp_dir().join(format!(
3883            "supercov-rust-report-{}-{nonce}-{}",
3884            std::process::id(),
3885            ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
3886        ));
3887        fs::create_dir_all(&root).unwrap();
3888        let path = root.join("evidence.raw.gz");
3889        if !entries
3890            .iter()
3891            .any(|entry| entry.path == "coverage-model.json")
3892        {
3893            entries.push(EvidenceArchiveEntry {
3894                path: "coverage-model.json".into(),
3895                contents: serde_json::to_vec(
3896                    &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
3897                        .unwrap(),
3898                )
3899                .unwrap(),
3900            });
3901        }
3902        if !entries.iter().any(|entry| entry.path == "frontend.json") {
3903            entries.push(EvidenceArchiveEntry {
3904                path: "frontend.json".into(),
3905                contents: serde_json::to_vec(&serde_json::json!({
3906                    "protocolVersion": 2,
3907                    "frontendId": "javascript",
3908                    "frontendVersion": "fixture-v1",
3909                    "language": "javascript",
3910                    "structuralSource": "owned-probes",
3911                    "runners": [{
3912                        "runner": "node:test",
3913                        "executionModel": "serial-in-process",
3914                        "attribution": {
3915                            "run": "exact",
3916                            "worker": "unavailable",
3917                            "test": "exact",
3918                            "retry": "exact",
3919                            "phase": "exact",
3920                            "action": "exact",
3921                            "assertion": "exact"
3922                        },
3923                        "limitations": [{
3924                            "id": "fixture-worker-unavailable",
3925                            "scopes": ["worker"],
3926                            "reason": "The fixture does not require worker identity"
3927                        }]
3928                    }],
3929                    "structuralLimitations": []
3930                }))
3931                .unwrap(),
3932            });
3933        }
3934        write_archive(entries, &path).unwrap();
3935        path
3936    }
3937
3938    #[test]
3939    fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
3940        let manifest = CoverageManifest {
3941            unmeasured: Vec::new(),
3942            decisions: vec![],
3943            points: vec![point("background-hit", 1), point("test-hit", 2)],
3944            branches: vec![],
3945            limitations: vec![],
3946            scope: None,
3947            assertion_sites: Vec::new(),
3948        };
3949        // Declare exactly the observed runners: the journal's node:test and the
3950        // synthesized background runner, shaped as the JavaScript run declares it.
3951        let unattributed = |axis: &str| {
3952            serde_json::json!({
3953                "id": format!("background-no-{axis}"),
3954                "scopes": [axis],
3955                "reason": format!("Runner background did not expose exact {axis} identity for every result")
3956            })
3957        };
3958        let frontend = serde_json::json!({
3959            "protocolVersion": 2,
3960            "frontendId": "javascript",
3961            "frontendVersion": "fixture-v1",
3962            "language": "javascript",
3963            "structuralSource": "owned-probes",
3964            "runners": [
3965                {
3966                    "runner": "node:test",
3967                    "executionModel": "serial-in-process",
3968                    "attribution": {
3969                        "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
3970                        "phase": "exact", "action": "exact", "assertion": "exact"
3971                    },
3972                    "limitations": [{
3973                        "id": "fixture-worker-unavailable",
3974                        "scopes": ["worker"],
3975                        "reason": "The fixture does not require worker identity"
3976                    }]
3977                },
3978                {
3979                    "runner": "background",
3980                    "executionModel": "parallel-unattributed",
3981                    "attribution": {
3982                        "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
3983                        "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
3984                    },
3985                    "limitations": [
3986                        unattributed("worker"), unattributed("test"), unattributed("retry"),
3987                        unattributed("phase"), unattributed("action"), unattributed("assertion")
3988                    ]
3989                }
3990            ],
3991            "structuralLimitations": []
3992        });
3993        let path = archive(vec![
3994            EvidenceArchiveEntry {
3995                path: "manifest.json".into(),
3996                contents: serde_json::to_vec(&manifest).unwrap(),
3997            },
3998            EvidenceArchiveEntry {
3999                path: "frontend.json".into(),
4000                contents: serde_json::to_vec(&frontend).unwrap(),
4001            },
4002            EvidenceArchiveEntry {
4003                path: "playwright-worker-1.mcdc.jsonl".into(),
4004                contents: {
4005                    let mut contents =
4006                        serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
4007                            .unwrap();
4008                    contents.extend_from_slice(b"\npartial-final-line");
4009                    contents
4010                },
4011            },
4012            EvidenceArchiveEntry {
4013                path: "server/background/worker.jsonl".into(),
4014                contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
4015            },
4016        ]);
4017        let result = analyze_coverage_archive(&ArchiveReportRequest {
4018            archive_path: path.clone(),
4019            run_id: "run".into(),
4020            generated_at: "time".into(),
4021            integrity: None,
4022            test_exit_code: ExitCodeInput::Missing,
4023        });
4024        assert!(matches!(
4025            result,
4026            Err(ReportError::InvalidJson { path, .. })
4027                if path == "playwright-worker-1.mcdc.jsonl"
4028        ));
4029        fs::remove_dir_all(path.parent().unwrap()).unwrap();
4030    }
4031
4032    #[test]
4033    fn a_torn_background_record_is_counted_not_fatal() {
4034        // Pool VMs restored from one snapshot are clones with the same pid and
4035        // cached shard path; their appends over a shared mount tear lines. One
4036        // torn line is one lost record: the report must still build, count it,
4037        // and surface it through the corrupt-evidence accounting.
4038        let manifest = CoverageManifest {
4039            unmeasured: Vec::new(),
4040            decisions: vec![],
4041            points: vec![],
4042            branches: vec![],
4043            limitations: vec![],
4044            scope: None,
4045            assertion_sites: Vec::new(),
4046        };
4047        // Declare exactly the observed runners: the journal's node:test and the
4048        // synthesized background runner, shaped as the JavaScript run declares it.
4049        let unattributed = |axis: &str| {
4050            serde_json::json!({
4051                "id": format!("background-no-{axis}"),
4052                "scopes": [axis],
4053                "reason": format!("Runner background did not expose exact {axis} identity for every result")
4054            })
4055        };
4056        let frontend = serde_json::json!({
4057            "protocolVersion": 2,
4058            "frontendId": "javascript",
4059            "frontendVersion": "fixture-v1",
4060            "language": "javascript",
4061            "structuralSource": "owned-probes",
4062            "runners": [
4063                {
4064                    "runner": "node:test",
4065                    "executionModel": "serial-in-process",
4066                    "attribution": {
4067                        "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
4068                        "phase": "exact", "action": "exact", "assertion": "exact"
4069                    },
4070                    "limitations": [{
4071                        "id": "fixture-worker-unavailable",
4072                        "scopes": ["worker"],
4073                        "reason": "The fixture does not require worker identity"
4074                    }]
4075                },
4076                {
4077                    "runner": "background",
4078                    "executionModel": "parallel-unattributed",
4079                    "attribution": {
4080                        "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
4081                        "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
4082                    },
4083                    "limitations": [
4084                        unattributed("worker"), unattributed("test"), unattributed("retry"),
4085                        unattributed("phase"), unattributed("action"), unattributed("assertion")
4086                    ]
4087                }
4088            ],
4089            "structuralLimitations": []
4090        });
4091        let path = archive(vec![
4092            EvidenceArchiveEntry {
4093                path: "manifest.json".into(),
4094                contents: serde_json::to_vec(&manifest).unwrap(),
4095            },
4096            EvidenceArchiveEntry {
4097                path: "frontend.json".into(),
4098                contents: serde_json::to_vec(&frontend).unwrap(),
4099            },
4100            EvidenceArchiveEntry {
4101                path: "playwright-worker-1.mcdc.jsonl".into(),
4102                contents: {
4103                    let mut contents =
4104                        serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
4105                            .unwrap();
4106                    contents.push(b'\n');
4107                    contents
4108                },
4109            },
4110            EvidenceArchiveEntry {
4111                path: "execution.37360-1.168.jsonl".into(),
4112                contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
4113            },
4114            EvidenceArchiveEntry {
4115                path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
4116                contents: concat!(
4117                    "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
4118                    "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
4119                    "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
4120                )
4121                .as_bytes()
4122                .to_vec(),
4123            },
4124        ]);
4125        let report = analyze_coverage_archive(&ArchiveReportRequest {
4126            archive_path: path.clone(),
4127            run_id: "run".into(),
4128            generated_at: "time".into(),
4129            integrity: None,
4130            test_exit_code: ExitCodeInput::Missing,
4131        })
4132        .expect("a torn evidence line must not fail the whole run");
4133        let transport = report.view.transport.expect("transport stats");
4134        assert_eq!(transport.corrupt_records, 2);
4135        assert_eq!(transport.corrupt_files, 2);
4136        assert_eq!(transport.background_server_records, 2);
4137        fs::remove_dir_all(path.parent().unwrap()).unwrap();
4138    }
4139
4140    #[test]
4141    fn archive_analysis_rejects_cross_run_evidence() {
4142        let manifest = CoverageManifest {
4143            unmeasured: Vec::new(),
4144            decisions: vec![],
4145            points: vec![],
4146            branches: vec![],
4147            limitations: vec![],
4148            scope: None,
4149            assertion_sites: Vec::new(),
4150        };
4151        let mut result = raw("test", 0, "passed", &[]);
4152        result.scope = Some(ExecutionScope {
4153            version: 1,
4154            run_id: "other-run".into(),
4155            worker_id: "worker".into(),
4156            test_id: "test".into(),
4157            test_key: "key".into(),
4158            retry: 0,
4159            attempt_id: "attempt".into(),
4160        });
4161        let path = archive(vec![
4162            EvidenceArchiveEntry {
4163                path: "manifest.json".into(),
4164                contents: serde_json::to_vec(&manifest).unwrap(),
4165            },
4166            EvidenceArchiveEntry {
4167                path: "worker/mcdc.json".into(),
4168                contents: serde_json::to_vec(&result).unwrap(),
4169            },
4170        ]);
4171        assert!(matches!(
4172            analyze_coverage_archive(&ArchiveReportRequest {
4173                archive_path: path.clone(),
4174                run_id: "run".into(),
4175                generated_at: "time".into(),
4176                integrity: None,
4177                test_exit_code: ExitCodeInput::Missing,
4178            }),
4179            Err(ReportError::InvalidArchive(reason))
4180                if reason.contains("expected=run actual=other-run")
4181        ));
4182        fs::remove_dir_all(path.parent().unwrap()).unwrap();
4183    }
4184
4185    #[test]
4186    fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
4187        let vectors: Value = serde_json::from_str(include_str!(
4188            "../test-assets/coverage-model-v1/vectors.json"
4189        ))
4190        .unwrap();
4191        for value in vectors["valid"].as_array().unwrap() {
4192            let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
4193            model.into_declaration().unwrap();
4194        }
4195        for vector in vectors["invalid"].as_array().unwrap() {
4196            let value = vector["value"].clone();
4197            if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
4198                assert!(
4199                    model.into_declaration().is_err(),
4200                    "accepted invalid model vector: {}",
4201                    vector["reason"]
4202                );
4203            }
4204        }
4205
4206        let manifest = CoverageManifest {
4207            unmeasured: Vec::new(),
4208            decisions: vec![],
4209            points: vec![],
4210            branches: vec![],
4211            limitations: vec![],
4212            scope: None,
4213            assertion_sites: Vec::new(),
4214        };
4215        let path = archive(vec![
4216            EvidenceArchiveEntry {
4217                path: "coverage-model.json".into(),
4218                contents: serde_json::to_vec(&PersistedCoverageModel {
4219                    schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
4220                    language: "rust".into(),
4221                    variant: "rust-source-v1".into(),
4222                    name: "Rust source coverage".into(),
4223                    completeness_meaning: "Every Rust obligation was satisfied.".into(),
4224                    measured: vec!["Rust statements".into()],
4225                    not_measured: vec![],
4226                })
4227                .unwrap(),
4228            },
4229            EvidenceArchiveEntry {
4230                path: "manifest.json".into(),
4231                contents: serde_json::to_vec(&manifest).unwrap(),
4232            },
4233        ]);
4234        assert!(matches!(
4235            analyze_coverage_archive(&ArchiveReportRequest {
4236                archive_path: path.clone(),
4237                run_id: "run".into(),
4238                generated_at: "time".into(),
4239                integrity: None,
4240                test_exit_code: ExitCodeInput::Missing,
4241            }),
4242            Err(ReportError::InvalidArchive(reason))
4243                if reason.contains("javascript differs from coverage model language rust")
4244        ));
4245        fs::remove_dir_all(path.parent().unwrap()).unwrap();
4246    }
4247
4248    #[test]
4249    fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
4250        let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
4251            "runId": "run",
4252            "manifest": { "decisions": [], "points": [], "branches": [] },
4253            "rawResults": [{
4254                "test": "test",
4255                "status": "passed",
4256                "browser": [],
4257                "server": []
4258            }],
4259            "generatedAt": "time",
4260            "testExitCode": null
4261        }))
4262        .unwrap();
4263        let report = analyze_coverage_results(&request).unwrap();
4264        assert_eq!(
4265            report.execution,
4266            Some(ExecutionResult {
4267                test_exit_code: None,
4268                valid: false,
4269            })
4270        );
4271    }
4272    #[test]
4273    fn a_phase_leaked_from_another_test_is_untagged_and_an_unknown_one_is_kept() {
4274        let record = |test: &str, phases: &[&str], events: &[(&str, &str)]| -> RawTestResult {
4275            serde_json::from_value(serde_json::json!({
4276                "test": test,
4277                "phases": phases.iter().map(|id| serde_json::json!({
4278                    "id": id, "kind": "assertion", "operation": "assert.ok", "startedAtMs": 0
4279                })).collect::<Vec<_>>(),
4280                "runtime": [{
4281                    "events": events.iter().map(|(id, phase)| serde_json::json!({
4282                        "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
4283                    })).collect::<Vec<_>>()
4284                }]
4285            }))
4286            .unwrap()
4287        };
4288        let mut results = vec![
4289            record(
4290                "upload",
4291                &["a-0:assertion:1"],
4292                &[("own", "a-0:assertion:1")],
4293            ),
4294            // The stream the upload left settling ran during the next test.
4295            record(
4296                "form data",
4297                &["b-0:assertion:1"],
4298                &[
4299                    ("late", "a-0:assertion:1"),
4300                    ("mine", "b-0:assertion:1"),
4301                    ("corrupt", "z-0:assertion:9"),
4302                ],
4303            ),
4304        ];
4305        release_leaked_phases(&mut results);
4306        let phases = |raw: &RawTestResult| {
4307            raw.runtime[0]
4308                .events
4309                .iter()
4310                .map(|event| (event.id.clone(), event.phase_id.clone()))
4311                .collect::<Vec<_>>()
4312        };
4313        assert_eq!(
4314            phases(&results[0]),
4315            [("own".into(), Some("a-0:assertion:1".into()))]
4316        );
4317        assert_eq!(
4318            phases(&results[1]),
4319            [
4320                ("late".into(), None),
4321                ("mine".into(), Some("b-0:assertion:1".into())),
4322                // No record declares it: that is corruption, and it is refused later.
4323                ("corrupt".into(), Some("z-0:assertion:9".into())),
4324            ]
4325        );
4326    }
4327
4328    #[test]
4329    fn a_test_run_twice_under_one_identity_keeps_both_executions_apart() {
4330        // ms runs its Jest suite twice, in Node's environment and the edge
4331        // runtime's; each process numbers the same test's attempt 0.
4332        let record = |status: &str, events: &[(&str, &str)]| -> RawTestResult {
4333            serde_json::from_value(serde_json::json!({
4334                "test": "parse",
4335                "testId": "parse",
4336                "retry": 0,
4337                "status": status,
4338                "phases": [
4339                    { "id": "t-0:assertion:1", "kind": "assertion", "operation": "expect", "startedAtMs": 0 },
4340                    { "id": "t-0:assertion:2", "kind": "assertion", "operation": "expect", "startedAtMs": 0, "causedByPhaseId": "t-0:assertion:1" }
4341                ],
4342                "runtime": [{
4343                    "events": events.iter().map(|(id, phase)| serde_json::json!({
4344                        "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
4345                    })).collect::<Vec<_>>()
4346                }]
4347            }))
4348            .unwrap()
4349        };
4350        let mut results = vec![
4351            record("failed", &[("node", "t-0:assertion:1")]),
4352            record("passed", &[("edge", "t-0:assertion:2")]),
4353            record("passed", &[("third", "t-0:assertion:1")]),
4354        ];
4355        separate_repeated_executions(&mut results);
4356        let ids = |raw: &RawTestResult| {
4357            raw.phases
4358                .iter()
4359                .map(|phase| (phase.id.clone(), phase.caused_by_phase_id.clone()))
4360                .collect::<Vec<_>>()
4361        };
4362        let events = |raw: &RawTestResult| {
4363            raw.runtime[0]
4364                .events
4365                .iter()
4366                .map(|event| event.phase_id.clone().unwrap())
4367                .collect::<Vec<_>>()
4368        };
4369        assert_eq!(
4370            ids(&results[0]),
4371            [
4372                ("t-0:assertion:1".into(), None),
4373                ("t-0:assertion:2".into(), Some("t-0:assertion:1".into()))
4374            ]
4375        );
4376        assert_eq!(
4377            ids(&results[1]),
4378            [
4379                ("t-0:assertion:1~2".into(), None),
4380                ("t-0:assertion:2~2".into(), Some("t-0:assertion:1~2".into()))
4381            ]
4382        );
4383        assert_eq!(events(&results[1]), ["t-0:assertion:2~2"]);
4384        assert_eq!(events(&results[2]), ["t-0:assertion:1~3"]);
4385
4386        let mut test = MutableTest {
4387            id: "parse".into(),
4388            name: "parse".into(),
4389            file: None,
4390            title: None,
4391            retries: BTreeSet::new(),
4392            attempts: BTreeMap::new(),
4393            unstarted: false,
4394            runner_reported_flaky: false,
4395            provenance: TestProvenance::default(),
4396            role: "test".into(),
4397            attribution: "exact".into(),
4398            hits: Vec::new(),
4399            decisions: BTreeMap::new(),
4400        };
4401        for raw in &results {
4402            record_attempt(&mut test, raw);
4403        }
4404        assert_eq!(
4405            test_outcome(&test),
4406            "failed",
4407            "a failed execution fails the attempt"
4408        );
4409    }
4410
4411    #[test]
4412    fn a_dimension_is_summarised_over_the_tests_it_can_describe() {
4413        // A test whose reach nothing recorded contributes no hits. Counting it
4414        // in the denominator of a coverage percentage reported a package
4415        // covered entirely by parallel tests as covered 0.00% -- a wrong
4416        // number, which is worse than a missing one, because it reads as a
4417        // suite that tests nothing.
4418        let exact = |id: &str, kind: &str| TestCoverageResult {
4419            id: id.into(),
4420            name: id.into(),
4421            file: None,
4422            title: None,
4423            retries: vec![],
4424            attempts: vec![],
4425            outcome: "passed".into(),
4426            provenance: TestProvenance {
4427                runner: "go-test".into(),
4428                kind: kind.into(),
4429                project: None,
4430                source: String::new(),
4431            },
4432            role: "test".into(),
4433            attribution: ATTRIBUTION_EXACT.into(),
4434            hits: vec![],
4435            decisions: vec![],
4436            lines: vec![],
4437        };
4438        let mut run_wide = exact("parallel", "unit");
4439        run_wide.attribution = ATTRIBUTION_RUN_WIDE.into();
4440
4441        // Whose coverage it is, and whether all of it is there, are separate
4442        // questions, and conflating them gets one of them wrong.
4443        //
4444        // `partial` coverage is the test's own -- Ruby credits a line to the
4445        // first test that reaches it, and that crediting is true -- so it
4446        // counts in a percentage, and a union over such tests is a union of
4447        // things that happened. What it cannot support is the opposite
4448        // inference: that a test not recorded against a line did not run it.
4449        assert!(coverage_is_its_own(&exact("serial", "unit").attribution));
4450        assert!(coverage_is_its_own(ATTRIBUTION_PARTIAL));
4451        assert!(!coverage_is_its_own(&run_wide.attribution));
4452
4453        assert!(coverage_is_complete(ATTRIBUTION_EXACT));
4454        assert!(!coverage_is_complete(ATTRIBUTION_PARTIAL));
4455        assert!(!coverage_is_complete(ATTRIBUTION_RUN_WIDE));
4456
4457        // A test's records merge to the least exact claim among them, in
4458        // whichever order they arrive.
4459        for (left, right, merged) in [
4460            (ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL, ATTRIBUTION_PARTIAL),
4461            (ATTRIBUTION_PARTIAL, ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL),
4462            (
4463                ATTRIBUTION_PARTIAL,
4464                ATTRIBUTION_RUN_WIDE,
4465                ATTRIBUTION_RUN_WIDE,
4466            ),
4467            (ATTRIBUTION_EXACT, ATTRIBUTION_EXACT, ATTRIBUTION_EXACT),
4468        ] {
4469            assert_eq!(least_exact(left, right), merged);
4470        }
4471
4472        // And a frontend that has never heard of the field keeps its meaning:
4473        // absent is exact, so nothing already measured changes shape.
4474        assert!(coverage_is_its_own(""));
4475        assert!(coverage_is_complete(""));
4476    }
4477}