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    }
533}
534
535pub struct OwnedFrontendRun {
536    pub declaration: FrontendRunDeclaration,
537    pub request: CoverageReportRequest,
538    pub tests: usize,
539}
540
541impl OwnedFrontendRun {
542    /// The archive a run publishes: what the numbers mean, who produced them,
543    /// the obligations they are measured against, and one record per test.
544    pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
545        let model = PersistedCoverageModel::from_declaration(
546            self.request
547                .coverage_model
548                .as_ref()
549                .expect("an owned frontend always declares a coverage model"),
550        )
551        .expect("owned coverage models are contract-valid");
552        let mut entries = vec![
553            EvidenceArchiveEntry {
554                path: "coverage-model.json".into(),
555                contents: serde_json::to_vec(&model)?,
556            },
557            EvidenceArchiveEntry {
558                path: "frontend.json".into(),
559                contents: serde_json::to_vec(&self.declaration)?,
560            },
561            EvidenceArchiveEntry {
562                path: "manifest.json".into(),
563                contents: serde_json::to_vec(&self.request.manifest)?,
564            },
565        ];
566        for (index, result) in self.request.raw_results.iter().enumerate() {
567            entries.push(EvidenceArchiveEntry {
568                path: format!("results/{index:08}/mcdc.json"),
569                contents: serde_json::to_vec(result)?,
570            });
571        }
572        Ok(entries)
573    }
574}
575
576/// Build the report request from what the run recorded.
577///
578/// A test the runner reported but that announced nothing still becomes a
579/// result, with no coverage. Dropping it would make a suite look smaller than
580/// it is, and a test that ran without reaching any measured line is a fact
581/// worth seeing rather than an absence worth hiding.
582/// Everything a report needs about one run of one owned frontend.
583pub struct OwnedRunInputs<'a> {
584    pub declaration: FrontendRunDeclaration,
585    /// The label runtime events carry, so a reader can tell which frontend
586    /// produced them.
587    pub environment: &'a str,
588    pub manifest: &'a CoverageManifest,
589    pub probes: &'a BTreeMap<u64, GoProbe>,
590    pub evidence: &'a OwnedEvidence,
591    pub outcomes: &'a [OwnedTestOutcome],
592    pub run_id: &'a str,
593    pub generated_at: &'a str,
594    pub test_exit_code: i32,
595    /// What the numbers mean. Carried rather than inferred from the language,
596    /// so a frontend cannot quietly inherit another's claims.
597    pub coverage_model: CoverageModelDeclaration,
598}
599
600/// Build the report request from what the run recorded.
601///
602/// A test the runner reported but that announced nothing still becomes a
603/// result, with no coverage. Dropping it would make a suite look smaller than
604/// it is, and a test that ran without reaching any measured line is a fact
605/// worth seeing rather than an absence worth hiding.
606pub fn build_frontend_run(inputs: OwnedRunInputs) -> Result<OwnedFrontendRun, OwnedEvidenceError> {
607    let OwnedRunInputs {
608        declaration,
609        environment,
610        manifest,
611        probes,
612        evidence,
613        outcomes,
614        run_id,
615        generated_at,
616        test_exit_code,
617        coverage_model,
618    } = inputs;
619    // The runner every result claims must be one the declaration names: a
620    // result attributed to a runner nobody declared is a result nothing can
621    // say the precision of, and the reader refuses it rather than guess.
622    let default_runner = declaration
623        .runners
624        .first()
625        .map(|runner| runner.runner.clone())
626        .ok_or(OwnedEvidenceError::NoTests)?;
627    let declared = declaration
628        .runners
629        .iter()
630        .map(|runner| runner.runner.as_str())
631        .collect::<BTreeSet<_>>();
632    let source = declaration.frontend_version.clone();
633    let by_probe = obligations(probes);
634    let recorded = evidence
635        .tests
636        .iter()
637        .map(|test| (test.name.clone(), test))
638        .collect::<BTreeMap<_, _>>();
639    let empty = OwnedTestEvidence::default();
640    let mut raw_results = outcomes
641        .iter()
642        .map(|outcome| {
643            let test = recorded.get(&outcome.name).copied().unwrap_or(&empty);
644            // A test nothing could be credited to takes no hits at all, and
645            // says so. Handing it the record's probes would be worse than
646            // wrong: the runtime gives an unattributed record none, so the
647            // result would read as a test that reached nothing.
648            let test = if outcome.attributed { test } else { &empty };
649            let test_id = format!("{}::{}", outcome.package, outcome.name);
650            let phase = test_phase(&test_id);
651            let provenance = TestProvenance {
652                // What the record says, when the frontend declares it. A JVM
653                // project can run JUnit and TestNG in one JVM, and a result
654                // naming the wrong one would claim it was attributed by a
655                // lifecycle that never saw it.
656                runner: if declared.contains(outcome.runner.as_str()) {
657                    outcome.runner.clone()
658                } else {
659                    default_runner.clone()
660                },
661                kind: "unit".into(),
662                project: Some(outcome.package.clone()),
663                source: source.clone(),
664            };
665            RawTestResult {
666                scope: Some(ExecutionScope {
667                    version: 1,
668                    run_id: run_id.to_owned(),
669                    // The unit that ran as its own process: a Go test binary
670                    // is built per package, and a JVM suite is one JVM. A
671                    // worker identity the reader can trust is what lets it
672                    // accept exact attribution at all.
673                    worker_id: outcome.package.clone(),
674                    test_id: test_id.clone(),
675                    test_key: stable_id("owned-test", &[&outcome.package, &outcome.name]),
676                    retry: 0,
677                    attempt_id: stable_id(
678                        "owned-attempt",
679                        &[run_id, &outcome.package, &outcome.name, "0"],
680                    ),
681                }),
682                test_id: Some(test_id),
683                test: outcome.name.clone(),
684                test_file: outcome.file.clone(),
685                title: None,
686                retry: Some(0),
687                status: Some(outcome.status.clone()),
688                expected_status: None,
689                flaky: false,
690                provenance: provenance.clone(),
691                role: "test".into(),
692                attribution: if outcome.attributed {
693                    crate::coverage_report::ATTRIBUTION_EXACT
694                } else {
695                    crate::coverage_report::ATTRIBUTION_RUN_WIDE
696                }
697                .into(),
698                // Every event a probe produces belongs to the test body: the
699                // runtime binds coverage at the test boundary and knows
700                // nothing of setup or teardown. One declared phase says
701                // exactly that, and leaves the events with somewhere real to
702                // point rather than at a phase nobody declared.
703                phases: vec![CoveragePhase {
704                    id: phase.clone(),
705                    kind: "test".into(),
706                    operation: format!("{} {}", provenance.runner, outcome.name),
707                    source: outcome.file.clone(),
708                    caused_by_phase_id: None,
709                    started_at_ms: 0,
710                    ended_at_ms: None,
711                    status: Some(outcome.status.clone()),
712                    error: None,
713                }],
714                runtime: vec![snapshot(environment, test, manifest, &by_probe, &phase)],
715                browser: Vec::new(),
716                server: Vec::new(),
717            }
718        })
719        .collect::<Vec<_>>();
720    // Everything the run reached that no test claimed.
721    //
722    // A Go test that calls t.Parallel() is deliberately left unattributed:
723    // probes are a store into one shared array, so what it reaches while
724    // others run beside it cannot be credited to it. The declaration says that
725    // coverage still counts run-wide -- and until now nothing carried it, so a
726    // suite written the way Go suites are written measured almost nothing and
727    // was told nothing about why. samber/lo calls t.Parallel() 1606 times.
728    //
729    // It is recorded as its own record, under a role that is not a test,
730    // holding only what the tests did not claim: reached, by nobody nameable.
731    let claimed = evidence
732        .tests
733        .iter()
734        .filter(|test| !test.unattributed)
735        .flat_map(|test| test.probes.keys().copied())
736        .collect::<BTreeSet<_>>();
737    let unclaimed = evidence
738        .global
739        .iter()
740        .enumerate()
741        .filter(|(index, mask)| **mask != 0 && !claimed.contains(&(*index as u32)))
742        .map(|(index, mask)| (index as u32, *mask))
743        .collect::<BTreeMap<_, _>>();
744    if !unclaimed.is_empty() {
745        // Whether this counts as coverage turns on whether it was produced by
746        // something that passed, which is the same rule every test record
747        // obeys: a failing test's coverage is not evidence that anything
748        // works. Here the producers cannot be named individually, so the
749        // question is asked of the run: if every test passed, everything this
750        // holds was produced by a passing test. If any failed, there is no
751        // telling which of this came from it, and none of it counts.
752        let status = if test_exit_code == 0 {
753            "passed"
754        } else {
755            "failed"
756        };
757        let test_id = format!("{}::background", declaration.language);
758        let phase = test_phase(&test_id);
759        let background = OwnedTestEvidence {
760            name: "background".into(),
761            status: "unknown".into(),
762            runner: String::new(),
763            // This record is where unattributed coverage goes; it is not
764            // itself a test whose coverage went elsewhere.
765            unattributed: false,
766            probes: unclaimed,
767            vectors: Vec::new(),
768        };
769        raw_results.push(RawTestResult {
770            scope: Some(ExecutionScope {
771                version: 1,
772                run_id: run_id.to_owned(),
773                worker_id: "background".into(),
774                test_id: test_id.clone(),
775                test_key: stable_id("owned-background", &[run_id]),
776                retry: 0,
777                attempt_id: stable_id("owned-background-attempt", &[run_id]),
778            }),
779            test_id: Some(test_id),
780            test: "execution no test could be credited with".into(),
781            test_file: None,
782            title: None,
783            retry: Some(0),
784            status: Some(status.into()),
785            expected_status: None,
786            flaky: false,
787            provenance: TestProvenance {
788                runner: default_runner.clone(),
789                kind: "background".into(),
790                project: None,
791                source: source.clone(),
792            },
793            role: "background".into(),
794            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
795            phases: vec![CoveragePhase {
796                id: phase.clone(),
797                kind: "background".into(),
798                operation: "execution outside any test".into(),
799                source: None,
800                caused_by_phase_id: None,
801                started_at_ms: 0,
802                ended_at_ms: Some(0),
803                status: Some(status.into()),
804                error: None,
805            }],
806            runtime: vec![snapshot(
807                environment,
808                &background,
809                manifest,
810                &by_probe,
811                &phase,
812            )],
813            browser: Vec::new(),
814            server: Vec::new(),
815        });
816    }
817
818    // Nothing to publish only when neither a test nor the run as a whole
819    // reached anything. An empty list of outcomes on its own is not that: a Go
820    // package whose every test calls `t.Parallel()` announces no test by
821    // design, and refusing it there discarded a fully measured package --
822    // lines, branches and MC/DC alike -- over the fact that the evidence had
823    // no owner. The background record above is the owner it has.
824    if raw_results.is_empty() {
825        return Err(OwnedEvidenceError::NoTests);
826    }
827
828    // A declaration naming a runner that produced nothing claims something the
829    // run did not do, and the reader refuses it — rightly. A JVM frontend can
830    // drive JUnit and TestNG, but any one project usually runs one of them, so
831    // the run declares the ones it actually observed.
832    let observed = raw_results
833        .iter()
834        .map(|result| result.provenance.runner.as_str())
835        .collect::<BTreeSet<_>>();
836    let mut declaration = declaration;
837
838    // What the manifest says could not be measured, the declaration has to
839    // name too: the reader checks that the two agree, so a limitation cannot
840    // appear in one and be missing from the other. The ids are derived per
841    // obligation from the file and the node, so a static declaration could
842    // never have listed them and this is the only place that knows them.
843    declaration.structural_limitations = manifest
844        .limitations
845        .iter()
846        .filter_map(|limitation| limitation.get("id")?.as_str().map(str::to_owned))
847        .collect::<BTreeSet<_>>()
848        .into_iter()
849        .collect();
850
851    if declaration
852        .runners
853        .iter()
854        .any(|runner| observed.contains(runner.runner.as_str()))
855    {
856        declaration
857            .runners
858            .retain(|runner| observed.contains(runner.runner.as_str()));
859    }
860    Ok(OwnedFrontendRun {
861        declaration,
862        tests: raw_results.len(),
863        request: CoverageReportRequest {
864            run_id: run_id.to_owned(),
865            manifest: manifest.clone(),
866            raw_results,
867            generated_at: generated_at.to_owned(),
868            coverage_model: Some(coverage_model),
869            integrity: None,
870            test_exit_code: ExitCodeInput::Present(Some(test_exit_code)),
871        },
872    })
873}
874
875#[cfg(test)]
876mod tests {
877    use super::*;
878
879    fn write(values: &[u64]) -> Vec<u8> {
880        values.iter().flat_map(|v| v.to_le_bytes()).collect()
881    }
882
883    #[test]
884    fn an_unevaluated_condition_is_absent_rather_than_false() {
885        // The distinction the whole measurement rests on. `a && b` with `a`
886        // false says nothing about `b`; recording it as false would claim it
887        // had been tested, and independence would be judged from a fact that
888        // never happened.
889        let short_circuited = unpack_vector(0b01, 2);
890        assert_eq!(short_circuited.values, [Some(false), None]);
891        assert!(!short_circuited.outcome);
892
893        let both = unpack_vector(0b11 | (0b11 << 24) | (1 << 48), 2);
894        assert_eq!(both.values, [Some(true), Some(true)]);
895        assert!(both.outcome);
896
897        // A condition evaluated and false is Some(false), which is a different
898        // vector from the one above.
899        let first_false = unpack_vector(0b11 | (0b01 << 24), 2);
900        assert_eq!(first_false.values, [Some(true), Some(false)]);
901    }
902
903    #[test]
904    fn a_truncated_file_is_refused_rather_than_read_short() {
905        // A run killed part way through has partial evidence. Reading what
906        // survived and reporting it as complete would understate coverage as
907        // though it were a fact about the tests.
908        let full = write(&[2, 0, 2, 1, 4, u64::from_le_bytes(*b"Test\0\0\0\0"), 0, 0, 0]);
909        for cut in [0, 8, 16, 24, 32] {
910            assert!(
911                read_evidence(&full[..cut.min(full.len())]).is_err(),
912                "a file cut at {cut} bytes must not decode"
913            );
914        }
915        assert!(matches!(
916            read_evidence(&write(&[5, 1])),
917            Err(OwnedEvidenceError::Truncated("probe totals"))
918        ));
919    }
920
921    #[test]
922    fn a_run_with_no_tests_is_an_error_not_an_empty_report() {
923        // Nothing announced itself and nothing was reached: there is no run
924        // here, and an empty report would claim a floor it never established.
925        // What makes this an error is the second half, not the first -- see
926        // the test below.
927        let evidence = OwnedEvidence::default();
928        let manifest = CoverageManifest {
929            decisions: Vec::new(),
930            points: Vec::new(),
931            branches: Vec::new(),
932            limitations: Vec::new(),
933            unmeasured: Vec::new(),
934            scope: None,
935        };
936        assert_eq!(
937            build_frontend_run(OwnedRunInputs {
938                declaration: go_declaration(),
939                environment: "go",
940                manifest: &manifest,
941                probes: &BTreeMap::new(),
942                evidence: &evidence,
943                outcomes: &[],
944                run_id: "run",
945                generated_at: "now",
946                test_exit_code: 0,
947                coverage_model: go_coverage_model(),
948            })
949            .err(),
950            Some(OwnedEvidenceError::NoTests)
951        );
952    }
953
954    #[test]
955    fn a_run_nothing_could_be_attributed_to_is_still_a_run() {
956        // A Go package where every test calls `t.Parallel()` announces no test
957        // by design: probes are a store into one shared array, so what a test
958        // reaches while others run beside it cannot be credited to it. The
959        // coverage is real and has no owner, and refusing the run over that
960        // discarded a fully measured package -- lines, branches and MC/DC
961        // alike -- which is how an idiomatic Go suite came to publish nothing
962        // at all.
963        let evidence = OwnedEvidence {
964            global: vec![1],
965            ..OwnedEvidence::default()
966        };
967        let manifest = CoverageManifest {
968            decisions: Vec::new(),
969            points: vec![crate::coverage_report::PointMeta {
970                id: "go:statement:one".into(),
971                kind: crate::coverage_analysis::PointKind::Statement,
972                file: "lib.go".into(),
973                line: 1,
974                column: 1,
975                source: "return 1".into(),
976                label: None,
977            }],
978            branches: Vec::new(),
979            limitations: Vec::new(),
980            unmeasured: Vec::new(),
981            scope: None,
982        };
983        let probes = BTreeMap::from([(
984            0_u64,
985            GoProbe {
986                id: 0,
987                target: GoProbeTarget::Statement {
988                    id: "go:statement:one".into(),
989                },
990                at: 0,
991            },
992        )]);
993        let run = build_frontend_run(OwnedRunInputs {
994            declaration: go_declaration(),
995            environment: "go",
996            manifest: &manifest,
997            probes: &probes,
998            evidence: &evidence,
999            outcomes: &[],
1000            run_id: "run",
1001            generated_at: "now",
1002            test_exit_code: 0,
1003            coverage_model: go_coverage_model(),
1004        })
1005        .expect("a measured package publishes even when no test can claim it");
1006        assert_eq!(run.tests, 1, "the one record is what nobody could claim");
1007        assert_eq!(run.request.raw_results[0].role, "background");
1008        assert_eq!(
1009            run.request.raw_results[0].status.as_deref(),
1010            Some("passed"),
1011            "it counts as coverage because the run passed"
1012        );
1013    }
1014
1015    #[test]
1016    fn each_declaration_says_what_its_runner_can_and_cannot_attribute() {
1017        // The JVM's differs in substance, not just in name: it lists TestNG as
1018        // a gap because TestNG is not a platform engine, and parallel
1019        // execution as another.
1020        let jvm = jvm_declaration();
1021        let jvm_runner = &jvm.runners[0];
1022        assert_eq!(jvm_runner.runner, "junit-platform");
1023        let gaps = jvm_runner
1024            .limitations
1025            .iter()
1026            .map(|limitation| limitation.id.as_str())
1027            .collect::<Vec<_>>();
1028        assert!(
1029            gaps.contains(&"junit-platform-parallel-execution"),
1030            "{gaps:?}"
1031        );
1032        // TestNG is a runner of its own rather than a gap in another: it is
1033        // not a platform engine, so it reports through its own listener.
1034        let named = jvm
1035            .runners
1036            .iter()
1037            .map(|runner| runner.runner.as_str())
1038            .collect::<Vec<_>>();
1039        assert_eq!(named, ["junit-platform", "testng"]);
1040    }
1041
1042    #[test]
1043    fn the_declaration_says_what_go_can_and_cannot_attribute() {
1044        // A frontend that overstates its precision is worse than one that
1045        // admits a gap: the report would present guesses as measurements.
1046        let declared = go_declaration();
1047        let runner = &declared.runners[0];
1048        assert_eq!(runner.execution_model, ExecutionModel::SerialInProcess);
1049        assert_eq!(runner.attribution.test, AttributionPrecision::Exact);
1050        assert_eq!(
1051            runner.attribution.assertion,
1052            AttributionPrecision::Unavailable
1053        );
1054        let gaps = runner
1055            .limitations
1056            .iter()
1057            .map(|limitation| limitation.id.as_str())
1058            .collect::<Vec<_>>();
1059        assert!(gaps.contains(&"go-parallel-tests"), "{gaps:?}");
1060        // And every precision below Exact is accounted for, which is what the
1061        // contract requires before a reader will accept the run at all.
1062        for gap in [
1063            "go-phase-linkage-aggregate",
1064            "go-action-linkage-unavailable",
1065            "go-assertion-linkage-unavailable",
1066        ] {
1067            assert!(gaps.contains(&gap), "{gaps:?}");
1068        }
1069    }
1070
1071    #[test]
1072    fn a_test_nothing_can_credit_is_named_without_being_handed_the_run() {
1073        // This is the whole of the fix in one place. A test that called
1074        // `t.Parallel()` is announced by name and with its real outcome, and a
1075        // record for it exists -- but the probes in that record are a store
1076        // into one shared array, so they are whatever ran beside it. Handing
1077        // them over would read as a measurement of this test, which is the
1078        // failure the run-wide label exists to prevent: it must take no hits
1079        // at all and say why, while the test beside it keeps its own.
1080        let manifest = CoverageManifest {
1081            decisions: Vec::new(),
1082            points: vec![
1083                crate::coverage_report::PointMeta {
1084                    id: "go:statement:one".into(),
1085                    kind: crate::coverage_analysis::PointKind::Statement,
1086                    file: "lib.go".into(),
1087                    line: 1,
1088                    column: 1,
1089                    source: "return 1".into(),
1090                    label: None,
1091                },
1092                crate::coverage_report::PointMeta {
1093                    id: "go:statement:two".into(),
1094                    kind: crate::coverage_analysis::PointKind::Statement,
1095                    file: "lib.go".into(),
1096                    line: 2,
1097                    column: 1,
1098                    source: "return 2".into(),
1099                    label: None,
1100                },
1101            ],
1102            branches: Vec::new(),
1103            limitations: Vec::new(),
1104            unmeasured: Vec::new(),
1105            scope: None,
1106        };
1107        let probes = BTreeMap::from([
1108            (
1109                0_u64,
1110                GoProbe {
1111                    id: 0,
1112                    target: GoProbeTarget::Statement {
1113                        id: "go:statement:one".into(),
1114                    },
1115                    at: 0,
1116                },
1117            ),
1118            (
1119                1_u64,
1120                GoProbe {
1121                    id: 1,
1122                    target: GoProbeTarget::Statement {
1123                        id: "go:statement:two".into(),
1124                    },
1125                    at: 0,
1126                },
1127            ),
1128        ]);
1129        // Both records hold probes. Only one of them owns what it holds.
1130        let evidence = OwnedEvidence {
1131            global: vec![1, 1],
1132            tests: vec![
1133                OwnedTestEvidence {
1134                    name: "TestSerial".into(),
1135                    status: "passed".into(),
1136                    runner: String::new(),
1137                    unattributed: false,
1138                    probes: BTreeMap::from([(0, 1)]),
1139                    vectors: Vec::new(),
1140                },
1141                OwnedTestEvidence {
1142                    name: "TestParallel".into(),
1143                    status: "passed".into(),
1144                    runner: String::new(),
1145                    unattributed: true,
1146                    probes: BTreeMap::from([(0, 1), (1, 1)]),
1147                    vectors: Vec::new(),
1148                },
1149            ],
1150            ..OwnedEvidence::default()
1151        };
1152        let outcome = |name: &str, attributed| OwnedTestOutcome {
1153            name: name.into(),
1154            runner: String::new(),
1155            package: "example.com/p".into(),
1156            file: Some("p/x_test.go".into()),
1157            status: "passed".into(),
1158            attributed,
1159        };
1160        let run = build_frontend_run(OwnedRunInputs {
1161            declaration: go_declaration(),
1162            environment: "go",
1163            manifest: &manifest,
1164            probes: &probes,
1165            evidence: &evidence,
1166            outcomes: &[outcome("TestSerial", true), outcome("TestParallel", false)],
1167            run_id: "run",
1168            generated_at: "now",
1169            test_exit_code: 0,
1170            coverage_model: go_coverage_model(),
1171        })
1172        .expect("run");
1173
1174        let result = |name: &str| {
1175            run.request
1176                .raw_results
1177                .iter()
1178                .find(|result| result.test == name)
1179                .unwrap_or_else(|| panic!("{name} is missing from the run"))
1180        };
1181        let serial = result("TestSerial");
1182        assert_eq!(
1183            serial.attribution,
1184            crate::coverage_report::ATTRIBUTION_EXACT
1185        );
1186        assert_eq!(
1187            serial.runtime[0].hits,
1188            ["go:statement:one"],
1189            "a test that ran alone keeps what it reached"
1190        );
1191
1192        let parallel = result("TestParallel");
1193        assert_eq!(
1194            parallel.attribution,
1195            crate::coverage_report::ATTRIBUTION_RUN_WIDE,
1196            "it ran, and what it reached belongs to the run rather than to it"
1197        );
1198        assert_eq!(
1199            parallel.status.as_deref(),
1200            Some("passed"),
1201            "unattributed is not unknown: the runner said how it ended"
1202        );
1203        assert!(
1204            parallel.runtime[0].hits.is_empty(),
1205            "it was handed the record's probes and they are not its own: {:?}",
1206            parallel.runtime[0].hits
1207        );
1208    }
1209
1210    #[test]
1211    fn a_test_the_runner_saw_but_that_recorded_nothing_still_appears() {
1212        // Dropping it would make the suite look smaller than it is, and a test
1213        // that reached no measured line is a fact worth seeing.
1214        let manifest = CoverageManifest {
1215            decisions: Vec::new(),
1216            points: Vec::new(),
1217            branches: Vec::new(),
1218            limitations: Vec::new(),
1219            unmeasured: Vec::new(),
1220            scope: None,
1221        };
1222        let run = build_frontend_run(OwnedRunInputs {
1223            declaration: go_declaration(),
1224            environment: "go",
1225            manifest: &manifest,
1226            probes: &BTreeMap::new(),
1227            evidence: &OwnedEvidence::default(),
1228            outcomes: &[OwnedTestOutcome {
1229                name: "TestSilent".into(),
1230                runner: String::new(),
1231                package: "example.com/p".into(),
1232                file: Some("p/x_test.go".into()),
1233                status: "passed".into(),
1234                attributed: true,
1235            }],
1236            run_id: "run",
1237            generated_at: "now",
1238            test_exit_code: 0,
1239            coverage_model: go_coverage_model(),
1240        })
1241        .expect("run");
1242        assert_eq!(run.tests, 1);
1243        let result = &run.request.raw_results[0];
1244        assert_eq!(result.test, "TestSilent");
1245        assert_eq!(result.test_id.as_deref(), Some("example.com/p::TestSilent"));
1246        assert!(result.runtime[0].hits.is_empty());
1247    }
1248
1249    #[test]
1250    fn both_owned_declarations_satisfy_the_contract_they_are_read_back_through() {
1251        // A declaration is written at publication and checked at read. The two
1252        // owned frontends once hardcoded a protocol version while the contract
1253        // moved on, so every Go and JVM run published cleanly and then failed
1254        // the moment anyone asked for its report.
1255        for declaration in [go_declaration(), jvm_declaration()] {
1256            let language = declaration.language.clone();
1257            supercov_contracts::validate_frontend_run_declaration(&declaration)
1258                .unwrap_or_else(|error| panic!("{language} declaration is unreadable: {error}"));
1259        }
1260    }
1261    #[test]
1262    fn merging_processes_unions_the_run_and_keeps_every_test() {
1263        let left = OwnedEvidence {
1264            global: vec![0b01, 0b00],
1265            tests: vec![OwnedTestEvidence {
1266                name: "TestA".into(),
1267                status: "passed".into(),
1268                ..Default::default()
1269            }],
1270            widths: vec![2],
1271            decision_vectors: vec![vec![0b01]],
1272        };
1273        let right = OwnedEvidence {
1274            global: vec![0b10, 0b10],
1275            tests: vec![OwnedTestEvidence {
1276                name: "TestB".into(),
1277                status: "failed".into(),
1278                ..Default::default()
1279            }],
1280            widths: vec![2],
1281            decision_vectors: vec![vec![0b01, 0b11]],
1282        };
1283        let merged = merge_evidence(vec![left, right]);
1284        // Run-wide totals are a union: a probe any package reached is reached.
1285        assert_eq!(merged.global, [0b11, 0b10]);
1286        // Tests accumulate, because each belongs to exactly one binary.
1287        assert_eq!(
1288            merged
1289                .tests
1290                .iter()
1291                .map(|test| test.name.as_str())
1292                .collect::<Vec<_>>(),
1293            ["TestA", "TestB"]
1294        );
1295        // And a vector seen in both packages is one vector, not two.
1296        assert_eq!(merged.decision_vectors, [vec![0b01, 0b11]]);
1297    }
1298}