1use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use supercov_contracts::{
15 COVERAGE_MODEL_SCHEMA_VERSION, EVIDENCE_ARCHIVE_V3_SCHEMA_VERSION, FrontendRunDeclaration,
16};
17
18use crate::coverage_analysis::{
19 AnalysisError, BranchCoverage, CoverageCoreInput, CoverageSummary, DecisionCoverage,
20 McdcVector, PointCoverage, PointKind, analyze_core, find_witnesses_for_conditions,
21};
22use crate::evidence_archive::{EvidenceArchiveEntry, read_versioned_archive};
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26pub struct DecisionMeta {
27 pub id: String,
28 pub file: String,
29 pub line: usize,
30 pub column: usize,
31 pub source: String,
32 pub conditions: Vec<String>,
33 pub kind: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase", deny_unknown_fields)]
38pub struct PointMeta {
39 pub id: String,
40 pub kind: PointKind,
41 pub file: String,
42 pub line: usize,
43 pub column: usize,
44 pub source: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub label: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51pub struct BranchAlternativeMeta {
52 pub id: String,
53 pub label: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58pub struct BranchMeta {
59 pub id: String,
60 pub kind: String,
61 pub file: String,
62 pub line: usize,
63 pub column: usize,
64 pub source: String,
65 pub alternatives: Vec<BranchAlternativeMeta>,
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70pub struct CoverageManifest {
71 pub decisions: Vec<DecisionMeta>,
72 pub points: Vec<PointMeta>,
73 pub branches: Vec<BranchMeta>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub limitations: Vec<Value>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub scope: Option<Value>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase", deny_unknown_fields)]
82pub struct DecisionSnapshot {
83 pub meta: DecisionMeta,
84 pub vectors: Vec<McdcVector>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89pub struct RuntimeEvent {
90 #[serde(rename = "type")]
91 pub event_type: String,
92 pub id: String,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub vector: Option<McdcVector>,
95 pub timestamp_ms: i64,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub phase_id: Option<String>,
98 pub environment: String,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103pub struct RuntimeSnapshot {
104 #[serde(default)]
105 pub decisions: Vec<DecisionSnapshot>,
106 #[serde(default)]
107 pub hits: Vec<String>,
108 #[serde(default)]
109 pub events: Vec<RuntimeEvent>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase", deny_unknown_fields)]
114pub struct ExecutionScope {
115 pub version: usize,
116 pub run_id: String,
117 pub worker_id: String,
118 pub test_id: String,
119 pub test_key: String,
120 pub retry: usize,
121 pub attempt_id: String,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase", deny_unknown_fields)]
126pub struct ServerRecord {
127 #[serde(rename = "type")]
128 pub record_type: String,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub meta: Option<DecisionMeta>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub vector: Option<McdcVector>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub id: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub timestamp_ms: Option<i64>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub phase_id: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub scope: Option<ExecutionScope>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase", deny_unknown_fields)]
145pub struct CoveragePhase {
146 pub id: String,
147 pub kind: String,
148 pub operation: String,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub source: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub caused_by_phase_id: Option<String>,
153 pub started_at_ms: i64,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub ended_at_ms: Option<i64>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub status: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub error: Option<String>,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase", deny_unknown_fields)]
164pub struct TestProvenance {
165 pub runner: String,
166 pub kind: String,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub project: Option<String>,
169 pub source: String,
170}
171
172impl Default for TestProvenance {
173 fn default() -> Self {
174 Self {
175 runner: "unknown".into(),
176 kind: "unknown".into(),
177 project: None,
178 source: "unknown".into(),
179 }
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct RawTestResult {
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub test_id: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub scope: Option<ExecutionScope>,
190 pub test: String,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub test_file: Option<String>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub title: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub retry: Option<usize>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub status: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub expected_status: Option<String>,
201 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
202 pub flaky: bool,
203 #[serde(default)]
204 pub provenance: TestProvenance,
205 #[serde(default = "default_test_role")]
206 pub role: String,
207 #[serde(default)]
208 pub phases: Vec<CoveragePhase>,
209 #[serde(default)]
210 pub runtime: Vec<RuntimeSnapshot>,
211 #[serde(default)]
212 pub browser: Vec<RuntimeSnapshot>,
213 #[serde(default)]
214 pub server: Vec<ServerRecord>,
215}
216
217fn default_test_role() -> String {
218 "test".into()
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
222#[serde(rename_all = "camelCase")]
223pub struct TestAttempt {
224 pub retry: usize,
225 pub status: String,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub expected_status: Option<String>,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
231#[serde(rename_all = "camelCase")]
232pub struct CoverageConfidence {
233 pub level: String,
234 pub setup_only: bool,
235 pub background_only: bool,
236 pub asserted: bool,
237 pub tests: Vec<String>,
238 pub asserted_tests: Vec<String>,
239 pub runners: Vec<String>,
240 pub kinds: Vec<String>,
241 pub e2e: bool,
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize)]
245#[serde(rename_all = "camelCase")]
246pub struct VectorObservation {
247 pub vector: McdcVector,
248 pub tests: Vec<String>,
249 #[serde(skip_serializing_if = "Vec::is_empty")]
250 pub phases: Vec<String>,
251 #[serde(skip_serializing_if = "Vec::is_empty")]
252 pub explicit_phases: Vec<String>,
253 pub confidence: CoverageConfidence,
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize)]
257#[serde(rename_all = "camelCase")]
258pub struct ConditionResult {
259 pub index: usize,
260 pub source: String,
261 pub covered: bool,
262 pub assertion_covered: bool,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub witness: Option<[McdcVector; 2]>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub witness_tests: Option<[Vec<String>; 2]>,
267}
268
269#[derive(Debug, Clone, PartialEq, Serialize)]
270#[serde(rename_all = "camelCase")]
271pub struct DecisionResult {
272 pub meta: DecisionMeta,
273 pub executed: bool,
274 pub covered: bool,
275 pub vectors: Vec<McdcVector>,
276 pub vector_observations: Vec<VectorObservation>,
277 pub conditions: Vec<ConditionResult>,
278 pub tests: Vec<String>,
279 pub confidence: CoverageConfidence,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct PointResult {
285 pub meta: PointMeta,
286 pub covered: bool,
287 pub tests: Vec<String>,
288 pub phases: Vec<String>,
289 pub confidence: CoverageConfidence,
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize)]
293#[serde(rename_all = "camelCase")]
294pub struct AlternativeResult {
295 pub id: String,
296 pub label: String,
297 pub covered: bool,
298 pub tests: Vec<String>,
299 pub phases: Vec<String>,
300 pub confidence: CoverageConfidence,
301}
302
303#[derive(Debug, Clone, PartialEq, Serialize)]
304#[serde(rename_all = "camelCase")]
305pub struct BranchResult {
306 pub meta: BranchMeta,
307 pub covered: bool,
308 pub alternatives: Vec<AlternativeResult>,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
312pub struct SourceLine {
313 pub file: String,
314 pub line: usize,
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize)]
318#[serde(rename_all = "camelCase")]
319pub struct LineResult {
320 pub file: String,
321 pub line: usize,
322 pub covered: bool,
323 pub tests: Vec<String>,
324 pub runners: Vec<String>,
325 pub kinds: Vec<String>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub exclusive_kind: Option<String>,
328 pub phases: Vec<String>,
329 pub confidence: CoverageConfidence,
330}
331
332#[derive(Debug, Clone, PartialEq, Serialize)]
333#[serde(rename_all = "camelCase")]
334pub struct TestDecisionResult {
335 pub id: String,
336 pub vectors: Vec<McdcVector>,
337}
338
339#[derive(Debug, Clone, PartialEq, Serialize)]
340#[serde(rename_all = "camelCase")]
341pub struct TestCoverageResult {
342 pub id: String,
343 pub name: String,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub file: Option<String>,
346 #[serde(skip_serializing_if = "Option::is_none")]
347 pub title: Option<String>,
348 pub retries: Vec<usize>,
349 pub attempts: Vec<TestAttempt>,
350 pub outcome: String,
351 pub provenance: TestProvenance,
352 pub role: String,
353 pub hits: Vec<String>,
354 pub decisions: Vec<TestDecisionResult>,
355 pub lines: Vec<SourceLine>,
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct TestFileResult {
361 pub file: String,
362 pub tests: Vec<String>,
363 pub runners: Vec<String>,
364 pub kinds: Vec<String>,
365 pub lines: Vec<SourceLine>,
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize)]
369#[serde(rename_all = "camelCase")]
370pub struct PhaseResult {
371 #[serde(flatten)]
372 pub phase: CoveragePhase,
373 pub test: String,
374 pub hits: Vec<String>,
375 pub decisions: Vec<TestDecisionResult>,
376 pub lines: Vec<SourceLine>,
377 pub browser_events: usize,
378 pub server_events: usize,
379 pub explicit_events: usize,
380 pub inferred_events: usize,
381 pub explicit_browser_events: usize,
382 pub inferred_browser_events: usize,
383 pub explicit_server_events: usize,
384 pub inferred_server_events: usize,
385}
386
387#[derive(Debug, Clone, PartialEq, Serialize)]
388#[serde(rename_all = "camelCase")]
389pub struct DimensionCoverage {
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub kind: Option<String>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub runner: Option<String>,
394 pub tests: usize,
395 pub setups: usize,
396 pub summary: CoverageSummary,
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
400#[serde(rename_all = "camelCase")]
401pub struct CoverageModel {
402 pub name: String,
403 pub completeness_meaning: String,
404 pub measured: Vec<String>,
405 pub not_measured: Vec<String>,
406}
407
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase", deny_unknown_fields)]
410pub struct CoverageModelDeclaration {
411 pub variant: String,
412 pub name: String,
413 pub completeness_meaning: String,
414 pub measured: Vec<String>,
415 pub not_measured: Vec<String>,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase", deny_unknown_fields)]
420pub struct PersistedCoverageModel {
421 pub schema_version: u32,
422 pub variant: String,
423 pub name: String,
424 pub completeness_meaning: String,
425 pub measured: Vec<String>,
426 pub not_measured: Vec<String>,
427}
428
429impl PersistedCoverageModel {
430 pub fn from_declaration(value: &CoverageModelDeclaration) -> Self {
431 Self {
432 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
433 variant: value.variant.clone(),
434 name: value.name.clone(),
435 completeness_meaning: value.completeness_meaning.clone(),
436 measured: value.measured.clone(),
437 not_measured: value.not_measured.clone(),
438 }
439 }
440
441 fn into_declaration(self) -> Result<CoverageModelDeclaration, &'static str> {
442 if self.schema_version != COVERAGE_MODEL_SCHEMA_VERSION {
443 return Err("unsupported coverage model schema");
444 }
445 let fields = [
446 self.variant.as_str(),
447 self.name.as_str(),
448 self.completeness_meaning.as_str(),
449 ];
450 if fields.iter().any(|field| field.trim().is_empty())
451 || self.measured.is_empty()
452 || self
453 .measured
454 .iter()
455 .chain(&self.not_measured)
456 .any(|item| item.trim().is_empty())
457 || self.measured.iter().collect::<BTreeSet<_>>().len() != self.measured.len()
458 || self.not_measured.iter().collect::<BTreeSet<_>>().len() != self.not_measured.len()
459 {
460 return Err("invalid coverage model declaration");
461 }
462 Ok(CoverageModelDeclaration {
463 variant: self.variant,
464 name: self.name,
465 completeness_meaning: self.completeness_meaning,
466 measured: self.measured,
467 not_measured: self.not_measured,
468 })
469 }
470}
471
472#[derive(Debug, Clone, PartialEq, Serialize)]
473#[serde(rename_all = "camelCase")]
474pub struct CoverageView {
475 pub generated_at: String,
476 pub variant: String,
477 #[serde(skip_serializing_if = "Option::is_none")]
478 pub scope: Option<Value>,
479 pub model: CoverageModel,
480 #[serde(skip_serializing_if = "Option::is_none")]
481 pub integrity: Option<Value>,
482 pub limitations: Vec<Value>,
483 #[serde(skip_serializing_if = "Option::is_none")]
484 pub transport: Option<TransportStats>,
485 pub summary: CoverageSummary,
486 pub coverage_by_kind: Vec<DimensionCoverage>,
487 pub coverage_by_runner: Vec<DimensionCoverage>,
488 pub decisions: Vec<DecisionResult>,
489 pub points: Vec<PointResult>,
490 pub branches: Vec<BranchResult>,
491 pub tests: Vec<TestCoverageResult>,
492 pub test_files: Vec<TestFileResult>,
493 pub phases: Vec<PhaseResult>,
494 pub lines: Vec<LineResult>,
495}
496
497#[derive(Debug, Clone, PartialEq, Serialize)]
498#[serde(rename_all = "camelCase")]
499pub struct CoverageFilters {
500 pub passed: CoverageView,
501 pub failed: CoverageView,
502}
503
504#[derive(Debug, Clone, PartialEq, Serialize)]
505#[serde(rename_all = "camelCase")]
506pub struct CoverageReport {
507 #[serde(flatten)]
508 pub view: CoverageView,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 pub execution: Option<ExecutionResult>,
511 pub filters: CoverageFilters,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
515#[serde(rename_all = "camelCase")]
516pub struct ExecutionResult {
517 pub test_exit_code: Option<i32>,
518 pub valid: bool,
519}
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
522#[serde(rename_all = "camelCase")]
523pub struct TransportStats {
524 pub processes: usize,
525 pub child_launches: usize,
526 pub remote_launches: usize,
527 pub workspace_capabilities: usize,
528 pub scoped_server_records: usize,
529 pub background_server_records: usize,
530 pub corrupt_records: usize,
531 pub corrupt_files: usize,
532}
533
534#[derive(Debug, Clone, PartialEq, Deserialize)]
535#[serde(rename_all = "camelCase", deny_unknown_fields)]
536pub struct CoverageReportRequest {
537 pub run_id: String,
538 pub manifest: CoverageManifest,
539 pub raw_results: Vec<RawTestResult>,
540 pub generated_at: String,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub coverage_model: Option<CoverageModelDeclaration>,
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub integrity: Option<Value>,
545 #[serde(default, deserialize_with = "deserialize_exit_code")]
546 pub test_exit_code: ExitCodeInput,
547}
548
549#[derive(Debug, Clone, PartialEq, Deserialize)]
550#[serde(rename_all = "camelCase", deny_unknown_fields)]
551pub struct ArchiveReportRequest {
552 pub archive_path: PathBuf,
553 pub run_id: String,
554 pub generated_at: String,
555 #[serde(skip_serializing_if = "Option::is_none")]
556 pub integrity: Option<Value>,
557 #[serde(default, deserialize_with = "deserialize_exit_code")]
558 pub test_exit_code: ExitCodeInput,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Default)]
562pub enum ExitCodeInput {
563 #[default]
564 Missing,
565 Present(Option<i32>),
566}
567
568fn deserialize_exit_code<'de, D>(deserializer: D) -> Result<ExitCodeInput, D::Error>
569where
570 D: serde::Deserializer<'de>,
571{
572 Option::<i32>::deserialize(deserializer).map(ExitCodeInput::Present)
573}
574
575#[derive(Debug, Clone, PartialEq, Eq)]
576pub enum ReportError {
577 Analysis(AnalysisError),
578 DecisionAnalysis {
579 decision_id: String,
580 error: AnalysisError,
581 },
582 InvalidEvent(String),
583 InvalidServerRecord(String),
584 InvalidArchive(String),
585 MissingManifest,
586 InvalidJson {
587 path: String,
588 reason: String,
589 },
590 ScopeMismatch {
591 expected: String,
592 actual: String,
593 },
594 NoEvidence(String),
595}
596
597impl From<AnalysisError> for ReportError {
598 fn from(value: AnalysisError) -> Self {
599 Self::Analysis(value)
600 }
601}
602
603#[derive(Clone, Default)]
604struct OrderedVectors {
605 values: Vec<McdcVector>,
606 indexes: HashMap<String, usize>,
607}
608
609impl OrderedVectors {
610 fn insert(&mut self, vector: &McdcVector) -> usize {
611 let key = vector_key(vector);
612 if let Some(index) = self.indexes.get(&key) {
613 return *index;
614 }
615 let index = self.values.len();
616 self.values.push(vector.clone());
617 self.indexes.insert(key, index);
618 index
619 }
620}
621
622#[derive(Clone)]
623struct MutableObservation {
624 vector: McdcVector,
625 tests: BTreeSet<String>,
626 phases: BTreeSet<String>,
627 explicit_phases: BTreeSet<String>,
628}
629
630#[derive(Clone)]
631struct MutableTest {
632 id: String,
633 name: String,
634 file: Option<String>,
635 title: Option<String>,
636 retries: BTreeSet<usize>,
637 attempts: BTreeMap<usize, TestAttempt>,
638 runner_reported_flaky: bool,
639 provenance: TestProvenance,
640 role: String,
641 hits: BTreeSet<String>,
642 decisions: BTreeMap<String, OrderedVectors>,
643}
644
645#[derive(Clone)]
646struct MutablePhase {
647 phase: CoveragePhase,
648 test: String,
649 hits: BTreeSet<String>,
650 decisions: BTreeMap<String, OrderedVectors>,
651 browser_events: usize,
652 server_events: usize,
653 explicit_events: usize,
654 inferred_events: usize,
655 explicit_browser_events: usize,
656 inferred_browser_events: usize,
657 explicit_server_events: usize,
658 inferred_server_events: usize,
659}
660
661fn vector_key(vector: &McdcVector) -> String {
662 let mut key = String::with_capacity(vector.values.len() + 2);
663 for value in &vector.values {
664 key.push(match value {
665 None => '-',
666 Some(false) => 'F',
667 Some(true) => 'T',
668 });
669 }
670 key.push(':');
671 key.push(if vector.outcome { 'T' } else { 'F' });
672 key
673}
674
675fn sorted<T: Clone + Ord>(values: &BTreeSet<T>) -> Vec<T> {
676 values.iter().cloned().collect()
677}
678
679fn record_attempt(test: &mut MutableTest, raw: &RawTestResult) {
680 let (Some(retry), Some(raw_status)) = (raw.retry, raw.status.as_ref()) else {
681 return;
682 };
683 let previous = test.attempts.get(&retry);
684 let status = if raw_status == "unknown" {
685 previous.map_or_else(|| raw_status.clone(), |attempt| attempt.status.clone())
686 } else {
687 raw_status.clone()
688 };
689 let expected_status = raw
690 .expected_status
691 .clone()
692 .or_else(|| previous.and_then(|attempt| attempt.expected_status.clone()));
693 test.attempts.insert(
694 retry,
695 TestAttempt {
696 retry,
697 status,
698 expected_status,
699 },
700 );
701}
702
703fn test_outcome(test: &MutableTest) -> String {
704 let Some(terminal) = test.attempts.values().next_back() else {
705 return "unknown".into();
706 };
707 if terminal.status == "passed"
708 && (test.runner_reported_flaky
709 || test
710 .attempts
711 .values()
712 .take(test.attempts.len().saturating_sub(1))
713 .any(|attempt| attempt.status != "passed"))
714 {
715 "flaky".into()
716 } else {
717 terminal.status.clone()
718 }
719}
720
721fn raw_test_id(raw: &RawTestResult) -> &str {
722 raw.test_id.as_deref().unwrap_or(&raw.test)
723}
724
725pub fn passing_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
726 let mut attempts: BTreeMap<(String, usize), (BTreeSet<String>, bool)> = BTreeMap::new();
727 for raw in raw_results {
728 let entry = attempts
729 .entry((raw_test_id(raw).into(), raw.retry.unwrap_or(0)))
730 .or_default();
731 if let Some(status) = &raw.status {
732 entry.0.insert(status.clone());
733 }
734 entry.1 |= raw.expected_status.as_deref() == Some("failed");
735 }
736 let mut terminal_retries = BTreeMap::<String, usize>::new();
737 for (test, retry) in attempts.keys() {
738 terminal_retries
739 .entry(test.clone())
740 .and_modify(|value| *value = (*value).max(*retry))
741 .or_insert(*retry);
742 }
743 let accepted = terminal_retries
744 .into_iter()
745 .filter_map(|(test, retry)| {
746 let (statuses, expected_failure) = attempts.get(&(test.clone(), retry))?;
747 (statuses.contains("passed") && !expected_failure).then_some((test, retry))
748 })
749 .collect::<BTreeSet<_>>();
750 raw_results
751 .iter()
752 .filter(|raw| accepted.contains(&(raw_test_id(raw).into(), raw.retry.unwrap_or(0))))
753 .cloned()
754 .collect()
755}
756
757pub fn failed_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
758 let failed = raw_results
759 .iter()
760 .filter(|raw| raw.status.as_deref() == Some("failed"))
761 .map(|raw| (raw_test_id(raw).to_owned(), raw.retry.unwrap_or(0)))
762 .collect::<BTreeSet<_>>();
763 raw_results
764 .iter()
765 .filter(|raw| failed.contains(&(raw_test_id(raw).into(), raw.retry.unwrap_or(0))))
766 .cloned()
767 .collect()
768}
769
770fn javascript_coverage_model() -> CoverageModelDeclaration {
771 CoverageModelDeclaration {
772 variant: "masking-short-circuit".into(),
773 name: "coverage-completeness-v2".into(),
774 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(),
775 measured: [
776 "executable source lines",
777 "executable statements",
778 "function entries",
779 "true and false outcomes of if, ternary, while, do/while, and classic for decisions",
780 "true and false outcomes of every atomic condition in those decisions",
781 "masking MC/DC independence for every atomic condition in those decisions",
782 "short-circuit and right-evaluated selections for &&, ||, and ?? value expressions, including JSX",
783 "short-circuit and evaluated alternatives for logical assignments and optional chains",
784 "provided and default-evaluated parameter and destructuring values",
785 "try success and catch entry",
786 "zero and entered for-in/for-of loops",
787 "entered switch cases, defaults, and implicit no-match alternatives",
788 ]
789 .into_iter()
790 .map(str::to_owned)
791 .collect(),
792 not_measured: [
793 "all input values or semantic input partitions",
794 "all execution paths or ordering/concurrency interleavings",
795 "destructuring defaults in classic for initializers (reported as blockers when discovered)",
796 "the internal statements and decisions of runtime-generated eval/Function source",
797 "mutation score or assertion fault-detection strength",
798 ]
799 .into_iter()
800 .map(str::to_owned)
801 .collect(),
802 }
803}
804
805fn summary_for_results(
806 decisions: &[DecisionResult],
807 points: &[PointResult],
808 branches: &[BranchResult],
809 lines: &[LineResult],
810 test_ids: Option<&BTreeSet<String>>,
811) -> Result<CoverageSummary, ReportError> {
812 let includes = |tests: &[String], covered: bool| {
813 test_ids.map_or(covered, |selected| {
814 tests.iter().any(|test| selected.contains(test))
815 })
816 };
817 let input = CoverageCoreInput {
818 decisions: decisions
819 .iter()
820 .map(|decision| DecisionCoverage {
821 condition_count: decision.meta.conditions.len(),
822 vectors: decision
823 .vector_observations
824 .iter()
825 .filter(|observation| includes(&observation.tests, true))
826 .map(|observation| observation.vector.clone())
827 .collect(),
828 })
829 .collect(),
830 points: points
831 .iter()
832 .map(|point| PointCoverage {
833 kind: point.meta.kind.clone(),
834 covered: includes(&point.tests, point.covered),
835 })
836 .collect(),
837 branches: branches
838 .iter()
839 .map(|branch| BranchCoverage {
840 kind: branch.meta.kind.clone(),
841 alternatives: branch
842 .alternatives
843 .iter()
844 .map(|alternative| includes(&alternative.tests, alternative.covered))
845 .collect(),
846 })
847 .collect(),
848 lines: lines
849 .iter()
850 .map(|line| includes(&line.tests, line.covered))
851 .collect(),
852 };
853 Ok(analyze_core(&input)?.summary)
854}
855
856pub fn coverage_summary_for_tests(
861 view: &CoverageView,
862 test_ids: &BTreeSet<String>,
863) -> Result<CoverageSummary, ReportError> {
864 summary_for_results(
865 &view.decisions,
866 &view.points,
867 &view.branches,
868 &view.lines,
869 Some(test_ids),
870 )
871}
872
873fn confidence_for(
874 test_ids: impl IntoIterator<Item = String>,
875 phase_ids: impl IntoIterator<Item = String>,
876 explicit_phase_ids: impl IntoIterator<Item = String>,
877 tests: &HashMap<String, MutableTest>,
878 phases: &HashMap<String, MutablePhase>,
879 asserted_phase_ids: &BTreeSet<String>,
880) -> CoverageConfidence {
881 let test_ids = test_ids.into_iter().collect::<BTreeSet<_>>();
882 let phase_ids = phase_ids.into_iter().collect::<BTreeSet<_>>();
883 let explicit_phase_ids = explicit_phase_ids.into_iter().collect::<BTreeSet<_>>();
884 let asserted_phases = explicit_phase_ids
885 .iter()
886 .filter(|id| asserted_phase_ids.contains(*id))
887 .collect::<Vec<_>>();
888 let asserted_tests = asserted_phases
889 .iter()
890 .filter_map(|id| phases.get(*id).map(|phase| phase.test.clone()))
891 .collect::<BTreeSet<_>>();
892 let provenances = test_ids
893 .iter()
894 .filter_map(|id| tests.get(id).map(|test| &test.provenance))
895 .collect::<Vec<_>>();
896 let roles = test_ids
897 .iter()
898 .filter_map(|id| tests.get(id).map(|test| test.role.as_str()))
899 .collect::<Vec<_>>();
900 let phase_kinds = phase_ids
901 .iter()
902 .filter_map(|id| phases.get(id).map(|phase| phase.phase.kind.as_str()))
903 .collect::<Vec<_>>();
904 let only = |phase_kind: &str, role: &str| {
905 if phase_kinds.is_empty() {
906 !roles.is_empty() && roles.iter().all(|value| *value == role)
907 } else {
908 phase_kinds.iter().all(|value| *value == phase_kind)
909 }
910 };
911 let has_action = explicit_phase_ids.iter().any(|id| {
912 phases
913 .get(id)
914 .is_some_and(|phase| phase.phase.kind == "action")
915 });
916 let level = if test_ids.is_empty() {
917 "unexecuted"
918 } else if !asserted_tests.is_empty() {
919 "asserted"
920 } else if has_action {
921 "action"
922 } else {
923 "executed"
924 };
925 let runners = provenances
926 .iter()
927 .map(|provenance| provenance.runner.clone())
928 .collect::<BTreeSet<_>>();
929 let kinds = provenances
930 .iter()
931 .map(|provenance| provenance.kind.clone())
932 .collect::<BTreeSet<_>>();
933 CoverageConfidence {
934 level: level.into(),
935 setup_only: only("setup", "setup"),
936 background_only: only("background", "background"),
937 asserted: !asserted_tests.is_empty(),
938 tests: sorted(&test_ids),
939 asserted_tests: sorted(&asserted_tests),
940 runners: sorted(&runners),
941 e2e: kinds.contains("e2e"),
942 kinds: sorted(&kinds),
943 }
944}
945
946fn add_reference(map: &mut HashMap<String, BTreeSet<String>>, id: &str, value: &str) {
947 map.entry(id.into()).or_default().insert(value.into());
948}
949
950pub fn create_coverage_view(
951 manifest: &CoverageManifest,
952 raw_results: &[RawTestResult],
953 generated_at: &str,
954) -> Result<CoverageView, ReportError> {
955 create_coverage_view_with_model(
956 manifest,
957 raw_results,
958 generated_at,
959 &javascript_coverage_model(),
960 )
961}
962
963fn create_coverage_view_with_model(
964 manifest: &CoverageManifest,
965 raw_results: &[RawTestResult],
966 generated_at: &str,
967 coverage_model: &CoverageModelDeclaration,
968) -> Result<CoverageView, ReportError> {
969 let mut decision_metadata = manifest.decisions.clone();
970 let decision_indexes = decision_metadata
971 .iter()
972 .enumerate()
973 .map(|(index, meta)| (meta.id.clone(), index))
974 .collect::<HashMap<_, _>>();
975 let manifest_files = manifest
976 .decisions
977 .iter()
978 .map(|meta| meta.file.clone())
979 .chain(manifest.points.iter().map(|meta| meta.file.clone()))
980 .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
981 .chain(
982 manifest
983 .scope
984 .as_ref()
985 .and_then(|scope| scope.get("entries"))
986 .and_then(Value::as_array)
987 .into_iter()
988 .flatten()
989 .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
990 .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
991 )
992 .collect::<BTreeSet<_>>();
993 let mut vectors_by_decision = HashMap::<String, Vec<MutableObservation>>::new();
994 let mut vector_indexes = HashMap::<String, HashMap<String, usize>>::new();
995 let mut tests_by_decision = HashMap::<String, BTreeSet<String>>::new();
996 let mut tests_by_hit = HashMap::<String, BTreeSet<String>>::new();
997 let mut tests_by_id = HashMap::<String, MutableTest>::new();
998 let mut test_order = Vec::<String>::new();
999 let mut phases_by_id = HashMap::<String, MutablePhase>::new();
1000 let mut phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1001 let mut explicit_phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1002
1003 for raw in raw_results {
1004 let id = raw_test_id(raw).to_owned();
1005 if !tests_by_id.contains_key(&id) {
1006 test_order.push(id.clone());
1007 tests_by_id.insert(
1008 id.clone(),
1009 MutableTest {
1010 id: id.clone(),
1011 name: raw.test.clone(),
1012 file: raw.test_file.clone(),
1013 title: raw.title.clone(),
1014 retries: raw.retry.into_iter().collect(),
1015 attempts: BTreeMap::new(),
1016 runner_reported_flaky: raw.flaky,
1017 provenance: raw.provenance.clone(),
1018 role: raw.role.clone(),
1019 hits: BTreeSet::new(),
1020 decisions: BTreeMap::new(),
1021 },
1022 );
1023 }
1024 let test = tests_by_id.get_mut(&id).expect("test was inserted");
1025 if let Some(retry) = raw.retry {
1026 test.retries.insert(retry);
1027 }
1028 test.runner_reported_flaky |= raw.flaky;
1029 record_attempt(test, raw);
1030
1031 let mut ordered_phases = raw.phases.clone();
1032 ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1033 for phase in &ordered_phases {
1034 phases_by_id.insert(
1035 phase.id.clone(),
1036 MutablePhase {
1037 phase: phase.clone(),
1038 test: id.clone(),
1039 hits: BTreeSet::new(),
1040 decisions: BTreeMap::new(),
1041 browser_events: 0,
1042 server_events: 0,
1043 explicit_events: 0,
1044 inferred_events: 0,
1045 explicit_browser_events: 0,
1046 inferred_browser_events: 0,
1047 explicit_server_events: 0,
1048 inferred_server_events: 0,
1049 },
1050 );
1051 }
1052
1053 let correlate = |event: &RuntimeEvent| {
1054 event.phase_id.clone().or_else(|| {
1055 ordered_phases
1056 .iter()
1057 .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1058 .last()
1059 .map(|phase| phase.id.clone())
1060 })
1061 };
1062
1063 let snapshots = raw.runtime.iter().chain(&raw.browser);
1064 for snapshot in snapshots {
1065 for decision in &snapshot.decisions {
1066 let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1067 if manifest_files.contains(&decision.meta.file) {
1068 return Err(ReportError::InvalidServerRecord(format!(
1069 "decision {} is absent from the frozen manifest",
1070 decision.meta.id
1071 )));
1072 }
1073 continue;
1074 };
1075 if decision_metadata[index] != decision.meta {
1076 return Err(ReportError::InvalidServerRecord(format!(
1077 "decision {} metadata differs from the frozen manifest",
1078 decision.meta.id
1079 )));
1080 }
1081 for vector in &decision.vectors {
1082 let key = vector_key(vector);
1083 let indexes = vector_indexes.entry(decision.meta.id.clone()).or_default();
1084 let observations = vectors_by_decision
1085 .entry(decision.meta.id.clone())
1086 .or_default();
1087 let observation_index = *indexes.entry(key.clone()).or_insert_with(|| {
1088 observations.push(MutableObservation {
1089 vector: vector.clone(),
1090 tests: BTreeSet::new(),
1091 phases: BTreeSet::new(),
1092 explicit_phases: BTreeSet::new(),
1093 });
1094 observations.len() - 1
1095 });
1096 observations[observation_index].tests.insert(id.clone());
1097 tests_by_id
1098 .get_mut(&id)
1099 .expect("registered test")
1100 .decisions
1101 .entry(decision.meta.id.clone())
1102 .or_default()
1103 .insert(vector);
1104 }
1105 if !decision.vectors.is_empty() {
1106 add_reference(&mut tests_by_decision, &decision.meta.id, &id);
1107 }
1108 }
1109 for hit in &snapshot.hits {
1110 add_reference(&mut tests_by_hit, hit, &id);
1111 tests_by_id
1112 .get_mut(&id)
1113 .expect("registered test")
1114 .hits
1115 .insert(hit.clone());
1116 }
1117 for event in &snapshot.events {
1118 let explicit = event.phase_id.is_some();
1119 let Some(phase_id) = correlate(event) else {
1120 continue;
1121 };
1122 let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1123 continue;
1124 };
1125 if event.environment == "browser" {
1126 phase.browser_events += 1;
1127 if explicit {
1128 phase.explicit_browser_events += 1;
1129 } else {
1130 phase.inferred_browser_events += 1;
1131 }
1132 } else {
1133 phase.server_events += 1;
1134 if explicit {
1135 phase.explicit_server_events += 1;
1136 } else {
1137 phase.inferred_server_events += 1;
1138 }
1139 }
1140 if explicit {
1141 phase.explicit_events += 1;
1142 } else {
1143 phase.inferred_events += 1;
1144 }
1145 if event.event_type == "hit" {
1146 phase.hits.insert(event.id.clone());
1147 add_reference(&mut phases_by_hit, &event.id, &phase_id);
1148 if explicit {
1149 add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1150 }
1151 } else if event.event_type == "decision" {
1152 let vector = event
1153 .vector
1154 .as_ref()
1155 .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1156 phase
1157 .decisions
1158 .entry(event.id.clone())
1159 .or_default()
1160 .insert(vector);
1161 if let Some(index) = vector_indexes
1162 .get(&event.id)
1163 .and_then(|indexes| indexes.get(&vector_key(vector)))
1164 .copied()
1165 && let Some(observation) = vectors_by_decision
1166 .get_mut(&event.id)
1167 .and_then(|observations| observations.get_mut(index))
1168 {
1169 observation.phases.insert(phase_id.clone());
1170 if explicit {
1171 observation.explicit_phases.insert(phase_id.clone());
1172 }
1173 }
1174 } else {
1175 return Err(ReportError::InvalidEvent(event.event_type.clone()));
1176 }
1177 }
1178 }
1179
1180 for record in &raw.server {
1181 let (record_id, decision) = if record.record_type == "decision" {
1182 let meta = record
1183 .meta
1184 .as_ref()
1185 .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1186 let vector = record
1187 .vector
1188 .as_ref()
1189 .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1190 let Some(index) = decision_indexes.get(&meta.id).copied() else {
1191 if manifest_files.contains(&meta.file) {
1192 return Err(ReportError::InvalidServerRecord(format!(
1193 "decision {} is absent from the frozen manifest",
1194 meta.id
1195 )));
1196 }
1197 continue;
1198 };
1199 if decision_metadata[index] != *meta {
1200 return Err(ReportError::InvalidServerRecord(format!(
1201 "decision {} metadata differs from the frozen manifest",
1202 meta.id
1203 )));
1204 }
1205 let key = vector_key(vector);
1206 let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1207 let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1208 let index = *indexes.entry(key).or_insert_with(|| {
1209 observations.push(MutableObservation {
1210 vector: vector.clone(),
1211 tests: BTreeSet::new(),
1212 phases: BTreeSet::new(),
1213 explicit_phases: BTreeSet::new(),
1214 });
1215 observations.len() - 1
1216 });
1217 observations[index].tests.insert(id.clone());
1218 tests_by_id
1219 .get_mut(&id)
1220 .expect("registered test")
1221 .decisions
1222 .entry(meta.id.clone())
1223 .or_default()
1224 .insert(vector);
1225 add_reference(&mut tests_by_decision, &meta.id, &id);
1226 (meta.id.clone(), Some(vector.clone()))
1227 } else if record.record_type == "hit" {
1228 let hit = record
1229 .id
1230 .as_ref()
1231 .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1232 add_reference(&mut tests_by_hit, hit, &id);
1233 tests_by_id
1234 .get_mut(&id)
1235 .expect("registered test")
1236 .hits
1237 .insert(hit.clone());
1238 (hit.clone(), None)
1239 } else {
1240 return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1241 };
1242 let Some(timestamp_ms) = record.timestamp_ms else {
1243 continue;
1244 };
1245 let event = RuntimeEvent {
1246 event_type: record.record_type.clone(),
1247 id: record_id,
1248 vector: decision,
1249 timestamp_ms,
1250 phase_id: record.phase_id.clone(),
1251 environment: "server".into(),
1252 };
1253 let explicit = event.phase_id.is_some();
1254 let phase_id = correlate(&event);
1255 let Some(phase_id) = phase_id else { continue };
1256 let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1257 continue;
1258 };
1259 phase.server_events += 1;
1260 if explicit {
1261 phase.explicit_events += 1;
1262 phase.explicit_server_events += 1;
1263 } else {
1264 phase.inferred_events += 1;
1265 phase.inferred_server_events += 1;
1266 }
1267 if event.event_type == "hit" {
1268 phase.hits.insert(event.id.clone());
1269 add_reference(&mut phases_by_hit, &event.id, &phase_id);
1270 if explicit {
1271 add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1272 }
1273 } else if let Some(vector) = &event.vector {
1274 phase
1275 .decisions
1276 .entry(event.id.clone())
1277 .or_default()
1278 .insert(vector);
1279 if let Some(index) = vector_indexes
1280 .get(&event.id)
1281 .and_then(|indexes| indexes.get(&vector_key(vector)))
1282 .copied()
1283 && let Some(observation) = vectors_by_decision
1284 .get_mut(&event.id)
1285 .and_then(|observations| observations.get_mut(index))
1286 {
1287 observation.phases.insert(phase_id.clone());
1288 if explicit {
1289 observation.explicit_phases.insert(phase_id.clone());
1290 }
1291 }
1292 }
1293 }
1294 }
1295
1296 let mut asserted_phase_ids = BTreeSet::new();
1297 for phase in phases_by_id.values() {
1298 if phase.phase.kind == "assertion" && phase.phase.status.as_deref() == Some("passed") {
1299 asserted_phase_ids.insert(phase.phase.id.clone());
1300 if let Some(cause) = &phase.phase.caused_by_phase_id {
1301 asserted_phase_ids.insert(cause.clone());
1302 }
1303 }
1304 }
1305
1306 decision_metadata.sort_by(|left, right| {
1307 left.file
1308 .cmp(&right.file)
1309 .then(left.line.cmp(&right.line))
1310 .then(left.column.cmp(&right.column))
1311 });
1312 let mut decisions = Vec::with_capacity(decision_metadata.len());
1313 for meta in decision_metadata {
1314 let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
1315 let mut observations = Vec::with_capacity(mutable.len());
1316 for observation in mutable {
1317 let confidence = confidence_for(
1318 sorted(&observation.tests),
1319 sorted(&observation.phases),
1320 sorted(&observation.explicit_phases),
1321 &tests_by_id,
1322 &phases_by_id,
1323 &asserted_phase_ids,
1324 );
1325 observations.push(VectorObservation {
1326 vector: observation.vector,
1327 tests: sorted(&observation.tests),
1328 phases: sorted(&observation.phases),
1329 explicit_phases: sorted(&observation.explicit_phases),
1330 confidence,
1331 });
1332 }
1333 let vectors = observations
1334 .iter()
1335 .map(|observation| observation.vector.clone())
1336 .collect::<Vec<_>>();
1337 let witnesses =
1338 find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
1339 ReportError::DecisionAnalysis {
1340 decision_id: meta.id.clone(),
1341 error,
1342 }
1343 })?;
1344 let mut conditions = Vec::with_capacity(meta.conditions.len());
1345 for (index, source) in meta.conditions.iter().enumerate() {
1346 let witness = witnesses[index].map(|witness| {
1347 [
1348 vectors[witness.first].clone(),
1349 vectors[witness.second].clone(),
1350 ]
1351 });
1352 let witness_tests = witnesses[index].map(|witness| {
1353 [
1354 observations[witness.first].tests.clone(),
1355 observations[witness.second].tests.clone(),
1356 ]
1357 });
1358 let assertion_covered = (0..observations.len()).any(|left| {
1359 ((left + 1)..observations.len()).any(|right| {
1360 observations[left].confidence.asserted
1361 && observations[right].confidence.asserted
1362 && crate::coverage_analysis::is_independence_pair(
1363 &observations[left].vector,
1364 &observations[right].vector,
1365 index,
1366 )
1367 })
1368 });
1369 conditions.push(ConditionResult {
1370 index,
1371 source: source.clone(),
1372 covered: witness.is_some(),
1373 assertion_covered,
1374 witness,
1375 witness_tests,
1376 });
1377 }
1378 let decision_tests = tests_by_decision.remove(&meta.id).unwrap_or_default();
1379 let confidence = confidence_for(
1380 sorted(&decision_tests),
1381 observations
1382 .iter()
1383 .flat_map(|observation| observation.phases.clone()),
1384 observations
1385 .iter()
1386 .flat_map(|observation| observation.explicit_phases.clone()),
1387 &tests_by_id,
1388 &phases_by_id,
1389 &asserted_phase_ids,
1390 );
1391 decisions.push(DecisionResult {
1392 executed: !vectors.is_empty(),
1393 covered: conditions.iter().all(|condition| condition.covered),
1394 meta,
1395 vectors,
1396 vector_observations: observations,
1397 conditions,
1398 tests: sorted(&decision_tests),
1399 confidence,
1400 });
1401 }
1402
1403 let points = manifest
1404 .points
1405 .iter()
1406 .cloned()
1407 .map(|meta| {
1408 let tests = tests_by_hit.get(&meta.id).cloned().unwrap_or_default();
1409 let phases = phases_by_hit.get(&meta.id).cloned().unwrap_or_default();
1410 let explicit = explicit_phases_by_hit
1411 .get(&meta.id)
1412 .cloned()
1413 .unwrap_or_default();
1414 PointResult {
1415 covered: tests_by_hit.contains_key(&meta.id),
1416 confidence: confidence_for(
1417 sorted(&tests),
1418 sorted(&phases),
1419 sorted(&explicit),
1420 &tests_by_id,
1421 &phases_by_id,
1422 &asserted_phase_ids,
1423 ),
1424 meta,
1425 tests: sorted(&tests),
1426 phases: sorted(&phases),
1427 }
1428 })
1429 .collect::<Vec<_>>();
1430
1431 let branches = manifest
1432 .branches
1433 .iter()
1434 .cloned()
1435 .map(|meta| {
1436 let alternatives = meta
1437 .alternatives
1438 .iter()
1439 .map(|alternative| {
1440 let tests = tests_by_hit
1441 .get(&alternative.id)
1442 .cloned()
1443 .unwrap_or_default();
1444 let phases = phases_by_hit
1445 .get(&alternative.id)
1446 .cloned()
1447 .unwrap_or_default();
1448 let explicit = explicit_phases_by_hit
1449 .get(&alternative.id)
1450 .cloned()
1451 .unwrap_or_default();
1452 AlternativeResult {
1453 id: alternative.id.clone(),
1454 label: alternative.label.clone(),
1455 covered: tests_by_hit.contains_key(&alternative.id),
1456 tests: sorted(&tests),
1457 phases: sorted(&phases),
1458 confidence: confidence_for(
1459 sorted(&tests),
1460 sorted(&phases),
1461 sorted(&explicit),
1462 &tests_by_id,
1463 &phases_by_id,
1464 &asserted_phase_ids,
1465 ),
1466 }
1467 })
1468 .collect::<Vec<_>>();
1469 BranchResult {
1470 covered: alternatives.iter().all(|alternative| alternative.covered),
1471 meta,
1472 alternatives,
1473 }
1474 })
1475 .collect::<Vec<_>>();
1476
1477 let mut line_aggregates =
1478 BTreeMap::<SourceLine, (bool, BTreeSet<String>, BTreeSet<String>, BTreeSet<String>)>::new();
1479 for point in &points {
1480 let aggregate = line_aggregates
1481 .entry(SourceLine {
1482 file: point.meta.file.clone(),
1483 line: point.meta.line,
1484 })
1485 .or_default();
1486 aggregate.0 |= point.covered;
1487 aggregate.1.extend(point.tests.clone());
1488 aggregate.2.extend(point.phases.clone());
1489 aggregate.3.extend(
1490 explicit_phases_by_hit
1491 .get(&point.meta.id)
1492 .into_iter()
1493 .flatten()
1494 .cloned(),
1495 );
1496 }
1497 let lines = line_aggregates
1498 .into_iter()
1499 .map(|(location, (covered, test_ids, phase_ids, explicit_ids))| {
1500 let provenances = test_ids
1501 .iter()
1502 .filter_map(|id| tests_by_id.get(id).map(|test| &test.provenance))
1503 .collect::<Vec<_>>();
1504 let runners = provenances
1505 .iter()
1506 .map(|provenance| provenance.runner.clone())
1507 .collect::<BTreeSet<_>>();
1508 let kinds = provenances
1509 .iter()
1510 .map(|provenance| provenance.kind.clone())
1511 .collect::<BTreeSet<_>>();
1512 LineResult {
1513 file: location.file,
1514 line: location.line,
1515 covered,
1516 tests: sorted(&test_ids),
1517 runners: sorted(&runners),
1518 exclusive_kind: (kinds.len() == 1).then(|| kinds.first().unwrap().clone()),
1519 phases: sorted(&phase_ids),
1520 confidence: confidence_for(
1521 sorted(&test_ids),
1522 sorted(&phase_ids),
1523 sorted(&explicit_ids),
1524 &tests_by_id,
1525 &phases_by_id,
1526 &asserted_phase_ids,
1527 ),
1528 kinds: sorted(&kinds),
1529 }
1530 })
1531 .collect::<Vec<_>>();
1532
1533 let point_locations = manifest
1534 .points
1535 .iter()
1536 .map(|point| {
1537 (
1538 point.id.clone(),
1539 SourceLine {
1540 file: point.file.clone(),
1541 line: point.line,
1542 },
1543 )
1544 })
1545 .collect::<HashMap<_, _>>();
1546 test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
1547 let tests = test_order
1548 .into_iter()
1549 .map(|id| {
1550 let test = tests_by_id.get(&id).expect("test order references test");
1551 let lines = test
1552 .hits
1553 .iter()
1554 .filter_map(|hit| point_locations.get(hit).cloned())
1555 .collect::<BTreeSet<_>>();
1556 TestCoverageResult {
1557 id: test.id.clone(),
1558 name: test.name.clone(),
1559 file: test.file.clone(),
1560 title: test.title.clone(),
1561 retries: sorted(&test.retries),
1562 attempts: test.attempts.values().cloned().collect(),
1563 outcome: test_outcome(test),
1564 provenance: test.provenance.clone(),
1565 role: test.role.clone(),
1566 hits: sorted(&test.hits),
1567 decisions: test
1568 .decisions
1569 .iter()
1570 .map(|(id, vectors)| TestDecisionResult {
1571 id: id.clone(),
1572 vectors: vectors.values.clone(),
1573 })
1574 .collect(),
1575 lines: lines.into_iter().collect(),
1576 }
1577 })
1578 .collect::<Vec<_>>();
1579
1580 let mut test_files = BTreeMap::<
1581 String,
1582 (
1583 BTreeSet<String>,
1584 BTreeSet<String>,
1585 BTreeSet<String>,
1586 BTreeSet<SourceLine>,
1587 ),
1588 >::new();
1589 for test in &tests {
1590 let aggregate = test_files
1591 .entry(
1592 test.file
1593 .clone()
1594 .unwrap_or_else(|| "(unknown test file)".into()),
1595 )
1596 .or_default();
1597 aggregate.0.insert(test.id.clone());
1598 aggregate.1.insert(test.provenance.runner.clone());
1599 aggregate.2.insert(test.provenance.kind.clone());
1600 aggregate.3.extend(test.lines.clone());
1601 }
1602 let test_files = test_files
1603 .into_iter()
1604 .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
1605 file,
1606 tests: sorted(&tests),
1607 runners: sorted(&runners),
1608 kinds: sorted(&kinds),
1609 lines: lines.into_iter().collect(),
1610 })
1611 .collect::<Vec<_>>();
1612
1613 let mut phases = phases_by_id.values().cloned().collect::<Vec<_>>();
1614 phases.sort_by(|left, right| {
1615 left.phase
1616 .started_at_ms
1617 .cmp(&right.phase.started_at_ms)
1618 .then(left.phase.id.cmp(&right.phase.id))
1619 });
1620 let phases = phases
1621 .into_iter()
1622 .map(|phase| {
1623 let lines = phase
1624 .hits
1625 .iter()
1626 .filter_map(|hit| point_locations.get(hit).cloned())
1627 .collect::<BTreeSet<_>>();
1628 PhaseResult {
1629 phase: phase.phase,
1630 test: phase.test,
1631 hits: sorted(&phase.hits),
1632 decisions: phase
1633 .decisions
1634 .into_iter()
1635 .map(|(id, vectors)| TestDecisionResult {
1636 id,
1637 vectors: vectors.values,
1638 })
1639 .collect(),
1640 lines: lines.into_iter().collect(),
1641 browser_events: phase.browser_events,
1642 server_events: phase.server_events,
1643 explicit_events: phase.explicit_events,
1644 inferred_events: phase.inferred_events,
1645 explicit_browser_events: phase.explicit_browser_events,
1646 inferred_browser_events: phase.inferred_browser_events,
1647 explicit_server_events: phase.explicit_server_events,
1648 inferred_server_events: phase.inferred_server_events,
1649 }
1650 })
1651 .collect::<Vec<_>>();
1652
1653 let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
1654 if !manifest.limitations.is_empty() {
1655 summary.coverage_complete = false;
1656 summary.completeness_blocked = Some(true);
1657 }
1658
1659 let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
1660 let values = tests
1661 .iter()
1662 .map(|test| {
1663 if field == "kind" {
1664 test.provenance.kind.clone()
1665 } else {
1666 test.provenance.runner.clone()
1667 }
1668 })
1669 .collect::<BTreeSet<_>>();
1670 values
1671 .into_iter()
1672 .map(|value| {
1673 let selected = tests
1674 .iter()
1675 .filter(|test| {
1676 if field == "kind" {
1677 test.provenance.kind == value
1678 } else {
1679 test.provenance.runner == value
1680 }
1681 })
1682 .map(|test| test.id.clone())
1683 .collect::<BTreeSet<_>>();
1684 Ok(DimensionCoverage {
1685 kind: (field == "kind").then(|| value.clone()),
1686 runner: (field == "runner").then(|| value.clone()),
1687 tests: tests
1688 .iter()
1689 .filter(|test| selected.contains(&test.id) && test.role == "test")
1690 .count(),
1691 setups: tests
1692 .iter()
1693 .filter(|test| selected.contains(&test.id) && test.role == "setup")
1694 .count(),
1695 summary: summary_for_results(
1696 &decisions,
1697 &points,
1698 &branches,
1699 &lines,
1700 Some(&selected),
1701 )?,
1702 })
1703 })
1704 .collect()
1705 };
1706
1707 Ok(CoverageView {
1708 generated_at: generated_at.into(),
1709 variant: coverage_model.variant.clone(),
1710 scope: manifest.scope.clone(),
1711 model: CoverageModel {
1712 name: coverage_model.name.clone(),
1713 completeness_meaning: coverage_model.completeness_meaning.clone(),
1714 measured: coverage_model.measured.clone(),
1715 not_measured: coverage_model.not_measured.clone(),
1716 },
1717 integrity: None,
1718 limitations: manifest.limitations.clone(),
1719 transport: None,
1720 summary,
1721 coverage_by_kind: dimension_coverage("kind")?,
1722 coverage_by_runner: dimension_coverage("runner")?,
1723 decisions,
1724 points,
1725 branches,
1726 tests,
1727 test_files,
1728 phases,
1729 lines,
1730 })
1731}
1732
1733pub fn analyze_coverage_results(
1734 request: &CoverageReportRequest,
1735) -> Result<CoverageReport, ReportError> {
1736 if let Some(scope) = request
1737 .raw_results
1738 .iter()
1739 .filter_map(|raw| raw.scope.as_ref())
1740 .find(|scope| scope.run_id != request.run_id)
1741 {
1742 return Err(ReportError::ScopeMismatch {
1743 expected: request.run_id.clone(),
1744 actual: scope.run_id.clone(),
1745 });
1746 }
1747 if request.raw_results.is_empty() {
1748 return Err(ReportError::NoEvidence(request.run_id.clone()));
1749 }
1750 let default_model = javascript_coverage_model();
1751 let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
1752 let view = create_coverage_view_with_model(
1753 &request.manifest,
1754 &request.raw_results,
1755 &request.generated_at,
1756 coverage_model,
1757 )?;
1758 let passed = create_coverage_view_with_model(
1759 &request.manifest,
1760 &passing_coverage_results(&request.raw_results),
1761 &request.generated_at,
1762 coverage_model,
1763 )?;
1764 let failed = create_coverage_view_with_model(
1765 &request.manifest,
1766 &failed_coverage_results(&request.raw_results),
1767 &request.generated_at,
1768 coverage_model,
1769 )?;
1770 let execution = match request.test_exit_code {
1771 ExitCodeInput::Missing => None,
1772 ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
1773 valid: test_exit_code == Some(0),
1774 test_exit_code,
1775 }),
1776 };
1777 let mut view = view;
1778 let mut passed = passed;
1779 let mut failed = failed;
1780 if let Some(integrity) = &request.integrity {
1781 view.integrity = Some(integrity.clone());
1782 passed.integrity = Some(integrity.clone());
1783 failed.integrity = Some(integrity.clone());
1784 }
1785 Ok(CoverageReport {
1786 view,
1787 execution,
1788 filters: CoverageFilters { passed, failed },
1789 })
1790}
1791
1792fn parse_entry<T: for<'de> Deserialize<'de>>(
1793 entry: &EvidenceArchiveEntry,
1794) -> Result<T, ReportError> {
1795 serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
1796 path: entry.path.clone(),
1797 reason: error.to_string(),
1798 })
1799}
1800
1801fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
1802 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
1803) -> (Vec<T>, usize, usize) {
1804 let mut records = Vec::new();
1805 let mut corrupt_records = 0;
1806 let mut corrupt_files = BTreeSet::new();
1807 for entry in entries {
1808 for line in entry.contents.split(|byte| *byte == b'\n') {
1809 if line.is_empty() {
1810 continue;
1811 }
1812 match serde_json::from_slice(line) {
1813 Ok(record) => records.push(record),
1814 Err(_) => {
1815 corrupt_records += 1;
1816 corrupt_files.insert(entry.path.clone());
1817 }
1818 }
1819 }
1820 }
1821 (records, corrupt_records, corrupt_files.len())
1822}
1823
1824fn is_mcdc_result(path: &str) -> bool {
1825 path == "mcdc.json" || path.ends_with("/mcdc.json")
1826}
1827
1828pub fn analyze_coverage_archive(
1829 request: &ArchiveReportRequest,
1830) -> Result<CoverageReport, ReportError> {
1831 let archive = read_versioned_archive(Path::new(&request.archive_path))
1832 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
1833 let entries = archive.entries;
1834 let manifest = entries
1835 .iter()
1836 .find(|entry| entry.path == "manifest.json")
1837 .ok_or(ReportError::MissingManifest)
1838 .and_then(parse_entry::<CoverageManifest>)?;
1839 let mut raw_results = entries
1840 .iter()
1841 .filter(|entry| is_mcdc_result(&entry.path))
1842 .map(parse_entry::<RawTestResult>)
1843 .collect::<Result<Vec<_>, _>>()?;
1844
1845 let (scoped_records, scoped_corrupt, scoped_corrupt_files) =
1846 parse_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
1847 entry.path.starts_with("server/")
1848 && !entry.path.starts_with("server/background/")
1849 && entry.path.ends_with(".jsonl")
1850 }));
1851 for record in &scoped_records {
1852 let Some(scope) = &record.scope else { continue };
1853 let Some(raw) = raw_results
1854 .iter_mut()
1855 .find(|raw| raw_test_id(raw) == scope.test_id && raw.retry.unwrap_or(0) == scope.retry)
1856 else {
1857 continue;
1858 };
1859 if !raw.server.contains(record) {
1860 raw.server.push(record.clone());
1861 }
1862 }
1863
1864 let (background_records, background_corrupt, background_corrupt_files) =
1865 parse_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
1866 entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
1867 }));
1868 if !background_records.is_empty() {
1869 raw_results.push(RawTestResult {
1870 test_id: Some(format!("background:{}", request.run_id)),
1871 scope: None,
1872 test: "Background / unattributed".into(),
1873 test_file: None,
1874 title: Some("Background / unattributed".into()),
1875 retry: None,
1876 status: Some("unknown".into()),
1877 expected_status: None,
1878 flaky: false,
1879 provenance: TestProvenance {
1880 runner: "background".into(),
1881 kind: "background".into(),
1882 project: None,
1883 source: "explicit".into(),
1884 },
1885 role: "background".into(),
1886 phases: vec![],
1887 runtime: vec![],
1888 browser: vec![],
1889 server: background_records.clone(),
1890 });
1891 }
1892
1893 let (execution_events, execution_corrupt, execution_corrupt_files) =
1894 parse_json_lines::<Value>(entries.iter().filter(|entry| {
1895 entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
1896 }));
1897 let count_event = |name: &str| {
1898 execution_events
1899 .iter()
1900 .filter(|event| event.get("event").and_then(Value::as_str) == Some(name))
1901 .count()
1902 };
1903 let transport = TransportStats {
1904 processes: count_event("process"),
1905 child_launches: count_event("child-launch"),
1906 remote_launches: count_event("remote-launch"),
1907 workspace_capabilities: count_event("workspace-capability"),
1908 scoped_server_records: scoped_records.len(),
1909 background_server_records: background_records.len(),
1910 corrupt_records: scoped_corrupt + background_corrupt + execution_corrupt,
1911 corrupt_files: scoped_corrupt_files + background_corrupt_files + execution_corrupt_files,
1912 };
1913 let (frontend, coverage_model) = if archive.schema_version == EVIDENCE_ARCHIVE_V3_SCHEMA_VERSION
1914 {
1915 let frontend = entries
1916 .iter()
1917 .find(|entry| entry.path == "frontend.json")
1918 .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
1919 .and_then(parse_entry::<FrontendRunDeclaration>)?;
1920 let persisted = entries
1921 .iter()
1922 .find(|entry| entry.path == "coverage-model.json")
1923 .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
1924 .and_then(parse_entry::<PersistedCoverageModel>)?;
1925 let model = persisted
1926 .into_declaration()
1927 .map_err(|reason| ReportError::InvalidJson {
1928 path: "coverage-model.json".into(),
1929 reason: reason.into(),
1930 })?;
1931 (Some(frontend), Some(model))
1932 } else {
1933 (None, None)
1934 };
1935 let normalized = CoverageReportRequest {
1936 run_id: request.run_id.clone(),
1937 manifest,
1938 raw_results,
1939 generated_at: request.generated_at.clone(),
1940 coverage_model,
1941 integrity: request.integrity.clone(),
1942 test_exit_code: request.test_exit_code.clone(),
1943 };
1944 let mut report = if let Some(frontend) = frontend {
1945 crate::frontend_protocol::analyze_frontend_results(&frontend, &normalized)
1946 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?
1947 } else {
1948 analyze_coverage_results(&normalized)?
1949 };
1950 report.view.transport = Some(transport.clone());
1951 report.filters.passed.transport = Some(transport.clone());
1952 report.filters.failed.transport = Some(transport);
1953 Ok(report)
1954}
1955
1956#[cfg(test)]
1957mod tests {
1958 use std::{fs, time::SystemTime};
1959
1960 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
1961
1962 use super::*;
1963
1964 fn point(id: &str, line: usize) -> PointMeta {
1965 PointMeta {
1966 id: id.into(),
1967 kind: PointKind::Statement,
1968 file: "src/app.js".into(),
1969 line,
1970 column: 0,
1971 source: "work();".into(),
1972 label: None,
1973 }
1974 }
1975
1976 fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
1977 RawTestResult {
1978 test_id: Some(id.into()),
1979 scope: None,
1980 test: id.into(),
1981 test_file: Some("tests/app.test.js".into()),
1982 title: None,
1983 retry: Some(retry),
1984 status: Some(status.into()),
1985 expected_status: None,
1986 flaky: false,
1987 provenance: TestProvenance {
1988 runner: "node:test".into(),
1989 kind: "unit".into(),
1990 project: None,
1991 source: "runner-default".into(),
1992 },
1993 role: "test".into(),
1994 phases: vec![],
1995 runtime: vec![RuntimeSnapshot {
1996 decisions: vec![],
1997 hits: hits.iter().map(|hit| (*hit).into()).collect(),
1998 events: vec![],
1999 }],
2000 browser: vec![],
2001 server: vec![],
2002 }
2003 }
2004
2005 #[test]
2006 fn report_retains_unexecuted_manifest_conditions() {
2007 let manifest = CoverageManifest {
2008 decisions: vec![DecisionMeta {
2009 id: "decision".into(),
2010 file: "src/app.js".into(),
2011 line: 1,
2012 column: 0,
2013 source: "left && right".into(),
2014 conditions: vec!["left".into(), "right".into()],
2015 kind: "if".into(),
2016 }],
2017 points: vec![],
2018 branches: vec![],
2019 limitations: vec![],
2020 scope: None,
2021 };
2022 let view =
2023 create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
2024 assert_eq!(view.summary.conditions, 2);
2025 assert_eq!(view.summary.covered_conditions, 0);
2026 assert_eq!(view.decisions[0].conditions.len(), 2);
2027 }
2028
2029 #[test]
2030 fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
2031 let manifest = CoverageManifest {
2032 decisions: vec![DecisionMeta {
2033 id: "application-decision".into(),
2034 file: "src/app.js".into(),
2035 line: 1,
2036 column: 0,
2037 source: "left && right".into(),
2038 conditions: vec!["left".into(), "right".into()],
2039 kind: "if".into(),
2040 }],
2041 points: vec![],
2042 branches: vec![],
2043 limitations: vec![],
2044 scope: None,
2045 };
2046 let mut attempt = raw("test", 0, "passed", &[]);
2047 attempt.runtime[0].decisions.push(DecisionSnapshot {
2048 meta: DecisionMeta {
2049 id: "synthetic-fixture".into(),
2050 file: "fixtures/generated.js".into(),
2051 line: 1,
2052 column: 0,
2053 source: "a && b && c".into(),
2054 conditions: vec!["a".into(), "b".into(), "c".into()],
2055 kind: "if".into(),
2056 },
2057 vectors: vec![McdcVector {
2058 values: vec![Some(true), Some(true), Some(true)],
2059 outcome: true,
2060 }],
2061 });
2062
2063 let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2064 assert_eq!(view.decisions.len(), 1);
2065 assert_eq!(view.decisions[0].meta.id, "application-decision");
2066 assert!(view.decisions[0].vectors.is_empty());
2067 }
2068
2069 #[test]
2070 fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
2071 let expected = DecisionMeta {
2072 id: "application-decision".into(),
2073 file: "src/app.js".into(),
2074 line: 1,
2075 column: 0,
2076 source: "left && right".into(),
2077 conditions: vec!["left".into(), "right".into()],
2078 kind: "if".into(),
2079 };
2080 let manifest = CoverageManifest {
2081 decisions: vec![expected.clone()],
2082 points: vec![],
2083 branches: vec![],
2084 limitations: vec![],
2085 scope: Some(serde_json::json!({
2086 "entries": [{ "file": "src/empty.js", "status": "included" }]
2087 })),
2088 };
2089 let vector = McdcVector {
2090 values: vec![Some(true), Some(true)],
2091 outcome: true,
2092 };
2093
2094 let mut unknown = raw("test", 0, "passed", &[]);
2095 unknown.runtime[0].decisions.push(DecisionSnapshot {
2096 meta: DecisionMeta {
2097 id: "unknown-decision".into(),
2098 file: "src/empty.js".into(),
2099 ..expected.clone()
2100 },
2101 vectors: vec![vector.clone()],
2102 });
2103 assert!(matches!(
2104 create_coverage_view(&manifest, &[unknown], "time"),
2105 Err(ReportError::InvalidServerRecord(reason))
2106 if reason.contains("absent from the frozen manifest")
2107 ));
2108
2109 let mut changed = raw("test", 0, "passed", &[]);
2110 changed.runtime[0].decisions.push(DecisionSnapshot {
2111 meta: DecisionMeta {
2112 source: "left || right".into(),
2113 ..expected
2114 },
2115 vectors: vec![vector],
2116 });
2117 assert!(matches!(
2118 create_coverage_view(&manifest, &[changed], "time"),
2119 Err(ReportError::InvalidServerRecord(reason))
2120 if reason.contains("differs from the frozen manifest")
2121 ));
2122 }
2123
2124 #[test]
2125 fn timestamp_overlap_cannot_upgrade_assertion_confidence() {
2126 let manifest = CoverageManifest {
2127 decisions: vec![],
2128 points: vec![point("hit", 1)],
2129 branches: vec![],
2130 limitations: vec![],
2131 scope: None,
2132 };
2133 let phase = CoveragePhase {
2134 id: "assertion".into(),
2135 kind: "assertion".into(),
2136 operation: "equal".into(),
2137 source: None,
2138 caused_by_phase_id: None,
2139 started_at_ms: 100,
2140 ended_at_ms: Some(120),
2141 status: Some("passed".into()),
2142 error: None,
2143 };
2144 let mut attempt = raw("test", 0, "passed", &["hit"]);
2145 attempt.phases.push(phase.clone());
2146 attempt.runtime[0].events.push(RuntimeEvent {
2147 event_type: "hit".into(),
2148 id: "hit".into(),
2149 vector: None,
2150 timestamp_ms: 110,
2151 phase_id: None,
2152 environment: "server".into(),
2153 });
2154 let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
2155 assert_eq!(inferred.points[0].confidence.level, "executed");
2156 assert!(!inferred.points[0].confidence.asserted);
2157
2158 attempt.runtime[0].events[0].phase_id = Some(phase.id);
2159 let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2160 assert_eq!(explicit.points[0].confidence.level, "asserted");
2161 assert!(explicit.points[0].confidence.asserted);
2162 }
2163
2164 #[test]
2165 fn verified_view_uses_only_the_terminal_successful_attempt() {
2166 let manifest = CoverageManifest {
2167 decisions: vec![],
2168 points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
2169 branches: vec![],
2170 limitations: vec![],
2171 scope: None,
2172 };
2173 let failed = raw("flaky", 0, "failed", &["failed"]);
2174 let mut passed = raw("flaky", 1, "passed", &["passed"]);
2175 passed.flaky = true;
2176 let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
2177 expected.expected_status = Some("failed".into());
2178 let request = CoverageReportRequest {
2179 run_id: "run".into(),
2180 manifest,
2181 raw_results: vec![failed, passed, expected],
2182 generated_at: "time".into(),
2183 coverage_model: None,
2184 integrity: None,
2185 test_exit_code: ExitCodeInput::Missing,
2186 };
2187 let report = analyze_coverage_results(&request).unwrap();
2188 assert!(!report.filters.passed.points[0].covered);
2189 assert!(report.filters.passed.points[1].covered);
2190 assert!(!report.filters.passed.points[2].covered);
2191 assert!(report.filters.failed.points[0].covered);
2192 assert_eq!(report.view.tests[1].outcome, "flaky");
2193 }
2194
2195 fn archive(entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
2196 let nonce = SystemTime::now()
2197 .duration_since(SystemTime::UNIX_EPOCH)
2198 .unwrap()
2199 .as_nanos();
2200 let root = std::env::temp_dir().join(format!(
2201 "supercov-rust-report-{}-{nonce}",
2202 std::process::id()
2203 ));
2204 fs::create_dir_all(&root).unwrap();
2205 let path = root.join("evidence.raw.gz");
2206 write_archive(entries, &path).unwrap();
2207 path
2208 }
2209
2210 #[test]
2211 fn archive_analysis_keeps_valid_lines_and_reports_transport_corruption() {
2212 let manifest = CoverageManifest {
2213 decisions: vec![],
2214 points: vec![point("background-hit", 1)],
2215 branches: vec![],
2216 limitations: vec![],
2217 scope: None,
2218 };
2219 let path = archive(vec![
2220 EvidenceArchiveEntry {
2221 path: "manifest.json".into(),
2222 contents: serde_json::to_vec(&manifest).unwrap(),
2223 },
2224 EvidenceArchiveEntry {
2225 path: "server/background/worker.jsonl".into(),
2226 contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
2227 },
2228 ]);
2229 let report = analyze_coverage_archive(&ArchiveReportRequest {
2230 archive_path: path.clone(),
2231 run_id: "run".into(),
2232 generated_at: "time".into(),
2233 integrity: None,
2234 test_exit_code: ExitCodeInput::Missing,
2235 })
2236 .unwrap();
2237 assert!(report.view.points[0].covered);
2238 assert_eq!(report.view.tests[0].role, "background");
2239 assert_eq!(report.view.transport.as_ref().unwrap().corrupt_records, 1);
2240 assert_eq!(report.view.transport.as_ref().unwrap().corrupt_files, 1);
2241 fs::remove_dir_all(path.parent().unwrap()).unwrap();
2242 }
2243
2244 #[test]
2245 fn archive_analysis_rejects_cross_run_evidence() {
2246 let manifest = CoverageManifest {
2247 decisions: vec![],
2248 points: vec![],
2249 branches: vec![],
2250 limitations: vec![],
2251 scope: None,
2252 };
2253 let mut result = raw("test", 0, "passed", &[]);
2254 result.scope = Some(ExecutionScope {
2255 version: 1,
2256 run_id: "other-run".into(),
2257 worker_id: "worker".into(),
2258 test_id: "test".into(),
2259 test_key: "key".into(),
2260 retry: 0,
2261 attempt_id: "attempt".into(),
2262 });
2263 let path = archive(vec![
2264 EvidenceArchiveEntry {
2265 path: "manifest.json".into(),
2266 contents: serde_json::to_vec(&manifest).unwrap(),
2267 },
2268 EvidenceArchiveEntry {
2269 path: "worker/mcdc.json".into(),
2270 contents: serde_json::to_vec(&result).unwrap(),
2271 },
2272 ]);
2273 assert_eq!(
2274 analyze_coverage_archive(&ArchiveReportRequest {
2275 archive_path: path.clone(),
2276 run_id: "run".into(),
2277 generated_at: "time".into(),
2278 integrity: None,
2279 test_exit_code: ExitCodeInput::Missing,
2280 }),
2281 Err(ReportError::ScopeMismatch {
2282 expected: "run".into(),
2283 actual: "other-run".into(),
2284 })
2285 );
2286 fs::remove_dir_all(path.parent().unwrap()).unwrap();
2287 }
2288
2289 #[test]
2290 fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
2291 let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
2292 "runId": "run",
2293 "manifest": { "decisions": [], "points": [], "branches": [] },
2294 "rawResults": [{
2295 "test": "test",
2296 "status": "passed",
2297 "browser": [],
2298 "server": []
2299 }],
2300 "generatedAt": "time",
2301 "testExitCode": null
2302 }))
2303 .unwrap();
2304 let report = analyze_coverage_results(&request).unwrap();
2305 assert_eq!(
2306 report.execution,
2307 Some(ExecutionResult {
2308 test_exit_code: None,
2309 valid: false,
2310 })
2311 );
2312 }
2313}