1use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use supercov_contracts::{COVERAGE_MODEL_SCHEMA_VERSION, FrontendRunDeclaration};
15
16use crate::coverage_analysis::{
17 AnalysisError, BranchCoverage, CoverageCoreInput, CoverageSummary, DecisionCoverage,
18 McdcVector, PointCoverage, PointKind, analyze_core, find_witnesses_for_conditions,
19};
20use crate::evidence_archive::{EvidenceArchiveEntry, read_archive};
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24pub struct DecisionMeta {
25 pub id: String,
26 pub file: String,
27 pub line: usize,
28 pub column: usize,
29 pub source: String,
30 pub conditions: Vec<String>,
31 pub kind: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct PointMeta {
37 pub id: String,
38 pub kind: PointKind,
39 pub file: String,
40 pub line: usize,
41 pub column: usize,
42 pub source: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub label: Option<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase", deny_unknown_fields)]
49pub struct BranchAlternativeMeta {
50 pub id: String,
51 pub label: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase", deny_unknown_fields)]
56pub struct BranchMeta {
57 pub id: String,
58 pub kind: String,
59 pub file: String,
60 pub line: usize,
61 pub column: usize,
62 pub source: String,
63 pub alternatives: Vec<BranchAlternativeMeta>,
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68pub struct CoverageManifest {
69 pub decisions: Vec<DecisionMeta>,
70 pub points: Vec<PointMeta>,
71 pub branches: Vec<BranchMeta>,
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub limitations: Vec<Value>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub unmeasured: Vec<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub scope: Option<Value>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85pub struct DecisionSnapshot {
86 pub meta: DecisionMeta,
87 pub vectors: Vec<McdcVector>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct RuntimeEvent {
93 #[serde(rename = "type")]
94 pub event_type: String,
95 pub id: String,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub vector: Option<McdcVector>,
98 pub timestamp_ms: i64,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub phase_id: Option<String>,
101 pub environment: String,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
105#[serde(rename_all = "camelCase", deny_unknown_fields)]
106pub struct RuntimeSnapshot {
107 #[serde(default)]
108 pub decisions: Vec<DecisionSnapshot>,
109 #[serde(default)]
110 pub hits: Vec<String>,
111 #[serde(default)]
112 pub events: Vec<RuntimeEvent>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117pub struct ExecutionScope {
118 pub version: usize,
119 pub run_id: String,
120 pub worker_id: String,
121 pub test_id: String,
122 pub test_key: String,
123 pub retry: usize,
124 pub attempt_id: String,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase", deny_unknown_fields)]
129pub struct ServerRecord {
130 #[serde(rename = "type")]
131 pub record_type: String,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub meta: Option<DecisionMeta>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub vector: Option<McdcVector>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub id: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub timestamp_ms: Option<i64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub phase_id: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub scope: Option<ExecutionScope>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct CoveragePhase {
149 pub id: String,
150 pub kind: String,
151 pub operation: String,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub source: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub caused_by_phase_id: Option<String>,
156 pub started_at_ms: i64,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub ended_at_ms: Option<i64>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub status: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub error: Option<String>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase", deny_unknown_fields)]
167pub struct TestProvenance {
168 pub runner: String,
169 pub kind: String,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub project: Option<String>,
172 pub source: String,
173}
174
175impl Default for TestProvenance {
176 fn default() -> Self {
177 Self {
178 runner: "unknown".into(),
179 kind: "unknown".into(),
180 project: None,
181 source: "unknown".into(),
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase", deny_unknown_fields)]
188pub struct RawTestResult {
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub test_id: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub scope: Option<ExecutionScope>,
193 pub test: String,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub test_file: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub title: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub retry: Option<usize>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub status: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub expected_status: Option<String>,
204 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
205 pub flaky: bool,
206 #[serde(default)]
207 pub provenance: TestProvenance,
208 #[serde(default = "default_test_role")]
209 pub role: String,
210 #[serde(default)]
211 pub phases: Vec<CoveragePhase>,
212 #[serde(default)]
213 pub runtime: Vec<RuntimeSnapshot>,
214 #[serde(default)]
215 pub browser: Vec<RuntimeSnapshot>,
216 #[serde(default)]
217 pub server: Vec<ServerRecord>,
218}
219
220fn default_test_role() -> String {
221 "test".into()
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
225#[serde(rename_all = "camelCase")]
226pub struct TestAttempt {
227 pub retry: usize,
228 pub status: String,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub expected_status: Option<String>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
234#[serde(rename_all = "camelCase")]
235pub struct CoverageConfidence {
236 pub level: String,
237 pub setup_only: bool,
238 pub background_only: bool,
239 pub asserted: bool,
240 pub tests: Vec<String>,
241 pub asserted_tests: Vec<String>,
242 pub runners: Vec<String>,
243 pub kinds: Vec<String>,
244 pub e2e: bool,
245}
246
247#[derive(Debug, Clone, PartialEq, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct VectorObservation {
250 pub vector: McdcVector,
251 pub tests: Vec<String>,
252 #[serde(skip_serializing_if = "Vec::is_empty")]
253 pub phases: Vec<String>,
254 #[serde(skip_serializing_if = "Vec::is_empty")]
255 pub explicit_phases: Vec<String>,
256 pub confidence: CoverageConfidence,
257}
258
259#[derive(Debug, Clone, PartialEq, Serialize)]
260#[serde(rename_all = "camelCase")]
261pub struct ConditionResult {
262 pub index: usize,
263 pub source: String,
264 pub covered: bool,
265 pub assertion_covered: bool,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub witness: Option<[McdcVector; 2]>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub witness_tests: Option<[Vec<String>; 2]>,
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct DecisionResult {
275 pub meta: DecisionMeta,
276 pub executed: bool,
277 pub covered: bool,
278 pub vectors: Vec<McdcVector>,
279 pub vector_observations: Vec<VectorObservation>,
280 pub conditions: Vec<ConditionResult>,
281 pub tests: Vec<String>,
282 pub confidence: CoverageConfidence,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize)]
286#[serde(rename_all = "camelCase")]
287pub struct PointResult {
288 pub meta: PointMeta,
289 pub covered: bool,
290 pub tests: Vec<String>,
291 pub phases: Vec<String>,
292 pub confidence: CoverageConfidence,
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct AlternativeResult {
298 pub id: String,
299 pub label: String,
300 pub covered: bool,
301 pub tests: Vec<String>,
302 pub phases: Vec<String>,
303 pub confidence: CoverageConfidence,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize)]
307#[serde(rename_all = "camelCase")]
308pub struct BranchResult {
309 pub meta: BranchMeta,
310 pub covered: bool,
311 pub alternatives: Vec<AlternativeResult>,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
315pub struct SourceLine {
316 pub file: String,
317 pub line: usize,
318}
319
320#[derive(Debug, Default)]
322struct LineAggregate {
323 covered: bool,
324 measured: bool,
325 tests: BTreeSet<String>,
326 phases: BTreeSet<String>,
327 explicit_phases: BTreeSet<String>,
328}
329
330#[derive(Debug, Clone, PartialEq, Serialize)]
331#[serde(rename_all = "camelCase")]
332pub struct LineResult {
333 pub file: String,
334 pub line: usize,
335 pub covered: bool,
336 #[serde(skip)]
341 pub measured: bool,
342 pub tests: Vec<String>,
343 pub runners: Vec<String>,
344 pub kinds: Vec<String>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub exclusive_kind: Option<String>,
347 pub phases: Vec<String>,
348 pub confidence: CoverageConfidence,
349}
350
351#[derive(Debug, Clone, PartialEq, Serialize)]
352#[serde(rename_all = "camelCase")]
353pub struct TestDecisionResult {
354 pub id: String,
355 pub vectors: Vec<McdcVector>,
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct TestCoverageResult {
361 pub id: String,
362 pub name: String,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub file: Option<String>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub title: Option<String>,
367 pub retries: Vec<usize>,
368 pub attempts: Vec<TestAttempt>,
369 pub outcome: String,
370 pub provenance: TestProvenance,
371 pub role: String,
372 pub hits: Vec<String>,
373 pub decisions: Vec<TestDecisionResult>,
374 pub lines: Vec<SourceLine>,
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize)]
378#[serde(rename_all = "camelCase")]
379pub struct TestFileResult {
380 pub file: String,
381 pub tests: Vec<String>,
382 pub runners: Vec<String>,
383 pub kinds: Vec<String>,
384 pub lines: Vec<SourceLine>,
385}
386
387#[derive(Debug, Clone, PartialEq, Serialize)]
388#[serde(rename_all = "camelCase")]
389pub struct PhaseResult {
390 #[serde(flatten)]
391 pub phase: CoveragePhase,
392 pub test: String,
393 pub hits: Vec<String>,
394 pub decisions: Vec<TestDecisionResult>,
395 pub lines: Vec<SourceLine>,
396 pub browser_events: usize,
397 pub server_events: usize,
398 pub explicit_events: usize,
399 pub inferred_events: usize,
400 pub explicit_browser_events: usize,
401 pub inferred_browser_events: usize,
402 pub explicit_server_events: usize,
403 pub inferred_server_events: usize,
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize)]
407#[serde(rename_all = "camelCase")]
408pub struct DimensionCoverage {
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub kind: Option<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub runner: Option<String>,
413 pub tests: usize,
414 pub setups: usize,
415 pub summary: CoverageSummary,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
419#[serde(rename_all = "camelCase")]
420pub struct CoverageModel {
421 pub language: String,
422 pub name: String,
423 pub completeness_meaning: String,
424 pub measured: Vec<String>,
425 pub not_measured: Vec<String>,
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase", deny_unknown_fields)]
430pub struct CoverageModelDeclaration {
431 pub language: String,
432 pub variant: String,
433 pub name: String,
434 pub completeness_meaning: String,
435 pub measured: Vec<String>,
436 pub not_measured: Vec<String>,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase", deny_unknown_fields)]
441pub struct PersistedCoverageModel {
442 pub schema_version: u32,
443 pub language: String,
444 pub variant: String,
445 pub name: String,
446 pub completeness_meaning: String,
447 pub measured: Vec<String>,
448 pub not_measured: Vec<String>,
449}
450
451impl PersistedCoverageModel {
452 pub fn from_declaration(value: &CoverageModelDeclaration) -> Result<Self, &'static str> {
453 let persisted = Self {
454 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
455 language: value.language.clone(),
456 variant: value.variant.clone(),
457 name: value.name.clone(),
458 completeness_meaning: value.completeness_meaning.clone(),
459 measured: value.measured.clone(),
460 not_measured: value.not_measured.clone(),
461 };
462 persisted.clone().into_declaration()?;
463 Ok(persisted)
464 }
465
466 fn into_declaration(self) -> Result<CoverageModelDeclaration, &'static str> {
467 if self.schema_version != COVERAGE_MODEL_SCHEMA_VERSION {
468 return Err("unsupported coverage model schema");
469 }
470 let valid_identifier = |value: &str| {
471 (1..=supercov_contracts::COVERAGE_MODEL_MAX_IDENTIFIER_BYTES).contains(&value.len())
472 && value.as_bytes()[0].is_ascii_lowercase()
473 && value.bytes().all(|byte| {
474 byte.is_ascii_lowercase()
475 || byte.is_ascii_digit()
476 || matches!(byte, b'.' | b'+' | b'-')
477 })
478 };
479 let valid_description = |value: &str| {
480 !value.is_empty()
481 && value.trim().len() == value.len()
482 && value.len() <= supercov_contracts::COVERAGE_MODEL_MAX_DESCRIPTION_BYTES
483 && !value.chars().any(char::is_control)
484 };
485 if !valid_identifier(&self.language)
486 || !valid_identifier(&self.variant)
487 || !valid_description(&self.name)
488 || !valid_description(&self.completeness_meaning)
489 || self.measured.is_empty()
490 || self.measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
491 || self.not_measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
492 || self
493 .measured
494 .iter()
495 .chain(&self.not_measured)
496 .any(|item| !valid_description(item))
497 {
498 return Err("invalid coverage model declaration");
499 }
500 let measured = self.measured.iter().collect::<BTreeSet<_>>();
501 let not_measured = self.not_measured.iter().collect::<BTreeSet<_>>();
502 if measured.len() != self.measured.len()
503 || not_measured.len() != self.not_measured.len()
504 || !measured.is_disjoint(¬_measured)
505 {
506 return Err("invalid coverage model surface partition");
507 }
508 Ok(CoverageModelDeclaration {
509 language: self.language,
510 variant: self.variant,
511 name: self.name,
512 completeness_meaning: self.completeness_meaning,
513 measured: self.measured,
514 not_measured: self.not_measured,
515 })
516 }
517}
518
519#[derive(Debug, Clone, PartialEq, Serialize)]
520#[serde(rename_all = "camelCase")]
521pub struct CoverageView {
522 pub generated_at: String,
523 pub variant: String,
524 #[serde(skip_serializing_if = "Option::is_none")]
525 pub scope: Option<Value>,
526 pub model: CoverageModel,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub integrity: Option<Value>,
529 pub limitations: Vec<Value>,
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub transport: Option<TransportStats>,
532 pub summary: CoverageSummary,
533 pub coverage_by_kind: Vec<DimensionCoverage>,
534 pub coverage_by_runner: Vec<DimensionCoverage>,
535 pub decisions: Vec<DecisionResult>,
536 pub points: Vec<PointResult>,
537 pub branches: Vec<BranchResult>,
538 pub tests: Vec<TestCoverageResult>,
539 pub test_files: Vec<TestFileResult>,
540 pub phases: Vec<PhaseResult>,
541 pub lines: Vec<LineResult>,
542}
543
544#[derive(Debug, Clone, PartialEq, Serialize)]
545#[serde(rename_all = "camelCase")]
546pub struct CoverageFilters {
547 pub passed: CoverageView,
548 pub failed: CoverageView,
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize)]
552#[serde(rename_all = "camelCase")]
553pub struct CoverageReport {
554 #[serde(flatten)]
555 pub view: CoverageView,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 pub execution: Option<ExecutionResult>,
558 pub filters: CoverageFilters,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
562#[serde(rename_all = "camelCase")]
563pub struct ExecutionResult {
564 pub test_exit_code: Option<i32>,
565 pub valid: bool,
566}
567
568#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
569#[serde(rename_all = "camelCase")]
570pub struct TransportStats {
571 pub processes: usize,
572 pub child_launches: usize,
573 pub remote_launches: usize,
574 pub workspace_capabilities: usize,
575 pub scoped_server_records: usize,
576 pub background_server_records: usize,
577 pub corrupt_records: usize,
578 pub corrupt_files: usize,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
582#[serde(untagged)]
583enum ExecutionSafeArgument {
584 Text(String),
585 Digest(ExecutionArgumentDigest),
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
589#[serde(rename_all = "camelCase", deny_unknown_fields)]
590struct ExecutionArgumentDigest {
591 bytes: usize,
592 sha256: String,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
596#[serde(rename_all = "camelCase", deny_unknown_fields)]
597struct ExecutionCommandSummary {
598 #[serde(default)]
599 executable: Option<ExecutionSafeArgument>,
600 arguments: Vec<ExecutionSafeArgument>,
601 argument_count: usize,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
605#[serde(
606 tag = "event",
607 rename_all = "kebab-case",
608 rename_all_fields = "camelCase",
609 deny_unknown_fields
610)]
611enum ExecutionTraceEvent {
612 Process {
613 at: String,
614 pid: u32,
615 ppid: u32,
616 cwd: String,
617 command: ExecutionCommandSummary,
618 #[serde(default)]
619 entrypoint: Option<String>,
620 },
621 ChildLaunch {
622 at: String,
623 pid: u32,
624 ppid: u32,
625 method: String,
626 command: ExecutionSafeArgument,
627 },
628 RemoteLaunch {
629 at: String,
630 pid: u32,
631 ppid: u32,
632 command: ExecutionCommandSummary,
633 guest_root: String,
634 },
635 WorkspaceCapability {
636 at: String,
637 pid: u32,
638 ppid: u32,
639 host_root: String,
640 guest_root: String,
641 cache_identities: Vec<String>,
642 },
643}
644
645impl ExecutionTraceEvent {
646 fn kind(&self) -> &'static str {
647 match self {
648 Self::Process { .. } => "process",
649 Self::ChildLaunch { .. } => "child-launch",
650 Self::RemoteLaunch { .. } => "remote-launch",
651 Self::WorkspaceCapability { .. } => "workspace-capability",
652 }
653 }
654}
655
656#[derive(Debug, Clone, PartialEq, Deserialize)]
657#[serde(rename_all = "camelCase", deny_unknown_fields)]
658pub struct CoverageReportRequest {
659 pub run_id: String,
660 pub manifest: CoverageManifest,
661 pub raw_results: Vec<RawTestResult>,
662 pub generated_at: String,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub coverage_model: Option<CoverageModelDeclaration>,
665 #[serde(skip_serializing_if = "Option::is_none")]
666 pub integrity: Option<Value>,
667 #[serde(default, deserialize_with = "deserialize_exit_code")]
668 pub test_exit_code: ExitCodeInput,
669}
670
671#[derive(Debug, Clone, PartialEq, Deserialize)]
672#[serde(rename_all = "camelCase", deny_unknown_fields)]
673pub struct ArchiveReportRequest {
674 pub archive_path: PathBuf,
675 pub run_id: String,
676 pub generated_at: String,
677 #[serde(skip_serializing_if = "Option::is_none")]
678 pub integrity: Option<Value>,
679 #[serde(default, deserialize_with = "deserialize_exit_code")]
680 pub test_exit_code: ExitCodeInput,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq, Default)]
684pub enum ExitCodeInput {
685 #[default]
686 Missing,
687 Present(Option<i32>),
688}
689
690fn deserialize_exit_code<'de, D>(deserializer: D) -> Result<ExitCodeInput, D::Error>
691where
692 D: serde::Deserializer<'de>,
693{
694 Option::<i32>::deserialize(deserializer).map(ExitCodeInput::Present)
695}
696
697#[derive(Debug, Clone, PartialEq, Eq)]
698pub enum ReportError {
699 Analysis(AnalysisError),
700 DecisionAnalysis {
701 decision_id: String,
702 error: AnalysisError,
703 },
704 InvalidEvent(String),
705 InvalidServerRecord(String),
706 InvalidArchive(String),
707 MissingManifest,
708 InvalidJson {
709 path: String,
710 reason: String,
711 },
712 ScopeMismatch {
713 expected: String,
714 actual: String,
715 },
716 NoEvidence(String),
717}
718
719impl From<AnalysisError> for ReportError {
720 fn from(value: AnalysisError) -> Self {
721 Self::Analysis(value)
722 }
723}
724
725#[derive(Clone, Default)]
726struct OrderedVectors {
727 values: Vec<McdcVector>,
728 indexes: HashMap<String, usize>,
729}
730
731impl OrderedVectors {
732 fn insert(&mut self, vector: &McdcVector) -> usize {
733 let key = vector_key(vector);
734 if let Some(index) = self.indexes.get(&key) {
735 return *index;
736 }
737 let index = self.values.len();
738 self.values.push(vector.clone());
739 self.indexes.insert(key, index);
740 index
741 }
742}
743
744#[derive(Clone)]
745struct MutableObservation {
746 vector: McdcVector,
747 tests: BTreeSet<String>,
748 phases: BTreeSet<String>,
749 explicit_phases: BTreeSet<String>,
750}
751
752#[derive(Clone)]
753struct MutableTest {
754 id: String,
755 name: String,
756 file: Option<String>,
757 title: Option<String>,
758 retries: BTreeSet<usize>,
759 attempts: BTreeMap<usize, TestAttempt>,
760 unstarted: bool,
761 runner_reported_flaky: bool,
762 provenance: TestProvenance,
763 role: String,
764 hits: BTreeSet<String>,
765 decisions: BTreeMap<String, OrderedVectors>,
766}
767
768#[derive(Clone)]
769struct MutablePhase {
770 phase: CoveragePhase,
771 test: String,
772 hits: BTreeSet<String>,
773 decisions: BTreeMap<String, OrderedVectors>,
774 browser_events: usize,
775 server_events: usize,
776 explicit_events: usize,
777 inferred_events: usize,
778 explicit_browser_events: usize,
779 inferred_browser_events: usize,
780 explicit_server_events: usize,
781 inferred_server_events: usize,
782}
783
784fn vector_key(vector: &McdcVector) -> String {
785 let mut key = String::with_capacity(vector.values.len() + 2);
786 for value in &vector.values {
787 key.push(match value {
788 None => '-',
789 Some(false) => 'F',
790 Some(true) => 'T',
791 });
792 }
793 key.push(':');
794 key.push(if vector.outcome { 'T' } else { 'F' });
795 key
796}
797
798fn sorted<T: Clone + Ord>(values: &BTreeSet<T>) -> Vec<T> {
799 values.iter().cloned().collect()
800}
801
802fn record_attempt(test: &mut MutableTest, raw: &RawTestResult) {
803 let (Some(retry), Some(raw_status)) = (raw.retry, raw.status.as_ref()) else {
804 return;
805 };
806 let previous = test.attempts.get(&retry);
807 let status = if raw_status == "unknown" {
808 previous.map_or_else(|| raw_status.clone(), |attempt| attempt.status.clone())
809 } else {
810 raw_status.clone()
811 };
812 let expected_status = raw
813 .expected_status
814 .clone()
815 .or_else(|| previous.and_then(|attempt| attempt.expected_status.clone()));
816 test.attempts.insert(
817 retry,
818 TestAttempt {
819 retry,
820 status,
821 expected_status,
822 },
823 );
824}
825
826fn effective_attempt_status(attempt: &TestAttempt) -> &str {
827 if attempt.expected_status.as_deref() == Some("failed") {
828 match attempt.status.as_str() {
829 "failed" => "passed",
830 "passed" => "failed",
831 status => status,
832 }
833 } else {
834 attempt.status.as_str()
835 }
836}
837
838fn test_outcome(test: &MutableTest) -> String {
839 let Some(terminal) = test.attempts.values().next_back() else {
840 return if test.unstarted {
841 "unstarted".into()
842 } else {
843 "unknown".into()
844 };
845 };
846 let terminal_status = effective_attempt_status(terminal);
847 if terminal_status == "passed"
848 && (test.runner_reported_flaky
849 || test
850 .attempts
851 .values()
852 .take(test.attempts.len().saturating_sub(1))
853 .any(|attempt| effective_attempt_status(attempt) != "passed"))
854 {
855 "flaky".into()
856 } else {
857 terminal_status.into()
858 }
859}
860
861fn raw_test_id(raw: &RawTestResult) -> &str {
862 raw.test_id.as_deref().unwrap_or(&raw.test)
863}
864
865pub fn passing_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
866 let mut attempts: BTreeMap<(String, usize), (BTreeSet<String>, bool)> = BTreeMap::new();
867 for raw in raw_results {
868 let Some(retry) = raw.retry else {
869 continue;
870 };
871 let entry = attempts
872 .entry((raw_test_id(raw).into(), retry))
873 .or_default();
874 if let Some(status) = &raw.status {
875 entry.0.insert(status.clone());
876 }
877 entry.1 |= raw.expected_status.as_deref() == Some("failed");
878 }
879 let mut terminal_retries = BTreeMap::<String, usize>::new();
880 for (test, retry) in attempts.keys() {
881 terminal_retries
882 .entry(test.clone())
883 .and_modify(|value| *value = (*value).max(*retry))
884 .or_insert(*retry);
885 }
886 let accepted = terminal_retries
887 .into_iter()
888 .filter_map(|(test, retry)| {
889 let (statuses, expected_failure) = attempts.get(&(test.clone(), retry))?;
890 (statuses.contains("passed") && !expected_failure).then_some((test, retry))
891 })
892 .collect::<BTreeSet<_>>();
893 raw_results
894 .iter()
895 .filter(|raw| {
896 raw.retry
897 .is_some_and(|retry| accepted.contains(&(raw_test_id(raw).into(), retry)))
898 })
899 .cloned()
900 .collect()
901}
902
903pub fn failed_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
904 let mut attempts = BTreeMap::<(String, usize), Vec<&RawTestResult>>::new();
905 for raw in raw_results {
906 if let Some(retry) = raw.retry {
907 attempts
908 .entry((raw_test_id(raw).to_owned(), retry))
909 .or_default()
910 .push(raw);
911 }
912 }
913 let failed = attempts
914 .into_iter()
915 .filter_map(|(identity, records)| {
916 let authoritative = records
921 .iter()
922 .copied()
923 .filter(|raw| raw.expected_status.is_some())
924 .collect::<Vec<_>>();
925 let authoritative = if authoritative.is_empty() {
926 records
927 } else {
928 authoritative
929 };
930 authoritative
931 .iter()
932 .any(
933 |raw| match (raw.status.as_deref(), raw.expected_status.as_deref()) {
934 (Some("failed"), Some("failed")) => false,
935 (Some("passed"), Some("failed")) => true,
936 (Some("failed"), _) => true,
937 _ => false,
938 },
939 )
940 .then_some(identity)
941 })
942 .collect::<BTreeSet<_>>();
943 raw_results
944 .iter()
945 .filter(|raw| {
946 raw.retry
947 .is_some_and(|retry| failed.contains(&(raw_test_id(raw).into(), retry)))
948 })
949 .cloned()
950 .collect()
951}
952
953pub(crate) fn javascript_coverage_model() -> CoverageModelDeclaration {
954 CoverageModelDeclaration {
955 language: "javascript".into(),
956 variant: "masking-short-circuit".into(),
957 name: "coverage-completeness-v2".into(),
958 completeness_meaning: "Every obligation in the measured model was observed by at least one existing test; test assertions and product correctness are separate assumptions.".into(),
959 measured: [
960 "executable source lines",
961 "executable statements",
962 "function entries",
963 "true and false outcomes of if, ternary, while, do/while, and classic for decisions",
964 "true and false outcomes of every atomic condition in those decisions",
965 "masking MC/DC independence for every atomic condition in those decisions",
966 "short-circuit and right-evaluated selections for &&, ||, and ?? value expressions, including JSX",
967 "short-circuit and evaluated alternatives for logical assignments and optional chains",
968 "provided and default-evaluated parameter and destructuring values",
969 "try success and catch entry",
970 "zero and entered for-in/for-of loops",
971 "entered switch cases, defaults, and implicit no-match alternatives",
972 ]
973 .into_iter()
974 .map(str::to_owned)
975 .collect(),
976 not_measured: [
977 "all input values or semantic input partitions",
978 "all execution paths or ordering/concurrency interleavings",
979 "destructuring defaults in classic for initializers (reported as blockers when discovered)",
980 "the internal statements and decisions of runtime-generated eval/Function source",
981 "mutation score or assertion fault-detection strength",
982 ]
983 .into_iter()
984 .map(str::to_owned)
985 .collect(),
986 }
987}
988
989fn summary_for_results(
990 decisions: &[DecisionResult],
991 points: &[PointResult],
992 branches: &[BranchResult],
993 lines: &[LineResult],
994 test_ids: Option<&BTreeSet<String>>,
995) -> Result<CoverageSummary, ReportError> {
996 let includes = |tests: &[String], covered: bool| {
997 test_ids.map_or(covered, |selected| {
998 tests.iter().any(|test| selected.contains(test))
999 })
1000 };
1001 let input = CoverageCoreInput {
1002 decisions: decisions
1003 .iter()
1004 .map(|decision| DecisionCoverage {
1005 condition_count: decision.meta.conditions.len(),
1006 vectors: decision
1007 .vector_observations
1008 .iter()
1009 .filter(|observation| includes(&observation.tests, true))
1010 .map(|observation| observation.vector.clone())
1011 .collect(),
1012 })
1013 .collect(),
1014 points: points
1015 .iter()
1016 .map(|point| PointCoverage {
1017 kind: point.meta.kind.clone(),
1018 covered: includes(&point.tests, point.covered),
1019 })
1020 .collect(),
1021 branches: branches
1022 .iter()
1023 .map(|branch| BranchCoverage {
1024 kind: branch.meta.kind.clone(),
1025 alternatives: branch
1026 .alternatives
1027 .iter()
1028 .map(|alternative| includes(&alternative.tests, alternative.covered))
1029 .collect(),
1030 })
1031 .collect(),
1032 lines: lines
1035 .iter()
1036 .filter(|line| line.measured)
1037 .map(|line| includes(&line.tests, line.covered))
1038 .collect(),
1039 };
1040 Ok(analyze_core(&input)?.summary)
1041}
1042
1043pub fn coverage_summary_for_tests(
1048 view: &CoverageView,
1049 test_ids: &BTreeSet<String>,
1050) -> Result<CoverageSummary, ReportError> {
1051 summary_for_results(
1052 &view.decisions,
1053 &view.points,
1054 &view.branches,
1055 &view.lines,
1056 Some(test_ids),
1057 )
1058}
1059
1060fn confidence_for(
1061 test_ids: impl IntoIterator<Item = String>,
1062 phase_ids: impl IntoIterator<Item = String>,
1063 explicit_phase_ids: impl IntoIterator<Item = String>,
1064 tests: &HashMap<String, MutableTest>,
1065 phases: &HashMap<String, MutablePhase>,
1066 asserted_phase_ids: &BTreeSet<String>,
1067) -> CoverageConfidence {
1068 let test_ids = test_ids.into_iter().collect::<BTreeSet<_>>();
1069 let phase_ids = phase_ids.into_iter().collect::<BTreeSet<_>>();
1070 let explicit_phase_ids = explicit_phase_ids.into_iter().collect::<BTreeSet<_>>();
1071 let asserted_phases = explicit_phase_ids
1072 .iter()
1073 .filter(|id| asserted_phase_ids.contains(*id))
1074 .collect::<Vec<_>>();
1075 let asserted_tests = asserted_phases
1076 .iter()
1077 .filter_map(|id| phases.get(*id).map(|phase| phase.test.clone()))
1078 .collect::<BTreeSet<_>>();
1079 let provenances = test_ids
1080 .iter()
1081 .filter_map(|id| tests.get(id).map(|test| &test.provenance))
1082 .collect::<Vec<_>>();
1083 let roles = test_ids
1084 .iter()
1085 .filter_map(|id| tests.get(id).map(|test| test.role.as_str()))
1086 .collect::<Vec<_>>();
1087 let phase_kinds = phase_ids
1088 .iter()
1089 .filter_map(|id| phases.get(id).map(|phase| phase.phase.kind.as_str()))
1090 .collect::<Vec<_>>();
1091 let only = |phase_kind: &str, role: &str| {
1092 if phase_kinds.is_empty() {
1093 !roles.is_empty() && roles.iter().all(|value| *value == role)
1094 } else {
1095 phase_kinds.iter().all(|value| *value == phase_kind)
1096 }
1097 };
1098 let has_action = explicit_phase_ids.iter().any(|id| {
1099 phases
1100 .get(id)
1101 .is_some_and(|phase| phase.phase.kind == "action")
1102 });
1103 let level = if test_ids.is_empty() {
1104 "unexecuted"
1105 } else if !asserted_tests.is_empty() {
1106 "asserted"
1107 } else if has_action {
1108 "action"
1109 } else {
1110 "executed"
1111 };
1112 let runners = provenances
1113 .iter()
1114 .map(|provenance| provenance.runner.clone())
1115 .collect::<BTreeSet<_>>();
1116 let kinds = provenances
1117 .iter()
1118 .map(|provenance| provenance.kind.clone())
1119 .collect::<BTreeSet<_>>();
1120 CoverageConfidence {
1121 level: level.into(),
1122 setup_only: only("setup", "setup"),
1123 background_only: only("background", "background"),
1124 asserted: !asserted_tests.is_empty(),
1125 tests: sorted(&test_ids),
1126 asserted_tests: sorted(&asserted_tests),
1127 runners: sorted(&runners),
1128 e2e: kinds.contains("e2e"),
1129 kinds: sorted(&kinds),
1130 }
1131}
1132
1133fn add_reference(map: &mut HashMap<String, BTreeSet<String>>, id: &str, value: &str) {
1134 map.entry(id.into()).or_default().insert(value.into());
1135}
1136
1137pub fn create_coverage_view(
1138 manifest: &CoverageManifest,
1139 raw_results: &[RawTestResult],
1140 generated_at: &str,
1141) -> Result<CoverageView, ReportError> {
1142 create_coverage_view_with_model(
1143 manifest,
1144 raw_results,
1145 generated_at,
1146 &javascript_coverage_model(),
1147 )
1148}
1149
1150fn create_coverage_view_with_model(
1151 manifest: &CoverageManifest,
1152 raw_results: &[RawTestResult],
1153 generated_at: &str,
1154 coverage_model: &CoverageModelDeclaration,
1155) -> Result<CoverageView, ReportError> {
1156 let mut decision_metadata = manifest.decisions.clone();
1157 let decision_indexes = decision_metadata
1158 .iter()
1159 .enumerate()
1160 .map(|(index, meta)| (meta.id.clone(), index))
1161 .collect::<HashMap<_, _>>();
1162 let manifest_files = manifest
1163 .decisions
1164 .iter()
1165 .map(|meta| meta.file.clone())
1166 .chain(manifest.points.iter().map(|meta| meta.file.clone()))
1167 .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
1168 .chain(
1169 manifest
1170 .scope
1171 .as_ref()
1172 .and_then(|scope| scope.get("entries"))
1173 .and_then(Value::as_array)
1174 .into_iter()
1175 .flatten()
1176 .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
1177 .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
1178 )
1179 .collect::<BTreeSet<_>>();
1180 let mut vectors_by_decision = HashMap::<String, Vec<MutableObservation>>::new();
1181 let mut vector_indexes = HashMap::<String, HashMap<String, usize>>::new();
1182 let mut tests_by_decision = HashMap::<String, BTreeSet<String>>::new();
1183 let mut tests_by_hit = HashMap::<String, BTreeSet<String>>::new();
1184 let mut tests_by_id = HashMap::<String, MutableTest>::new();
1185 let mut test_order = Vec::<String>::new();
1186 let mut phases_by_id = HashMap::<String, MutablePhase>::new();
1187 let mut phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1188 let mut explicit_phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1189
1190 for raw in raw_results {
1191 let id = raw_test_id(raw).to_owned();
1192 if !tests_by_id.contains_key(&id) {
1193 test_order.push(id.clone());
1194 tests_by_id.insert(
1195 id.clone(),
1196 MutableTest {
1197 id: id.clone(),
1198 name: raw.test.clone(),
1199 file: raw.test_file.clone(),
1200 title: raw.title.clone(),
1201 retries: raw.retry.into_iter().collect(),
1202 attempts: BTreeMap::new(),
1203 unstarted: raw.status.as_deref() == Some("unstarted"),
1204 runner_reported_flaky: raw.flaky,
1205 provenance: raw.provenance.clone(),
1206 role: raw.role.clone(),
1207 hits: BTreeSet::new(),
1208 decisions: BTreeMap::new(),
1209 },
1210 );
1211 }
1212 let test = tests_by_id.get_mut(&id).expect("test was inserted");
1213 test.unstarted |= raw.status.as_deref() == Some("unstarted");
1214 if let Some(retry) = raw.retry {
1215 test.retries.insert(retry);
1216 }
1217 test.runner_reported_flaky |= raw.flaky;
1218 record_attempt(test, raw);
1219
1220 let mut ordered_phases = raw.phases.clone();
1221 ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1222 for phase in &ordered_phases {
1223 phases_by_id.insert(
1224 phase.id.clone(),
1225 MutablePhase {
1226 phase: phase.clone(),
1227 test: id.clone(),
1228 hits: BTreeSet::new(),
1229 decisions: BTreeMap::new(),
1230 browser_events: 0,
1231 server_events: 0,
1232 explicit_events: 0,
1233 inferred_events: 0,
1234 explicit_browser_events: 0,
1235 inferred_browser_events: 0,
1236 explicit_server_events: 0,
1237 inferred_server_events: 0,
1238 },
1239 );
1240 }
1241
1242 let correlate = |event: &RuntimeEvent| {
1243 event.phase_id.clone().or_else(|| {
1244 ordered_phases
1245 .iter()
1246 .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1247 .last()
1248 .map(|phase| phase.id.clone())
1249 })
1250 };
1251
1252 let snapshots = raw.runtime.iter().chain(&raw.browser);
1253 for snapshot in snapshots {
1254 for decision in &snapshot.decisions {
1255 let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1256 if manifest_files.contains(&decision.meta.file) {
1257 return Err(ReportError::InvalidServerRecord(format!(
1258 "decision {} is absent from the frozen manifest",
1259 decision.meta.id
1260 )));
1261 }
1262 continue;
1263 };
1264 if decision_metadata[index] != decision.meta {
1265 return Err(ReportError::InvalidServerRecord(format!(
1266 "decision {} metadata differs from the frozen manifest",
1267 decision.meta.id
1268 )));
1269 }
1270 for vector in &decision.vectors {
1271 let key = vector_key(vector);
1272 let indexes = vector_indexes.entry(decision.meta.id.clone()).or_default();
1273 let observations = vectors_by_decision
1274 .entry(decision.meta.id.clone())
1275 .or_default();
1276 let observation_index = *indexes.entry(key.clone()).or_insert_with(|| {
1277 observations.push(MutableObservation {
1278 vector: vector.clone(),
1279 tests: BTreeSet::new(),
1280 phases: BTreeSet::new(),
1281 explicit_phases: BTreeSet::new(),
1282 });
1283 observations.len() - 1
1284 });
1285 observations[observation_index].tests.insert(id.clone());
1286 tests_by_id
1287 .get_mut(&id)
1288 .expect("registered test")
1289 .decisions
1290 .entry(decision.meta.id.clone())
1291 .or_default()
1292 .insert(vector);
1293 }
1294 if !decision.vectors.is_empty() {
1295 add_reference(&mut tests_by_decision, &decision.meta.id, &id);
1296 }
1297 }
1298 for hit in &snapshot.hits {
1299 add_reference(&mut tests_by_hit, hit, &id);
1300 tests_by_id
1301 .get_mut(&id)
1302 .expect("registered test")
1303 .hits
1304 .insert(hit.clone());
1305 }
1306 for event in &snapshot.events {
1307 let explicit = event.phase_id.is_some();
1308 let Some(phase_id) = correlate(event) else {
1309 continue;
1310 };
1311 let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1312 continue;
1313 };
1314 if event.environment == "browser" {
1315 phase.browser_events += 1;
1316 if explicit {
1317 phase.explicit_browser_events += 1;
1318 } else {
1319 phase.inferred_browser_events += 1;
1320 }
1321 } else {
1322 phase.server_events += 1;
1323 if explicit {
1324 phase.explicit_server_events += 1;
1325 } else {
1326 phase.inferred_server_events += 1;
1327 }
1328 }
1329 if explicit {
1330 phase.explicit_events += 1;
1331 } else {
1332 phase.inferred_events += 1;
1333 }
1334 if event.event_type == "hit" {
1335 phase.hits.insert(event.id.clone());
1336 add_reference(&mut phases_by_hit, &event.id, &phase_id);
1337 if explicit {
1338 add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1339 }
1340 } else if event.event_type == "decision" {
1341 let vector = event
1342 .vector
1343 .as_ref()
1344 .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1345 phase
1346 .decisions
1347 .entry(event.id.clone())
1348 .or_default()
1349 .insert(vector);
1350 if let Some(index) = vector_indexes
1351 .get(&event.id)
1352 .and_then(|indexes| indexes.get(&vector_key(vector)))
1353 .copied()
1354 && let Some(observation) = vectors_by_decision
1355 .get_mut(&event.id)
1356 .and_then(|observations| observations.get_mut(index))
1357 {
1358 observation.phases.insert(phase_id.clone());
1359 if explicit {
1360 observation.explicit_phases.insert(phase_id.clone());
1361 }
1362 }
1363 } else {
1364 return Err(ReportError::InvalidEvent(event.event_type.clone()));
1365 }
1366 }
1367 }
1368
1369 for record in &raw.server {
1370 let (record_id, decision) = if record.record_type == "decision" {
1371 let meta = record
1372 .meta
1373 .as_ref()
1374 .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1375 let vector = record
1376 .vector
1377 .as_ref()
1378 .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1379 let Some(index) = decision_indexes.get(&meta.id).copied() else {
1380 if manifest_files.contains(&meta.file) {
1381 return Err(ReportError::InvalidServerRecord(format!(
1382 "decision {} is absent from the frozen manifest",
1383 meta.id
1384 )));
1385 }
1386 continue;
1387 };
1388 if decision_metadata[index] != *meta {
1389 return Err(ReportError::InvalidServerRecord(format!(
1390 "decision {} metadata differs from the frozen manifest",
1391 meta.id
1392 )));
1393 }
1394 let key = vector_key(vector);
1395 let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1396 let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1397 let index = *indexes.entry(key).or_insert_with(|| {
1398 observations.push(MutableObservation {
1399 vector: vector.clone(),
1400 tests: BTreeSet::new(),
1401 phases: BTreeSet::new(),
1402 explicit_phases: BTreeSet::new(),
1403 });
1404 observations.len() - 1
1405 });
1406 observations[index].tests.insert(id.clone());
1407 tests_by_id
1408 .get_mut(&id)
1409 .expect("registered test")
1410 .decisions
1411 .entry(meta.id.clone())
1412 .or_default()
1413 .insert(vector);
1414 add_reference(&mut tests_by_decision, &meta.id, &id);
1415 (meta.id.clone(), Some(vector.clone()))
1416 } else if record.record_type == "hit" {
1417 let hit = record
1418 .id
1419 .as_ref()
1420 .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1421 add_reference(&mut tests_by_hit, hit, &id);
1422 tests_by_id
1423 .get_mut(&id)
1424 .expect("registered test")
1425 .hits
1426 .insert(hit.clone());
1427 (hit.clone(), None)
1428 } else {
1429 return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1430 };
1431 let Some(timestamp_ms) = record.timestamp_ms else {
1432 continue;
1433 };
1434 let event = RuntimeEvent {
1435 event_type: record.record_type.clone(),
1436 id: record_id,
1437 vector: decision,
1438 timestamp_ms,
1439 phase_id: record.phase_id.clone(),
1440 environment: "server".into(),
1441 };
1442 let explicit = event.phase_id.is_some();
1443 let phase_id = correlate(&event);
1444 let Some(phase_id) = phase_id else { continue };
1445 let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1446 continue;
1447 };
1448 phase.server_events += 1;
1449 if explicit {
1450 phase.explicit_events += 1;
1451 phase.explicit_server_events += 1;
1452 } else {
1453 phase.inferred_events += 1;
1454 phase.inferred_server_events += 1;
1455 }
1456 if event.event_type == "hit" {
1457 phase.hits.insert(event.id.clone());
1458 add_reference(&mut phases_by_hit, &event.id, &phase_id);
1459 if explicit {
1460 add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1461 }
1462 } else if let Some(vector) = &event.vector {
1463 phase
1464 .decisions
1465 .entry(event.id.clone())
1466 .or_default()
1467 .insert(vector);
1468 if let Some(index) = vector_indexes
1469 .get(&event.id)
1470 .and_then(|indexes| indexes.get(&vector_key(vector)))
1471 .copied()
1472 && let Some(observation) = vectors_by_decision
1473 .get_mut(&event.id)
1474 .and_then(|observations| observations.get_mut(index))
1475 {
1476 observation.phases.insert(phase_id.clone());
1477 if explicit {
1478 observation.explicit_phases.insert(phase_id.clone());
1479 }
1480 }
1481 }
1482 }
1483 }
1484
1485 let mut asserted_phase_ids = BTreeSet::new();
1486 for phase in phases_by_id.values() {
1487 if phase.phase.kind == "assertion" && phase.phase.status.as_deref() == Some("passed") {
1488 asserted_phase_ids.insert(phase.phase.id.clone());
1489 if let Some(cause) = &phase.phase.caused_by_phase_id {
1490 asserted_phase_ids.insert(cause.clone());
1491 }
1492 }
1493 }
1494
1495 decision_metadata.sort_by(|left, right| {
1496 left.file
1497 .cmp(&right.file)
1498 .then(left.line.cmp(&right.line))
1499 .then(left.column.cmp(&right.column))
1500 });
1501 let mut decisions = Vec::with_capacity(decision_metadata.len());
1502 for meta in decision_metadata {
1503 let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
1504 let mut observations = Vec::with_capacity(mutable.len());
1505 for observation in mutable {
1506 let confidence = confidence_for(
1507 sorted(&observation.tests),
1508 sorted(&observation.phases),
1509 sorted(&observation.explicit_phases),
1510 &tests_by_id,
1511 &phases_by_id,
1512 &asserted_phase_ids,
1513 );
1514 observations.push(VectorObservation {
1515 vector: observation.vector,
1516 tests: sorted(&observation.tests),
1517 phases: sorted(&observation.phases),
1518 explicit_phases: sorted(&observation.explicit_phases),
1519 confidence,
1520 });
1521 }
1522 let vectors = observations
1523 .iter()
1524 .map(|observation| observation.vector.clone())
1525 .collect::<Vec<_>>();
1526 let witnesses =
1527 find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
1528 ReportError::DecisionAnalysis {
1529 decision_id: meta.id.clone(),
1530 error,
1531 }
1532 })?;
1533 let mut conditions = Vec::with_capacity(meta.conditions.len());
1534 for (index, source) in meta.conditions.iter().enumerate() {
1535 let witness = witnesses[index].map(|witness| {
1536 [
1537 vectors[witness.first].clone(),
1538 vectors[witness.second].clone(),
1539 ]
1540 });
1541 let witness_tests = witnesses[index].map(|witness| {
1542 [
1543 observations[witness.first].tests.clone(),
1544 observations[witness.second].tests.clone(),
1545 ]
1546 });
1547 let assertion_covered = (0..observations.len()).any(|left| {
1548 ((left + 1)..observations.len()).any(|right| {
1549 observations[left].confidence.asserted
1550 && observations[right].confidence.asserted
1551 && crate::coverage_analysis::is_independence_pair(
1552 &observations[left].vector,
1553 &observations[right].vector,
1554 index,
1555 )
1556 })
1557 });
1558 conditions.push(ConditionResult {
1559 index,
1560 source: source.clone(),
1561 covered: witness.is_some(),
1562 assertion_covered,
1563 witness,
1564 witness_tests,
1565 });
1566 }
1567 let decision_tests = tests_by_decision.remove(&meta.id).unwrap_or_default();
1568 let confidence = confidence_for(
1569 sorted(&decision_tests),
1570 observations
1571 .iter()
1572 .flat_map(|observation| observation.phases.clone()),
1573 observations
1574 .iter()
1575 .flat_map(|observation| observation.explicit_phases.clone()),
1576 &tests_by_id,
1577 &phases_by_id,
1578 &asserted_phase_ids,
1579 );
1580 decisions.push(DecisionResult {
1581 executed: !vectors.is_empty(),
1582 covered: conditions.iter().all(|condition| condition.covered),
1583 meta,
1584 vectors,
1585 vector_observations: observations,
1586 conditions,
1587 tests: sorted(&decision_tests),
1588 confidence,
1589 });
1590 }
1591
1592 let points = manifest
1593 .points
1594 .iter()
1595 .cloned()
1596 .map(|meta| {
1597 let tests = tests_by_hit.get(&meta.id).cloned().unwrap_or_default();
1598 let phases = phases_by_hit.get(&meta.id).cloned().unwrap_or_default();
1599 let explicit = explicit_phases_by_hit
1600 .get(&meta.id)
1601 .cloned()
1602 .unwrap_or_default();
1603 PointResult {
1604 covered: tests_by_hit.contains_key(&meta.id),
1605 confidence: confidence_for(
1606 sorted(&tests),
1607 sorted(&phases),
1608 sorted(&explicit),
1609 &tests_by_id,
1610 &phases_by_id,
1611 &asserted_phase_ids,
1612 ),
1613 meta,
1614 tests: sorted(&tests),
1615 phases: sorted(&phases),
1616 }
1617 })
1618 .collect::<Vec<_>>();
1619
1620 let branches = manifest
1621 .branches
1622 .iter()
1623 .cloned()
1624 .map(|meta| {
1625 let alternatives = meta
1626 .alternatives
1627 .iter()
1628 .map(|alternative| {
1629 let tests = tests_by_hit
1630 .get(&alternative.id)
1631 .cloned()
1632 .unwrap_or_default();
1633 let phases = phases_by_hit
1634 .get(&alternative.id)
1635 .cloned()
1636 .unwrap_or_default();
1637 let explicit = explicit_phases_by_hit
1638 .get(&alternative.id)
1639 .cloned()
1640 .unwrap_or_default();
1641 AlternativeResult {
1642 id: alternative.id.clone(),
1643 label: alternative.label.clone(),
1644 covered: tests_by_hit.contains_key(&alternative.id),
1645 tests: sorted(&tests),
1646 phases: sorted(&phases),
1647 confidence: confidence_for(
1648 sorted(&tests),
1649 sorted(&phases),
1650 sorted(&explicit),
1651 &tests_by_id,
1652 &phases_by_id,
1653 &asserted_phase_ids,
1654 ),
1655 }
1656 })
1657 .collect::<Vec<_>>();
1658 BranchResult {
1659 covered: alternatives.iter().all(|alternative| alternative.covered),
1660 meta,
1661 alternatives,
1662 }
1663 })
1664 .collect::<Vec<_>>();
1665
1666 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
1672
1673 let mut line_aggregates = BTreeMap::<SourceLine, LineAggregate>::new();
1678 for point in &points {
1679 let aggregate = line_aggregates
1680 .entry(SourceLine {
1681 file: point.meta.file.clone(),
1682 line: point.meta.line,
1683 })
1684 .or_default();
1685 if declined.contains(&point.meta.id) {
1686 continue;
1687 }
1688 aggregate.measured = true;
1689 aggregate.covered |= point.covered;
1690 aggregate.tests.extend(point.tests.clone());
1691 aggregate.phases.extend(point.phases.clone());
1692 aggregate.explicit_phases.extend(
1693 explicit_phases_by_hit
1694 .get(&point.meta.id)
1695 .into_iter()
1696 .flatten()
1697 .cloned(),
1698 );
1699 }
1700 let lines = line_aggregates
1701 .into_iter()
1702 .map(|(location, aggregate)| {
1703 let LineAggregate {
1704 covered,
1705 measured,
1706 tests: test_ids,
1707 phases: phase_ids,
1708 explicit_phases: explicit_ids,
1709 } = aggregate;
1710 let provenances = test_ids
1711 .iter()
1712 .filter_map(|id| tests_by_id.get(id).map(|test| &test.provenance))
1713 .collect::<Vec<_>>();
1714 let runners = provenances
1715 .iter()
1716 .map(|provenance| provenance.runner.clone())
1717 .collect::<BTreeSet<_>>();
1718 let kinds = provenances
1719 .iter()
1720 .map(|provenance| provenance.kind.clone())
1721 .collect::<BTreeSet<_>>();
1722 LineResult {
1723 file: location.file,
1724 line: location.line,
1725 covered,
1726 measured,
1727 tests: sorted(&test_ids),
1728 runners: sorted(&runners),
1729 exclusive_kind: (kinds.len() == 1).then(|| kinds.first().unwrap().clone()),
1730 phases: sorted(&phase_ids),
1731 confidence: confidence_for(
1732 sorted(&test_ids),
1733 sorted(&phase_ids),
1734 sorted(&explicit_ids),
1735 &tests_by_id,
1736 &phases_by_id,
1737 &asserted_phase_ids,
1738 ),
1739 kinds: sorted(&kinds),
1740 }
1741 })
1742 .collect::<Vec<_>>();
1743
1744 let point_locations = manifest
1745 .points
1746 .iter()
1747 .map(|point| {
1748 (
1749 point.id.clone(),
1750 SourceLine {
1751 file: point.file.clone(),
1752 line: point.line,
1753 },
1754 )
1755 })
1756 .collect::<HashMap<_, _>>();
1757 test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
1758 let tests = test_order
1759 .into_iter()
1760 .map(|id| {
1761 let test = tests_by_id.get(&id).expect("test order references test");
1762 let lines = test
1763 .hits
1764 .iter()
1765 .filter_map(|hit| point_locations.get(hit).cloned())
1766 .collect::<BTreeSet<_>>();
1767 TestCoverageResult {
1768 id: test.id.clone(),
1769 name: test.name.clone(),
1770 file: test.file.clone(),
1771 title: test.title.clone(),
1772 retries: sorted(&test.retries),
1773 attempts: test.attempts.values().cloned().collect(),
1774 outcome: test_outcome(test),
1775 provenance: test.provenance.clone(),
1776 role: test.role.clone(),
1777 hits: sorted(&test.hits),
1778 decisions: test
1779 .decisions
1780 .iter()
1781 .map(|(id, vectors)| TestDecisionResult {
1782 id: id.clone(),
1783 vectors: vectors.values.clone(),
1784 })
1785 .collect(),
1786 lines: lines.into_iter().collect(),
1787 }
1788 })
1789 .collect::<Vec<_>>();
1790
1791 let mut test_files = BTreeMap::<
1792 String,
1793 (
1794 BTreeSet<String>,
1795 BTreeSet<String>,
1796 BTreeSet<String>,
1797 BTreeSet<SourceLine>,
1798 ),
1799 >::new();
1800 for test in &tests {
1801 let aggregate = test_files
1802 .entry(
1803 test.file
1804 .clone()
1805 .unwrap_or_else(|| "(unknown test file)".into()),
1806 )
1807 .or_default();
1808 aggregate.0.insert(test.id.clone());
1809 aggregate.1.insert(test.provenance.runner.clone());
1810 aggregate.2.insert(test.provenance.kind.clone());
1811 aggregate.3.extend(test.lines.clone());
1812 }
1813 let test_files = test_files
1814 .into_iter()
1815 .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
1816 file,
1817 tests: sorted(&tests),
1818 runners: sorted(&runners),
1819 kinds: sorted(&kinds),
1820 lines: lines.into_iter().collect(),
1821 })
1822 .collect::<Vec<_>>();
1823
1824 let mut phases = phases_by_id.values().cloned().collect::<Vec<_>>();
1825 phases.sort_by(|left, right| {
1826 left.phase
1827 .started_at_ms
1828 .cmp(&right.phase.started_at_ms)
1829 .then(left.phase.id.cmp(&right.phase.id))
1830 });
1831 let phases = phases
1832 .into_iter()
1833 .map(|phase| {
1834 let lines = phase
1835 .hits
1836 .iter()
1837 .filter_map(|hit| point_locations.get(hit).cloned())
1838 .collect::<BTreeSet<_>>();
1839 PhaseResult {
1840 phase: phase.phase,
1841 test: phase.test,
1842 hits: sorted(&phase.hits),
1843 decisions: phase
1844 .decisions
1845 .into_iter()
1846 .map(|(id, vectors)| TestDecisionResult {
1847 id,
1848 vectors: vectors.values,
1849 })
1850 .collect(),
1851 lines: lines.into_iter().collect(),
1852 browser_events: phase.browser_events,
1853 server_events: phase.server_events,
1854 explicit_events: phase.explicit_events,
1855 inferred_events: phase.inferred_events,
1856 explicit_browser_events: phase.explicit_browser_events,
1857 inferred_browser_events: phase.inferred_browser_events,
1858 explicit_server_events: phase.explicit_server_events,
1859 inferred_server_events: phase.inferred_server_events,
1860 }
1861 })
1862 .collect::<Vec<_>>();
1863
1864 let total_obligations = decisions.len() + points.len() + branches.len();
1865 let (decisions, points, branches) = if declined.is_empty() {
1866 (decisions, points, branches)
1867 } else {
1868 (
1869 decisions
1870 .into_iter()
1871 .filter(|result| !declined.contains(&result.meta.id))
1872 .collect::<Vec<_>>(),
1873 points
1874 .into_iter()
1875 .filter(|result| !declined.contains(&result.meta.id))
1876 .collect::<Vec<_>>(),
1877 branches
1878 .into_iter()
1879 .filter(|result| !declined.contains(&result.meta.id))
1880 .collect::<Vec<_>>(),
1881 )
1882 };
1883 let measured_obligations = decisions.len() + points.len() + branches.len();
1884 let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
1885 if total_obligations > measured_obligations {
1886 summary.unmeasured_obligations = Some(total_obligations - measured_obligations);
1887 summary.exact_fraction_pct = Some(if total_obligations == 0 {
1888 100.0
1889 } else {
1890 (measured_obligations as f64) * 100.0 / (total_obligations as f64)
1891 });
1892 }
1893 if !manifest.limitations.is_empty() {
1894 summary.coverage_complete = false;
1895 summary.completeness_blocked = Some(true);
1896 }
1897
1898 let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
1899 let values = tests
1900 .iter()
1901 .map(|test| {
1902 if field == "kind" {
1903 test.provenance.kind.clone()
1904 } else {
1905 test.provenance.runner.clone()
1906 }
1907 })
1908 .collect::<BTreeSet<_>>();
1909 values
1910 .into_iter()
1911 .map(|value| {
1912 let selected = tests
1913 .iter()
1914 .filter(|test| {
1915 if field == "kind" {
1916 test.provenance.kind == value
1917 } else {
1918 test.provenance.runner == value
1919 }
1920 })
1921 .map(|test| test.id.clone())
1922 .collect::<BTreeSet<_>>();
1923 Ok(DimensionCoverage {
1924 kind: (field == "kind").then(|| value.clone()),
1925 runner: (field == "runner").then(|| value.clone()),
1926 tests: tests
1927 .iter()
1928 .filter(|test| selected.contains(&test.id) && test.role == "test")
1929 .count(),
1930 setups: tests
1931 .iter()
1932 .filter(|test| selected.contains(&test.id) && test.role == "setup")
1933 .count(),
1934 summary: summary_for_results(
1935 &decisions,
1936 &points,
1937 &branches,
1938 &lines,
1939 Some(&selected),
1940 )?,
1941 })
1942 })
1943 .collect()
1944 };
1945
1946 Ok(CoverageView {
1947 generated_at: generated_at.into(),
1948 variant: coverage_model.variant.clone(),
1949 scope: manifest.scope.clone(),
1950 model: CoverageModel {
1951 language: coverage_model.language.clone(),
1952 name: coverage_model.name.clone(),
1953 completeness_meaning: coverage_model.completeness_meaning.clone(),
1954 measured: coverage_model.measured.clone(),
1955 not_measured: coverage_model.not_measured.clone(),
1956 },
1957 integrity: None,
1958 limitations: manifest.limitations.clone(),
1959 transport: None,
1960 summary,
1961 coverage_by_kind: dimension_coverage("kind")?,
1962 coverage_by_runner: dimension_coverage("runner")?,
1963 decisions,
1964 points,
1965 branches,
1966 tests,
1967 test_files,
1968 phases,
1969 lines,
1970 })
1971}
1972
1973pub fn analyze_coverage_results(
1974 request: &CoverageReportRequest,
1975) -> Result<CoverageReport, ReportError> {
1976 if let Some(scope) = request
1977 .raw_results
1978 .iter()
1979 .filter_map(|raw| raw.scope.as_ref())
1980 .find(|scope| scope.run_id != request.run_id)
1981 {
1982 return Err(ReportError::ScopeMismatch {
1983 expected: request.run_id.clone(),
1984 actual: scope.run_id.clone(),
1985 });
1986 }
1987 if request.raw_results.is_empty() {
1988 return Err(ReportError::NoEvidence(request.run_id.clone()));
1989 }
1990 let default_model = javascript_coverage_model();
1991 let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
1992 PersistedCoverageModel::from_declaration(coverage_model).map_err(|reason| {
1993 ReportError::InvalidJson {
1994 path: "coverage-model.json".into(),
1995 reason: reason.into(),
1996 }
1997 })?;
1998 let view = create_coverage_view_with_model(
1999 &request.manifest,
2000 &request.raw_results,
2001 &request.generated_at,
2002 coverage_model,
2003 )?;
2004 let passed = create_coverage_view_with_model(
2005 &request.manifest,
2006 &passing_coverage_results(&request.raw_results),
2007 &request.generated_at,
2008 coverage_model,
2009 )?;
2010 let failed = create_coverage_view_with_model(
2011 &request.manifest,
2012 &failed_coverage_results(&request.raw_results),
2013 &request.generated_at,
2014 coverage_model,
2015 )?;
2016 let execution = match request.test_exit_code {
2017 ExitCodeInput::Missing => None,
2018 ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
2019 valid: test_exit_code == Some(0),
2020 test_exit_code,
2021 }),
2022 };
2023 let mut view = view;
2024 let mut passed = passed;
2025 let mut failed = failed;
2026 if let Some(integrity) = &request.integrity {
2027 view.integrity = Some(integrity.clone());
2028 passed.integrity = Some(integrity.clone());
2029 failed.integrity = Some(integrity.clone());
2030 }
2031 Ok(CoverageReport {
2032 view,
2033 execution,
2034 filters: CoverageFilters { passed, failed },
2035 })
2036}
2037
2038fn parse_entry<T: for<'de> Deserialize<'de>>(
2039 entry: &EvidenceArchiveEntry,
2040) -> Result<T, ReportError> {
2041 serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
2042 path: entry.path.clone(),
2043 reason: error.to_string(),
2044 })
2045}
2046
2047fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
2048 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2049) -> Result<Vec<T>, ReportError> {
2050 let mut records = Vec::new();
2051 for entry in entries {
2052 let Some(contents) = entry.contents.strip_suffix(b"\n") else {
2053 return Err(ReportError::InvalidJson {
2054 path: entry.path.clone(),
2055 reason: "recognized JSONL evidence must end with a newline".into(),
2056 });
2057 };
2058 if contents.is_empty() {
2059 return Err(ReportError::InvalidJson {
2060 path: entry.path.clone(),
2061 reason: "recognized JSONL evidence must contain at least one record".into(),
2062 });
2063 }
2064 for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
2065 if line.is_empty() {
2066 return Err(ReportError::InvalidJson {
2067 path: entry.path.clone(),
2068 reason: format!("blank JSONL record at line {}", index + 1),
2069 });
2070 }
2071 records.push(serde_json::from_slice(line).map_err(|error| {
2072 ReportError::InvalidJson {
2073 path: entry.path.clone(),
2074 reason: format!("invalid JSONL record at line {}: {error}", index + 1),
2075 }
2076 })?);
2077 }
2078 }
2079 Ok(records)
2080}
2081
2082struct TolerantJsonLines<T> {
2088 records: Vec<T>,
2089 corrupt_records: usize,
2090 corrupt_files: usize,
2091}
2092
2093fn parse_server_json_lines<'a, T: for<'de> Deserialize<'de>>(
2094 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2095) -> TolerantJsonLines<T> {
2096 let mut parsed = TolerantJsonLines {
2097 records: Vec::new(),
2098 corrupt_records: 0,
2099 corrupt_files: 0,
2100 };
2101 for entry in entries {
2102 let mut corrupt_here = 0;
2103 for line in entry.contents.split(|byte| *byte == b'\n') {
2104 if line.is_empty() {
2105 continue;
2106 }
2107 match serde_json::from_slice(line) {
2108 Ok(record) => parsed.records.push(record),
2109 Err(_) => corrupt_here += 1,
2110 }
2111 }
2112 if corrupt_here > 0 {
2113 parsed.corrupt_records += corrupt_here;
2114 parsed.corrupt_files += 1;
2115 }
2116 }
2117 parsed
2118}
2119
2120fn is_mcdc_result(path: &str) -> bool {
2121 path == "mcdc.json" || path.ends_with("/mcdc.json")
2122}
2123
2124fn is_mcdc_journal(path: &str) -> bool {
2125 path == "mcdc.jsonl" || path.ends_with(".mcdc.jsonl")
2126}
2127
2128fn validate_rust_compiler_scope(manifest: &CoverageManifest) -> Result<(), ReportError> {
2129 let scope = manifest
2130 .scope
2131 .as_ref()
2132 .and_then(Value::as_object)
2133 .ok_or_else(|| ReportError::InvalidArchive("missing Rust compiler source scope".into()))?;
2134 let expected = BTreeSet::from([
2135 "crate",
2136 "language",
2137 "measurementComplete",
2138 "model",
2139 "sourceFingerprint",
2140 ]);
2141 if scope.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected
2142 || scope.get("language").and_then(Value::as_str) != Some("rust")
2143 || scope.get("model").and_then(Value::as_str) != Some("rust-source-v1")
2144 || scope
2145 .get("crate")
2146 .and_then(Value::as_str)
2147 .is_none_or(str::is_empty)
2148 || !scope
2149 .get("measurementComplete")
2150 .is_some_and(Value::is_boolean)
2151 {
2152 return Err(ReportError::InvalidArchive(
2153 "malformed Rust compiler source scope".into(),
2154 ));
2155 }
2156 let fingerprint = scope
2157 .get("sourceFingerprint")
2158 .and_then(Value::as_object)
2159 .ok_or_else(|| {
2160 ReportError::InvalidArchive("missing Rust compiler source fingerprint".into())
2161 })?;
2162 let expected_fingerprint = BTreeSet::from(["algorithm", "digest", "files", "generatedFiles"]);
2163 let digest = fingerprint.get("digest").and_then(Value::as_str);
2164 let files = fingerprint.get("files").and_then(Value::as_u64);
2165 let generated = fingerprint.get("generatedFiles").and_then(Value::as_u64);
2166 if fingerprint
2167 .keys()
2168 .map(String::as_str)
2169 .collect::<BTreeSet<_>>()
2170 != expected_fingerprint
2171 || fingerprint.get("algorithm").and_then(Value::as_str) != Some("sha256")
2172 || !digest.is_some_and(|digest| {
2173 digest.len() == 64
2174 && digest
2175 .bytes()
2176 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2177 })
2178 || files.is_none_or(|files| files == 0)
2179 || generated
2180 .zip(files)
2181 .is_none_or(|(generated, files)| generated > files)
2182 {
2183 return Err(ReportError::InvalidArchive(
2184 "malformed Rust compiler source fingerprint".into(),
2185 ));
2186 }
2187 Ok(())
2188}
2189
2190pub fn analyze_coverage_archive(
2191 request: &ArchiveReportRequest,
2192) -> Result<CoverageReport, ReportError> {
2193 let entries = read_archive(Path::new(&request.archive_path))
2194 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2195 let manifest = entries
2196 .iter()
2197 .find(|entry| entry.path == "manifest.json")
2198 .ok_or(ReportError::MissingManifest)
2199 .and_then(parse_entry::<CoverageManifest>)?;
2200 let mut raw_results = entries
2201 .iter()
2202 .filter(|entry| is_mcdc_result(&entry.path))
2203 .map(parse_entry::<RawTestResult>)
2204 .collect::<Result<Vec<_>, _>>()?;
2205 let journal_results = parse_json_lines::<RawTestResult>(
2206 entries.iter().filter(|entry| is_mcdc_journal(&entry.path)),
2207 )?;
2208 raw_results.extend(journal_results);
2209
2210 let scoped = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2211 entry.path.starts_with("server/")
2212 && !entry.path.starts_with("server/background/")
2213 && entry.path.ends_with(".jsonl")
2214 }));
2215 let scoped_records = scoped.records;
2216 for record in &scoped_records {
2217 let Some(scope) = &record.scope else { continue };
2218 let Some(raw) = raw_results
2219 .iter_mut()
2220 .find(|raw| raw.scope.as_ref() == Some(scope))
2221 else {
2222 continue;
2223 };
2224 if !raw.server.contains(record) {
2225 raw.server.push(record.clone());
2226 }
2227 }
2228
2229 let background = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2230 entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2231 }));
2232 let background_records = background.records;
2233 if !background_records.is_empty() {
2234 raw_results.push(RawTestResult {
2235 test_id: Some(format!("background:{}", request.run_id)),
2236 scope: None,
2237 test: "Background / unattributed".into(),
2238 test_file: None,
2239 title: Some("Background / unattributed".into()),
2240 retry: None,
2241 status: Some("unknown".into()),
2242 expected_status: None,
2243 flaky: false,
2244 provenance: TestProvenance {
2245 runner: "background".into(),
2246 kind: "background".into(),
2247 project: None,
2248 source: "explicit".into(),
2249 },
2250 role: "background".into(),
2251 phases: vec![],
2252 runtime: vec![],
2253 browser: vec![],
2254 server: background_records.clone(),
2255 });
2256 }
2257
2258 let execution =
2261 parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2262 entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2263 }));
2264 let execution_events = execution.records;
2265 let count_event = |name: &str| {
2266 execution_events
2267 .iter()
2268 .filter(|event| event.kind() == name)
2269 .count()
2270 };
2271 let transport = TransportStats {
2272 processes: count_event("process"),
2273 child_launches: count_event("child-launch"),
2274 remote_launches: count_event("remote-launch"),
2275 workspace_capabilities: count_event("workspace-capability"),
2276 scoped_server_records: scoped_records.len(),
2277 background_server_records: background_records.len(),
2278 corrupt_records: scoped.corrupt_records
2279 + background.corrupt_records
2280 + execution.corrupt_records,
2281 corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
2282 };
2283 let frontend = entries
2284 .iter()
2285 .find(|entry| entry.path == "frontend.json")
2286 .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
2287 .and_then(parse_entry::<FrontendRunDeclaration>)?;
2288 let persisted = entries
2289 .iter()
2290 .find(|entry| entry.path == "coverage-model.json")
2291 .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
2292 .and_then(parse_entry::<PersistedCoverageModel>)?;
2293 let coverage_model =
2294 persisted
2295 .into_declaration()
2296 .map_err(|reason| ReportError::InvalidJson {
2297 path: "coverage-model.json".into(),
2298 reason: reason.into(),
2299 })?;
2300 if frontend.language != coverage_model.language {
2301 return Err(ReportError::InvalidArchive(format!(
2302 "frontend language {} differs from coverage model language {}",
2303 frontend.language, coverage_model.language
2304 )));
2305 }
2306 if frontend.frontend_version == "rust-compiler-v1" {
2307 validate_rust_compiler_scope(&manifest)?;
2308 }
2309 let normalized = CoverageReportRequest {
2310 run_id: request.run_id.clone(),
2311 manifest,
2312 raw_results,
2313 generated_at: request.generated_at.clone(),
2314 coverage_model: Some(coverage_model),
2315 integrity: request.integrity.clone(),
2316 test_exit_code: request.test_exit_code.clone(),
2317 };
2318 let mut report = crate::frontend_protocol::analyze_frontend_results(&frontend, &normalized)
2319 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2320 report.view.transport = Some(transport.clone());
2321 report.filters.passed.transport = Some(transport.clone());
2322 report.filters.failed.transport = Some(transport);
2323 Ok(report)
2324}
2325
2326#[cfg(test)]
2327mod tests {
2328 use std::{
2329 fs,
2330 sync::atomic::{AtomicU64, Ordering},
2331 time::SystemTime,
2332 };
2333
2334 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
2335
2336 use super::*;
2337
2338 static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
2339
2340 fn point(id: &str, line: usize) -> PointMeta {
2341 PointMeta {
2342 id: id.into(),
2343 kind: PointKind::Statement,
2344 file: "src/app.js".into(),
2345 line,
2346 column: 0,
2347 source: "work();".into(),
2348 label: None,
2349 }
2350 }
2351
2352 fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
2353 RawTestResult {
2354 test_id: Some(id.into()),
2355 scope: None,
2356 test: id.into(),
2357 test_file: Some("tests/app.test.js".into()),
2358 title: None,
2359 retry: Some(retry),
2360 status: Some(status.into()),
2361 expected_status: None,
2362 flaky: false,
2363 provenance: TestProvenance {
2364 runner: "node:test".into(),
2365 kind: "unit".into(),
2366 project: None,
2367 source: "runner-default".into(),
2368 },
2369 role: "test".into(),
2370 phases: vec![],
2371 runtime: vec![RuntimeSnapshot {
2372 decisions: vec![],
2373 hits: hits.iter().map(|hit| (*hit).into()).collect(),
2374 events: vec![],
2375 }],
2376 browser: vec![],
2377 server: vec![],
2378 }
2379 }
2380
2381 #[test]
2382 fn declined_obligations_are_unmeasured_never_uncovered() {
2383 let point = |id: &str, line: usize| PointMeta {
2387 id: id.into(),
2388 kind: PointKind::Statement,
2389 file: "src/app.js".into(),
2390 line,
2391 column: 0,
2392 source: "work();".into(),
2393 label: None,
2394 };
2395 let measured_only = CoverageManifest {
2396 decisions: vec![],
2397 points: vec![point("measured", 1)],
2398 branches: vec![],
2399 limitations: vec![],
2400 unmeasured: Vec::new(),
2401 scope: None,
2402 };
2403 let with_declined = CoverageManifest {
2404 decisions: vec![],
2405 points: vec![point("measured", 1), point("declined", 2)],
2406 branches: vec![],
2407 limitations: vec![],
2408 unmeasured: vec!["declined".into()],
2409 scope: None,
2410 };
2411
2412 let baseline =
2413 create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
2414 let view =
2415 create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
2416
2417 assert_eq!(
2419 view.summary.statements.total, baseline.summary.statements.total,
2420 "a declined obligation stayed in the covered/uncovered denominator"
2421 );
2422 assert_eq!(view.summary.unmeasured_obligations, Some(1));
2423 assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
2424
2425 assert_eq!(
2429 view.summary.lines.total, baseline.summary.lines.total,
2430 "a declined obligation kept its line in the line denominator"
2431 );
2432 let declined_line = view
2433 .lines
2434 .iter()
2435 .find(|line| line.line == 2)
2436 .expect("the declined line stays addressable");
2437 assert!(!declined_line.measured);
2438 assert!(!declined_line.covered);
2439
2440 assert_eq!(baseline.summary.unmeasured_obligations, None);
2443 assert_eq!(baseline.summary.exact_fraction_pct, None);
2444 let encoded = serde_json::to_string(&baseline.summary).unwrap();
2445 assert!(
2446 !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
2447 "a fully measured summary must serialize unchanged: {encoded}"
2448 );
2449 }
2450
2451 #[test]
2452 fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
2453 let point = |id: &str| PointMeta {
2457 id: id.into(),
2458 kind: PointKind::Statement,
2459 file: "src/app.js".into(),
2460 line: 1,
2461 column: 0,
2462 source: "work();".into(),
2463 label: None,
2464 };
2465 let manifest = CoverageManifest {
2466 decisions: vec![],
2467 points: vec![point("declined")],
2468 branches: vec![],
2469 limitations: vec![],
2470 unmeasured: vec!["declined".into()],
2471 scope: None,
2472 };
2473 let view = create_coverage_view(
2474 &manifest,
2475 &[raw("test", 0, "passed", &["declined"])],
2476 "time",
2477 )
2478 .unwrap();
2479
2480 assert_eq!(view.summary.lines.total, 0);
2481 assert_eq!(view.summary.lines.covered, 0);
2482 let line = view.lines.first().expect("the line stays addressable");
2483 assert!(!line.measured);
2484 assert!(!line.covered, "an unmeasured line is not covered either");
2485 }
2486
2487 #[test]
2488 fn report_retains_unexecuted_manifest_conditions() {
2489 let manifest = CoverageManifest {
2490 unmeasured: Vec::new(),
2491 decisions: vec![DecisionMeta {
2492 id: "decision".into(),
2493 file: "src/app.js".into(),
2494 line: 1,
2495 column: 0,
2496 source: "left && right".into(),
2497 conditions: vec!["left".into(), "right".into()],
2498 kind: "if".into(),
2499 }],
2500 points: vec![],
2501 branches: vec![],
2502 limitations: vec![],
2503 scope: None,
2504 };
2505 let view =
2506 create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
2507 assert_eq!(view.summary.conditions, 2);
2508 assert_eq!(view.summary.covered_conditions, 0);
2509 assert_eq!(view.decisions[0].conditions.len(), 2);
2510 }
2511
2512 #[test]
2513 fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
2514 let manifest = CoverageManifest {
2515 unmeasured: Vec::new(),
2516 decisions: vec![DecisionMeta {
2517 id: "application-decision".into(),
2518 file: "src/app.js".into(),
2519 line: 1,
2520 column: 0,
2521 source: "left && right".into(),
2522 conditions: vec!["left".into(), "right".into()],
2523 kind: "if".into(),
2524 }],
2525 points: vec![],
2526 branches: vec![],
2527 limitations: vec![],
2528 scope: None,
2529 };
2530 let mut attempt = raw("test", 0, "passed", &[]);
2531 attempt.runtime[0].decisions.push(DecisionSnapshot {
2532 meta: DecisionMeta {
2533 id: "synthetic-fixture".into(),
2534 file: "fixtures/generated.js".into(),
2535 line: 1,
2536 column: 0,
2537 source: "a && b && c".into(),
2538 conditions: vec!["a".into(), "b".into(), "c".into()],
2539 kind: "if".into(),
2540 },
2541 vectors: vec![McdcVector {
2542 values: vec![Some(true), Some(true), Some(true)],
2543 outcome: true,
2544 }],
2545 });
2546
2547 let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2548 assert_eq!(view.decisions.len(), 1);
2549 assert_eq!(view.decisions[0].meta.id, "application-decision");
2550 assert!(view.decisions[0].vectors.is_empty());
2551 }
2552
2553 #[test]
2554 fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
2555 let expected = DecisionMeta {
2556 id: "application-decision".into(),
2557 file: "src/app.js".into(),
2558 line: 1,
2559 column: 0,
2560 source: "left && right".into(),
2561 conditions: vec!["left".into(), "right".into()],
2562 kind: "if".into(),
2563 };
2564 let manifest = CoverageManifest {
2565 unmeasured: Vec::new(),
2566 decisions: vec![expected.clone()],
2567 points: vec![],
2568 branches: vec![],
2569 limitations: vec![],
2570 scope: Some(serde_json::json!({
2571 "entries": [{ "file": "src/empty.js", "status": "included" }]
2572 })),
2573 };
2574 let vector = McdcVector {
2575 values: vec![Some(true), Some(true)],
2576 outcome: true,
2577 };
2578
2579 let mut unknown = raw("test", 0, "passed", &[]);
2580 unknown.runtime[0].decisions.push(DecisionSnapshot {
2581 meta: DecisionMeta {
2582 id: "unknown-decision".into(),
2583 file: "src/empty.js".into(),
2584 ..expected.clone()
2585 },
2586 vectors: vec![vector.clone()],
2587 });
2588 assert!(matches!(
2589 create_coverage_view(&manifest, &[unknown], "time"),
2590 Err(ReportError::InvalidServerRecord(reason))
2591 if reason.contains("absent from the frozen manifest")
2592 ));
2593
2594 let mut changed = raw("test", 0, "passed", &[]);
2595 changed.runtime[0].decisions.push(DecisionSnapshot {
2596 meta: DecisionMeta {
2597 source: "left || right".into(),
2598 ..expected
2599 },
2600 vectors: vec![vector],
2601 });
2602 assert!(matches!(
2603 create_coverage_view(&manifest, &[changed], "time"),
2604 Err(ReportError::InvalidServerRecord(reason))
2605 if reason.contains("differs from the frozen manifest")
2606 ));
2607 }
2608
2609 #[test]
2610 fn timestamp_overlap_cannot_upgrade_assertion_confidence() {
2611 let manifest = CoverageManifest {
2612 unmeasured: Vec::new(),
2613 decisions: vec![],
2614 points: vec![point("hit", 1)],
2615 branches: vec![],
2616 limitations: vec![],
2617 scope: None,
2618 };
2619 let phase = CoveragePhase {
2620 id: "assertion".into(),
2621 kind: "assertion".into(),
2622 operation: "equal".into(),
2623 source: None,
2624 caused_by_phase_id: None,
2625 started_at_ms: 100,
2626 ended_at_ms: Some(120),
2627 status: Some("passed".into()),
2628 error: None,
2629 };
2630 let mut attempt = raw("test", 0, "passed", &["hit"]);
2631 attempt.phases.push(phase.clone());
2632 attempt.runtime[0].events.push(RuntimeEvent {
2633 event_type: "hit".into(),
2634 id: "hit".into(),
2635 vector: None,
2636 timestamp_ms: 110,
2637 phase_id: None,
2638 environment: "server".into(),
2639 });
2640 let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
2641 assert_eq!(inferred.points[0].confidence.level, "executed");
2642 assert!(!inferred.points[0].confidence.asserted);
2643
2644 attempt.runtime[0].events[0].phase_id = Some(phase.id);
2645 let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2646 assert_eq!(explicit.points[0].confidence.level, "asserted");
2647 assert!(explicit.points[0].confidence.asserted);
2648 }
2649
2650 #[test]
2651 fn verified_view_uses_only_the_terminal_successful_attempt() {
2652 let manifest = CoverageManifest {
2653 unmeasured: Vec::new(),
2654 decisions: vec![],
2655 points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
2656 branches: vec![],
2657 limitations: vec![],
2658 scope: None,
2659 };
2660 let failed = raw("flaky", 0, "failed", &["failed"]);
2661 let mut passed = raw("flaky", 1, "passed", &["passed"]);
2662 passed.flaky = true;
2663 let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
2664 expected.expected_status = Some("failed".into());
2665 let request = CoverageReportRequest {
2666 run_id: "run".into(),
2667 manifest,
2668 raw_results: vec![failed, passed, expected],
2669 generated_at: "time".into(),
2670 coverage_model: None,
2671 integrity: None,
2672 test_exit_code: ExitCodeInput::Missing,
2673 };
2674 let report = analyze_coverage_results(&request).unwrap();
2675 assert!(!report.filters.passed.points[0].covered);
2676 assert!(report.filters.passed.points[1].covered);
2677 assert!(!report.filters.passed.points[2].covered);
2678 assert!(report.filters.failed.points[0].covered);
2679 assert_eq!(
2680 report
2681 .view
2682 .tests
2683 .iter()
2684 .find(|test| test.name == "expected-failure")
2685 .unwrap()
2686 .outcome,
2687 "failed"
2688 );
2689 assert_eq!(report.view.tests[1].outcome, "flaky");
2690 }
2691
2692 #[test]
2693 fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
2694 let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
2695 expected.expected_status = Some("failed".into());
2696 let companion = raw("expected-failure", 0, "failed", &["expected"]);
2697 let request = CoverageReportRequest {
2698 run_id: "run".into(),
2699 manifest: CoverageManifest {
2700 unmeasured: Vec::new(),
2701 decisions: vec![],
2702 points: vec![point("expected", 1)],
2703 branches: vec![],
2704 limitations: vec![],
2705 scope: None,
2706 },
2707 raw_results: vec![companion, expected],
2708 generated_at: "time".into(),
2709 coverage_model: None,
2710 integrity: None,
2711 test_exit_code: ExitCodeInput::Missing,
2712 };
2713 let report = analyze_coverage_results(&request).unwrap();
2714 assert_eq!(report.view.tests[0].outcome, "passed");
2715 assert!(!report.filters.passed.points[0].covered);
2716 assert!(!report.filters.failed.points[0].covered);
2717 }
2718
2719 #[test]
2720 fn selected_but_unstarted_test_is_not_an_invented_attempt() {
2721 let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
2722 unstarted.scope = None;
2723 unstarted.retry = None;
2724 unstarted.runtime.clear();
2725 let request = CoverageReportRequest {
2726 run_id: "run".into(),
2727 manifest: CoverageManifest {
2728 unmeasured: Vec::new(),
2729 decisions: vec![],
2730 points: vec![],
2731 branches: vec![],
2732 limitations: vec![],
2733 scope: None,
2734 },
2735 raw_results: vec![unstarted],
2736 generated_at: "time".into(),
2737 coverage_model: None,
2738 integrity: None,
2739 test_exit_code: ExitCodeInput::Present(Some(100)),
2740 };
2741 let report = analyze_coverage_results(&request).unwrap();
2742 assert_eq!(report.view.tests[0].outcome, "unstarted");
2743 assert!(report.view.tests[0].attempts.is_empty());
2744 assert!(report.filters.passed.tests.is_empty());
2745 assert!(report.filters.failed.tests.is_empty());
2746 }
2747
2748 fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
2749 let nonce = SystemTime::now()
2750 .duration_since(SystemTime::UNIX_EPOCH)
2751 .unwrap()
2752 .as_nanos();
2753 let root = std::env::temp_dir().join(format!(
2754 "supercov-rust-report-{}-{nonce}-{}",
2755 std::process::id(),
2756 ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
2757 ));
2758 fs::create_dir_all(&root).unwrap();
2759 let path = root.join("evidence.raw.gz");
2760 if !entries
2761 .iter()
2762 .any(|entry| entry.path == "coverage-model.json")
2763 {
2764 entries.push(EvidenceArchiveEntry {
2765 path: "coverage-model.json".into(),
2766 contents: serde_json::to_vec(
2767 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
2768 .unwrap(),
2769 )
2770 .unwrap(),
2771 });
2772 }
2773 if !entries.iter().any(|entry| entry.path == "frontend.json") {
2774 entries.push(EvidenceArchiveEntry {
2775 path: "frontend.json".into(),
2776 contents: serde_json::to_vec(&serde_json::json!({
2777 "protocolVersion": 2,
2778 "frontendId": "javascript",
2779 "frontendVersion": "fixture-v1",
2780 "language": "javascript",
2781 "structuralSource": "owned-probes",
2782 "runners": [{
2783 "runner": "node:test",
2784 "executionModel": "serial-in-process",
2785 "attribution": {
2786 "run": "exact",
2787 "worker": "unavailable",
2788 "test": "exact",
2789 "retry": "exact",
2790 "phase": "exact",
2791 "action": "exact",
2792 "assertion": "exact"
2793 },
2794 "limitations": [{
2795 "id": "fixture-worker-unavailable",
2796 "scopes": ["worker"],
2797 "reason": "The fixture does not require worker identity"
2798 }]
2799 }],
2800 "structuralLimitations": []
2801 }))
2802 .unwrap(),
2803 });
2804 }
2805 write_archive(entries, &path).unwrap();
2806 path
2807 }
2808
2809 #[test]
2810 fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
2811 let manifest = CoverageManifest {
2812 unmeasured: Vec::new(),
2813 decisions: vec![],
2814 points: vec![point("background-hit", 1), point("test-hit", 2)],
2815 branches: vec![],
2816 limitations: vec![],
2817 scope: None,
2818 };
2819 let unattributed = |axis: &str| {
2822 serde_json::json!({
2823 "id": format!("background-no-{axis}"),
2824 "scopes": [axis],
2825 "reason": format!("Runner background did not expose exact {axis} identity for every result")
2826 })
2827 };
2828 let frontend = serde_json::json!({
2829 "protocolVersion": 2,
2830 "frontendId": "javascript",
2831 "frontendVersion": "fixture-v1",
2832 "language": "javascript",
2833 "structuralSource": "owned-probes",
2834 "runners": [
2835 {
2836 "runner": "node:test",
2837 "executionModel": "serial-in-process",
2838 "attribution": {
2839 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2840 "phase": "exact", "action": "exact", "assertion": "exact"
2841 },
2842 "limitations": [{
2843 "id": "fixture-worker-unavailable",
2844 "scopes": ["worker"],
2845 "reason": "The fixture does not require worker identity"
2846 }]
2847 },
2848 {
2849 "runner": "background",
2850 "executionModel": "parallel-unattributed",
2851 "attribution": {
2852 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2853 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2854 },
2855 "limitations": [
2856 unattributed("worker"), unattributed("test"), unattributed("retry"),
2857 unattributed("phase"), unattributed("action"), unattributed("assertion")
2858 ]
2859 }
2860 ],
2861 "structuralLimitations": []
2862 });
2863 let path = archive(vec![
2864 EvidenceArchiveEntry {
2865 path: "manifest.json".into(),
2866 contents: serde_json::to_vec(&manifest).unwrap(),
2867 },
2868 EvidenceArchiveEntry {
2869 path: "frontend.json".into(),
2870 contents: serde_json::to_vec(&frontend).unwrap(),
2871 },
2872 EvidenceArchiveEntry {
2873 path: "playwright-worker-1.mcdc.jsonl".into(),
2874 contents: {
2875 let mut contents =
2876 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2877 .unwrap();
2878 contents.extend_from_slice(b"\npartial-final-line");
2879 contents
2880 },
2881 },
2882 EvidenceArchiveEntry {
2883 path: "server/background/worker.jsonl".into(),
2884 contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
2885 },
2886 ]);
2887 let result = analyze_coverage_archive(&ArchiveReportRequest {
2888 archive_path: path.clone(),
2889 run_id: "run".into(),
2890 generated_at: "time".into(),
2891 integrity: None,
2892 test_exit_code: ExitCodeInput::Missing,
2893 });
2894 assert!(matches!(
2895 result,
2896 Err(ReportError::InvalidJson { path, .. })
2897 if path == "playwright-worker-1.mcdc.jsonl"
2898 ));
2899 fs::remove_dir_all(path.parent().unwrap()).unwrap();
2900 }
2901
2902 #[test]
2903 fn a_torn_background_record_is_counted_not_fatal() {
2904 let manifest = CoverageManifest {
2909 unmeasured: Vec::new(),
2910 decisions: vec![],
2911 points: vec![],
2912 branches: vec![],
2913 limitations: vec![],
2914 scope: None,
2915 };
2916 let unattributed = |axis: &str| {
2919 serde_json::json!({
2920 "id": format!("background-no-{axis}"),
2921 "scopes": [axis],
2922 "reason": format!("Runner background did not expose exact {axis} identity for every result")
2923 })
2924 };
2925 let frontend = serde_json::json!({
2926 "protocolVersion": 2,
2927 "frontendId": "javascript",
2928 "frontendVersion": "fixture-v1",
2929 "language": "javascript",
2930 "structuralSource": "owned-probes",
2931 "runners": [
2932 {
2933 "runner": "node:test",
2934 "executionModel": "serial-in-process",
2935 "attribution": {
2936 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2937 "phase": "exact", "action": "exact", "assertion": "exact"
2938 },
2939 "limitations": [{
2940 "id": "fixture-worker-unavailable",
2941 "scopes": ["worker"],
2942 "reason": "The fixture does not require worker identity"
2943 }]
2944 },
2945 {
2946 "runner": "background",
2947 "executionModel": "parallel-unattributed",
2948 "attribution": {
2949 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2950 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2951 },
2952 "limitations": [
2953 unattributed("worker"), unattributed("test"), unattributed("retry"),
2954 unattributed("phase"), unattributed("action"), unattributed("assertion")
2955 ]
2956 }
2957 ],
2958 "structuralLimitations": []
2959 });
2960 let path = archive(vec![
2961 EvidenceArchiveEntry {
2962 path: "manifest.json".into(),
2963 contents: serde_json::to_vec(&manifest).unwrap(),
2964 },
2965 EvidenceArchiveEntry {
2966 path: "frontend.json".into(),
2967 contents: serde_json::to_vec(&frontend).unwrap(),
2968 },
2969 EvidenceArchiveEntry {
2970 path: "playwright-worker-1.mcdc.jsonl".into(),
2971 contents: {
2972 let mut contents =
2973 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2974 .unwrap();
2975 contents.push(b'\n');
2976 contents
2977 },
2978 },
2979 EvidenceArchiveEntry {
2980 path: "execution.37360-1.168.jsonl".into(),
2981 contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
2982 },
2983 EvidenceArchiveEntry {
2984 path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
2985 contents: concat!(
2986 "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
2987 "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
2988 "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
2989 )
2990 .as_bytes()
2991 .to_vec(),
2992 },
2993 ]);
2994 let report = analyze_coverage_archive(&ArchiveReportRequest {
2995 archive_path: path.clone(),
2996 run_id: "run".into(),
2997 generated_at: "time".into(),
2998 integrity: None,
2999 test_exit_code: ExitCodeInput::Missing,
3000 })
3001 .expect("a torn evidence line must not fail the whole run");
3002 let transport = report.view.transport.expect("transport stats");
3003 assert_eq!(transport.corrupt_records, 2);
3004 assert_eq!(transport.corrupt_files, 2);
3005 assert_eq!(transport.background_server_records, 2);
3006 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3007 }
3008
3009 #[test]
3010 fn archive_analysis_rejects_cross_run_evidence() {
3011 let manifest = CoverageManifest {
3012 unmeasured: Vec::new(),
3013 decisions: vec![],
3014 points: vec![],
3015 branches: vec![],
3016 limitations: vec![],
3017 scope: None,
3018 };
3019 let mut result = raw("test", 0, "passed", &[]);
3020 result.scope = Some(ExecutionScope {
3021 version: 1,
3022 run_id: "other-run".into(),
3023 worker_id: "worker".into(),
3024 test_id: "test".into(),
3025 test_key: "key".into(),
3026 retry: 0,
3027 attempt_id: "attempt".into(),
3028 });
3029 let path = archive(vec![
3030 EvidenceArchiveEntry {
3031 path: "manifest.json".into(),
3032 contents: serde_json::to_vec(&manifest).unwrap(),
3033 },
3034 EvidenceArchiveEntry {
3035 path: "worker/mcdc.json".into(),
3036 contents: serde_json::to_vec(&result).unwrap(),
3037 },
3038 ]);
3039 assert!(matches!(
3040 analyze_coverage_archive(&ArchiveReportRequest {
3041 archive_path: path.clone(),
3042 run_id: "run".into(),
3043 generated_at: "time".into(),
3044 integrity: None,
3045 test_exit_code: ExitCodeInput::Missing,
3046 }),
3047 Err(ReportError::InvalidArchive(reason))
3048 if reason.contains("expected=run actual=other-run")
3049 ));
3050 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3051 }
3052
3053 #[test]
3054 fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
3055 let vectors: Value = serde_json::from_str(include_str!(
3056 "../test-assets/coverage-model-v1/vectors.json"
3057 ))
3058 .unwrap();
3059 for value in vectors["valid"].as_array().unwrap() {
3060 let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
3061 model.into_declaration().unwrap();
3062 }
3063 for vector in vectors["invalid"].as_array().unwrap() {
3064 let value = vector["value"].clone();
3065 if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
3066 assert!(
3067 model.into_declaration().is_err(),
3068 "accepted invalid model vector: {}",
3069 vector["reason"]
3070 );
3071 }
3072 }
3073
3074 let manifest = CoverageManifest {
3075 unmeasured: Vec::new(),
3076 decisions: vec![],
3077 points: vec![],
3078 branches: vec![],
3079 limitations: vec![],
3080 scope: None,
3081 };
3082 let path = archive(vec![
3083 EvidenceArchiveEntry {
3084 path: "coverage-model.json".into(),
3085 contents: serde_json::to_vec(&PersistedCoverageModel {
3086 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
3087 language: "rust".into(),
3088 variant: "rust-source-v1".into(),
3089 name: "Rust source coverage".into(),
3090 completeness_meaning: "Every Rust obligation was satisfied.".into(),
3091 measured: vec!["Rust statements".into()],
3092 not_measured: vec![],
3093 })
3094 .unwrap(),
3095 },
3096 EvidenceArchiveEntry {
3097 path: "manifest.json".into(),
3098 contents: serde_json::to_vec(&manifest).unwrap(),
3099 },
3100 ]);
3101 assert!(matches!(
3102 analyze_coverage_archive(&ArchiveReportRequest {
3103 archive_path: path.clone(),
3104 run_id: "run".into(),
3105 generated_at: "time".into(),
3106 integrity: None,
3107 test_exit_code: ExitCodeInput::Missing,
3108 }),
3109 Err(ReportError::InvalidArchive(reason))
3110 if reason.contains("javascript differs from coverage model language rust")
3111 ));
3112 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3113 }
3114
3115 #[test]
3116 fn rust_compiler_scope_requires_an_exact_full_source_fingerprint() {
3117 let mut manifest = CoverageManifest {
3118 unmeasured: Vec::new(),
3119 decisions: vec![],
3120 points: vec![],
3121 branches: vec![],
3122 limitations: vec![],
3123 scope: Some(serde_json::json!({
3124 "language": "rust",
3125 "model": "rust-source-v1",
3126 "crate": "fixture",
3127 "measurementComplete": false,
3128 "sourceFingerprint": {
3129 "algorithm": "sha256",
3130 "digest": "1".repeat(64),
3131 "files": 2,
3132 "generatedFiles": 1,
3133 },
3134 })),
3135 };
3136 validate_rust_compiler_scope(&manifest).unwrap();
3137
3138 manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3139 Value::String("not-a-digest".into());
3140 assert!(matches!(
3141 validate_rust_compiler_scope(&manifest),
3142 Err(ReportError::InvalidArchive(reason))
3143 if reason == "malformed Rust compiler source fingerprint"
3144 ));
3145
3146 manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3147 Value::String("1".repeat(64));
3148 manifest.scope.as_mut().unwrap()["sourceFingerprint"]["unexpected"] = Value::Bool(true);
3149 assert!(validate_rust_compiler_scope(&manifest).is_err());
3150 }
3151
3152 #[test]
3153 fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
3154 let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
3155 "runId": "run",
3156 "manifest": { "decisions": [], "points": [], "branches": [] },
3157 "rawResults": [{
3158 "test": "test",
3159 "status": "passed",
3160 "browser": [],
3161 "server": []
3162 }],
3163 "generatedAt": "time",
3164 "testExitCode": null
3165 }))
3166 .unwrap();
3167 let report = analyze_coverage_results(&request).unwrap();
3168 assert_eq!(
3169 report.execution,
3170 Some(ExecutionResult {
3171 test_exit_code: None,
3172 valid: false,
3173 })
3174 );
3175 }
3176}