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 tags: Vec<String>,
77 pub unattributed: bool,
82 pub probes: BTreeMap<u32, u32>,
84 pub vectors: Vec<PackedVector>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Default)]
88pub struct OwnedEvidence {
89 pub global: Vec<u32>,
91 pub tests: Vec<OwnedTestEvidence>,
92 pub widths: Vec<u8>,
94 pub decision_vectors: Vec<Vec<u64>>,
96}
97
98struct Cursor<'a> {
99 bytes: &'a [u8],
100 offset: usize,
101}
102
103impl Cursor<'_> {
104 fn u64(&mut self, part: &'static str) -> Result<u64, OwnedEvidenceError> {
105 let end = self.offset + 8;
106 let slice = self
107 .bytes
108 .get(self.offset..end)
109 .ok_or(OwnedEvidenceError::Truncated(part))?;
110 self.offset = end;
111 Ok(u64::from_le_bytes(slice.try_into().expect("eight bytes")))
112 }
113
114 fn text(&mut self, length: usize, part: &'static str) -> Result<String, OwnedEvidenceError> {
115 let end = self.offset + length;
116 let slice = self
117 .bytes
118 .get(self.offset..end)
119 .ok_or(OwnedEvidenceError::Truncated(part))?;
120 self.offset = end;
121 String::from_utf8(slice.to_vec()).map_err(|_| OwnedEvidenceError::Truncated(part))
122 }
123}
124
125pub fn read_evidence(bytes: &[u8]) -> Result<OwnedEvidence, OwnedEvidenceError> {
129 let mut cursor = Cursor { bytes, offset: 0 };
130 let probe_count = cursor.u64("probe totals")? as usize;
131 let mut global = Vec::with_capacity(probe_count);
132 for _ in 0..probe_count {
133 global.push(cursor.u64("probe totals")? as u32);
134 }
135 let test_count = cursor.u64("test count")? as usize;
136 let mut tests = Vec::with_capacity(test_count);
137 for _ in 0..test_count {
138 let length = cursor.u64("test name")? as usize;
139 let name = cursor.text(length, "test name")?;
140 let status_length = cursor.u64("test status")? as usize;
141 let status = cursor.text(status_length, "test status")?;
142 let unattributed = cursor.u64("test attribution")? != 0;
146 let runner_length = cursor.u64("test runner")? as usize;
147 let runner = cursor.text(runner_length, "test runner")?;
148 let tag_count = cursor.u64("test tags")? as usize;
149 let mut tags = Vec::with_capacity(tag_count.min(64));
150 for _ in 0..tag_count {
151 let tag_length = cursor.u64("test tags")? as usize;
152 let tag = cursor.text(tag_length, "test tags")?;
153 if !tags.contains(&tag) {
154 tags.push(tag);
155 }
156 }
157 let hits = cursor.u64("test probes")? as usize;
158 let mut probes = BTreeMap::new();
159 for _ in 0..hits {
160 let index = cursor.u64("test probes")? as u32;
161 probes.insert(index, cursor.u64("test probes")? as u32);
162 }
163 let vector_count = cursor.u64("test vectors")? as usize;
164 let mut vectors = Vec::with_capacity(vector_count);
165 for _ in 0..vector_count {
166 let decision = cursor.u64("test vectors")? as u32;
167 vectors.push(PackedVector {
168 decision,
169 key: cursor.u64("test vectors")?,
170 });
171 }
172 tests.push(OwnedTestEvidence {
173 name,
174 status,
175 runner,
176 tags,
177 unattributed,
178 probes,
179 vectors,
180 });
181 }
182 let decision_count = cursor.u64("decision table")? as usize;
183 let mut widths = Vec::with_capacity(decision_count);
184 let mut decision_vectors = Vec::with_capacity(decision_count);
185 for _ in 0..decision_count {
186 widths.push(cursor.u64("decision table")? as u8);
187 let keys = cursor.u64("decision table")? as usize;
188 let mut seen = Vec::with_capacity(keys);
189 for _ in 0..keys {
190 seen.push(cursor.u64("decision table")?);
191 }
192 decision_vectors.push(seen);
193 }
194 Ok(OwnedEvidence {
195 global,
196 tests,
197 widths,
198 decision_vectors,
199 })
200}
201
202const PACKED_VALUE_SHIFT: u32 = 24;
203const PACKED_OUTCOME_SHIFT: u32 = 48;
204
205pub fn unpack_vector(key: u64, width: u8) -> McdcVector {
212 let evaluated = key & ((1 << PACKED_VALUE_SHIFT) - 1);
213 let values = (key >> PACKED_VALUE_SHIFT) & ((1 << PACKED_VALUE_SHIFT) - 1);
214 McdcVector {
215 values: (0..width)
216 .map(|index| {
217 let bit = 1_u64 << index;
218 (evaluated & bit != 0).then_some(values & bit != 0)
219 })
220 .collect(),
221 outcome: (key >> PACKED_OUTCOME_SHIFT) & 1 == 1,
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct OwnedTestOutcome {
228 pub name: String,
229 pub package: String,
230 pub file: Option<String>,
231 pub status: String,
233 pub runner: String,
236 pub attributed: bool,
240}
241
242pub fn go_declaration() -> FrontendRunDeclaration {
248 FrontendRunDeclaration {
249 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
250 frontend_id: "supercov-go".into(),
251 frontend_version: "go-owned-v1".into(),
252 language: "go".into(),
253 structural_source: supercov_contracts::StructuralSource::OwnedProbes,
256 runners: vec![FrontendRunnerDeclaration {
257 runner: "go-test".into(),
258 execution_model: ExecutionModel::SerialInProcess,
261 attribution: exact_per_test(),
262 limitations: {
263 let mut limitations = owned_attribution_limitations("go");
264 limitations.push(FrontendLimitation {
265 id: "go-parallel-tests".into(),
266 scopes: vec![FrontendLimitationScope::Test],
267 reason:
268 "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"
269 .into(),
270 });
271 limitations
272 },
273 }],
274 structural_limitations: Vec::new(),
275 }
276}
277
278pub fn go_coverage_model() -> CoverageModelDeclaration {
280 CoverageModelDeclaration {
281 language: "go".into(),
282 variant: "go-owned-probes-v1".into(),
283 name: "supercov-go-owned-v1".into(),
284 completeness_meaning: "Every obligation Supercov derived from the module's own Go sources was observed; explicit manifest limitations identify unmeasured Go surfaces.".into(),
285 measured: vec![
286 "owned Go statements and function entries".into(),
287 "owned atomic condition vectors and decision outcomes".into(),
288 "exact per-test attribution for tests that do not call t.Parallel()".into(),
289 ],
290 not_measured: vec![
291 "generated code, vendored packages and testdata".into(),
292 "work a test does after calling t.Parallel(), which counts run-wide".into(),
293 "causal linkage to individual actions".into(),
294 "all input values, semantic partitions, paths, or concurrency interleavings".into(),
295 "mutation score or assertion fault-detection strength".into(),
296 ],
297 }
298}
299
300pub fn jvm_declaration() -> FrontendRunDeclaration {
309 let runner = |name: &str, concurrency: &str| FrontendRunnerDeclaration {
310 runner: name.into(),
311 execution_model: ExecutionModel::SerialInProcess,
312 attribution: exact_per_test(),
313 limitations: {
314 let mut limitations = owned_attribution_limitations(name);
315 limitations.push(FrontendLimitation {
316 id: format!("{name}-parallel-execution"),
317 scopes: vec![FrontendLimitationScope::Test],
318 reason: format!(
319 "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"
320 ),
321 });
322 limitations
323 },
324 };
325 FrontendRunDeclaration {
326 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
327 frontend_id: "supercov-jvm".into(),
328 frontend_version: "jvm-owned-v1".into(),
329 language: "jvm".into(),
330 structural_source: supercov_contracts::StructuralSource::OwnedProbes,
331 runners: vec![
332 runner("junit-platform", "JUnit parallel execution enabled"),
333 runner("testng", "TestNG's parallel suites or methods"),
334 ],
335 structural_limitations: Vec::new(),
336 }
337}
338
339pub fn jvm_coverage_model() -> CoverageModelDeclaration {
341 CoverageModelDeclaration {
342 language: "jvm".into(),
343 variant: "jvm-owned-probes-v1".into(),
344 name: "supercov-jvm-owned-v1".into(),
345 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(),
346 measured: vec![
347 "owned Java and Kotlin statements and method entries".into(),
348 "owned atomic condition vectors and decision outcomes".into(),
349 "exact per-test attribution through the JUnit Platform's own lifecycle".into(),
350 ],
351 not_measured: vec![
352 "generated sources, and bytecode with no source in the project".into(),
353 "tests run concurrently, which count run-wide and drop condition coverage".into(),
354 "causal linkage to individual actions".into(),
355 "all input values, semantic partitions, paths, or concurrency interleavings".into(),
356 "mutation score or assertion fault-detection strength".into(),
357 ],
358 }
359}
360
361fn owned_attribution_limitations(language: &str) -> Vec<FrontendLimitation> {
370 vec![
371 FrontendLimitation {
372 id: format!("{language}-phase-linkage-aggregate"),
373 scopes: vec![FrontendLimitationScope::Phase],
374 reason:
375 "probes record what a test reached, not which of its setup, body or teardown phases reached it"
376 .into(),
377 },
378 FrontendLimitation {
379 id: format!("{language}-action-linkage-unavailable"),
380 scopes: vec![FrontendLimitationScope::Action],
381 reason: "there is no general application-action lifecycle to link coverage to".into(),
382 },
383 FrontendLimitation {
384 id: format!("{language}-assertion-linkage-unavailable"),
385 scopes: vec![FrontendLimitationScope::Assertion],
386 reason:
387 "coverage is attributed to the test that reached the code, not to the assertion that checked it"
388 .into(),
389 },
390 ]
391}
392
393fn exact_per_test() -> FrontendAttribution {
394 FrontendAttribution {
395 run: AttributionPrecision::Exact,
396 worker: AttributionPrecision::Exact,
397 test: AttributionPrecision::Exact,
398 retry: AttributionPrecision::Exact,
399 phase: AttributionPrecision::Aggregate,
400 action: AttributionPrecision::Unavailable,
403 assertion: AttributionPrecision::Unavailable,
404 }
405}
406
407pub fn merge_evidence(parts: Vec<OwnedEvidence>) -> OwnedEvidence {
415 let mut merged = OwnedEvidence::default();
416 for part in parts {
417 if merged.global.len() < part.global.len() {
418 merged.global.resize(part.global.len(), 0);
419 }
420 for (slot, value) in merged.global.iter_mut().zip(part.global) {
421 *slot |= value;
422 }
423 if merged.widths.len() < part.widths.len() {
424 merged.widths.resize(part.widths.len(), 0);
425 merged
426 .decision_vectors
427 .resize(part.widths.len(), Vec::new());
428 }
429 for (id, width) in part.widths.into_iter().enumerate() {
430 merged.widths[id] = merged.widths[id].max(width);
431 }
432 for (id, keys) in part.decision_vectors.into_iter().enumerate() {
433 let seen = &mut merged.decision_vectors[id];
434 for key in keys {
435 if !seen.contains(&key) {
436 seen.push(key);
437 }
438 }
439 }
440 merged.tests.extend(part.tests);
441 }
442 merged
443}
444
445fn stable_id(prefix: &str, values: &[&str]) -> String {
448 use sha2::{Digest, Sha256};
449 let mut hash = Sha256::new();
450 for value in values {
451 hash.update(value.as_bytes());
452 hash.update([0]);
453 }
454 let digest = hash.finalize();
455 let mut encoded = String::with_capacity(prefix.len() + 25);
456 encoded.push_str(prefix);
457 encoded.push(':');
458 for byte in &digest[..12] {
459 use std::fmt::Write as _;
460 write!(&mut encoded, "{byte:02x}").expect("string formatting");
461 }
462 encoded
463}
464
465fn obligations(probes: &BTreeMap<u64, GoProbe>) -> BTreeMap<u32, String> {
467 probes
468 .iter()
469 .map(|(id, probe)| {
470 let obligation = match &probe.target {
471 GoProbeTarget::Statement { id } | GoProbeTarget::Function { id } => id.clone(),
472 GoProbeTarget::Alternative { alternative, .. } => alternative.clone(),
473 };
474 (*id as u32, obligation)
475 })
476 .collect()
477}
478
479fn snapshot(
480 environment: &str,
481 evidence: &OwnedTestEvidence,
482 manifest: &CoverageManifest,
483 by_probe: &BTreeMap<u32, String>,
484 phase: &str,
485) -> RuntimeSnapshot {
486 let mut events = Vec::new();
487 let mut clock = 0_i64;
488 let hits = evidence
489 .probes
490 .keys()
491 .filter_map(|probe| by_probe.get(probe).cloned())
492 .collect::<BTreeSet<_>>();
493 for id in &hits {
494 events.push(RuntimeEvent {
495 event_type: "hit".into(),
496 id: id.clone(),
497 vector: None,
498 timestamp_ms: clock,
499 phase_id: Some(phase.to_owned()),
500 statement_id: None,
501 environment: environment.into(),
502 });
503 clock += 1;
504 }
505 let mut by_decision: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
506 for vector in &evidence.vectors {
507 by_decision
508 .entry(vector.decision)
509 .or_default()
510 .push(vector.key);
511 }
512 let mut decisions = Vec::new();
513 for (index, meta) in manifest.decisions.iter().enumerate() {
514 let Some(keys) = by_decision.get(&(index as u32)) else {
515 continue;
516 };
517 let width = meta.conditions.len() as u8;
518 let observed = keys
519 .iter()
520 .map(|key| unpack_vector(*key, width))
521 .collect::<Vec<_>>();
522 for vector in &observed {
523 events.push(RuntimeEvent {
524 event_type: "decision".into(),
525 id: meta.id.clone(),
526 vector: Some(vector.clone()),
527 timestamp_ms: clock,
528 phase_id: Some(phase.to_owned()),
529 statement_id: None,
530 environment: environment.into(),
531 });
532 clock += 1;
533 }
534 decisions.push(DecisionSnapshot {
535 meta: meta.clone(),
536 vectors: observed,
537 });
538 }
539 RuntimeSnapshot {
540 decisions,
541 hits: hits.into_iter().collect(),
542 events,
543 logicals: Vec::new(),
544 phase_id: None,
545 }
546}
547
548pub struct OwnedFrontendRun {
549 pub declaration: FrontendRunDeclaration,
550 pub request: CoverageReportRequest,
551 pub tests: usize,
552}
553
554impl OwnedFrontendRun {
555 pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
558 let model = PersistedCoverageModel::from_declaration(
559 self.request
560 .coverage_model
561 .as_ref()
562 .expect("an owned frontend always declares a coverage model"),
563 )
564 .expect("owned coverage models are contract-valid");
565 let mut entries = vec![
566 EvidenceArchiveEntry {
567 path: "coverage-model.json".into(),
568 contents: serde_json::to_vec(&model)?,
569 },
570 EvidenceArchiveEntry {
571 path: "frontend.json".into(),
572 contents: serde_json::to_vec(&self.declaration)?,
573 },
574 EvidenceArchiveEntry {
575 path: "manifest.json".into(),
576 contents: serde_json::to_vec(&self.request.manifest)?,
577 },
578 ];
579 for (index, result) in self.request.raw_results.iter().enumerate() {
580 entries.push(EvidenceArchiveEntry {
581 path: format!("results/{index:08}/mcdc.json"),
582 contents: serde_json::to_vec(result)?,
583 });
584 }
585 Ok(entries)
586 }
587}
588
589pub struct OwnedRunInputs<'a> {
597 pub declaration: FrontendRunDeclaration,
598 pub environment: &'a str,
601 pub manifest: &'a CoverageManifest,
602 pub probes: &'a BTreeMap<u64, GoProbe>,
603 pub evidence: &'a OwnedEvidence,
604 pub outcomes: &'a [OwnedTestOutcome],
605 pub run_id: &'a str,
606 pub generated_at: &'a str,
607 pub test_exit_code: i32,
608 pub coverage_model: CoverageModelDeclaration,
611}
612
613pub fn build_frontend_run(inputs: OwnedRunInputs) -> Result<OwnedFrontendRun, OwnedEvidenceError> {
620 let OwnedRunInputs {
621 declaration,
622 environment,
623 manifest,
624 probes,
625 evidence,
626 outcomes,
627 run_id,
628 generated_at,
629 test_exit_code,
630 coverage_model,
631 } = inputs;
632 let default_runner = declaration
636 .runners
637 .first()
638 .map(|runner| runner.runner.clone())
639 .ok_or(OwnedEvidenceError::NoTests)?;
640 let declared = declaration
641 .runners
642 .iter()
643 .map(|runner| runner.runner.as_str())
644 .collect::<BTreeSet<_>>();
645 let source = declaration.frontend_version.clone();
646 let by_probe = obligations(probes);
647 let recorded = evidence
648 .tests
649 .iter()
650 .map(|test| (test.name.clone(), test))
651 .collect::<BTreeMap<_, _>>();
652 let empty = OwnedTestEvidence::default();
653 let mut raw_results = outcomes
654 .iter()
655 .map(|outcome| {
656 let test = recorded.get(&outcome.name).copied().unwrap_or(&empty);
657 let test = if outcome.attributed { test } else { &empty };
662 let test_id = format!("{}::{}", outcome.package, outcome.name);
663 let phase = test_phase(&test_id);
664 let provenance = TestProvenance {
665 runner: if declared.contains(outcome.runner.as_str()) {
670 outcome.runner.clone()
671 } else {
672 default_runner.clone()
673 },
674 kind: crate::test_kind::from_tags(&test.tags)
675 .unwrap_or("unit")
676 .into(),
677 project: Some(outcome.package.clone()),
678 source: source.clone(),
679 tags: test.tags.clone(),
680 };
681 RawTestResult {
682 scope: Some(ExecutionScope {
683 version: 1,
684 run_id: run_id.to_owned(),
685 worker_id: outcome.package.clone(),
690 test_id: test_id.clone(),
691 test_key: stable_id("owned-test", &[&outcome.package, &outcome.name]),
692 retry: 0,
693 attempt_id: stable_id(
694 "owned-attempt",
695 &[run_id, &outcome.package, &outcome.name, "0"],
696 ),
697 }),
698 test_id: Some(test_id),
699 test: outcome.name.clone(),
700 test_file: outcome.file.clone(),
701 title: None,
702 retry: Some(0),
703 status: Some(outcome.status.clone()),
704 expected_status: None,
705 flaky: false,
706 provenance: provenance.clone(),
707 role: "test".into(),
708 attribution: if outcome.attributed {
709 crate::coverage_report::ATTRIBUTION_EXACT
710 } else {
711 crate::coverage_report::ATTRIBUTION_RUN_WIDE
712 }
713 .into(),
714 phases: vec![CoveragePhase {
720 id: phase.clone(),
721 kind: "test".into(),
722 operation: format!("{} {}", provenance.runner, outcome.name),
723 source: outcome.file.clone(),
724 caused_by_phase_id: None,
725 started_at_ms: 0,
726 ended_at_ms: None,
727 status: Some(outcome.status.clone()),
728 error: None,
729 }],
730 runtime: vec![snapshot(environment, test, manifest, &by_probe, &phase)],
731 browser: Vec::new(),
732 server: Vec::new(),
733 }
734 })
735 .collect::<Vec<_>>();
736 let claimed = evidence
748 .tests
749 .iter()
750 .filter(|test| !test.unattributed)
751 .flat_map(|test| test.probes.keys().copied())
752 .collect::<BTreeSet<_>>();
753 let unclaimed = evidence
754 .global
755 .iter()
756 .enumerate()
757 .filter(|(index, mask)| **mask != 0 && !claimed.contains(&(*index as u32)))
758 .map(|(index, mask)| (index as u32, *mask))
759 .collect::<BTreeMap<_, _>>();
760 if !unclaimed.is_empty() {
761 let status = if test_exit_code == 0 {
769 "passed"
770 } else {
771 "failed"
772 };
773 let test_id = format!("{}::background", declaration.language);
774 let phase = test_phase(&test_id);
775 let background = OwnedTestEvidence {
776 name: "background".into(),
777 status: "unknown".into(),
778 runner: String::new(),
779 tags: Vec::new(),
780 unattributed: false,
783 probes: unclaimed,
784 vectors: Vec::new(),
785 };
786 raw_results.push(RawTestResult {
787 scope: Some(ExecutionScope {
788 version: 1,
789 run_id: run_id.to_owned(),
790 worker_id: "background".into(),
791 test_id: test_id.clone(),
792 test_key: stable_id("owned-background", &[run_id]),
793 retry: 0,
794 attempt_id: stable_id("owned-background-attempt", &[run_id]),
795 }),
796 test_id: Some(test_id),
797 test: "execution no test could be credited with".into(),
798 test_file: None,
799 title: None,
800 retry: Some(0),
801 status: Some(status.into()),
802 expected_status: None,
803 flaky: false,
804 provenance: TestProvenance {
805 runner: default_runner.clone(),
806 kind: "background".into(),
807 project: None,
808 source: source.clone(),
809 tags: Vec::new(),
810 },
811 role: "background".into(),
812 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
813 phases: vec![CoveragePhase {
814 id: phase.clone(),
815 kind: "background".into(),
816 operation: "execution outside any test".into(),
817 source: None,
818 caused_by_phase_id: None,
819 started_at_ms: 0,
820 ended_at_ms: Some(0),
821 status: Some(status.into()),
822 error: None,
823 }],
824 runtime: vec![snapshot(
825 environment,
826 &background,
827 manifest,
828 &by_probe,
829 &phase,
830 )],
831 browser: Vec::new(),
832 server: Vec::new(),
833 });
834 }
835
836 if raw_results.is_empty() {
843 return Err(OwnedEvidenceError::NoTests);
844 }
845
846 let observed = raw_results
851 .iter()
852 .map(|result| result.provenance.runner.as_str())
853 .collect::<BTreeSet<_>>();
854 let mut declaration = declaration;
855
856 declaration.structural_limitations = manifest
862 .limitations
863 .iter()
864 .filter_map(|limitation| limitation.get("id")?.as_str().map(str::to_owned))
865 .collect::<BTreeSet<_>>()
866 .into_iter()
867 .collect();
868
869 if declaration
870 .runners
871 .iter()
872 .any(|runner| observed.contains(runner.runner.as_str()))
873 {
874 declaration
875 .runners
876 .retain(|runner| observed.contains(runner.runner.as_str()));
877 }
878 Ok(OwnedFrontendRun {
879 declaration,
880 tests: raw_results.len(),
881 request: CoverageReportRequest {
882 run_id: run_id.to_owned(),
883 manifest: manifest.clone(),
884 raw_results,
885 generated_at: generated_at.to_owned(),
886 coverage_model: Some(coverage_model),
887 integrity: None,
888 test_exit_code: ExitCodeInput::Present(Some(test_exit_code)),
889 },
890 })
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
896
897 fn write(values: &[u64]) -> Vec<u8> {
898 values.iter().flat_map(|v| v.to_le_bytes()).collect()
899 }
900
901 #[test]
902 fn an_unevaluated_condition_is_absent_rather_than_false() {
903 let short_circuited = unpack_vector(0b01, 2);
908 assert_eq!(short_circuited.values, [Some(false), None]);
909 assert!(!short_circuited.outcome);
910
911 let both = unpack_vector(0b11 | (0b11 << 24) | (1 << 48), 2);
912 assert_eq!(both.values, [Some(true), Some(true)]);
913 assert!(both.outcome);
914
915 let first_false = unpack_vector(0b11 | (0b01 << 24), 2);
918 assert_eq!(first_false.values, [Some(true), Some(false)]);
919 }
920
921 #[test]
922 fn a_test_carries_the_tags_its_framework_gave_it() {
923 let mut bytes = Vec::new();
924 let mut put = |value: u64| bytes.extend_from_slice(&value.to_le_bytes());
925 put(0); put(1); let text = |bytes: &mut Vec<u8>, value: &str| {
928 bytes.extend_from_slice(&(value.len() as u64).to_le_bytes());
929 bytes.extend_from_slice(value.as_bytes());
930 };
931 text(&mut bytes, "ShopTest.discounts()");
932 text(&mut bytes, "passed");
933 bytes.extend_from_slice(&0_u64.to_le_bytes()); text(&mut bytes, "junit-platform");
935 bytes.extend_from_slice(&3_u64.to_le_bytes());
936 for tag in ["api", "e2e", "api"] {
937 text(&mut bytes, tag);
938 }
939 for value in [0_u64, 0, 0] {
940 bytes.extend_from_slice(&value.to_le_bytes());
942 }
943 let evidence = read_evidence(&bytes).unwrap();
944 assert_eq!(evidence.tests[0].tags, ["api", "e2e"]);
945 assert_eq!(evidence.tests[0].runner, "junit-platform");
946 }
947
948 #[test]
949 fn a_truncated_file_is_refused_rather_than_read_short() {
950 let full = write(&[2, 0, 2, 1, 4, u64::from_le_bytes(*b"Test\0\0\0\0"), 0, 0, 0]);
954 for cut in [0, 8, 16, 24, 32] {
955 assert!(
956 read_evidence(&full[..cut.min(full.len())]).is_err(),
957 "a file cut at {cut} bytes must not decode"
958 );
959 }
960 assert!(matches!(
961 read_evidence(&write(&[5, 1])),
962 Err(OwnedEvidenceError::Truncated("probe totals"))
963 ));
964 }
965
966 #[test]
967 fn a_run_with_no_tests_is_an_error_not_an_empty_report() {
968 let evidence = OwnedEvidence::default();
973 let manifest = CoverageManifest {
974 decisions: Vec::new(),
975 points: Vec::new(),
976 branches: Vec::new(),
977 limitations: Vec::new(),
978 unmeasured: Vec::new(),
979 scope: None,
980 assertion_sites: Vec::new(),
981 };
982 assert_eq!(
983 build_frontend_run(OwnedRunInputs {
984 declaration: go_declaration(),
985 environment: "go",
986 manifest: &manifest,
987 probes: &BTreeMap::new(),
988 evidence: &evidence,
989 outcomes: &[],
990 run_id: "run",
991 generated_at: "now",
992 test_exit_code: 0,
993 coverage_model: go_coverage_model(),
994 })
995 .err(),
996 Some(OwnedEvidenceError::NoTests)
997 );
998 }
999
1000 #[test]
1001 fn a_run_nothing_could_be_attributed_to_is_still_a_run() {
1002 let evidence = OwnedEvidence {
1010 global: vec![1],
1011 ..OwnedEvidence::default()
1012 };
1013 let manifest = CoverageManifest {
1014 decisions: Vec::new(),
1015 points: vec![crate::coverage_report::PointMeta {
1016 id: "go:statement:one".into(),
1017 kind: crate::coverage_analysis::PointKind::Statement,
1018 file: "lib.go".into(),
1019 line: 1,
1020 column: 1,
1021 source: "return 1".into(),
1022 label: None,
1023 }],
1024 branches: Vec::new(),
1025 limitations: Vec::new(),
1026 unmeasured: Vec::new(),
1027 scope: None,
1028 assertion_sites: Vec::new(),
1029 };
1030 let probes = BTreeMap::from([(
1031 0_u64,
1032 GoProbe {
1033 id: 0,
1034 target: GoProbeTarget::Statement {
1035 id: "go:statement:one".into(),
1036 },
1037 at: 0,
1038 },
1039 )]);
1040 let run = build_frontend_run(OwnedRunInputs {
1041 declaration: go_declaration(),
1042 environment: "go",
1043 manifest: &manifest,
1044 probes: &probes,
1045 evidence: &evidence,
1046 outcomes: &[],
1047 run_id: "run",
1048 generated_at: "now",
1049 test_exit_code: 0,
1050 coverage_model: go_coverage_model(),
1051 })
1052 .expect("a measured package publishes even when no test can claim it");
1053 assert_eq!(run.tests, 1, "the one record is what nobody could claim");
1054 assert_eq!(run.request.raw_results[0].role, "background");
1055 assert_eq!(
1056 run.request.raw_results[0].status.as_deref(),
1057 Some("passed"),
1058 "it counts as coverage because the run passed"
1059 );
1060 }
1061
1062 #[test]
1063 fn each_declaration_says_what_its_runner_can_and_cannot_attribute() {
1064 let jvm = jvm_declaration();
1068 let jvm_runner = &jvm.runners[0];
1069 assert_eq!(jvm_runner.runner, "junit-platform");
1070 let gaps = jvm_runner
1071 .limitations
1072 .iter()
1073 .map(|limitation| limitation.id.as_str())
1074 .collect::<Vec<_>>();
1075 assert!(
1076 gaps.contains(&"junit-platform-parallel-execution"),
1077 "{gaps:?}"
1078 );
1079 let named = jvm
1082 .runners
1083 .iter()
1084 .map(|runner| runner.runner.as_str())
1085 .collect::<Vec<_>>();
1086 assert_eq!(named, ["junit-platform", "testng"]);
1087 }
1088
1089 #[test]
1090 fn the_declaration_says_what_go_can_and_cannot_attribute() {
1091 let declared = go_declaration();
1094 let runner = &declared.runners[0];
1095 assert_eq!(runner.execution_model, ExecutionModel::SerialInProcess);
1096 assert_eq!(runner.attribution.test, AttributionPrecision::Exact);
1097 assert_eq!(
1098 runner.attribution.assertion,
1099 AttributionPrecision::Unavailable
1100 );
1101 let gaps = runner
1102 .limitations
1103 .iter()
1104 .map(|limitation| limitation.id.as_str())
1105 .collect::<Vec<_>>();
1106 assert!(gaps.contains(&"go-parallel-tests"), "{gaps:?}");
1107 for gap in [
1110 "go-phase-linkage-aggregate",
1111 "go-action-linkage-unavailable",
1112 "go-assertion-linkage-unavailable",
1113 ] {
1114 assert!(gaps.contains(&gap), "{gaps:?}");
1115 }
1116 }
1117
1118 #[test]
1119 fn a_test_nothing_can_credit_is_named_without_being_handed_the_run() {
1120 let manifest = CoverageManifest {
1128 decisions: Vec::new(),
1129 points: vec![
1130 crate::coverage_report::PointMeta {
1131 id: "go:statement:one".into(),
1132 kind: crate::coverage_analysis::PointKind::Statement,
1133 file: "lib.go".into(),
1134 line: 1,
1135 column: 1,
1136 source: "return 1".into(),
1137 label: None,
1138 },
1139 crate::coverage_report::PointMeta {
1140 id: "go:statement:two".into(),
1141 kind: crate::coverage_analysis::PointKind::Statement,
1142 file: "lib.go".into(),
1143 line: 2,
1144 column: 1,
1145 source: "return 2".into(),
1146 label: None,
1147 },
1148 ],
1149 branches: Vec::new(),
1150 limitations: Vec::new(),
1151 unmeasured: Vec::new(),
1152 scope: None,
1153 assertion_sites: Vec::new(),
1154 };
1155 let probes = BTreeMap::from([
1156 (
1157 0_u64,
1158 GoProbe {
1159 id: 0,
1160 target: GoProbeTarget::Statement {
1161 id: "go:statement:one".into(),
1162 },
1163 at: 0,
1164 },
1165 ),
1166 (
1167 1_u64,
1168 GoProbe {
1169 id: 1,
1170 target: GoProbeTarget::Statement {
1171 id: "go:statement:two".into(),
1172 },
1173 at: 0,
1174 },
1175 ),
1176 ]);
1177 let evidence = OwnedEvidence {
1179 global: vec![1, 1],
1180 tests: vec![
1181 OwnedTestEvidence {
1182 name: "TestSerial".into(),
1183 status: "passed".into(),
1184 runner: String::new(),
1185 tags: Vec::new(),
1186 unattributed: false,
1187 probes: BTreeMap::from([(0, 1)]),
1188 vectors: Vec::new(),
1189 },
1190 OwnedTestEvidence {
1191 name: "TestParallel".into(),
1192 status: "passed".into(),
1193 runner: String::new(),
1194 tags: Vec::new(),
1195 unattributed: true,
1196 probes: BTreeMap::from([(0, 1), (1, 1)]),
1197 vectors: Vec::new(),
1198 },
1199 ],
1200 ..OwnedEvidence::default()
1201 };
1202 let outcome = |name: &str, attributed| OwnedTestOutcome {
1203 name: name.into(),
1204 runner: String::new(),
1205 package: "example.com/p".into(),
1206 file: Some("p/x_test.go".into()),
1207 status: "passed".into(),
1208 attributed,
1209 };
1210 let run = build_frontend_run(OwnedRunInputs {
1211 declaration: go_declaration(),
1212 environment: "go",
1213 manifest: &manifest,
1214 probes: &probes,
1215 evidence: &evidence,
1216 outcomes: &[outcome("TestSerial", true), outcome("TestParallel", false)],
1217 run_id: "run",
1218 generated_at: "now",
1219 test_exit_code: 0,
1220 coverage_model: go_coverage_model(),
1221 })
1222 .expect("run");
1223
1224 let result = |name: &str| {
1225 run.request
1226 .raw_results
1227 .iter()
1228 .find(|result| result.test == name)
1229 .unwrap_or_else(|| panic!("{name} is missing from the run"))
1230 };
1231 let serial = result("TestSerial");
1232 assert_eq!(
1233 serial.attribution,
1234 crate::coverage_report::ATTRIBUTION_EXACT
1235 );
1236 assert_eq!(
1237 serial.runtime[0].hits,
1238 ["go:statement:one"],
1239 "a test that ran alone keeps what it reached"
1240 );
1241
1242 let parallel = result("TestParallel");
1243 assert_eq!(
1244 parallel.attribution,
1245 crate::coverage_report::ATTRIBUTION_RUN_WIDE,
1246 "it ran, and what it reached belongs to the run rather than to it"
1247 );
1248 assert_eq!(
1249 parallel.status.as_deref(),
1250 Some("passed"),
1251 "unattributed is not unknown: the runner said how it ended"
1252 );
1253 assert!(
1254 parallel.runtime[0].hits.is_empty(),
1255 "it was handed the record's probes and they are not its own: {:?}",
1256 parallel.runtime[0].hits
1257 );
1258 }
1259
1260 #[test]
1261 fn a_test_the_runner_saw_but_that_recorded_nothing_still_appears() {
1262 let manifest = CoverageManifest {
1265 decisions: Vec::new(),
1266 points: Vec::new(),
1267 branches: Vec::new(),
1268 limitations: Vec::new(),
1269 unmeasured: Vec::new(),
1270 scope: None,
1271 assertion_sites: Vec::new(),
1272 };
1273 let run = build_frontend_run(OwnedRunInputs {
1274 declaration: go_declaration(),
1275 environment: "go",
1276 manifest: &manifest,
1277 probes: &BTreeMap::new(),
1278 evidence: &OwnedEvidence::default(),
1279 outcomes: &[OwnedTestOutcome {
1280 name: "TestSilent".into(),
1281 runner: String::new(),
1282 package: "example.com/p".into(),
1283 file: Some("p/x_test.go".into()),
1284 status: "passed".into(),
1285 attributed: true,
1286 }],
1287 run_id: "run",
1288 generated_at: "now",
1289 test_exit_code: 0,
1290 coverage_model: go_coverage_model(),
1291 })
1292 .expect("run");
1293 assert_eq!(run.tests, 1);
1294 let result = &run.request.raw_results[0];
1295 assert_eq!(result.test, "TestSilent");
1296 assert_eq!(result.test_id.as_deref(), Some("example.com/p::TestSilent"));
1297 assert!(result.runtime[0].hits.is_empty());
1298 }
1299
1300 #[test]
1301 fn both_owned_declarations_satisfy_the_contract_they_are_read_back_through() {
1302 for declaration in [go_declaration(), jvm_declaration()] {
1307 let language = declaration.language.clone();
1308 supercov_contracts::validate_frontend_run_declaration(&declaration)
1309 .unwrap_or_else(|error| panic!("{language} declaration is unreadable: {error}"));
1310 }
1311 }
1312 #[test]
1313 fn merging_processes_unions_the_run_and_keeps_every_test() {
1314 let left = OwnedEvidence {
1315 global: vec![0b01, 0b00],
1316 tests: vec![OwnedTestEvidence {
1317 name: "TestA".into(),
1318 status: "passed".into(),
1319 ..Default::default()
1320 }],
1321 widths: vec![2],
1322 decision_vectors: vec![vec![0b01]],
1323 };
1324 let right = OwnedEvidence {
1325 global: vec![0b10, 0b10],
1326 tests: vec![OwnedTestEvidence {
1327 name: "TestB".into(),
1328 status: "failed".into(),
1329 ..Default::default()
1330 }],
1331 widths: vec![2],
1332 decision_vectors: vec![vec![0b01, 0b11]],
1333 };
1334 let merged = merge_evidence(vec![left, right]);
1335 assert_eq!(merged.global, [0b11, 0b10]);
1337 assert_eq!(
1339 merged
1340 .tests
1341 .iter()
1342 .map(|test| test.name.as_str())
1343 .collect::<Vec<_>>(),
1344 ["TestA", "TestB"]
1345 );
1346 assert_eq!(merged.decision_vectors, [vec![0b01, 0b11]]);
1348 }
1349}