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