Skip to main content

supercov_engine/
owned_evidence.rs

1//! Turning what a Supercov-owned runtime wrote into the model the report reads.
2//!
3//! Go, Java and Kotlin all write the same transport, because there is no
4//! reason for them to differ and every reason not to: one format means one
5//! reader, one set of edge cases, and no way for two languages to disagree
6//! about what an evaluation was. Only the frontend declaration is per
7//! language, because only that differs in substance.
8//!
9//! The transport itself is deliberately dumb — sparse pairs of probe ids and packed
10//! vector words — because everything it could have computed instead is cheaper
11//! to compute here, once, than in a process that is trying to run tests. The
12//! mapping from probe id back to obligation lives in the manifest this module
13//! is handed, so the runtime never carries a string.
14
15use std::collections::{BTreeMap, BTreeSet};
16
17use supercov_contracts::{
18    AttributionPrecision, ExecutionModel, FrontendAttribution, FrontendLimitation,
19    FrontendLimitationScope, FrontendRunDeclaration, FrontendRunnerDeclaration,
20    LANGUAGE_FRONTEND_PROTOCOL_VERSION,
21};
22
23use crate::coverage_analysis::McdcVector;
24use crate::coverage_report::{
25    CoverageManifest, CoverageModelDeclaration, CoveragePhase, CoverageReportRequest,
26    DecisionSnapshot, ExecutionScope, ExitCodeInput, PersistedCoverageModel, RawTestResult,
27    RuntimeEvent, RuntimeSnapshot, TestProvenance,
28};
29
30/// The one phase an owned frontend can speak for is the test body itself, but
31/// a phase identifies itself across the whole run: two tests naming their
32/// bodies the same thing would be one phase claimed twice.
33fn test_phase(test_id: &str) -> String {
34    format!("{test_id}#call")
35}
36use crate::evidence_archive::EvidenceArchiveEntry;
37use crate::go_instrumenter::{GoProbe, GoProbeTarget};
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum OwnedEvidenceError {
41    Truncated(&'static str),
42    NoTests,
43}
44
45impl std::fmt::Display for OwnedEvidenceError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            OwnedEvidenceError::Truncated(part) => {
49                write!(f, "Coverage evidence ended in the middle of its {part}")
50            }
51            OwnedEvidenceError::NoTests => write!(
52                f,
53                "the run produced coverage evidence but no test announced itself"
54            ),
55        }
56    }
57}
58
59/// One evaluation of a decision, as the runtime packed it.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct PackedVector {
62    pub decision: u32,
63    pub key: u64,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Default)]
67pub struct OwnedTestEvidence {
68    pub name: String,
69    /// How the test ended, as the framework saw it: `passed`, `failed` or
70    /// `skipped`.
71    pub status: String,
72    /// Which runner announced it. Empty where the frontend has only one, in
73    /// which case that one is the answer.
74    pub runner: String,
75    /// Whether what this test reached could be credited to it. False for a Go
76    /// test that called `t.Parallel()`, an Example or a Fuzz target: the test
77    /// ran and its coverage is in the run-wide totals, and nothing can say
78    /// which of those totals were its.
79    pub unattributed: bool,
80    /// Probe id to the bitmask it was observed with.
81    pub probes: BTreeMap<u32, u32>,
82    pub vectors: Vec<PackedVector>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Default)]
86pub struct OwnedEvidence {
87    /// Run-wide probe totals, the union of every test's.
88    pub global: Vec<u32>,
89    pub tests: Vec<OwnedTestEvidence>,
90    /// Conditions per decision, as the manifest ordered them.
91    pub widths: Vec<u8>,
92    /// Every distinct vector each decision produced, run-wide.
93    pub decision_vectors: Vec<Vec<u64>>,
94}
95
96struct Cursor<'a> {
97    bytes: &'a [u8],
98    offset: usize,
99}
100
101impl Cursor<'_> {
102    fn u64(&mut self, part: &'static str) -> Result<u64, OwnedEvidenceError> {
103        let end = self.offset + 8;
104        let slice = self
105            .bytes
106            .get(self.offset..end)
107            .ok_or(OwnedEvidenceError::Truncated(part))?;
108        self.offset = end;
109        Ok(u64::from_le_bytes(slice.try_into().expect("eight bytes")))
110    }
111
112    fn text(&mut self, length: usize, part: &'static str) -> Result<String, OwnedEvidenceError> {
113        let end = self.offset + length;
114        let slice = self
115            .bytes
116            .get(self.offset..end)
117            .ok_or(OwnedEvidenceError::Truncated(part))?;
118        self.offset = end;
119        String::from_utf8(slice.to_vec()).map_err(|_| OwnedEvidenceError::Truncated(part))
120    }
121}
122
123/// Read the transport. A truncated file is an error rather than a short read:
124/// a run that was killed half way through has partial evidence, and reporting
125/// it as though it were complete would understate coverage as a fact.
126pub fn read_evidence(bytes: &[u8]) -> Result<OwnedEvidence, OwnedEvidenceError> {
127    let mut cursor = Cursor { bytes, offset: 0 };
128    let probe_count = cursor.u64("probe totals")? as usize;
129    let mut global = Vec::with_capacity(probe_count);
130    for _ in 0..probe_count {
131        global.push(cursor.u64("probe totals")? as u32);
132    }
133    let test_count = cursor.u64("test count")? as usize;
134    let mut tests = Vec::with_capacity(test_count);
135    for _ in 0..test_count {
136        let length = cursor.u64("test name")? as usize;
137        let name = cursor.text(length, "test name")?;
138        let status_length = cursor.u64("test status")? as usize;
139        let status = cursor.text(status_length, "test status")?;
140        // Whether the probes below are this test's own. An empty probe list
141        // means "reached nothing" when they are and "nothing can say what it
142        // reached" when they are not, and the two must never be conflated.
143        let unattributed = cursor.u64("test attribution")? != 0;
144        let runner_length = cursor.u64("test runner")? as usize;
145        let runner = cursor.text(runner_length, "test runner")?;
146        let hits = cursor.u64("test probes")? as usize;
147        let mut probes = BTreeMap::new();
148        for _ in 0..hits {
149            let index = cursor.u64("test probes")? as u32;
150            probes.insert(index, cursor.u64("test probes")? as u32);
151        }
152        let vector_count = cursor.u64("test vectors")? as usize;
153        let mut vectors = Vec::with_capacity(vector_count);
154        for _ in 0..vector_count {
155            let decision = cursor.u64("test vectors")? as u32;
156            vectors.push(PackedVector {
157                decision,
158                key: cursor.u64("test vectors")?,
159            });
160        }
161        tests.push(OwnedTestEvidence {
162            name,
163            status,
164            runner,
165            unattributed,
166            probes,
167            vectors,
168        });
169    }
170    let decision_count = cursor.u64("decision table")? as usize;
171    let mut widths = Vec::with_capacity(decision_count);
172    let mut decision_vectors = Vec::with_capacity(decision_count);
173    for _ in 0..decision_count {
174        widths.push(cursor.u64("decision table")? as u8);
175        let keys = cursor.u64("decision table")? as usize;
176        let mut seen = Vec::with_capacity(keys);
177        for _ in 0..keys {
178            seen.push(cursor.u64("decision table")?);
179        }
180        decision_vectors.push(seen);
181    }
182    Ok(OwnedEvidence {
183        global,
184        tests,
185        widths,
186        decision_vectors,
187    })
188}
189
190const PACKED_VALUE_SHIFT: u32 = 24;
191const PACKED_OUTCOME_SHIFT: u32 = 48;
192
193/// Unpack one evaluation into the vector MC/DC is judged from.
194///
195/// A condition the run never evaluated is `None`, not `false`. That is the
196/// distinction the whole measurement rests on: `a && b` with `a` false says
197/// nothing about `b`, and recording it as `false` would claim it had been
198/// tested.
199pub fn unpack_vector(key: u64, width: u8) -> McdcVector {
200    let evaluated = key & ((1 << PACKED_VALUE_SHIFT) - 1);
201    let values = (key >> PACKED_VALUE_SHIFT) & ((1 << PACKED_VALUE_SHIFT) - 1);
202    McdcVector {
203        values: (0..width)
204            .map(|index| {
205                let bit = 1_u64 << index;
206                (evaluated & bit != 0).then_some(values & bit != 0)
207            })
208            .collect(),
209        outcome: (key >> PACKED_OUTCOME_SHIFT) & 1 == 1,
210    }
211}
212
213/// How a test's name and package became the identity the report uses.
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct OwnedTestOutcome {
216    pub name: String,
217    pub package: String,
218    pub file: Option<String>,
219    /// `passed`, `failed` or `skipped`, as the runner reported it.
220    pub status: String,
221    /// Which runner announced it, as the frontend declares that runner. Empty
222    /// where the frontend has only one.
223    pub runner: String,
224    /// Whether what this test reached could be credited to it. False for a Go
225    /// test that called `t.Parallel()`, an Example or a Fuzz target: it ran,
226    /// and its coverage is in the run-wide record rather than in its own.
227    pub attributed: bool,
228}
229
230/// How a language's runner attributes what it records.
231///
232/// Declared rather than assumed. A frontend that overstates its precision is
233/// worse than one that admits a gap, because the report would then present a
234/// guess as a measurement.
235pub fn go_declaration() -> FrontendRunDeclaration {
236    FrontendRunDeclaration {
237        protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
238        frontend_id: "supercov-go".into(),
239        frontend_version: "go-owned-v1".into(),
240        language: "go".into(),
241        // Supercov owns the probes: `go test -cover` is a development oracle
242        // here, not a product input.
243        structural_source: supercov_contracts::StructuralSource::OwnedProbes,
244        runners: vec![FrontendRunnerDeclaration {
245            runner: "go-test".into(),
246            // Go runs a package's tests one after another unless a test opts
247            // into parallelism, and Supercov runs one package at a time.
248            execution_model: ExecutionModel::SerialInProcess,
249            attribution: exact_per_test(),
250            limitations: {
251                let mut limitations = owned_attribution_limitations("go");
252                limitations.push(FrontendLimitation {
253                    id: "go-parallel-tests".into(),
254                    scopes: vec![FrontendLimitationScope::Test],
255                    reason:
256                        "a test that calls t.Parallel() runs alongside others, so work it does after that call is recorded run-wide rather than against that test"
257                            .into(),
258                });
259                limitations
260            },
261        }],
262        structural_limitations: Vec::new(),
263    }
264}
265
266/// What a Go run's numbers mean, so a reader is never left to infer it.
267pub fn go_coverage_model() -> CoverageModelDeclaration {
268    CoverageModelDeclaration {
269        language: "go".into(),
270        variant: "go-owned-probes-v1".into(),
271        name: "supercov-go-owned-v1".into(),
272        completeness_meaning: "Every obligation Supercov derived from the module's own Go sources was observed; explicit manifest limitations identify unmeasured Go surfaces.".into(),
273        measured: vec![
274            "owned Go statements and function entries".into(),
275            "owned atomic condition vectors and decision outcomes".into(),
276            "exact per-test attribution for tests that do not call t.Parallel()".into(),
277        ],
278        not_measured: vec![
279            "generated code, vendored packages and testdata".into(),
280            "work a test does after calling t.Parallel(), which counts run-wide".into(),
281            "causal linkage to individual actions".into(),
282            "all input values, semantic partitions, paths, or concurrency interleavings".into(),
283            "mutation score or assertion fault-detection strength".into(),
284        ],
285    }
286}
287
288/// The JVM's, where each framework reports a test's start and finish and
289/// attribution follows the lifecycle the framework itself defines.
290///
291/// Two runners, because there are two lifecycles. The JUnit Platform covers
292/// every engine built on it — Jupiter, Vintage, Kotest, Spock — and TestNG,
293/// which is not one, reports through its own listener interface. They measure
294/// the same way and say so with the same precision; what differs is who does
295/// the announcing.
296pub fn jvm_declaration() -> FrontendRunDeclaration {
297    let runner = |name: &str, concurrency: &str| FrontendRunnerDeclaration {
298        runner: name.into(),
299        execution_model: ExecutionModel::SerialInProcess,
300        attribution: exact_per_test(),
301        limitations: {
302            let mut limitations = owned_attribution_limitations(name);
303            limitations.push(FrontendLimitation {
304                id: format!("{name}-parallel-execution"),
305                scopes: vec![FrontendLimitationScope::Test],
306                reason: format!(
307                    "with {concurrency}, tests overlap in one process; work they do concurrently is recorded run-wide rather than against a single test, and condition coverage is dropped because concurrent evaluations corrupt it"
308                ),
309            });
310            limitations
311        },
312    };
313    FrontendRunDeclaration {
314        protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
315        frontend_id: "supercov-jvm".into(),
316        frontend_version: "jvm-owned-v1".into(),
317        language: "jvm".into(),
318        structural_source: supercov_contracts::StructuralSource::OwnedProbes,
319        runners: vec![
320            runner("junit-platform", "JUnit parallel execution enabled"),
321            runner("testng", "TestNG's parallel suites or methods"),
322        ],
323        structural_limitations: Vec::new(),
324    }
325}
326
327/// What a JVM run's numbers mean.
328pub fn jvm_coverage_model() -> CoverageModelDeclaration {
329    CoverageModelDeclaration {
330        language: "jvm".into(),
331        variant: "jvm-owned-probes-v1".into(),
332        name: "supercov-jvm-owned-v1".into(),
333        completeness_meaning: "Every obligation Supercov derived from the project's own Java and Kotlin sources was observed; explicit manifest limitations identify unmeasured JVM surfaces.".into(),
334        measured: vec![
335            "owned Java and Kotlin statements and method entries".into(),
336            "owned atomic condition vectors and decision outcomes".into(),
337            "exact per-test attribution through the JUnit Platform's own lifecycle".into(),
338        ],
339        not_measured: vec![
340            "generated sources, and bytecode with no source in the project".into(),
341            "tests run concurrently, which count run-wide and drop condition coverage".into(),
342            "causal linkage to individual actions".into(),
343            "all input values, semantic partitions, paths, or concurrency interleavings".into(),
344            "mutation score or assertion fault-detection strength".into(),
345        ],
346    }
347}
348
349/// What the owned frontends cannot attribute, and why.
350///
351/// The contract will not accept a precision below `Exact` without a limitation
352/// naming that scope, which is the right rule: a number that quietly means
353/// less than it appears to is worse than one that says so. Supercov measures
354/// statements and decisions here, so a test's individual actions and
355/// assertions are outside what it claims, and a phase is a lifecycle the
356/// probes never see.
357fn owned_attribution_limitations(language: &str) -> Vec<FrontendLimitation> {
358    vec![
359        FrontendLimitation {
360            id: format!("{language}-phase-linkage-aggregate"),
361            scopes: vec![FrontendLimitationScope::Phase],
362            reason:
363                "probes record what a test reached, not which of its setup, body or teardown phases reached it"
364                    .into(),
365        },
366        FrontendLimitation {
367            id: format!("{language}-action-linkage-unavailable"),
368            scopes: vec![FrontendLimitationScope::Action],
369            reason: "there is no general application-action lifecycle to link coverage to".into(),
370        },
371        FrontendLimitation {
372            id: format!("{language}-assertion-linkage-unavailable"),
373            scopes: vec![FrontendLimitationScope::Assertion],
374            reason:
375                "coverage is attributed to the test that reached the code, not to the assertion that checked it"
376                    .into(),
377        },
378    ]
379}
380
381fn exact_per_test() -> FrontendAttribution {
382    FrontendAttribution {
383        run: AttributionPrecision::Exact,
384        worker: AttributionPrecision::Exact,
385        test: AttributionPrecision::Exact,
386        retry: AttributionPrecision::Exact,
387        phase: AttributionPrecision::Aggregate,
388        // Supercov measures statements and decisions here, not the individual
389        // actions or assertions inside a test.
390        action: AttributionPrecision::Unavailable,
391        assertion: AttributionPrecision::Unavailable,
392    }
393}
394
395/// Merge what several processes recorded into one run's evidence.
396///
397/// Both owned frontends need this, because both run a suite as more than one
398/// process: `go test` builds a binary per package, and a multi-module JVM
399/// build forks a JVM per module. Probe indices are project-wide, so run-wide
400/// totals are a union of equal-length arrays, and test records simply
401/// accumulate — each belongs to exactly the process that produced it.
402pub fn merge_evidence(parts: Vec<OwnedEvidence>) -> OwnedEvidence {
403    let mut merged = OwnedEvidence::default();
404    for part in parts {
405        if merged.global.len() < part.global.len() {
406            merged.global.resize(part.global.len(), 0);
407        }
408        for (slot, value) in merged.global.iter_mut().zip(part.global) {
409            *slot |= value;
410        }
411        if merged.widths.len() < part.widths.len() {
412            merged.widths.resize(part.widths.len(), 0);
413            merged
414                .decision_vectors
415                .resize(part.widths.len(), Vec::new());
416        }
417        for (id, width) in part.widths.into_iter().enumerate() {
418            merged.widths[id] = merged.widths[id].max(width);
419        }
420        for (id, keys) in part.decision_vectors.into_iter().enumerate() {
421            let seen = &mut merged.decision_vectors[id];
422            for key in keys {
423                if !seen.contains(&key) {
424                    seen.push(key);
425                }
426            }
427        }
428        merged.tests.extend(part.tests);
429    }
430    merged
431}
432
433/// A short, stable identity derived from what makes the thing itself, so two
434/// runs of the same test agree on what to call it.
435fn stable_id(prefix: &str, values: &[&str]) -> String {
436    use sha2::{Digest, Sha256};
437    let mut hash = Sha256::new();
438    for value in values {
439        hash.update(value.as_bytes());
440        hash.update([0]);
441    }
442    let digest = hash.finalize();
443    let mut encoded = String::with_capacity(prefix.len() + 25);
444    encoded.push_str(prefix);
445    encoded.push(':');
446    for byte in &digest[..12] {
447        use std::fmt::Write as _;
448        write!(&mut encoded, "{byte:02x}").expect("string formatting");
449    }
450    encoded
451}
452
453/// The obligation each probe answers for, by id.
454fn obligations(probes: &BTreeMap<u64, GoProbe>) -> BTreeMap<u32, String> {
455    probes
456        .iter()
457        .map(|(id, probe)| {
458            let obligation = match &probe.target {
459                GoProbeTarget::Statement { id } | GoProbeTarget::Function { id } => id.clone(),
460                GoProbeTarget::Alternative { alternative, .. } => alternative.clone(),
461            };
462            (*id as u32, obligation)
463        })
464        .collect()
465}
466
467fn snapshot(
468    environment: &str,
469    evidence: &OwnedTestEvidence,
470    manifest: &CoverageManifest,
471    by_probe: &BTreeMap<u32, String>,
472    phase: &str,
473) -> RuntimeSnapshot {
474    let mut events = Vec::new();
475    let mut clock = 0_i64;
476    let hits = evidence
477        .probes
478        .keys()
479        .filter_map(|probe| by_probe.get(probe).cloned())
480        .collect::<BTreeSet<_>>();
481    for id in &hits {
482        events.push(RuntimeEvent {
483            event_type: "hit".into(),
484            id: id.clone(),
485            vector: None,
486            timestamp_ms: clock,
487            phase_id: Some(phase.to_owned()),
488            statement_id: None,
489            environment: environment.into(),
490        });
491        clock += 1;
492    }
493    let mut by_decision: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
494    for vector in &evidence.vectors {
495        by_decision
496            .entry(vector.decision)
497            .or_default()
498            .push(vector.key);
499    }
500    let mut decisions = Vec::new();
501    for (index, meta) in manifest.decisions.iter().enumerate() {
502        let Some(keys) = by_decision.get(&(index as u32)) else {
503            continue;
504        };
505        let width = meta.conditions.len() as u8;
506        let observed = keys
507            .iter()
508            .map(|key| unpack_vector(*key, width))
509            .collect::<Vec<_>>();
510        for vector in &observed {
511            events.push(RuntimeEvent {
512                event_type: "decision".into(),
513                id: meta.id.clone(),
514                vector: Some(vector.clone()),
515                timestamp_ms: clock,
516                phase_id: Some(phase.to_owned()),
517                statement_id: None,
518                environment: environment.into(),
519            });
520            clock += 1;
521        }
522        decisions.push(DecisionSnapshot {
523            meta: meta.clone(),
524            vectors: observed,
525        });
526    }
527    RuntimeSnapshot {
528        decisions,
529        hits: hits.into_iter().collect(),
530        events,
531        logicals: Vec::new(),
532        phase_id: None,
533    }
534}
535
536pub struct OwnedFrontendRun {
537    pub declaration: FrontendRunDeclaration,
538    pub request: CoverageReportRequest,
539    pub tests: usize,
540}
541
542impl OwnedFrontendRun {
543    /// The archive a run publishes: what the numbers mean, who produced them,
544    /// the obligations they are measured against, and one record per test.
545    pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
546        let model = PersistedCoverageModel::from_declaration(
547            self.request
548                .coverage_model
549                .as_ref()
550                .expect("an owned frontend always declares a coverage model"),
551        )
552        .expect("owned coverage models are contract-valid");
553        let mut entries = vec![
554            EvidenceArchiveEntry {
555                path: "coverage-model.json".into(),
556                contents: serde_json::to_vec(&model)?,
557            },
558            EvidenceArchiveEntry {
559                path: "frontend.json".into(),
560                contents: serde_json::to_vec(&self.declaration)?,
561            },
562            EvidenceArchiveEntry {
563                path: "manifest.json".into(),
564                contents: serde_json::to_vec(&self.request.manifest)?,
565            },
566        ];
567        for (index, result) in self.request.raw_results.iter().enumerate() {
568            entries.push(EvidenceArchiveEntry {
569                path: format!("results/{index:08}/mcdc.json"),
570                contents: serde_json::to_vec(result)?,
571            });
572        }
573        Ok(entries)
574    }
575}
576
577/// Build the report request from what the run recorded.
578///
579/// A test the runner reported but that announced nothing still becomes a
580/// result, with no coverage. Dropping it would make a suite look smaller than
581/// it is, and a test that ran without reaching any measured line is a fact
582/// worth seeing rather than an absence worth hiding.
583/// Everything a report needs about one run of one owned frontend.
584pub struct OwnedRunInputs<'a> {
585    pub declaration: FrontendRunDeclaration,
586    /// The label runtime events carry, so a reader can tell which frontend
587    /// produced them.
588    pub environment: &'a str,
589    pub manifest: &'a CoverageManifest,
590    pub probes: &'a BTreeMap<u64, GoProbe>,
591    pub evidence: &'a OwnedEvidence,
592    pub outcomes: &'a [OwnedTestOutcome],
593    pub run_id: &'a str,
594    pub generated_at: &'a str,
595    pub test_exit_code: i32,
596    /// What the numbers mean. Carried rather than inferred from the language,
597    /// so a frontend cannot quietly inherit another's claims.
598    pub coverage_model: CoverageModelDeclaration,
599}
600
601/// Build the report request from what the run recorded.
602///
603/// A test the runner reported but that announced nothing still becomes a
604/// result, with no coverage. Dropping it would make a suite look smaller than
605/// it is, and a test that ran without reaching any measured line is a fact
606/// worth seeing rather than an absence worth hiding.
607pub fn build_frontend_run(inputs: OwnedRunInputs) -> Result<OwnedFrontendRun, OwnedEvidenceError> {
608    let OwnedRunInputs {
609        declaration,
610        environment,
611        manifest,
612        probes,
613        evidence,
614        outcomes,
615        run_id,
616        generated_at,
617        test_exit_code,
618        coverage_model,
619    } = inputs;
620    // The runner every result claims must be one the declaration names: a
621    // result attributed to a runner nobody declared is a result nothing can
622    // say the precision of, and the reader refuses it rather than guess.
623    let default_runner = declaration
624        .runners
625        .first()
626        .map(|runner| runner.runner.clone())
627        .ok_or(OwnedEvidenceError::NoTests)?;
628    let declared = declaration
629        .runners
630        .iter()
631        .map(|runner| runner.runner.as_str())
632        .collect::<BTreeSet<_>>();
633    let source = declaration.frontend_version.clone();
634    let by_probe = obligations(probes);
635    let recorded = evidence
636        .tests
637        .iter()
638        .map(|test| (test.name.clone(), test))
639        .collect::<BTreeMap<_, _>>();
640    let empty = OwnedTestEvidence::default();
641    let mut raw_results = outcomes
642        .iter()
643        .map(|outcome| {
644            let test = recorded.get(&outcome.name).copied().unwrap_or(&empty);
645            // A test nothing could be credited to takes no hits at all, and
646            // says so. Handing it the record's probes would be worse than
647            // wrong: the runtime gives an unattributed record none, so the
648            // result would read as a test that reached nothing.
649            let test = if outcome.attributed { test } else { &empty };
650            let test_id = format!("{}::{}", outcome.package, outcome.name);
651            let phase = test_phase(&test_id);
652            let provenance = TestProvenance {
653                // What the record says, when the frontend declares it. A JVM
654                // project can run JUnit and TestNG in one JVM, and a result
655                // naming the wrong one would claim it was attributed by a
656                // lifecycle that never saw it.
657                runner: if declared.contains(outcome.runner.as_str()) {
658                    outcome.runner.clone()
659                } else {
660                    default_runner.clone()
661                },
662                kind: "unit".into(),
663                project: Some(outcome.package.clone()),
664                source: source.clone(),
665            };
666            RawTestResult {
667                scope: Some(ExecutionScope {
668                    version: 1,
669                    run_id: run_id.to_owned(),
670                    // The unit that ran as its own process: a Go test binary
671                    // is built per package, and a JVM suite is one JVM. A
672                    // worker identity the reader can trust is what lets it
673                    // accept exact attribution at all.
674                    worker_id: outcome.package.clone(),
675                    test_id: test_id.clone(),
676                    test_key: stable_id("owned-test", &[&outcome.package, &outcome.name]),
677                    retry: 0,
678                    attempt_id: stable_id(
679                        "owned-attempt",
680                        &[run_id, &outcome.package, &outcome.name, "0"],
681                    ),
682                }),
683                test_id: Some(test_id),
684                test: outcome.name.clone(),
685                test_file: outcome.file.clone(),
686                title: None,
687                retry: Some(0),
688                status: Some(outcome.status.clone()),
689                expected_status: None,
690                flaky: false,
691                provenance: provenance.clone(),
692                role: "test".into(),
693                attribution: if outcome.attributed {
694                    crate::coverage_report::ATTRIBUTION_EXACT
695                } else {
696                    crate::coverage_report::ATTRIBUTION_RUN_WIDE
697                }
698                .into(),
699                // Every event a probe produces belongs to the test body: the
700                // runtime binds coverage at the test boundary and knows
701                // nothing of setup or teardown. One declared phase says
702                // exactly that, and leaves the events with somewhere real to
703                // point rather than at a phase nobody declared.
704                phases: vec![CoveragePhase {
705                    id: phase.clone(),
706                    kind: "test".into(),
707                    operation: format!("{} {}", provenance.runner, outcome.name),
708                    source: outcome.file.clone(),
709                    caused_by_phase_id: None,
710                    started_at_ms: 0,
711                    ended_at_ms: None,
712                    status: Some(outcome.status.clone()),
713                    error: None,
714                }],
715                runtime: vec![snapshot(environment, test, manifest, &by_probe, &phase)],
716                browser: Vec::new(),
717                server: Vec::new(),
718            }
719        })
720        .collect::<Vec<_>>();
721    // Everything the run reached that no test claimed.
722    //
723    // A Go test that calls t.Parallel() is deliberately left unattributed:
724    // probes are a store into one shared array, so what it reaches while
725    // others run beside it cannot be credited to it. The declaration says that
726    // coverage still counts run-wide -- and until now nothing carried it, so a
727    // suite written the way Go suites are written measured almost nothing and
728    // was told nothing about why. samber/lo calls t.Parallel() 1606 times.
729    //
730    // It is recorded as its own record, under a role that is not a test,
731    // holding only what the tests did not claim: reached, by nobody nameable.
732    let claimed = evidence
733        .tests
734        .iter()
735        .filter(|test| !test.unattributed)
736        .flat_map(|test| test.probes.keys().copied())
737        .collect::<BTreeSet<_>>();
738    let unclaimed = evidence
739        .global
740        .iter()
741        .enumerate()
742        .filter(|(index, mask)| **mask != 0 && !claimed.contains(&(*index as u32)))
743        .map(|(index, mask)| (index as u32, *mask))
744        .collect::<BTreeMap<_, _>>();
745    if !unclaimed.is_empty() {
746        // Whether this counts as coverage turns on whether it was produced by
747        // something that passed, which is the same rule every test record
748        // obeys: a failing test's coverage is not evidence that anything
749        // works. Here the producers cannot be named individually, so the
750        // question is asked of the run: if every test passed, everything this
751        // holds was produced by a passing test. If any failed, there is no
752        // telling which of this came from it, and none of it counts.
753        let status = if test_exit_code == 0 {
754            "passed"
755        } else {
756            "failed"
757        };
758        let test_id = format!("{}::background", declaration.language);
759        let phase = test_phase(&test_id);
760        let background = OwnedTestEvidence {
761            name: "background".into(),
762            status: "unknown".into(),
763            runner: String::new(),
764            // This record is where unattributed coverage goes; it is not
765            // itself a test whose coverage went elsewhere.
766            unattributed: false,
767            probes: unclaimed,
768            vectors: Vec::new(),
769        };
770        raw_results.push(RawTestResult {
771            scope: Some(ExecutionScope {
772                version: 1,
773                run_id: run_id.to_owned(),
774                worker_id: "background".into(),
775                test_id: test_id.clone(),
776                test_key: stable_id("owned-background", &[run_id]),
777                retry: 0,
778                attempt_id: stable_id("owned-background-attempt", &[run_id]),
779            }),
780            test_id: Some(test_id),
781            test: "execution no test could be credited with".into(),
782            test_file: None,
783            title: None,
784            retry: Some(0),
785            status: Some(status.into()),
786            expected_status: None,
787            flaky: false,
788            provenance: TestProvenance {
789                runner: default_runner.clone(),
790                kind: "background".into(),
791                project: None,
792                source: source.clone(),
793            },
794            role: "background".into(),
795            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
796            phases: vec![CoveragePhase {
797                id: phase.clone(),
798                kind: "background".into(),
799                operation: "execution outside any test".into(),
800                source: None,
801                caused_by_phase_id: None,
802                started_at_ms: 0,
803                ended_at_ms: Some(0),
804                status: Some(status.into()),
805                error: None,
806            }],
807            runtime: vec![snapshot(
808                environment,
809                &background,
810                manifest,
811                &by_probe,
812                &phase,
813            )],
814            browser: Vec::new(),
815            server: Vec::new(),
816        });
817    }
818
819    // Nothing to publish only when neither a test nor the run as a whole
820    // reached anything. An empty list of outcomes on its own is not that: a Go
821    // package whose every test calls `t.Parallel()` announces no test by
822    // design, and refusing it there discarded a fully measured package --
823    // lines, branches and MC/DC alike -- over the fact that the evidence had
824    // no owner. The background record above is the owner it has.
825    if raw_results.is_empty() {
826        return Err(OwnedEvidenceError::NoTests);
827    }
828
829    // A declaration naming a runner that produced nothing claims something the
830    // run did not do, and the reader refuses it — rightly. A JVM frontend can
831    // drive JUnit and TestNG, but any one project usually runs one of them, so
832    // the run declares the ones it actually observed.
833    let observed = raw_results
834        .iter()
835        .map(|result| result.provenance.runner.as_str())
836        .collect::<BTreeSet<_>>();
837    let mut declaration = declaration;
838
839    // What the manifest says could not be measured, the declaration has to
840    // name too: the reader checks that the two agree, so a limitation cannot
841    // appear in one and be missing from the other. The ids are derived per
842    // obligation from the file and the node, so a static declaration could
843    // never have listed them and this is the only place that knows them.
844    declaration.structural_limitations = manifest
845        .limitations
846        .iter()
847        .filter_map(|limitation| limitation.get("id")?.as_str().map(str::to_owned))
848        .collect::<BTreeSet<_>>()
849        .into_iter()
850        .collect();
851
852    if declaration
853        .runners
854        .iter()
855        .any(|runner| observed.contains(runner.runner.as_str()))
856    {
857        declaration
858            .runners
859            .retain(|runner| observed.contains(runner.runner.as_str()));
860    }
861    Ok(OwnedFrontendRun {
862        declaration,
863        tests: raw_results.len(),
864        request: CoverageReportRequest {
865            run_id: run_id.to_owned(),
866            manifest: manifest.clone(),
867            raw_results,
868            generated_at: generated_at.to_owned(),
869            coverage_model: Some(coverage_model),
870            integrity: None,
871            test_exit_code: ExitCodeInput::Present(Some(test_exit_code)),
872        },
873    })
874}
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879
880    fn write(values: &[u64]) -> Vec<u8> {
881        values.iter().flat_map(|v| v.to_le_bytes()).collect()
882    }
883
884    #[test]
885    fn an_unevaluated_condition_is_absent_rather_than_false() {
886        // The distinction the whole measurement rests on. `a && b` with `a`
887        // false says nothing about `b`; recording it as false would claim it
888        // had been tested, and independence would be judged from a fact that
889        // never happened.
890        let short_circuited = unpack_vector(0b01, 2);
891        assert_eq!(short_circuited.values, [Some(false), None]);
892        assert!(!short_circuited.outcome);
893
894        let both = unpack_vector(0b11 | (0b11 << 24) | (1 << 48), 2);
895        assert_eq!(both.values, [Some(true), Some(true)]);
896        assert!(both.outcome);
897
898        // A condition evaluated and false is Some(false), which is a different
899        // vector from the one above.
900        let first_false = unpack_vector(0b11 | (0b01 << 24), 2);
901        assert_eq!(first_false.values, [Some(true), Some(false)]);
902    }
903
904    #[test]
905    fn a_truncated_file_is_refused_rather_than_read_short() {
906        // A run killed part way through has partial evidence. Reading what
907        // survived and reporting it as complete would understate coverage as
908        // though it were a fact about the tests.
909        let full = write(&[2, 0, 2, 1, 4, u64::from_le_bytes(*b"Test\0\0\0\0"), 0, 0, 0]);
910        for cut in [0, 8, 16, 24, 32] {
911            assert!(
912                read_evidence(&full[..cut.min(full.len())]).is_err(),
913                "a file cut at {cut} bytes must not decode"
914            );
915        }
916        assert!(matches!(
917            read_evidence(&write(&[5, 1])),
918            Err(OwnedEvidenceError::Truncated("probe totals"))
919        ));
920    }
921
922    #[test]
923    fn a_run_with_no_tests_is_an_error_not_an_empty_report() {
924        // Nothing announced itself and nothing was reached: there is no run
925        // here, and an empty report would claim a floor it never established.
926        // What makes this an error is the second half, not the first -- see
927        // the test below.
928        let evidence = OwnedEvidence::default();
929        let manifest = CoverageManifest {
930            decisions: Vec::new(),
931            points: Vec::new(),
932            branches: Vec::new(),
933            limitations: Vec::new(),
934            unmeasured: Vec::new(),
935            scope: None,
936        };
937        assert_eq!(
938            build_frontend_run(OwnedRunInputs {
939                declaration: go_declaration(),
940                environment: "go",
941                manifest: &manifest,
942                probes: &BTreeMap::new(),
943                evidence: &evidence,
944                outcomes: &[],
945                run_id: "run",
946                generated_at: "now",
947                test_exit_code: 0,
948                coverage_model: go_coverage_model(),
949            })
950            .err(),
951            Some(OwnedEvidenceError::NoTests)
952        );
953    }
954
955    #[test]
956    fn a_run_nothing_could_be_attributed_to_is_still_a_run() {
957        // A Go package where every test calls `t.Parallel()` announces no test
958        // by design: probes are a store into one shared array, so what a test
959        // reaches while others run beside it cannot be credited to it. The
960        // coverage is real and has no owner, and refusing the run over that
961        // discarded a fully measured package -- lines, branches and MC/DC
962        // alike -- which is how an idiomatic Go suite came to publish nothing
963        // at all.
964        let evidence = OwnedEvidence {
965            global: vec![1],
966            ..OwnedEvidence::default()
967        };
968        let manifest = CoverageManifest {
969            decisions: Vec::new(),
970            points: vec![crate::coverage_report::PointMeta {
971                id: "go:statement:one".into(),
972                kind: crate::coverage_analysis::PointKind::Statement,
973                file: "lib.go".into(),
974                line: 1,
975                column: 1,
976                source: "return 1".into(),
977                label: None,
978            }],
979            branches: Vec::new(),
980            limitations: Vec::new(),
981            unmeasured: Vec::new(),
982            scope: None,
983        };
984        let probes = BTreeMap::from([(
985            0_u64,
986            GoProbe {
987                id: 0,
988                target: GoProbeTarget::Statement {
989                    id: "go:statement:one".into(),
990                },
991                at: 0,
992            },
993        )]);
994        let run = build_frontend_run(OwnedRunInputs {
995            declaration: go_declaration(),
996            environment: "go",
997            manifest: &manifest,
998            probes: &probes,
999            evidence: &evidence,
1000            outcomes: &[],
1001            run_id: "run",
1002            generated_at: "now",
1003            test_exit_code: 0,
1004            coverage_model: go_coverage_model(),
1005        })
1006        .expect("a measured package publishes even when no test can claim it");
1007        assert_eq!(run.tests, 1, "the one record is what nobody could claim");
1008        assert_eq!(run.request.raw_results[0].role, "background");
1009        assert_eq!(
1010            run.request.raw_results[0].status.as_deref(),
1011            Some("passed"),
1012            "it counts as coverage because the run passed"
1013        );
1014    }
1015
1016    #[test]
1017    fn each_declaration_says_what_its_runner_can_and_cannot_attribute() {
1018        // The JVM's differs in substance, not just in name: it lists TestNG as
1019        // a gap because TestNG is not a platform engine, and parallel
1020        // execution as another.
1021        let jvm = jvm_declaration();
1022        let jvm_runner = &jvm.runners[0];
1023        assert_eq!(jvm_runner.runner, "junit-platform");
1024        let gaps = jvm_runner
1025            .limitations
1026            .iter()
1027            .map(|limitation| limitation.id.as_str())
1028            .collect::<Vec<_>>();
1029        assert!(
1030            gaps.contains(&"junit-platform-parallel-execution"),
1031            "{gaps:?}"
1032        );
1033        // TestNG is a runner of its own rather than a gap in another: it is
1034        // not a platform engine, so it reports through its own listener.
1035        let named = jvm
1036            .runners
1037            .iter()
1038            .map(|runner| runner.runner.as_str())
1039            .collect::<Vec<_>>();
1040        assert_eq!(named, ["junit-platform", "testng"]);
1041    }
1042
1043    #[test]
1044    fn the_declaration_says_what_go_can_and_cannot_attribute() {
1045        // A frontend that overstates its precision is worse than one that
1046        // admits a gap: the report would present guesses as measurements.
1047        let declared = go_declaration();
1048        let runner = &declared.runners[0];
1049        assert_eq!(runner.execution_model, ExecutionModel::SerialInProcess);
1050        assert_eq!(runner.attribution.test, AttributionPrecision::Exact);
1051        assert_eq!(
1052            runner.attribution.assertion,
1053            AttributionPrecision::Unavailable
1054        );
1055        let gaps = runner
1056            .limitations
1057            .iter()
1058            .map(|limitation| limitation.id.as_str())
1059            .collect::<Vec<_>>();
1060        assert!(gaps.contains(&"go-parallel-tests"), "{gaps:?}");
1061        // And every precision below Exact is accounted for, which is what the
1062        // contract requires before a reader will accept the run at all.
1063        for gap in [
1064            "go-phase-linkage-aggregate",
1065            "go-action-linkage-unavailable",
1066            "go-assertion-linkage-unavailable",
1067        ] {
1068            assert!(gaps.contains(&gap), "{gaps:?}");
1069        }
1070    }
1071
1072    #[test]
1073    fn a_test_nothing_can_credit_is_named_without_being_handed_the_run() {
1074        // This is the whole of the fix in one place. A test that called
1075        // `t.Parallel()` is announced by name and with its real outcome, and a
1076        // record for it exists -- but the probes in that record are a store
1077        // into one shared array, so they are whatever ran beside it. Handing
1078        // them over would read as a measurement of this test, which is the
1079        // failure the run-wide label exists to prevent: it must take no hits
1080        // at all and say why, while the test beside it keeps its own.
1081        let manifest = CoverageManifest {
1082            decisions: Vec::new(),
1083            points: vec![
1084                crate::coverage_report::PointMeta {
1085                    id: "go:statement:one".into(),
1086                    kind: crate::coverage_analysis::PointKind::Statement,
1087                    file: "lib.go".into(),
1088                    line: 1,
1089                    column: 1,
1090                    source: "return 1".into(),
1091                    label: None,
1092                },
1093                crate::coverage_report::PointMeta {
1094                    id: "go:statement:two".into(),
1095                    kind: crate::coverage_analysis::PointKind::Statement,
1096                    file: "lib.go".into(),
1097                    line: 2,
1098                    column: 1,
1099                    source: "return 2".into(),
1100                    label: None,
1101                },
1102            ],
1103            branches: Vec::new(),
1104            limitations: Vec::new(),
1105            unmeasured: Vec::new(),
1106            scope: None,
1107        };
1108        let probes = BTreeMap::from([
1109            (
1110                0_u64,
1111                GoProbe {
1112                    id: 0,
1113                    target: GoProbeTarget::Statement {
1114                        id: "go:statement:one".into(),
1115                    },
1116                    at: 0,
1117                },
1118            ),
1119            (
1120                1_u64,
1121                GoProbe {
1122                    id: 1,
1123                    target: GoProbeTarget::Statement {
1124                        id: "go:statement:two".into(),
1125                    },
1126                    at: 0,
1127                },
1128            ),
1129        ]);
1130        // Both records hold probes. Only one of them owns what it holds.
1131        let evidence = OwnedEvidence {
1132            global: vec![1, 1],
1133            tests: vec![
1134                OwnedTestEvidence {
1135                    name: "TestSerial".into(),
1136                    status: "passed".into(),
1137                    runner: String::new(),
1138                    unattributed: false,
1139                    probes: BTreeMap::from([(0, 1)]),
1140                    vectors: Vec::new(),
1141                },
1142                OwnedTestEvidence {
1143                    name: "TestParallel".into(),
1144                    status: "passed".into(),
1145                    runner: String::new(),
1146                    unattributed: true,
1147                    probes: BTreeMap::from([(0, 1), (1, 1)]),
1148                    vectors: Vec::new(),
1149                },
1150            ],
1151            ..OwnedEvidence::default()
1152        };
1153        let outcome = |name: &str, attributed| OwnedTestOutcome {
1154            name: name.into(),
1155            runner: String::new(),
1156            package: "example.com/p".into(),
1157            file: Some("p/x_test.go".into()),
1158            status: "passed".into(),
1159            attributed,
1160        };
1161        let run = build_frontend_run(OwnedRunInputs {
1162            declaration: go_declaration(),
1163            environment: "go",
1164            manifest: &manifest,
1165            probes: &probes,
1166            evidence: &evidence,
1167            outcomes: &[outcome("TestSerial", true), outcome("TestParallel", false)],
1168            run_id: "run",
1169            generated_at: "now",
1170            test_exit_code: 0,
1171            coverage_model: go_coverage_model(),
1172        })
1173        .expect("run");
1174
1175        let result = |name: &str| {
1176            run.request
1177                .raw_results
1178                .iter()
1179                .find(|result| result.test == name)
1180                .unwrap_or_else(|| panic!("{name} is missing from the run"))
1181        };
1182        let serial = result("TestSerial");
1183        assert_eq!(
1184            serial.attribution,
1185            crate::coverage_report::ATTRIBUTION_EXACT
1186        );
1187        assert_eq!(
1188            serial.runtime[0].hits,
1189            ["go:statement:one"],
1190            "a test that ran alone keeps what it reached"
1191        );
1192
1193        let parallel = result("TestParallel");
1194        assert_eq!(
1195            parallel.attribution,
1196            crate::coverage_report::ATTRIBUTION_RUN_WIDE,
1197            "it ran, and what it reached belongs to the run rather than to it"
1198        );
1199        assert_eq!(
1200            parallel.status.as_deref(),
1201            Some("passed"),
1202            "unattributed is not unknown: the runner said how it ended"
1203        );
1204        assert!(
1205            parallel.runtime[0].hits.is_empty(),
1206            "it was handed the record's probes and they are not its own: {:?}",
1207            parallel.runtime[0].hits
1208        );
1209    }
1210
1211    #[test]
1212    fn a_test_the_runner_saw_but_that_recorded_nothing_still_appears() {
1213        // Dropping it would make the suite look smaller than it is, and a test
1214        // that reached no measured line is a fact worth seeing.
1215        let manifest = CoverageManifest {
1216            decisions: Vec::new(),
1217            points: Vec::new(),
1218            branches: Vec::new(),
1219            limitations: Vec::new(),
1220            unmeasured: Vec::new(),
1221            scope: None,
1222        };
1223        let run = build_frontend_run(OwnedRunInputs {
1224            declaration: go_declaration(),
1225            environment: "go",
1226            manifest: &manifest,
1227            probes: &BTreeMap::new(),
1228            evidence: &OwnedEvidence::default(),
1229            outcomes: &[OwnedTestOutcome {
1230                name: "TestSilent".into(),
1231                runner: String::new(),
1232                package: "example.com/p".into(),
1233                file: Some("p/x_test.go".into()),
1234                status: "passed".into(),
1235                attributed: true,
1236            }],
1237            run_id: "run",
1238            generated_at: "now",
1239            test_exit_code: 0,
1240            coverage_model: go_coverage_model(),
1241        })
1242        .expect("run");
1243        assert_eq!(run.tests, 1);
1244        let result = &run.request.raw_results[0];
1245        assert_eq!(result.test, "TestSilent");
1246        assert_eq!(result.test_id.as_deref(), Some("example.com/p::TestSilent"));
1247        assert!(result.runtime[0].hits.is_empty());
1248    }
1249
1250    #[test]
1251    fn both_owned_declarations_satisfy_the_contract_they_are_read_back_through() {
1252        // A declaration is written at publication and checked at read. The two
1253        // owned frontends once hardcoded a protocol version while the contract
1254        // moved on, so every Go and JVM run published cleanly and then failed
1255        // the moment anyone asked for its report.
1256        for declaration in [go_declaration(), jvm_declaration()] {
1257            let language = declaration.language.clone();
1258            supercov_contracts::validate_frontend_run_declaration(&declaration)
1259                .unwrap_or_else(|error| panic!("{language} declaration is unreadable: {error}"));
1260        }
1261    }
1262    #[test]
1263    fn merging_processes_unions_the_run_and_keeps_every_test() {
1264        let left = OwnedEvidence {
1265            global: vec![0b01, 0b00],
1266            tests: vec![OwnedTestEvidence {
1267                name: "TestA".into(),
1268                status: "passed".into(),
1269                ..Default::default()
1270            }],
1271            widths: vec![2],
1272            decision_vectors: vec![vec![0b01]],
1273        };
1274        let right = OwnedEvidence {
1275            global: vec![0b10, 0b10],
1276            tests: vec![OwnedTestEvidence {
1277                name: "TestB".into(),
1278                status: "failed".into(),
1279                ..Default::default()
1280            }],
1281            widths: vec![2],
1282            decision_vectors: vec![vec![0b01, 0b11]],
1283        };
1284        let merged = merge_evidence(vec![left, right]);
1285        // Run-wide totals are a union: a probe any package reached is reached.
1286        assert_eq!(merged.global, [0b11, 0b10]);
1287        // Tests accumulate, because each belongs to exactly one binary.
1288        assert_eq!(
1289            merged
1290                .tests
1291                .iter()
1292                .map(|test| test.name.as_str())
1293                .collect::<Vec<_>>(),
1294            ["TestA", "TestB"]
1295        );
1296        // And a vector seen in both packages is one vector, not two.
1297        assert_eq!(merged.decision_vectors, [vec![0b01, 0b11]]);
1298    }
1299}