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 }
533}
534
535pub struct OwnedFrontendRun {
536 pub declaration: FrontendRunDeclaration,
537 pub request: CoverageReportRequest,
538 pub tests: usize,
539}
540
541impl OwnedFrontendRun {
542 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
576pub struct OwnedRunInputs<'a> {
584 pub declaration: FrontendRunDeclaration,
585 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 pub coverage_model: CoverageModelDeclaration,
598}
599
600pub 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 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 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 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 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 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 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 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 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 if raw_results.is_empty() {
825 return Err(OwnedEvidenceError::NoTests);
826 }
827
828 let observed = raw_results
833 .iter()
834 .map(|result| result.provenance.runner.as_str())
835 .collect::<BTreeSet<_>>();
836 let mut declaration = declaration;
837
838 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(merged.global, [0b11, 0b10]);
1286 assert_eq!(
1288 merged
1289 .tests
1290 .iter()
1291 .map(|test| test.name.as_str())
1292 .collect::<Vec<_>>(),
1293 ["TestA", "TestB"]
1294 );
1295 assert_eq!(merged.decision_vectors, [vec![0b01, 0b11]]);
1297 }
1298}