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