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