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
1060pub fn blocking_limitation(limitation: &Value) -> bool {
1063 limitation
1064 .get("blocking")
1065 .and_then(Value::as_bool)
1066 .unwrap_or(true)
1067}
1068
1069fn confidence_for(
1070 test_ids: impl IntoIterator<Item = String>,
1071 phase_ids: impl IntoIterator<Item = String>,
1072 explicit_phase_ids: impl IntoIterator<Item = String>,
1073 tests: &HashMap<String, MutableTest>,
1074 phases: &HashMap<String, MutablePhase>,
1075 asserted_phase_ids: &BTreeSet<String>,
1076) -> CoverageConfidence {
1077 let test_ids = test_ids.into_iter().collect::<BTreeSet<_>>();
1078 let phase_ids = phase_ids.into_iter().collect::<BTreeSet<_>>();
1079 let explicit_phase_ids = explicit_phase_ids.into_iter().collect::<BTreeSet<_>>();
1080 let asserted_phases = explicit_phase_ids
1081 .iter()
1082 .filter(|id| asserted_phase_ids.contains(*id))
1083 .collect::<Vec<_>>();
1084 let asserted_tests = asserted_phases
1085 .iter()
1086 .filter_map(|id| phases.get(*id).map(|phase| phase.test.clone()))
1087 .collect::<BTreeSet<_>>();
1088 let provenances = test_ids
1089 .iter()
1090 .filter_map(|id| tests.get(id).map(|test| &test.provenance))
1091 .collect::<Vec<_>>();
1092 let roles = test_ids
1093 .iter()
1094 .filter_map(|id| tests.get(id).map(|test| test.role.as_str()))
1095 .collect::<Vec<_>>();
1096 let phase_kinds = phase_ids
1097 .iter()
1098 .filter_map(|id| phases.get(id).map(|phase| phase.phase.kind.as_str()))
1099 .collect::<Vec<_>>();
1100 let only = |phase_kind: &str, role: &str| {
1101 if phase_kinds.is_empty() {
1102 !roles.is_empty() && roles.iter().all(|value| *value == role)
1103 } else {
1104 phase_kinds.iter().all(|value| *value == phase_kind)
1105 }
1106 };
1107 let has_action = explicit_phase_ids.iter().any(|id| {
1108 phases
1109 .get(id)
1110 .is_some_and(|phase| phase.phase.kind == "action")
1111 });
1112 let level = if test_ids.is_empty() {
1113 "unexecuted"
1114 } else if !asserted_tests.is_empty() {
1115 "asserted"
1116 } else if has_action {
1117 "action"
1118 } else {
1119 "executed"
1120 };
1121 let runners = provenances
1122 .iter()
1123 .map(|provenance| provenance.runner.clone())
1124 .collect::<BTreeSet<_>>();
1125 let kinds = provenances
1126 .iter()
1127 .map(|provenance| provenance.kind.clone())
1128 .collect::<BTreeSet<_>>();
1129 CoverageConfidence {
1130 level: level.into(),
1131 setup_only: only("setup", "setup"),
1132 background_only: only("background", "background"),
1133 asserted: !asserted_tests.is_empty(),
1134 tests: sorted(&test_ids),
1135 asserted_tests: sorted(&asserted_tests),
1136 runners: sorted(&runners),
1137 e2e: kinds.contains("e2e"),
1138 kinds: sorted(&kinds),
1139 }
1140}
1141
1142fn add_reference(map: &mut HashMap<String, BTreeSet<String>>, id: &str, value: &str) {
1143 map.entry(id.into()).or_default().insert(value.into());
1144}
1145
1146pub fn create_coverage_view(
1147 manifest: &CoverageManifest,
1148 raw_results: &[RawTestResult],
1149 generated_at: &str,
1150) -> Result<CoverageView, ReportError> {
1151 create_coverage_view_with_model(
1152 manifest,
1153 raw_results,
1154 generated_at,
1155 &javascript_coverage_model(),
1156 )
1157}
1158
1159fn create_coverage_view_with_model(
1160 manifest: &CoverageManifest,
1161 raw_results: &[RawTestResult],
1162 generated_at: &str,
1163 coverage_model: &CoverageModelDeclaration,
1164) -> Result<CoverageView, ReportError> {
1165 let mut decision_metadata = manifest.decisions.clone();
1166 let decision_indexes = decision_metadata
1167 .iter()
1168 .enumerate()
1169 .map(|(index, meta)| (meta.id.clone(), index))
1170 .collect::<HashMap<_, _>>();
1171 let manifest_files = manifest
1172 .decisions
1173 .iter()
1174 .map(|meta| meta.file.clone())
1175 .chain(manifest.points.iter().map(|meta| meta.file.clone()))
1176 .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
1177 .chain(
1178 manifest
1179 .scope
1180 .as_ref()
1181 .and_then(|scope| scope.get("entries"))
1182 .and_then(Value::as_array)
1183 .into_iter()
1184 .flatten()
1185 .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
1186 .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
1187 )
1188 .collect::<BTreeSet<_>>();
1189 let mut vectors_by_decision = HashMap::<String, Vec<MutableObservation>>::new();
1190 let mut vector_indexes = HashMap::<String, HashMap<String, usize>>::new();
1191 let mut tests_by_decision = HashMap::<String, BTreeSet<String>>::new();
1192 let mut tests_by_hit = HashMap::<String, BTreeSet<String>>::new();
1193 let mut tests_by_id = HashMap::<String, MutableTest>::new();
1194 let mut test_order = Vec::<String>::new();
1195 let mut phases_by_id = HashMap::<String, MutablePhase>::new();
1196 let mut phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1197 let mut explicit_phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1198
1199 for raw in raw_results {
1200 let id = raw_test_id(raw).to_owned();
1201 if !tests_by_id.contains_key(&id) {
1202 test_order.push(id.clone());
1203 tests_by_id.insert(
1204 id.clone(),
1205 MutableTest {
1206 id: id.clone(),
1207 name: raw.test.clone(),
1208 file: raw.test_file.clone(),
1209 title: raw.title.clone(),
1210 retries: raw.retry.into_iter().collect(),
1211 attempts: BTreeMap::new(),
1212 unstarted: raw.status.as_deref() == Some("unstarted"),
1213 runner_reported_flaky: raw.flaky,
1214 provenance: raw.provenance.clone(),
1215 role: raw.role.clone(),
1216 hits: BTreeSet::new(),
1217 decisions: BTreeMap::new(),
1218 },
1219 );
1220 }
1221 let test = tests_by_id.get_mut(&id).expect("test was inserted");
1222 test.unstarted |= raw.status.as_deref() == Some("unstarted");
1223 if let Some(retry) = raw.retry {
1224 test.retries.insert(retry);
1225 }
1226 test.runner_reported_flaky |= raw.flaky;
1227 record_attempt(test, raw);
1228
1229 let mut ordered_phases = raw.phases.clone();
1230 ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1231 for phase in &ordered_phases {
1232 phases_by_id.insert(
1233 phase.id.clone(),
1234 MutablePhase {
1235 phase: phase.clone(),
1236 test: id.clone(),
1237 hits: BTreeSet::new(),
1238 decisions: BTreeMap::new(),
1239 browser_events: 0,
1240 server_events: 0,
1241 explicit_events: 0,
1242 inferred_events: 0,
1243 explicit_browser_events: 0,
1244 inferred_browser_events: 0,
1245 explicit_server_events: 0,
1246 inferred_server_events: 0,
1247 },
1248 );
1249 }
1250
1251 let correlate = |event: &RuntimeEvent| {
1252 event.phase_id.clone().or_else(|| {
1253 ordered_phases
1254 .iter()
1255 .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1256 .last()
1257 .map(|phase| phase.id.clone())
1258 })
1259 };
1260
1261 let snapshots = raw.runtime.iter().chain(&raw.browser);
1262 for snapshot in snapshots {
1263 for decision in &snapshot.decisions {
1264 let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1265 if manifest_files.contains(&decision.meta.file) {
1266 return Err(ReportError::InvalidServerRecord(format!(
1267 "decision {} is absent from the frozen manifest",
1268 decision.meta.id
1269 )));
1270 }
1271 continue;
1272 };
1273 if decision_metadata[index] != decision.meta {
1274 return Err(ReportError::InvalidServerRecord(format!(
1275 "decision {} metadata differs from the frozen manifest",
1276 decision.meta.id
1277 )));
1278 }
1279 for vector in &decision.vectors {
1280 let key = vector_key(vector);
1281 let indexes = vector_indexes.entry(decision.meta.id.clone()).or_default();
1282 let observations = vectors_by_decision
1283 .entry(decision.meta.id.clone())
1284 .or_default();
1285 let observation_index = *indexes.entry(key.clone()).or_insert_with(|| {
1286 observations.push(MutableObservation {
1287 vector: vector.clone(),
1288 tests: BTreeSet::new(),
1289 phases: BTreeSet::new(),
1290 explicit_phases: BTreeSet::new(),
1291 });
1292 observations.len() - 1
1293 });
1294 observations[observation_index].tests.insert(id.clone());
1295 tests_by_id
1296 .get_mut(&id)
1297 .expect("registered test")
1298 .decisions
1299 .entry(decision.meta.id.clone())
1300 .or_default()
1301 .insert(vector);
1302 }
1303 if !decision.vectors.is_empty() {
1304 add_reference(&mut tests_by_decision, &decision.meta.id, &id);
1305 }
1306 }
1307 for hit in &snapshot.hits {
1308 add_reference(&mut tests_by_hit, hit, &id);
1309 tests_by_id
1310 .get_mut(&id)
1311 .expect("registered test")
1312 .hits
1313 .insert(hit.clone());
1314 }
1315 for event in &snapshot.events {
1316 let explicit = event.phase_id.is_some();
1317 let Some(phase_id) = correlate(event) else {
1318 continue;
1319 };
1320 let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1321 continue;
1322 };
1323 if event.environment == "browser" {
1324 phase.browser_events += 1;
1325 if explicit {
1326 phase.explicit_browser_events += 1;
1327 } else {
1328 phase.inferred_browser_events += 1;
1329 }
1330 } else {
1331 phase.server_events += 1;
1332 if explicit {
1333 phase.explicit_server_events += 1;
1334 } else {
1335 phase.inferred_server_events += 1;
1336 }
1337 }
1338 if explicit {
1339 phase.explicit_events += 1;
1340 } else {
1341 phase.inferred_events += 1;
1342 }
1343 if event.event_type == "hit" {
1344 phase.hits.insert(event.id.clone());
1345 add_reference(&mut phases_by_hit, &event.id, &phase_id);
1346 if explicit {
1347 add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1348 }
1349 } else if event.event_type == "decision" {
1350 let vector = event
1351 .vector
1352 .as_ref()
1353 .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1354 phase
1355 .decisions
1356 .entry(event.id.clone())
1357 .or_default()
1358 .insert(vector);
1359 if let Some(index) = vector_indexes
1360 .get(&event.id)
1361 .and_then(|indexes| indexes.get(&vector_key(vector)))
1362 .copied()
1363 && let Some(observation) = vectors_by_decision
1364 .get_mut(&event.id)
1365 .and_then(|observations| observations.get_mut(index))
1366 {
1367 observation.phases.insert(phase_id.clone());
1368 if explicit {
1369 observation.explicit_phases.insert(phase_id.clone());
1370 }
1371 }
1372 } else {
1373 return Err(ReportError::InvalidEvent(event.event_type.clone()));
1374 }
1375 }
1376 }
1377
1378 for record in &raw.server {
1379 let (record_id, decision) = if record.record_type == "decision" {
1380 let meta = record
1381 .meta
1382 .as_ref()
1383 .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1384 let vector = record
1385 .vector
1386 .as_ref()
1387 .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1388 let Some(index) = decision_indexes.get(&meta.id).copied() else {
1389 if manifest_files.contains(&meta.file) {
1390 return Err(ReportError::InvalidServerRecord(format!(
1391 "decision {} is absent from the frozen manifest",
1392 meta.id
1393 )));
1394 }
1395 continue;
1396 };
1397 if decision_metadata[index] != *meta {
1398 return Err(ReportError::InvalidServerRecord(format!(
1399 "decision {} metadata differs from the frozen manifest",
1400 meta.id
1401 )));
1402 }
1403 let key = vector_key(vector);
1404 let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1405 let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1406 let index = *indexes.entry(key).or_insert_with(|| {
1407 observations.push(MutableObservation {
1408 vector: vector.clone(),
1409 tests: BTreeSet::new(),
1410 phases: BTreeSet::new(),
1411 explicit_phases: BTreeSet::new(),
1412 });
1413 observations.len() - 1
1414 });
1415 observations[index].tests.insert(id.clone());
1416 tests_by_id
1417 .get_mut(&id)
1418 .expect("registered test")
1419 .decisions
1420 .entry(meta.id.clone())
1421 .or_default()
1422 .insert(vector);
1423 add_reference(&mut tests_by_decision, &meta.id, &id);
1424 (meta.id.clone(), Some(vector.clone()))
1425 } else if record.record_type == "hit" {
1426 let hit = record
1427 .id
1428 .as_ref()
1429 .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1430 add_reference(&mut tests_by_hit, hit, &id);
1431 tests_by_id
1432 .get_mut(&id)
1433 .expect("registered test")
1434 .hits
1435 .insert(hit.clone());
1436 (hit.clone(), None)
1437 } else {
1438 return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1439 };
1440 let Some(timestamp_ms) = record.timestamp_ms else {
1441 continue;
1442 };
1443 let event = RuntimeEvent {
1444 event_type: record.record_type.clone(),
1445 id: record_id,
1446 vector: decision,
1447 timestamp_ms,
1448 phase_id: record.phase_id.clone(),
1449 environment: "server".into(),
1450 };
1451 let explicit = event.phase_id.is_some();
1452 let phase_id = correlate(&event);
1453 let Some(phase_id) = phase_id else { continue };
1454 let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1455 continue;
1456 };
1457 phase.server_events += 1;
1458 if explicit {
1459 phase.explicit_events += 1;
1460 phase.explicit_server_events += 1;
1461 } else {
1462 phase.inferred_events += 1;
1463 phase.inferred_server_events += 1;
1464 }
1465 if event.event_type == "hit" {
1466 phase.hits.insert(event.id.clone());
1467 add_reference(&mut phases_by_hit, &event.id, &phase_id);
1468 if explicit {
1469 add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1470 }
1471 } else if let Some(vector) = &event.vector {
1472 phase
1473 .decisions
1474 .entry(event.id.clone())
1475 .or_default()
1476 .insert(vector);
1477 if let Some(index) = vector_indexes
1478 .get(&event.id)
1479 .and_then(|indexes| indexes.get(&vector_key(vector)))
1480 .copied()
1481 && let Some(observation) = vectors_by_decision
1482 .get_mut(&event.id)
1483 .and_then(|observations| observations.get_mut(index))
1484 {
1485 observation.phases.insert(phase_id.clone());
1486 if explicit {
1487 observation.explicit_phases.insert(phase_id.clone());
1488 }
1489 }
1490 }
1491 }
1492 }
1493
1494 let mut asserted_phase_ids = BTreeSet::new();
1495 for phase in phases_by_id.values() {
1496 if phase.phase.kind == "assertion" && phase.phase.status.as_deref() == Some("passed") {
1497 asserted_phase_ids.insert(phase.phase.id.clone());
1498 if let Some(cause) = &phase.phase.caused_by_phase_id {
1499 asserted_phase_ids.insert(cause.clone());
1500 }
1501 }
1502 }
1503
1504 decision_metadata.sort_by(|left, right| {
1505 left.file
1506 .cmp(&right.file)
1507 .then(left.line.cmp(&right.line))
1508 .then(left.column.cmp(&right.column))
1509 });
1510 let mut decisions = Vec::with_capacity(decision_metadata.len());
1511 for meta in decision_metadata {
1512 let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
1513 let mut observations = Vec::with_capacity(mutable.len());
1514 for observation in mutable {
1515 let confidence = confidence_for(
1516 sorted(&observation.tests),
1517 sorted(&observation.phases),
1518 sorted(&observation.explicit_phases),
1519 &tests_by_id,
1520 &phases_by_id,
1521 &asserted_phase_ids,
1522 );
1523 observations.push(VectorObservation {
1524 vector: observation.vector,
1525 tests: sorted(&observation.tests),
1526 phases: sorted(&observation.phases),
1527 explicit_phases: sorted(&observation.explicit_phases),
1528 confidence,
1529 });
1530 }
1531 let vectors = observations
1532 .iter()
1533 .map(|observation| observation.vector.clone())
1534 .collect::<Vec<_>>();
1535 let witnesses =
1536 find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
1537 ReportError::DecisionAnalysis {
1538 decision_id: meta.id.clone(),
1539 error,
1540 }
1541 })?;
1542 let mut conditions = Vec::with_capacity(meta.conditions.len());
1543 for (index, source) in meta.conditions.iter().enumerate() {
1544 let witness = witnesses[index].map(|witness| {
1545 [
1546 vectors[witness.first].clone(),
1547 vectors[witness.second].clone(),
1548 ]
1549 });
1550 let witness_tests = witnesses[index].map(|witness| {
1551 [
1552 observations[witness.first].tests.clone(),
1553 observations[witness.second].tests.clone(),
1554 ]
1555 });
1556 let assertion_covered = (0..observations.len()).any(|left| {
1557 ((left + 1)..observations.len()).any(|right| {
1558 observations[left].confidence.asserted
1559 && observations[right].confidence.asserted
1560 && crate::coverage_analysis::is_independence_pair(
1561 &observations[left].vector,
1562 &observations[right].vector,
1563 index,
1564 )
1565 })
1566 });
1567 conditions.push(ConditionResult {
1568 index,
1569 source: source.clone(),
1570 covered: witness.is_some(),
1571 assertion_covered,
1572 witness,
1573 witness_tests,
1574 });
1575 }
1576 let decision_tests = tests_by_decision.remove(&meta.id).unwrap_or_default();
1577 let confidence = confidence_for(
1578 sorted(&decision_tests),
1579 observations
1580 .iter()
1581 .flat_map(|observation| observation.phases.clone()),
1582 observations
1583 .iter()
1584 .flat_map(|observation| observation.explicit_phases.clone()),
1585 &tests_by_id,
1586 &phases_by_id,
1587 &asserted_phase_ids,
1588 );
1589 decisions.push(DecisionResult {
1590 executed: !vectors.is_empty(),
1591 covered: conditions.iter().all(|condition| condition.covered),
1592 meta,
1593 vectors,
1594 vector_observations: observations,
1595 conditions,
1596 tests: sorted(&decision_tests),
1597 confidence,
1598 });
1599 }
1600
1601 let points = manifest
1602 .points
1603 .iter()
1604 .cloned()
1605 .map(|meta| {
1606 let tests = tests_by_hit.get(&meta.id).cloned().unwrap_or_default();
1607 let phases = phases_by_hit.get(&meta.id).cloned().unwrap_or_default();
1608 let explicit = explicit_phases_by_hit
1609 .get(&meta.id)
1610 .cloned()
1611 .unwrap_or_default();
1612 PointResult {
1613 covered: tests_by_hit.contains_key(&meta.id),
1614 confidence: confidence_for(
1615 sorted(&tests),
1616 sorted(&phases),
1617 sorted(&explicit),
1618 &tests_by_id,
1619 &phases_by_id,
1620 &asserted_phase_ids,
1621 ),
1622 meta,
1623 tests: sorted(&tests),
1624 phases: sorted(&phases),
1625 }
1626 })
1627 .collect::<Vec<_>>();
1628
1629 let branches = manifest
1630 .branches
1631 .iter()
1632 .cloned()
1633 .map(|meta| {
1634 let alternatives = meta
1635 .alternatives
1636 .iter()
1637 .map(|alternative| {
1638 let tests = tests_by_hit
1639 .get(&alternative.id)
1640 .cloned()
1641 .unwrap_or_default();
1642 let phases = phases_by_hit
1643 .get(&alternative.id)
1644 .cloned()
1645 .unwrap_or_default();
1646 let explicit = explicit_phases_by_hit
1647 .get(&alternative.id)
1648 .cloned()
1649 .unwrap_or_default();
1650 AlternativeResult {
1651 id: alternative.id.clone(),
1652 label: alternative.label.clone(),
1653 covered: tests_by_hit.contains_key(&alternative.id),
1654 tests: sorted(&tests),
1655 phases: sorted(&phases),
1656 confidence: confidence_for(
1657 sorted(&tests),
1658 sorted(&phases),
1659 sorted(&explicit),
1660 &tests_by_id,
1661 &phases_by_id,
1662 &asserted_phase_ids,
1663 ),
1664 }
1665 })
1666 .collect::<Vec<_>>();
1667 BranchResult {
1668 covered: alternatives.iter().all(|alternative| alternative.covered),
1669 meta,
1670 alternatives,
1671 }
1672 })
1673 .collect::<Vec<_>>();
1674
1675 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
1681
1682 let mut line_aggregates = BTreeMap::<SourceLine, LineAggregate>::new();
1687 for point in &points {
1688 let aggregate = line_aggregates
1689 .entry(SourceLine {
1690 file: point.meta.file.clone(),
1691 line: point.meta.line,
1692 })
1693 .or_default();
1694 if declined.contains(&point.meta.id) {
1695 continue;
1696 }
1697 aggregate.measured = true;
1698 aggregate.covered |= point.covered;
1699 aggregate.tests.extend(point.tests.clone());
1700 aggregate.phases.extend(point.phases.clone());
1701 aggregate.explicit_phases.extend(
1702 explicit_phases_by_hit
1703 .get(&point.meta.id)
1704 .into_iter()
1705 .flatten()
1706 .cloned(),
1707 );
1708 }
1709 let lines = line_aggregates
1710 .into_iter()
1711 .map(|(location, aggregate)| {
1712 let LineAggregate {
1713 covered,
1714 measured,
1715 tests: test_ids,
1716 phases: phase_ids,
1717 explicit_phases: explicit_ids,
1718 } = aggregate;
1719 let provenances = test_ids
1720 .iter()
1721 .filter_map(|id| tests_by_id.get(id).map(|test| &test.provenance))
1722 .collect::<Vec<_>>();
1723 let runners = provenances
1724 .iter()
1725 .map(|provenance| provenance.runner.clone())
1726 .collect::<BTreeSet<_>>();
1727 let kinds = provenances
1728 .iter()
1729 .map(|provenance| provenance.kind.clone())
1730 .collect::<BTreeSet<_>>();
1731 LineResult {
1732 file: location.file,
1733 line: location.line,
1734 covered,
1735 measured,
1736 tests: sorted(&test_ids),
1737 runners: sorted(&runners),
1738 exclusive_kind: (kinds.len() == 1).then(|| kinds.first().unwrap().clone()),
1739 phases: sorted(&phase_ids),
1740 confidence: confidence_for(
1741 sorted(&test_ids),
1742 sorted(&phase_ids),
1743 sorted(&explicit_ids),
1744 &tests_by_id,
1745 &phases_by_id,
1746 &asserted_phase_ids,
1747 ),
1748 kinds: sorted(&kinds),
1749 }
1750 })
1751 .collect::<Vec<_>>();
1752
1753 let point_locations = manifest
1754 .points
1755 .iter()
1756 .map(|point| {
1757 (
1758 point.id.clone(),
1759 SourceLine {
1760 file: point.file.clone(),
1761 line: point.line,
1762 },
1763 )
1764 })
1765 .collect::<HashMap<_, _>>();
1766 test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
1767 let tests = test_order
1768 .into_iter()
1769 .map(|id| {
1770 let test = tests_by_id.get(&id).expect("test order references test");
1771 let lines = test
1772 .hits
1773 .iter()
1774 .filter_map(|hit| point_locations.get(hit).cloned())
1775 .collect::<BTreeSet<_>>();
1776 TestCoverageResult {
1777 id: test.id.clone(),
1778 name: test.name.clone(),
1779 file: test.file.clone(),
1780 title: test.title.clone(),
1781 retries: sorted(&test.retries),
1782 attempts: test.attempts.values().cloned().collect(),
1783 outcome: test_outcome(test),
1784 provenance: test.provenance.clone(),
1785 role: test.role.clone(),
1786 hits: sorted(&test.hits),
1787 decisions: test
1788 .decisions
1789 .iter()
1790 .map(|(id, vectors)| TestDecisionResult {
1791 id: id.clone(),
1792 vectors: vectors.values.clone(),
1793 })
1794 .collect(),
1795 lines: lines.into_iter().collect(),
1796 }
1797 })
1798 .collect::<Vec<_>>();
1799
1800 let mut test_files = BTreeMap::<
1801 String,
1802 (
1803 BTreeSet<String>,
1804 BTreeSet<String>,
1805 BTreeSet<String>,
1806 BTreeSet<SourceLine>,
1807 ),
1808 >::new();
1809 for test in &tests {
1810 let aggregate = test_files
1811 .entry(
1812 test.file
1813 .clone()
1814 .unwrap_or_else(|| "(unknown test file)".into()),
1815 )
1816 .or_default();
1817 aggregate.0.insert(test.id.clone());
1818 aggregate.1.insert(test.provenance.runner.clone());
1819 aggregate.2.insert(test.provenance.kind.clone());
1820 aggregate.3.extend(test.lines.clone());
1821 }
1822 let test_files = test_files
1823 .into_iter()
1824 .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
1825 file,
1826 tests: sorted(&tests),
1827 runners: sorted(&runners),
1828 kinds: sorted(&kinds),
1829 lines: lines.into_iter().collect(),
1830 })
1831 .collect::<Vec<_>>();
1832
1833 let mut phases = phases_by_id.values().cloned().collect::<Vec<_>>();
1834 phases.sort_by(|left, right| {
1835 left.phase
1836 .started_at_ms
1837 .cmp(&right.phase.started_at_ms)
1838 .then(left.phase.id.cmp(&right.phase.id))
1839 });
1840 let phases = phases
1841 .into_iter()
1842 .map(|phase| {
1843 let lines = phase
1844 .hits
1845 .iter()
1846 .filter_map(|hit| point_locations.get(hit).cloned())
1847 .collect::<BTreeSet<_>>();
1848 PhaseResult {
1849 phase: phase.phase,
1850 test: phase.test,
1851 hits: sorted(&phase.hits),
1852 decisions: phase
1853 .decisions
1854 .into_iter()
1855 .map(|(id, vectors)| TestDecisionResult {
1856 id,
1857 vectors: vectors.values,
1858 })
1859 .collect(),
1860 lines: lines.into_iter().collect(),
1861 browser_events: phase.browser_events,
1862 server_events: phase.server_events,
1863 explicit_events: phase.explicit_events,
1864 inferred_events: phase.inferred_events,
1865 explicit_browser_events: phase.explicit_browser_events,
1866 inferred_browser_events: phase.inferred_browser_events,
1867 explicit_server_events: phase.explicit_server_events,
1868 inferred_server_events: phase.inferred_server_events,
1869 }
1870 })
1871 .collect::<Vec<_>>();
1872
1873 let total_obligations = decisions.len() + points.len() + branches.len();
1874 let (decisions, points, branches) = if declined.is_empty() {
1875 (decisions, points, branches)
1876 } else {
1877 (
1878 decisions
1879 .into_iter()
1880 .filter(|result| !declined.contains(&result.meta.id))
1881 .collect::<Vec<_>>(),
1882 points
1883 .into_iter()
1884 .filter(|result| !declined.contains(&result.meta.id))
1885 .collect::<Vec<_>>(),
1886 branches
1887 .into_iter()
1888 .filter(|result| !declined.contains(&result.meta.id))
1889 .collect::<Vec<_>>(),
1890 )
1891 };
1892 let measured_obligations = decisions.len() + points.len() + branches.len();
1893 let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
1894 if total_obligations > measured_obligations {
1895 summary.unmeasured_obligations = Some(total_obligations - measured_obligations);
1896 summary.exact_fraction_pct = Some(if total_obligations == 0 {
1897 100.0
1898 } else {
1899 (measured_obligations as f64) * 100.0 / (total_obligations as f64)
1900 });
1901 }
1902 if manifest.limitations.iter().any(blocking_limitation) {
1909 summary.coverage_complete = false;
1910 summary.completeness_blocked = Some(true);
1911 }
1912
1913 let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
1914 let values = tests
1915 .iter()
1916 .map(|test| {
1917 if field == "kind" {
1918 test.provenance.kind.clone()
1919 } else {
1920 test.provenance.runner.clone()
1921 }
1922 })
1923 .collect::<BTreeSet<_>>();
1924 values
1925 .into_iter()
1926 .map(|value| {
1927 let selected = tests
1928 .iter()
1929 .filter(|test| {
1930 if field == "kind" {
1931 test.provenance.kind == value
1932 } else {
1933 test.provenance.runner == value
1934 }
1935 })
1936 .map(|test| test.id.clone())
1937 .collect::<BTreeSet<_>>();
1938 Ok(DimensionCoverage {
1939 kind: (field == "kind").then(|| value.clone()),
1940 runner: (field == "runner").then(|| value.clone()),
1941 tests: tests
1942 .iter()
1943 .filter(|test| selected.contains(&test.id) && test.role == "test")
1944 .count(),
1945 setups: tests
1946 .iter()
1947 .filter(|test| selected.contains(&test.id) && test.role == "setup")
1948 .count(),
1949 summary: summary_for_results(
1950 &decisions,
1951 &points,
1952 &branches,
1953 &lines,
1954 Some(&selected),
1955 )?,
1956 })
1957 })
1958 .collect()
1959 };
1960
1961 Ok(CoverageView {
1962 generated_at: generated_at.into(),
1963 variant: coverage_model.variant.clone(),
1964 scope: manifest.scope.clone(),
1965 model: CoverageModel {
1966 language: coverage_model.language.clone(),
1967 name: coverage_model.name.clone(),
1968 completeness_meaning: coverage_model.completeness_meaning.clone(),
1969 measured: coverage_model.measured.clone(),
1970 not_measured: coverage_model.not_measured.clone(),
1971 },
1972 integrity: None,
1973 limitations: manifest.limitations.clone(),
1974 transport: None,
1975 summary,
1976 coverage_by_kind: dimension_coverage("kind")?,
1977 coverage_by_runner: dimension_coverage("runner")?,
1978 decisions,
1979 points,
1980 branches,
1981 tests,
1982 test_files,
1983 phases,
1984 lines,
1985 })
1986}
1987
1988pub fn analyze_coverage_results(
1989 request: &CoverageReportRequest,
1990) -> Result<CoverageReport, ReportError> {
1991 if let Some(scope) = request
1992 .raw_results
1993 .iter()
1994 .filter_map(|raw| raw.scope.as_ref())
1995 .find(|scope| scope.run_id != request.run_id)
1996 {
1997 return Err(ReportError::ScopeMismatch {
1998 expected: request.run_id.clone(),
1999 actual: scope.run_id.clone(),
2000 });
2001 }
2002 if request.raw_results.is_empty() {
2003 return Err(ReportError::NoEvidence(request.run_id.clone()));
2004 }
2005 let default_model = javascript_coverage_model();
2006 let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
2007 PersistedCoverageModel::from_declaration(coverage_model).map_err(|reason| {
2008 ReportError::InvalidJson {
2009 path: "coverage-model.json".into(),
2010 reason: reason.into(),
2011 }
2012 })?;
2013 let view = create_coverage_view_with_model(
2014 &request.manifest,
2015 &request.raw_results,
2016 &request.generated_at,
2017 coverage_model,
2018 )?;
2019 let passed = create_coverage_view_with_model(
2020 &request.manifest,
2021 &passing_coverage_results(&request.raw_results),
2022 &request.generated_at,
2023 coverage_model,
2024 )?;
2025 let failed = create_coverage_view_with_model(
2026 &request.manifest,
2027 &failed_coverage_results(&request.raw_results),
2028 &request.generated_at,
2029 coverage_model,
2030 )?;
2031 let execution = match request.test_exit_code {
2032 ExitCodeInput::Missing => None,
2033 ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
2034 valid: test_exit_code == Some(0),
2035 test_exit_code,
2036 }),
2037 };
2038 let mut view = view;
2039 let mut passed = passed;
2040 let mut failed = failed;
2041 if let Some(integrity) = &request.integrity {
2042 view.integrity = Some(integrity.clone());
2043 passed.integrity = Some(integrity.clone());
2044 failed.integrity = Some(integrity.clone());
2045 }
2046 Ok(CoverageReport {
2047 view,
2048 execution,
2049 filters: CoverageFilters { passed, failed },
2050 })
2051}
2052
2053fn parse_entry<T: for<'de> Deserialize<'de>>(
2054 entry: &EvidenceArchiveEntry,
2055) -> Result<T, ReportError> {
2056 serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
2057 path: entry.path.clone(),
2058 reason: error.to_string(),
2059 })
2060}
2061
2062fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
2063 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2064) -> Result<Vec<T>, ReportError> {
2065 let mut records = Vec::new();
2066 for entry in entries {
2067 let Some(contents) = entry.contents.strip_suffix(b"\n") else {
2068 return Err(ReportError::InvalidJson {
2069 path: entry.path.clone(),
2070 reason: "recognized JSONL evidence must end with a newline".into(),
2071 });
2072 };
2073 if contents.is_empty() {
2074 return Err(ReportError::InvalidJson {
2075 path: entry.path.clone(),
2076 reason: "recognized JSONL evidence must contain at least one record".into(),
2077 });
2078 }
2079 for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
2080 if line.is_empty() {
2081 return Err(ReportError::InvalidJson {
2082 path: entry.path.clone(),
2083 reason: format!("blank JSONL record at line {}", index + 1),
2084 });
2085 }
2086 records.push(serde_json::from_slice(line).map_err(|error| {
2087 ReportError::InvalidJson {
2088 path: entry.path.clone(),
2089 reason: format!("invalid JSONL record at line {}: {error}", index + 1),
2090 }
2091 })?);
2092 }
2093 }
2094 Ok(records)
2095}
2096
2097struct TolerantJsonLines<T> {
2103 records: Vec<T>,
2104 corrupt_records: usize,
2105 corrupt_files: usize,
2106}
2107
2108fn parse_server_json_lines<'a, T: for<'de> Deserialize<'de>>(
2109 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2110) -> TolerantJsonLines<T> {
2111 let mut parsed = TolerantJsonLines {
2112 records: Vec::new(),
2113 corrupt_records: 0,
2114 corrupt_files: 0,
2115 };
2116 for entry in entries {
2117 let mut corrupt_here = 0;
2118 for line in entry.contents.split(|byte| *byte == b'\n') {
2119 if line.is_empty() {
2120 continue;
2121 }
2122 match serde_json::from_slice(line) {
2123 Ok(record) => parsed.records.push(record),
2124 Err(_) => corrupt_here += 1,
2125 }
2126 }
2127 if corrupt_here > 0 {
2128 parsed.corrupt_records += corrupt_here;
2129 parsed.corrupt_files += 1;
2130 }
2131 }
2132 parsed
2133}
2134
2135fn is_mcdc_result(path: &str) -> bool {
2136 path == "mcdc.json" || path.ends_with("/mcdc.json")
2137}
2138
2139fn is_mcdc_journal(path: &str) -> bool {
2140 path == "mcdc.jsonl" || path.ends_with(".mcdc.jsonl")
2141}
2142
2143fn validate_rust_compiler_scope(manifest: &CoverageManifest) -> Result<(), ReportError> {
2144 let scope = manifest
2145 .scope
2146 .as_ref()
2147 .and_then(Value::as_object)
2148 .ok_or_else(|| ReportError::InvalidArchive("missing Rust compiler source scope".into()))?;
2149 let expected = BTreeSet::from([
2150 "crate",
2151 "language",
2152 "measurementComplete",
2153 "model",
2154 "sourceFingerprint",
2155 ]);
2156 if scope.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected
2157 || scope.get("language").and_then(Value::as_str) != Some("rust")
2158 || scope.get("model").and_then(Value::as_str) != Some("rust-source-v1")
2159 || scope
2160 .get("crate")
2161 .and_then(Value::as_str)
2162 .is_none_or(str::is_empty)
2163 || !scope
2164 .get("measurementComplete")
2165 .is_some_and(Value::is_boolean)
2166 {
2167 return Err(ReportError::InvalidArchive(
2168 "malformed Rust compiler source scope".into(),
2169 ));
2170 }
2171 let fingerprint = scope
2172 .get("sourceFingerprint")
2173 .and_then(Value::as_object)
2174 .ok_or_else(|| {
2175 ReportError::InvalidArchive("missing Rust compiler source fingerprint".into())
2176 })?;
2177 let expected_fingerprint = BTreeSet::from(["algorithm", "digest", "files", "generatedFiles"]);
2178 let digest = fingerprint.get("digest").and_then(Value::as_str);
2179 let files = fingerprint.get("files").and_then(Value::as_u64);
2180 let generated = fingerprint.get("generatedFiles").and_then(Value::as_u64);
2181 if fingerprint
2182 .keys()
2183 .map(String::as_str)
2184 .collect::<BTreeSet<_>>()
2185 != expected_fingerprint
2186 || fingerprint.get("algorithm").and_then(Value::as_str) != Some("sha256")
2187 || !digest.is_some_and(|digest| {
2188 digest.len() == 64
2189 && digest
2190 .bytes()
2191 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2192 })
2193 || files.is_none_or(|files| files == 0)
2194 || generated
2195 .zip(files)
2196 .is_none_or(|(generated, files)| generated > files)
2197 {
2198 return Err(ReportError::InvalidArchive(
2199 "malformed Rust compiler source fingerprint".into(),
2200 ));
2201 }
2202 Ok(())
2203}
2204
2205pub fn analyze_coverage_archive(
2206 request: &ArchiveReportRequest,
2207) -> Result<CoverageReport, ReportError> {
2208 let entries = read_archive(Path::new(&request.archive_path))
2209 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2210 let manifest = entries
2211 .iter()
2212 .find(|entry| entry.path == "manifest.json")
2213 .ok_or(ReportError::MissingManifest)
2214 .and_then(parse_entry::<CoverageManifest>)?;
2215 let mut raw_results = entries
2216 .iter()
2217 .filter(|entry| is_mcdc_result(&entry.path))
2218 .map(parse_entry::<RawTestResult>)
2219 .collect::<Result<Vec<_>, _>>()?;
2220 let journal_results = parse_json_lines::<RawTestResult>(
2221 entries.iter().filter(|entry| is_mcdc_journal(&entry.path)),
2222 )?;
2223 raw_results.extend(journal_results);
2224
2225 let scoped = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2226 entry.path.starts_with("server/")
2227 && !entry.path.starts_with("server/background/")
2228 && entry.path.ends_with(".jsonl")
2229 }));
2230 let scoped_records = scoped.records;
2231 for record in &scoped_records {
2232 let Some(scope) = &record.scope else { continue };
2233 let Some(raw) = raw_results
2234 .iter_mut()
2235 .find(|raw| raw.scope.as_ref() == Some(scope))
2236 else {
2237 continue;
2238 };
2239 if !raw.server.contains(record) {
2240 raw.server.push(record.clone());
2241 }
2242 }
2243
2244 let background = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2245 entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2246 }));
2247 let background_records = background.records;
2248 if !background_records.is_empty() {
2249 raw_results.push(RawTestResult {
2250 test_id: Some(format!("background:{}", request.run_id)),
2251 scope: None,
2252 test: "Background / unattributed".into(),
2253 test_file: None,
2254 title: Some("Background / unattributed".into()),
2255 retry: None,
2256 status: Some("unknown".into()),
2257 expected_status: None,
2258 flaky: false,
2259 provenance: TestProvenance {
2260 runner: "background".into(),
2261 kind: "background".into(),
2262 project: None,
2263 source: "explicit".into(),
2264 },
2265 role: "background".into(),
2266 phases: vec![],
2267 runtime: vec![],
2268 browser: vec![],
2269 server: background_records.clone(),
2270 });
2271 }
2272
2273 let execution =
2276 parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2277 entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2278 }));
2279 let execution_events = execution.records;
2280 let count_event = |name: &str| {
2281 execution_events
2282 .iter()
2283 .filter(|event| event.kind() == name)
2284 .count()
2285 };
2286 let transport = TransportStats {
2287 processes: count_event("process"),
2288 child_launches: count_event("child-launch"),
2289 remote_launches: count_event("remote-launch"),
2290 workspace_capabilities: count_event("workspace-capability"),
2291 scoped_server_records: scoped_records.len(),
2292 background_server_records: background_records.len(),
2293 corrupt_records: scoped.corrupt_records
2294 + background.corrupt_records
2295 + execution.corrupt_records,
2296 corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
2297 };
2298 let frontend = entries
2299 .iter()
2300 .find(|entry| entry.path == "frontend.json")
2301 .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
2302 .and_then(parse_entry::<FrontendRunDeclaration>)?;
2303 let persisted = entries
2304 .iter()
2305 .find(|entry| entry.path == "coverage-model.json")
2306 .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
2307 .and_then(parse_entry::<PersistedCoverageModel>)?;
2308 let coverage_model =
2309 persisted
2310 .into_declaration()
2311 .map_err(|reason| ReportError::InvalidJson {
2312 path: "coverage-model.json".into(),
2313 reason: reason.into(),
2314 })?;
2315 if frontend.language != coverage_model.language {
2316 return Err(ReportError::InvalidArchive(format!(
2317 "frontend language {} differs from coverage model language {}",
2318 frontend.language, coverage_model.language
2319 )));
2320 }
2321 if frontend.frontend_version == "rust-compiler-v1" {
2322 validate_rust_compiler_scope(&manifest)?;
2323 }
2324 let normalized = CoverageReportRequest {
2325 run_id: request.run_id.clone(),
2326 manifest,
2327 raw_results,
2328 generated_at: request.generated_at.clone(),
2329 coverage_model: Some(coverage_model),
2330 integrity: request.integrity.clone(),
2331 test_exit_code: request.test_exit_code.clone(),
2332 };
2333 let mut report = crate::frontend_protocol::analyze_frontend_results(&frontend, &normalized)
2334 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2335 report.view.transport = Some(transport.clone());
2336 report.filters.passed.transport = Some(transport.clone());
2337 report.filters.failed.transport = Some(transport);
2338 Ok(report)
2339}
2340
2341#[cfg(test)]
2342mod tests {
2343 use std::{
2344 fs,
2345 sync::atomic::{AtomicU64, Ordering},
2346 time::SystemTime,
2347 };
2348
2349 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
2350
2351 use super::*;
2352
2353 static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
2354
2355 fn point(id: &str, line: usize) -> PointMeta {
2356 PointMeta {
2357 id: id.into(),
2358 kind: PointKind::Statement,
2359 file: "src/app.js".into(),
2360 line,
2361 column: 0,
2362 source: "work();".into(),
2363 label: None,
2364 }
2365 }
2366
2367 fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
2368 RawTestResult {
2369 test_id: Some(id.into()),
2370 scope: None,
2371 test: id.into(),
2372 test_file: Some("tests/app.test.js".into()),
2373 title: None,
2374 retry: Some(retry),
2375 status: Some(status.into()),
2376 expected_status: None,
2377 flaky: false,
2378 provenance: TestProvenance {
2379 runner: "node:test".into(),
2380 kind: "unit".into(),
2381 project: None,
2382 source: "runner-default".into(),
2383 },
2384 role: "test".into(),
2385 phases: vec![],
2386 runtime: vec![RuntimeSnapshot {
2387 decisions: vec![],
2388 hits: hits.iter().map(|hit| (*hit).into()).collect(),
2389 events: vec![],
2390 }],
2391 browser: vec![],
2392 server: vec![],
2393 }
2394 }
2395
2396 #[test]
2397 fn declined_obligations_are_unmeasured_never_uncovered() {
2398 let point = |id: &str, line: usize| PointMeta {
2402 id: id.into(),
2403 kind: PointKind::Statement,
2404 file: "src/app.js".into(),
2405 line,
2406 column: 0,
2407 source: "work();".into(),
2408 label: None,
2409 };
2410 let measured_only = CoverageManifest {
2411 decisions: vec![],
2412 points: vec![point("measured", 1)],
2413 branches: vec![],
2414 limitations: vec![],
2415 unmeasured: Vec::new(),
2416 scope: None,
2417 };
2418 let with_declined = CoverageManifest {
2419 decisions: vec![],
2420 points: vec![point("measured", 1), point("declined", 2)],
2421 branches: vec![],
2422 limitations: vec![],
2423 unmeasured: vec!["declined".into()],
2424 scope: None,
2425 };
2426
2427 let baseline =
2428 create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
2429 let view =
2430 create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
2431
2432 assert_eq!(
2434 view.summary.statements.total, baseline.summary.statements.total,
2435 "a declined obligation stayed in the covered/uncovered denominator"
2436 );
2437 assert_eq!(view.summary.unmeasured_obligations, Some(1));
2438 assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
2439
2440 assert_eq!(
2444 view.summary.lines.total, baseline.summary.lines.total,
2445 "a declined obligation kept its line in the line denominator"
2446 );
2447 let declined_line = view
2448 .lines
2449 .iter()
2450 .find(|line| line.line == 2)
2451 .expect("the declined line stays addressable");
2452 assert!(!declined_line.measured);
2453 assert!(!declined_line.covered);
2454
2455 assert_eq!(baseline.summary.unmeasured_obligations, None);
2458 assert_eq!(baseline.summary.exact_fraction_pct, None);
2459 let encoded = serde_json::to_string(&baseline.summary).unwrap();
2460 assert!(
2461 !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
2462 "a fully measured summary must serialize unchanged: {encoded}"
2463 );
2464 }
2465
2466 #[test]
2467 fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
2468 let point = |id: &str| PointMeta {
2472 id: id.into(),
2473 kind: PointKind::Statement,
2474 file: "src/app.js".into(),
2475 line: 1,
2476 column: 0,
2477 source: "work();".into(),
2478 label: None,
2479 };
2480 let manifest = CoverageManifest {
2481 decisions: vec![],
2482 points: vec![point("declined")],
2483 branches: vec![],
2484 limitations: vec![],
2485 unmeasured: vec!["declined".into()],
2486 scope: None,
2487 };
2488 let view = create_coverage_view(
2489 &manifest,
2490 &[raw("test", 0, "passed", &["declined"])],
2491 "time",
2492 )
2493 .unwrap();
2494
2495 assert_eq!(view.summary.lines.total, 0);
2496 assert_eq!(view.summary.lines.covered, 0);
2497 let line = view.lines.first().expect("the line stays addressable");
2498 assert!(!line.measured);
2499 assert!(!line.covered, "an unmeasured line is not covered either");
2500 }
2501
2502 #[test]
2503 fn report_retains_unexecuted_manifest_conditions() {
2504 let manifest = CoverageManifest {
2505 unmeasured: Vec::new(),
2506 decisions: vec![DecisionMeta {
2507 id: "decision".into(),
2508 file: "src/app.js".into(),
2509 line: 1,
2510 column: 0,
2511 source: "left && right".into(),
2512 conditions: vec!["left".into(), "right".into()],
2513 kind: "if".into(),
2514 }],
2515 points: vec![],
2516 branches: vec![],
2517 limitations: vec![],
2518 scope: None,
2519 };
2520 let view =
2521 create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
2522 assert_eq!(view.summary.conditions, 2);
2523 assert_eq!(view.summary.covered_conditions, 0);
2524 assert_eq!(view.decisions[0].conditions.len(), 2);
2525 }
2526
2527 #[test]
2528 fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
2529 let manifest = CoverageManifest {
2530 unmeasured: Vec::new(),
2531 decisions: vec![DecisionMeta {
2532 id: "application-decision".into(),
2533 file: "src/app.js".into(),
2534 line: 1,
2535 column: 0,
2536 source: "left && right".into(),
2537 conditions: vec!["left".into(), "right".into()],
2538 kind: "if".into(),
2539 }],
2540 points: vec![],
2541 branches: vec![],
2542 limitations: vec![],
2543 scope: None,
2544 };
2545 let mut attempt = raw("test", 0, "passed", &[]);
2546 attempt.runtime[0].decisions.push(DecisionSnapshot {
2547 meta: DecisionMeta {
2548 id: "synthetic-fixture".into(),
2549 file: "fixtures/generated.js".into(),
2550 line: 1,
2551 column: 0,
2552 source: "a && b && c".into(),
2553 conditions: vec!["a".into(), "b".into(), "c".into()],
2554 kind: "if".into(),
2555 },
2556 vectors: vec![McdcVector {
2557 values: vec![Some(true), Some(true), Some(true)],
2558 outcome: true,
2559 }],
2560 });
2561
2562 let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2563 assert_eq!(view.decisions.len(), 1);
2564 assert_eq!(view.decisions[0].meta.id, "application-decision");
2565 assert!(view.decisions[0].vectors.is_empty());
2566 }
2567
2568 #[test]
2569 fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
2570 let expected = DecisionMeta {
2571 id: "application-decision".into(),
2572 file: "src/app.js".into(),
2573 line: 1,
2574 column: 0,
2575 source: "left && right".into(),
2576 conditions: vec!["left".into(), "right".into()],
2577 kind: "if".into(),
2578 };
2579 let manifest = CoverageManifest {
2580 unmeasured: Vec::new(),
2581 decisions: vec![expected.clone()],
2582 points: vec![],
2583 branches: vec![],
2584 limitations: vec![],
2585 scope: Some(serde_json::json!({
2586 "entries": [{ "file": "src/empty.js", "status": "included" }]
2587 })),
2588 };
2589 let vector = McdcVector {
2590 values: vec![Some(true), Some(true)],
2591 outcome: true,
2592 };
2593
2594 let mut unknown = raw("test", 0, "passed", &[]);
2595 unknown.runtime[0].decisions.push(DecisionSnapshot {
2596 meta: DecisionMeta {
2597 id: "unknown-decision".into(),
2598 file: "src/empty.js".into(),
2599 ..expected.clone()
2600 },
2601 vectors: vec![vector.clone()],
2602 });
2603 assert!(matches!(
2604 create_coverage_view(&manifest, &[unknown], "time"),
2605 Err(ReportError::InvalidServerRecord(reason))
2606 if reason.contains("absent from the frozen manifest")
2607 ));
2608
2609 let mut changed = raw("test", 0, "passed", &[]);
2610 changed.runtime[0].decisions.push(DecisionSnapshot {
2611 meta: DecisionMeta {
2612 source: "left || right".into(),
2613 ..expected
2614 },
2615 vectors: vec![vector],
2616 });
2617 assert!(matches!(
2618 create_coverage_view(&manifest, &[changed], "time"),
2619 Err(ReportError::InvalidServerRecord(reason))
2620 if reason.contains("differs from the frozen manifest")
2621 ));
2622 }
2623
2624 #[test]
2625 fn timestamp_overlap_cannot_upgrade_assertion_confidence() {
2626 let manifest = CoverageManifest {
2627 unmeasured: Vec::new(),
2628 decisions: vec![],
2629 points: vec![point("hit", 1)],
2630 branches: vec![],
2631 limitations: vec![],
2632 scope: None,
2633 };
2634 let phase = CoveragePhase {
2635 id: "assertion".into(),
2636 kind: "assertion".into(),
2637 operation: "equal".into(),
2638 source: None,
2639 caused_by_phase_id: None,
2640 started_at_ms: 100,
2641 ended_at_ms: Some(120),
2642 status: Some("passed".into()),
2643 error: None,
2644 };
2645 let mut attempt = raw("test", 0, "passed", &["hit"]);
2646 attempt.phases.push(phase.clone());
2647 attempt.runtime[0].events.push(RuntimeEvent {
2648 event_type: "hit".into(),
2649 id: "hit".into(),
2650 vector: None,
2651 timestamp_ms: 110,
2652 phase_id: None,
2653 environment: "server".into(),
2654 });
2655 let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
2656 assert_eq!(inferred.points[0].confidence.level, "executed");
2657 assert!(!inferred.points[0].confidence.asserted);
2658
2659 attempt.runtime[0].events[0].phase_id = Some(phase.id);
2660 let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2661 assert_eq!(explicit.points[0].confidence.level, "asserted");
2662 assert!(explicit.points[0].confidence.asserted);
2663 }
2664
2665 #[test]
2666 fn verified_view_uses_only_the_terminal_successful_attempt() {
2667 let manifest = CoverageManifest {
2668 unmeasured: Vec::new(),
2669 decisions: vec![],
2670 points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
2671 branches: vec![],
2672 limitations: vec![],
2673 scope: None,
2674 };
2675 let failed = raw("flaky", 0, "failed", &["failed"]);
2676 let mut passed = raw("flaky", 1, "passed", &["passed"]);
2677 passed.flaky = true;
2678 let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
2679 expected.expected_status = Some("failed".into());
2680 let request = CoverageReportRequest {
2681 run_id: "run".into(),
2682 manifest,
2683 raw_results: vec![failed, passed, expected],
2684 generated_at: "time".into(),
2685 coverage_model: None,
2686 integrity: None,
2687 test_exit_code: ExitCodeInput::Missing,
2688 };
2689 let report = analyze_coverage_results(&request).unwrap();
2690 assert!(!report.filters.passed.points[0].covered);
2691 assert!(report.filters.passed.points[1].covered);
2692 assert!(!report.filters.passed.points[2].covered);
2693 assert!(report.filters.failed.points[0].covered);
2694 assert_eq!(
2695 report
2696 .view
2697 .tests
2698 .iter()
2699 .find(|test| test.name == "expected-failure")
2700 .unwrap()
2701 .outcome,
2702 "failed"
2703 );
2704 assert_eq!(report.view.tests[1].outcome, "flaky");
2705 }
2706
2707 #[test]
2708 fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
2709 let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
2710 expected.expected_status = Some("failed".into());
2711 let companion = raw("expected-failure", 0, "failed", &["expected"]);
2712 let request = CoverageReportRequest {
2713 run_id: "run".into(),
2714 manifest: CoverageManifest {
2715 unmeasured: Vec::new(),
2716 decisions: vec![],
2717 points: vec![point("expected", 1)],
2718 branches: vec![],
2719 limitations: vec![],
2720 scope: None,
2721 },
2722 raw_results: vec![companion, expected],
2723 generated_at: "time".into(),
2724 coverage_model: None,
2725 integrity: None,
2726 test_exit_code: ExitCodeInput::Missing,
2727 };
2728 let report = analyze_coverage_results(&request).unwrap();
2729 assert_eq!(report.view.tests[0].outcome, "passed");
2730 assert!(!report.filters.passed.points[0].covered);
2731 assert!(!report.filters.failed.points[0].covered);
2732 }
2733
2734 #[test]
2735 fn selected_but_unstarted_test_is_not_an_invented_attempt() {
2736 let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
2737 unstarted.scope = None;
2738 unstarted.retry = None;
2739 unstarted.runtime.clear();
2740 let request = CoverageReportRequest {
2741 run_id: "run".into(),
2742 manifest: CoverageManifest {
2743 unmeasured: Vec::new(),
2744 decisions: vec![],
2745 points: vec![],
2746 branches: vec![],
2747 limitations: vec![],
2748 scope: None,
2749 },
2750 raw_results: vec![unstarted],
2751 generated_at: "time".into(),
2752 coverage_model: None,
2753 integrity: None,
2754 test_exit_code: ExitCodeInput::Present(Some(100)),
2755 };
2756 let report = analyze_coverage_results(&request).unwrap();
2757 assert_eq!(report.view.tests[0].outcome, "unstarted");
2758 assert!(report.view.tests[0].attempts.is_empty());
2759 assert!(report.filters.passed.tests.is_empty());
2760 assert!(report.filters.failed.tests.is_empty());
2761 }
2762
2763 fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
2764 let nonce = SystemTime::now()
2765 .duration_since(SystemTime::UNIX_EPOCH)
2766 .unwrap()
2767 .as_nanos();
2768 let root = std::env::temp_dir().join(format!(
2769 "supercov-rust-report-{}-{nonce}-{}",
2770 std::process::id(),
2771 ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
2772 ));
2773 fs::create_dir_all(&root).unwrap();
2774 let path = root.join("evidence.raw.gz");
2775 if !entries
2776 .iter()
2777 .any(|entry| entry.path == "coverage-model.json")
2778 {
2779 entries.push(EvidenceArchiveEntry {
2780 path: "coverage-model.json".into(),
2781 contents: serde_json::to_vec(
2782 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
2783 .unwrap(),
2784 )
2785 .unwrap(),
2786 });
2787 }
2788 if !entries.iter().any(|entry| entry.path == "frontend.json") {
2789 entries.push(EvidenceArchiveEntry {
2790 path: "frontend.json".into(),
2791 contents: serde_json::to_vec(&serde_json::json!({
2792 "protocolVersion": 2,
2793 "frontendId": "javascript",
2794 "frontendVersion": "fixture-v1",
2795 "language": "javascript",
2796 "structuralSource": "owned-probes",
2797 "runners": [{
2798 "runner": "node:test",
2799 "executionModel": "serial-in-process",
2800 "attribution": {
2801 "run": "exact",
2802 "worker": "unavailable",
2803 "test": "exact",
2804 "retry": "exact",
2805 "phase": "exact",
2806 "action": "exact",
2807 "assertion": "exact"
2808 },
2809 "limitations": [{
2810 "id": "fixture-worker-unavailable",
2811 "scopes": ["worker"],
2812 "reason": "The fixture does not require worker identity"
2813 }]
2814 }],
2815 "structuralLimitations": []
2816 }))
2817 .unwrap(),
2818 });
2819 }
2820 write_archive(entries, &path).unwrap();
2821 path
2822 }
2823
2824 #[test]
2825 fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
2826 let manifest = CoverageManifest {
2827 unmeasured: Vec::new(),
2828 decisions: vec![],
2829 points: vec![point("background-hit", 1), point("test-hit", 2)],
2830 branches: vec![],
2831 limitations: vec![],
2832 scope: None,
2833 };
2834 let unattributed = |axis: &str| {
2837 serde_json::json!({
2838 "id": format!("background-no-{axis}"),
2839 "scopes": [axis],
2840 "reason": format!("Runner background did not expose exact {axis} identity for every result")
2841 })
2842 };
2843 let frontend = serde_json::json!({
2844 "protocolVersion": 2,
2845 "frontendId": "javascript",
2846 "frontendVersion": "fixture-v1",
2847 "language": "javascript",
2848 "structuralSource": "owned-probes",
2849 "runners": [
2850 {
2851 "runner": "node:test",
2852 "executionModel": "serial-in-process",
2853 "attribution": {
2854 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2855 "phase": "exact", "action": "exact", "assertion": "exact"
2856 },
2857 "limitations": [{
2858 "id": "fixture-worker-unavailable",
2859 "scopes": ["worker"],
2860 "reason": "The fixture does not require worker identity"
2861 }]
2862 },
2863 {
2864 "runner": "background",
2865 "executionModel": "parallel-unattributed",
2866 "attribution": {
2867 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2868 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2869 },
2870 "limitations": [
2871 unattributed("worker"), unattributed("test"), unattributed("retry"),
2872 unattributed("phase"), unattributed("action"), unattributed("assertion")
2873 ]
2874 }
2875 ],
2876 "structuralLimitations": []
2877 });
2878 let path = archive(vec![
2879 EvidenceArchiveEntry {
2880 path: "manifest.json".into(),
2881 contents: serde_json::to_vec(&manifest).unwrap(),
2882 },
2883 EvidenceArchiveEntry {
2884 path: "frontend.json".into(),
2885 contents: serde_json::to_vec(&frontend).unwrap(),
2886 },
2887 EvidenceArchiveEntry {
2888 path: "playwright-worker-1.mcdc.jsonl".into(),
2889 contents: {
2890 let mut contents =
2891 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2892 .unwrap();
2893 contents.extend_from_slice(b"\npartial-final-line");
2894 contents
2895 },
2896 },
2897 EvidenceArchiveEntry {
2898 path: "server/background/worker.jsonl".into(),
2899 contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
2900 },
2901 ]);
2902 let result = analyze_coverage_archive(&ArchiveReportRequest {
2903 archive_path: path.clone(),
2904 run_id: "run".into(),
2905 generated_at: "time".into(),
2906 integrity: None,
2907 test_exit_code: ExitCodeInput::Missing,
2908 });
2909 assert!(matches!(
2910 result,
2911 Err(ReportError::InvalidJson { path, .. })
2912 if path == "playwright-worker-1.mcdc.jsonl"
2913 ));
2914 fs::remove_dir_all(path.parent().unwrap()).unwrap();
2915 }
2916
2917 #[test]
2918 fn a_torn_background_record_is_counted_not_fatal() {
2919 let manifest = CoverageManifest {
2924 unmeasured: Vec::new(),
2925 decisions: vec![],
2926 points: vec![],
2927 branches: vec![],
2928 limitations: vec![],
2929 scope: None,
2930 };
2931 let unattributed = |axis: &str| {
2934 serde_json::json!({
2935 "id": format!("background-no-{axis}"),
2936 "scopes": [axis],
2937 "reason": format!("Runner background did not expose exact {axis} identity for every result")
2938 })
2939 };
2940 let frontend = serde_json::json!({
2941 "protocolVersion": 2,
2942 "frontendId": "javascript",
2943 "frontendVersion": "fixture-v1",
2944 "language": "javascript",
2945 "structuralSource": "owned-probes",
2946 "runners": [
2947 {
2948 "runner": "node:test",
2949 "executionModel": "serial-in-process",
2950 "attribution": {
2951 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2952 "phase": "exact", "action": "exact", "assertion": "exact"
2953 },
2954 "limitations": [{
2955 "id": "fixture-worker-unavailable",
2956 "scopes": ["worker"],
2957 "reason": "The fixture does not require worker identity"
2958 }]
2959 },
2960 {
2961 "runner": "background",
2962 "executionModel": "parallel-unattributed",
2963 "attribution": {
2964 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2965 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2966 },
2967 "limitations": [
2968 unattributed("worker"), unattributed("test"), unattributed("retry"),
2969 unattributed("phase"), unattributed("action"), unattributed("assertion")
2970 ]
2971 }
2972 ],
2973 "structuralLimitations": []
2974 });
2975 let path = archive(vec![
2976 EvidenceArchiveEntry {
2977 path: "manifest.json".into(),
2978 contents: serde_json::to_vec(&manifest).unwrap(),
2979 },
2980 EvidenceArchiveEntry {
2981 path: "frontend.json".into(),
2982 contents: serde_json::to_vec(&frontend).unwrap(),
2983 },
2984 EvidenceArchiveEntry {
2985 path: "playwright-worker-1.mcdc.jsonl".into(),
2986 contents: {
2987 let mut contents =
2988 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2989 .unwrap();
2990 contents.push(b'\n');
2991 contents
2992 },
2993 },
2994 EvidenceArchiveEntry {
2995 path: "execution.37360-1.168.jsonl".into(),
2996 contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
2997 },
2998 EvidenceArchiveEntry {
2999 path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
3000 contents: concat!(
3001 "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
3002 "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
3003 "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
3004 )
3005 .as_bytes()
3006 .to_vec(),
3007 },
3008 ]);
3009 let report = analyze_coverage_archive(&ArchiveReportRequest {
3010 archive_path: path.clone(),
3011 run_id: "run".into(),
3012 generated_at: "time".into(),
3013 integrity: None,
3014 test_exit_code: ExitCodeInput::Missing,
3015 })
3016 .expect("a torn evidence line must not fail the whole run");
3017 let transport = report.view.transport.expect("transport stats");
3018 assert_eq!(transport.corrupt_records, 2);
3019 assert_eq!(transport.corrupt_files, 2);
3020 assert_eq!(transport.background_server_records, 2);
3021 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3022 }
3023
3024 #[test]
3025 fn archive_analysis_rejects_cross_run_evidence() {
3026 let manifest = CoverageManifest {
3027 unmeasured: Vec::new(),
3028 decisions: vec![],
3029 points: vec![],
3030 branches: vec![],
3031 limitations: vec![],
3032 scope: None,
3033 };
3034 let mut result = raw("test", 0, "passed", &[]);
3035 result.scope = Some(ExecutionScope {
3036 version: 1,
3037 run_id: "other-run".into(),
3038 worker_id: "worker".into(),
3039 test_id: "test".into(),
3040 test_key: "key".into(),
3041 retry: 0,
3042 attempt_id: "attempt".into(),
3043 });
3044 let path = archive(vec![
3045 EvidenceArchiveEntry {
3046 path: "manifest.json".into(),
3047 contents: serde_json::to_vec(&manifest).unwrap(),
3048 },
3049 EvidenceArchiveEntry {
3050 path: "worker/mcdc.json".into(),
3051 contents: serde_json::to_vec(&result).unwrap(),
3052 },
3053 ]);
3054 assert!(matches!(
3055 analyze_coverage_archive(&ArchiveReportRequest {
3056 archive_path: path.clone(),
3057 run_id: "run".into(),
3058 generated_at: "time".into(),
3059 integrity: None,
3060 test_exit_code: ExitCodeInput::Missing,
3061 }),
3062 Err(ReportError::InvalidArchive(reason))
3063 if reason.contains("expected=run actual=other-run")
3064 ));
3065 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3066 }
3067
3068 #[test]
3069 fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
3070 let vectors: Value = serde_json::from_str(include_str!(
3071 "../test-assets/coverage-model-v1/vectors.json"
3072 ))
3073 .unwrap();
3074 for value in vectors["valid"].as_array().unwrap() {
3075 let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
3076 model.into_declaration().unwrap();
3077 }
3078 for vector in vectors["invalid"].as_array().unwrap() {
3079 let value = vector["value"].clone();
3080 if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
3081 assert!(
3082 model.into_declaration().is_err(),
3083 "accepted invalid model vector: {}",
3084 vector["reason"]
3085 );
3086 }
3087 }
3088
3089 let manifest = CoverageManifest {
3090 unmeasured: Vec::new(),
3091 decisions: vec![],
3092 points: vec![],
3093 branches: vec![],
3094 limitations: vec![],
3095 scope: None,
3096 };
3097 let path = archive(vec![
3098 EvidenceArchiveEntry {
3099 path: "coverage-model.json".into(),
3100 contents: serde_json::to_vec(&PersistedCoverageModel {
3101 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
3102 language: "rust".into(),
3103 variant: "rust-source-v1".into(),
3104 name: "Rust source coverage".into(),
3105 completeness_meaning: "Every Rust obligation was satisfied.".into(),
3106 measured: vec!["Rust statements".into()],
3107 not_measured: vec![],
3108 })
3109 .unwrap(),
3110 },
3111 EvidenceArchiveEntry {
3112 path: "manifest.json".into(),
3113 contents: serde_json::to_vec(&manifest).unwrap(),
3114 },
3115 ]);
3116 assert!(matches!(
3117 analyze_coverage_archive(&ArchiveReportRequest {
3118 archive_path: path.clone(),
3119 run_id: "run".into(),
3120 generated_at: "time".into(),
3121 integrity: None,
3122 test_exit_code: ExitCodeInput::Missing,
3123 }),
3124 Err(ReportError::InvalidArchive(reason))
3125 if reason.contains("javascript differs from coverage model language rust")
3126 ));
3127 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3128 }
3129
3130 #[test]
3131 fn rust_compiler_scope_requires_an_exact_full_source_fingerprint() {
3132 let mut manifest = CoverageManifest {
3133 unmeasured: Vec::new(),
3134 decisions: vec![],
3135 points: vec![],
3136 branches: vec![],
3137 limitations: vec![],
3138 scope: Some(serde_json::json!({
3139 "language": "rust",
3140 "model": "rust-source-v1",
3141 "crate": "fixture",
3142 "measurementComplete": false,
3143 "sourceFingerprint": {
3144 "algorithm": "sha256",
3145 "digest": "1".repeat(64),
3146 "files": 2,
3147 "generatedFiles": 1,
3148 },
3149 })),
3150 };
3151 validate_rust_compiler_scope(&manifest).unwrap();
3152
3153 manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3154 Value::String("not-a-digest".into());
3155 assert!(matches!(
3156 validate_rust_compiler_scope(&manifest),
3157 Err(ReportError::InvalidArchive(reason))
3158 if reason == "malformed Rust compiler source fingerprint"
3159 ));
3160
3161 manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3162 Value::String("1".repeat(64));
3163 manifest.scope.as_mut().unwrap()["sourceFingerprint"]["unexpected"] = Value::Bool(true);
3164 assert!(validate_rust_compiler_scope(&manifest).is_err());
3165 }
3166
3167 #[test]
3168 fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
3169 let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
3170 "runId": "run",
3171 "manifest": { "decisions": [], "points": [], "branches": [] },
3172 "rawResults": [{
3173 "test": "test",
3174 "status": "passed",
3175 "browser": [],
3176 "server": []
3177 }],
3178 "generatedAt": "time",
3179 "testExitCode": null
3180 }))
3181 .unwrap();
3182 let report = analyze_coverage_results(&request).unwrap();
3183 assert_eq!(
3184 report.execution,
3185 Some(ExecutionResult {
3186 test_exit_code: None,
3187 valid: false,
3188 })
3189 );
3190 }
3191}