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