1use 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
30fn 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#[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 pub status: String,
72 pub runner: String,
75 pub unattributed: bool,
80 pub probes: BTreeMap<u32, u32>,
82 pub vectors: Vec<PackedVector>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Default)]
86pub struct OwnedEvidence {
87 pub global: Vec<u32>,
89 pub tests: Vec<OwnedTestEvidence>,
90 pub widths: Vec<u8>,
92 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
123pub 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 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
193pub 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#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct OwnedTestOutcome {
216 pub name: String,
217 pub package: String,
218 pub file: Option<String>,
219 pub status: String,
221 pub runner: String,
224 pub attributed: bool,
228}
229
230pub 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 structural_source: supercov_contracts::StructuralSource::OwnedProbes,
244 runners: vec![FrontendRunnerDeclaration {
245 runner: "go-test".into(),
246 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
266pub 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
288pub 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
327pub 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
349fn 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 action: AttributionPrecision::Unavailable,
391 assertion: AttributionPrecision::Unavailable,
392 }
393}
394
395pub 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
433fn 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
453fn 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 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
577pub struct OwnedRunInputs<'a> {
585 pub declaration: FrontendRunDeclaration,
586 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 pub coverage_model: CoverageModelDeclaration,
599}
600
601pub 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 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 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 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 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 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 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 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 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 if raw_results.is_empty() {
826 return Err(OwnedEvidenceError::NoTests);
827 }
828
829 let observed = raw_results
834 .iter()
835 .map(|result| result.provenance.runner.as_str())
836 .collect::<BTreeSet<_>>();
837 let mut declaration = declaration;
838
839 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(merged.global, [0b11, 0b10]);
1287 assert_eq!(
1289 merged
1290 .tests
1291 .iter()
1292 .map(|test| test.name.as_str())
1293 .collect::<Vec<_>>(),
1294 ["TestA", "TestB"]
1295 );
1296 assert_eq!(merged.decision_vectors, [vec![0b01, 0b11]]);
1298 }
1299}