1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
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};
21use crate::interned::{FastMap, Id, Interner};
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase", deny_unknown_fields)]
25pub struct DecisionMeta {
26 pub id: String,
27 pub file: String,
28 pub line: usize,
29 pub column: usize,
30 pub source: String,
31 pub conditions: Vec<String>,
32 pub kind: String,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37pub struct PointMeta {
38 pub id: String,
39 pub kind: PointKind,
40 pub file: String,
41 pub line: usize,
42 pub column: usize,
43 pub source: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub label: Option<String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50pub struct BranchAlternativeMeta {
51 pub id: String,
52 pub label: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase", deny_unknown_fields)]
57pub struct BranchMeta {
58 pub id: String,
59 pub kind: String,
60 pub file: String,
61 pub line: usize,
62 pub column: usize,
63 pub source: String,
64 pub alternatives: Vec<BranchAlternativeMeta>,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct CoverageManifest {
70 pub decisions: Vec<DecisionMeta>,
71 pub points: Vec<PointMeta>,
72 pub branches: Vec<BranchMeta>,
73 #[serde(default, skip_serializing_if = "Vec::is_empty")]
74 pub limitations: Vec<Value>,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub unmeasured: Vec<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub scope: Option<Value>,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub assertion_sites: Vec<PointMeta>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase", deny_unknown_fields)]
91pub struct DecisionSnapshot {
92 pub meta: DecisionMeta,
93 pub vectors: Vec<McdcVector>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct RuntimeEvent {
99 #[serde(rename = "type")]
100 pub event_type: String,
101 pub id: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub vector: Option<McdcVector>,
104 pub timestamp_ms: i64,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub phase_id: Option<String>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub statement_id: Option<String>,
111 pub environment: String,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117pub struct LogicalVector {
118 pub right: bool,
120 pub truthy: bool,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase", deny_unknown_fields)]
128pub struct LogicalSnapshot {
129 pub id: String,
130 pub vectors: Vec<LogicalVector>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
134#[serde(rename_all = "camelCase", deny_unknown_fields)]
135pub struct RuntimeSnapshot {
136 #[serde(default)]
137 pub decisions: Vec<DecisionSnapshot>,
138 #[serde(default)]
139 pub hits: Vec<String>,
140 #[serde(default)]
141 pub events: Vec<RuntimeEvent>,
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
143 pub logicals: Vec<LogicalSnapshot>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub phase_id: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase", deny_unknown_fields)]
155pub struct ExecutionScope {
156 pub version: usize,
157 pub run_id: String,
158 pub worker_id: String,
159 pub test_id: String,
160 pub test_key: String,
161 pub retry: usize,
162 pub attempt_id: String,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase", deny_unknown_fields)]
167pub struct ServerRecord {
168 #[serde(rename = "type")]
169 pub record_type: String,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub meta: Option<DecisionMeta>,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub vector: Option<McdcVector>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub id: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub timestamp_ms: Option<i64>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub phase_id: Option<String>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub statement_id: Option<String>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub scope: Option<ExecutionScope>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase", deny_unknown_fields)]
189pub struct CoveragePhase {
190 pub id: String,
191 pub kind: String,
192 pub operation: String,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub source: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub caused_by_phase_id: Option<String>,
197 pub started_at_ms: i64,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub ended_at_ms: Option<i64>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub status: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub error: Option<String>,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase", deny_unknown_fields)]
208pub struct TestProvenance {
209 pub runner: String,
210 pub kind: String,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub project: Option<String>,
213 pub source: String,
214}
215
216impl Default for TestProvenance {
217 fn default() -> Self {
218 Self {
219 runner: "unknown".into(),
220 kind: "unknown".into(),
221 project: None,
222 source: "unknown".into(),
223 }
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase", deny_unknown_fields)]
229pub struct RawTestResult {
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub test_id: Option<String>,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub scope: Option<ExecutionScope>,
234 pub test: String,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub test_file: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub title: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub retry: Option<usize>,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub status: Option<String>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub expected_status: Option<String>,
245 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
246 pub flaky: bool,
247 #[serde(default)]
248 pub provenance: TestProvenance,
249 #[serde(default = "default_test_role")]
250 pub role: String,
251 #[serde(default = "default_attribution")]
264 pub attribution: String,
265 #[serde(default)]
266 pub phases: Vec<CoveragePhase>,
267 #[serde(default)]
268 pub runtime: Vec<RuntimeSnapshot>,
269 #[serde(default)]
270 pub browser: Vec<RuntimeSnapshot>,
271 #[serde(default)]
272 pub server: Vec<ServerRecord>,
273}
274
275fn default_test_role() -> String {
276 "test".into()
277}
278
279fn default_attribution() -> String {
280 ATTRIBUTION_EXACT.into()
281}
282
283pub const ATTRIBUTION_EXACT: &str = "exact";
285
286pub const ATTRIBUTION_RUN_WIDE: &str = "run-wide";
289
290pub const ATTRIBUTION_PARTIAL: &str = "partial";
298
299fn least_exact(left: &str, right: &str) -> String {
301 let rank = |attribution: &str| match attribution {
302 ATTRIBUTION_RUN_WIDE => 2,
303 ATTRIBUTION_PARTIAL => 1,
304 _ => 0,
305 };
306 if rank(right) > rank(left) {
307 right.to_owned()
308 } else {
309 left.to_owned()
310 }
311}
312
313pub fn coverage_is_its_own(attribution: &str) -> bool {
320 attribution != ATTRIBUTION_RUN_WIDE
321}
322
323pub fn coverage_is_complete(attribution: &str) -> bool {
331 attribution == ATTRIBUTION_EXACT || attribution.is_empty()
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
335#[serde(rename_all = "camelCase")]
336pub struct TestAttempt {
337 pub retry: usize,
338 pub status: String,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub expected_status: Option<String>,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
344#[serde(rename_all = "camelCase")]
345pub struct CoverageConfidence {
346 pub level: String,
347 pub setup_only: bool,
348 pub background_only: bool,
349 pub asserted: bool,
350 pub tests: Vec<Id>,
351 pub asserted_tests: Vec<Id>,
352 pub runners: Vec<String>,
353 pub kinds: Vec<String>,
354 pub e2e: bool,
355}
356
357#[derive(Debug, Clone, PartialEq, Serialize)]
358#[serde(rename_all = "camelCase")]
359pub struct VectorObservation {
360 pub vector: McdcVector,
361 pub tests: Vec<Id>,
362 #[serde(skip_serializing_if = "Vec::is_empty")]
363 pub phases: Vec<Id>,
364 #[serde(skip_serializing_if = "Vec::is_empty")]
365 pub explicit_phases: Vec<Id>,
366 pub confidence: CoverageConfidence,
367}
368
369#[derive(Debug, Clone, PartialEq, Serialize)]
370#[serde(rename_all = "camelCase")]
371pub struct ConditionResult {
372 pub index: usize,
373 pub source: String,
374 pub covered: bool,
375 pub assertion_covered: bool,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub witness: Option<[McdcVector; 2]>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub witness_tests: Option<[Vec<Id>; 2]>,
380}
381
382#[derive(Debug, Clone, PartialEq, Serialize)]
383#[serde(rename_all = "camelCase")]
384pub struct DecisionResult {
385 pub meta: DecisionMeta,
386 pub executed: bool,
387 pub covered: bool,
388 pub vectors: Vec<McdcVector>,
389 pub vector_observations: Vec<VectorObservation>,
390 pub conditions: Vec<ConditionResult>,
391 pub tests: Vec<Id>,
392 pub confidence: CoverageConfidence,
393}
394
395#[derive(Debug, Clone, PartialEq, Serialize)]
396#[serde(rename_all = "camelCase")]
397pub struct PointResult {
398 pub meta: PointMeta,
399 pub covered: bool,
400 #[serde(skip)]
401 pub measured: bool,
402 pub tests: Vec<Id>,
403 pub phases: Vec<Id>,
404 pub confidence: CoverageConfidence,
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize)]
408#[serde(rename_all = "camelCase")]
409pub struct AlternativeResult {
410 pub id: String,
411 pub label: String,
412 pub covered: bool,
413 pub tests: Vec<Id>,
414 pub phases: Vec<Id>,
415 pub confidence: CoverageConfidence,
416}
417
418#[derive(Debug, Clone, PartialEq, Serialize)]
419#[serde(rename_all = "camelCase")]
420pub struct BranchResult {
421 pub meta: BranchMeta,
422 pub covered: bool,
423 pub alternatives: Vec<AlternativeResult>,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
427pub struct SourceLine {
428 pub file: Id,
429 pub line: usize,
430}
431
432#[derive(Debug, Default)]
434struct LineAggregate {
435 covered: bool,
436 measured: bool,
437 tests: Vec<u32>,
439 phases: Vec<u32>,
440 explicit_phases: Vec<u32>,
441}
442
443#[derive(Debug, Clone, PartialEq, Serialize)]
444#[serde(rename_all = "camelCase")]
445pub struct LineResult {
446 pub file: String,
447 pub line: usize,
448 pub covered: bool,
449 #[serde(skip)]
454 pub measured: bool,
455 pub tests: Vec<Id>,
456 pub runners: Vec<String>,
457 pub kinds: Vec<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
459 pub exclusive_kind: Option<String>,
460 pub phases: Vec<Id>,
461 pub confidence: CoverageConfidence,
462}
463
464#[derive(Debug, Clone, PartialEq, Serialize)]
465#[serde(rename_all = "camelCase")]
466pub struct TestDecisionResult {
467 pub id: String,
468 pub vectors: Vec<McdcVector>,
469}
470
471#[derive(Debug, Clone, PartialEq, Serialize)]
472#[serde(rename_all = "camelCase")]
473pub struct TestCoverageResult {
474 pub id: String,
475 pub name: String,
476 #[serde(skip_serializing_if = "Option::is_none")]
477 pub file: Option<String>,
478 #[serde(skip_serializing_if = "Option::is_none")]
479 pub title: Option<String>,
480 pub retries: Vec<usize>,
481 pub attempts: Vec<TestAttempt>,
482 pub outcome: String,
483 pub provenance: TestProvenance,
484 pub role: String,
485 pub attribution: String,
488 pub hits: Vec<Id>,
489 pub decisions: Vec<TestDecisionResult>,
490 pub lines: Vec<SourceLine>,
491}
492
493#[derive(Debug, Clone, PartialEq, Serialize)]
494#[serde(rename_all = "camelCase")]
495pub struct TestFileResult {
496 pub file: String,
497 pub tests: Vec<String>,
498 pub runners: Vec<String>,
499 pub kinds: Vec<String>,
500 pub lines: Vec<SourceLine>,
501}
502
503#[derive(Debug, Clone, PartialEq, Serialize)]
504#[serde(rename_all = "camelCase")]
505pub struct PhaseResult {
506 #[serde(flatten)]
507 pub phase: CoveragePhase,
508 pub test: Id,
509 pub hits: Vec<Id>,
510 pub decisions: Vec<TestDecisionResult>,
511 pub lines: Vec<SourceLine>,
512 pub browser_events: usize,
513 pub server_events: usize,
514 pub explicit_events: usize,
515 pub inferred_events: usize,
516 pub explicit_browser_events: usize,
517 pub inferred_browser_events: usize,
518 pub explicit_server_events: usize,
519 pub inferred_server_events: usize,
520}
521
522#[derive(Debug, Clone, PartialEq, Serialize)]
523#[serde(rename_all = "camelCase")]
524pub struct DimensionCoverage {
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub kind: Option<String>,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub runner: Option<String>,
529 pub tests: usize,
530 pub setups: usize,
531 pub attributed: usize,
536 pub summary: CoverageSummary,
537}
538
539#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
540#[serde(rename_all = "camelCase")]
541pub struct CoverageModel {
542 pub language: String,
543 pub name: String,
544 pub completeness_meaning: String,
545 pub measured: Vec<String>,
546 pub not_measured: Vec<String>,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(rename_all = "camelCase", deny_unknown_fields)]
551pub struct CoverageModelDeclaration {
552 pub language: String,
553 pub variant: String,
554 pub name: String,
555 pub completeness_meaning: String,
556 pub measured: Vec<String>,
557 pub not_measured: Vec<String>,
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561#[serde(rename_all = "camelCase", deny_unknown_fields)]
562pub struct PersistedCoverageModel {
563 pub schema_version: u32,
564 pub language: String,
565 pub variant: String,
566 pub name: String,
567 pub completeness_meaning: String,
568 pub measured: Vec<String>,
569 pub not_measured: Vec<String>,
570}
571
572impl PersistedCoverageModel {
573 pub fn from_declaration(value: &CoverageModelDeclaration) -> Result<Self, &'static str> {
574 let persisted = Self {
575 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
576 language: value.language.clone(),
577 variant: value.variant.clone(),
578 name: value.name.clone(),
579 completeness_meaning: value.completeness_meaning.clone(),
580 measured: value.measured.clone(),
581 not_measured: value.not_measured.clone(),
582 };
583 persisted.clone().into_declaration()?;
584 Ok(persisted)
585 }
586
587 fn into_declaration(self) -> Result<CoverageModelDeclaration, &'static str> {
588 if self.schema_version != COVERAGE_MODEL_SCHEMA_VERSION {
589 return Err("unsupported coverage model schema");
590 }
591 let valid_identifier = |value: &str| {
592 (1..=supercov_contracts::COVERAGE_MODEL_MAX_IDENTIFIER_BYTES).contains(&value.len())
593 && value.as_bytes()[0].is_ascii_lowercase()
594 && value.bytes().all(|byte| {
595 byte.is_ascii_lowercase()
596 || byte.is_ascii_digit()
597 || matches!(byte, b'.' | b'+' | b'-')
598 })
599 };
600 let valid_description = |value: &str| {
601 !value.is_empty()
602 && value.trim().len() == value.len()
603 && value.len() <= supercov_contracts::COVERAGE_MODEL_MAX_DESCRIPTION_BYTES
604 && !value.chars().any(char::is_control)
605 };
606 if !valid_identifier(&self.language)
607 || !valid_identifier(&self.variant)
608 || !valid_description(&self.name)
609 || !valid_description(&self.completeness_meaning)
610 || self.measured.is_empty()
611 || self.measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
612 || self.not_measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
613 || self
614 .measured
615 .iter()
616 .chain(&self.not_measured)
617 .any(|item| !valid_description(item))
618 {
619 return Err("invalid coverage model declaration");
620 }
621 let measured = self.measured.iter().collect::<BTreeSet<_>>();
622 let not_measured = self.not_measured.iter().collect::<BTreeSet<_>>();
623 if measured.len() != self.measured.len()
624 || not_measured.len() != self.not_measured.len()
625 || !measured.is_disjoint(¬_measured)
626 {
627 return Err("invalid coverage model surface partition");
628 }
629 Ok(CoverageModelDeclaration {
630 language: self.language,
631 variant: self.variant,
632 name: self.name,
633 completeness_meaning: self.completeness_meaning,
634 measured: self.measured,
635 not_measured: self.not_measured,
636 })
637 }
638}
639
640#[derive(Debug, Clone, PartialEq, Serialize)]
641#[serde(rename_all = "camelCase")]
642pub struct CoverageView {
643 pub generated_at: String,
644 pub variant: String,
645 #[serde(skip_serializing_if = "Option::is_none")]
646 pub scope: Option<Value>,
647 pub model: CoverageModel,
648 #[serde(skip_serializing_if = "Option::is_none")]
649 pub integrity: Option<Value>,
650 pub limitations: Vec<Value>,
651 #[serde(skip_serializing_if = "Option::is_none")]
652 pub transport: Option<TransportStats>,
653 pub summary: CoverageSummary,
654 pub coverage_by_kind: Vec<DimensionCoverage>,
655 pub coverage_by_runner: Vec<DimensionCoverage>,
656 pub decisions: Vec<DecisionResult>,
657 pub points: Vec<PointResult>,
658 pub branches: Vec<BranchResult>,
659 pub tests: Vec<TestCoverageResult>,
660 pub test_files: Vec<TestFileResult>,
661 pub phases: Vec<PhaseResult>,
662 pub lines: Vec<LineResult>,
663}
664
665#[derive(Debug, Clone, PartialEq, Serialize)]
666#[serde(rename_all = "camelCase")]
667pub struct CoverageFilters {
668 pub passed: CoverageView,
669 pub failed: CoverageView,
670}
671
672#[derive(Debug, Clone, PartialEq, Serialize)]
673#[serde(rename_all = "camelCase")]
674pub struct CoverageReport {
675 #[serde(flatten)]
676 pub view: CoverageView,
677 #[serde(skip_serializing_if = "Option::is_none")]
678 pub execution: Option<ExecutionResult>,
679 pub filters: CoverageFilters,
680}
681
682#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
683#[serde(rename_all = "camelCase")]
684pub struct ExecutionResult {
685 pub test_exit_code: Option<i32>,
686 pub valid: bool,
687}
688
689#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
690#[serde(rename_all = "camelCase")]
691pub struct TransportStats {
692 pub processes: usize,
693 pub child_launches: usize,
694 pub remote_launches: usize,
695 pub workspace_capabilities: usize,
696 pub guest_processes: usize,
700 pub scoped_server_records: usize,
701 pub background_server_records: usize,
702 pub corrupt_records: usize,
703 pub corrupt_files: usize,
704}
705
706#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
707#[serde(untagged)]
708enum ExecutionSafeArgument {
709 Text(String),
710 Digest(ExecutionArgumentDigest),
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
714#[serde(rename_all = "camelCase", deny_unknown_fields)]
715struct ExecutionArgumentDigest {
716 bytes: usize,
717 sha256: String,
718}
719
720#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
721#[serde(rename_all = "camelCase", deny_unknown_fields)]
722struct ExecutionCommandSummary {
723 #[serde(default)]
724 executable: Option<ExecutionSafeArgument>,
725 arguments: Vec<ExecutionSafeArgument>,
726 argument_count: usize,
727}
728
729#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
730#[serde(
731 tag = "event",
732 rename_all = "kebab-case",
733 rename_all_fields = "camelCase",
734 deny_unknown_fields
735)]
736enum ExecutionTraceEvent {
737 Process {
738 at: String,
739 pid: u32,
740 ppid: u32,
741 cwd: String,
742 command: ExecutionCommandSummary,
743 #[serde(default)]
744 entrypoint: Option<String>,
745 },
746 ChildLaunch {
747 at: String,
748 pid: u32,
749 ppid: u32,
750 method: String,
751 command: ExecutionSafeArgument,
752 },
753 RemoteLaunch {
754 at: String,
755 pid: u32,
756 ppid: u32,
757 command: ExecutionCommandSummary,
758 guest_root: String,
759 },
760 WorkspaceCapability {
761 at: String,
762 pid: u32,
763 ppid: u32,
764 host_root: String,
765 guest_root: String,
766 cache_identities: Vec<String>,
767 },
768 GuestProcess {
769 at: String,
770 pid: u32,
771 ppid: u32,
772 guest_root: String,
773 },
774}
775
776impl ExecutionTraceEvent {
777 fn kind(&self) -> &'static str {
778 match self {
779 Self::Process { .. } => "process",
780 Self::ChildLaunch { .. } => "child-launch",
781 Self::RemoteLaunch { .. } => "remote-launch",
782 Self::WorkspaceCapability { .. } => "workspace-capability",
783 Self::GuestProcess { .. } => "guest-process",
784 }
785 }
786}
787
788#[derive(Debug, Clone, PartialEq, Deserialize)]
789#[serde(rename_all = "camelCase", deny_unknown_fields)]
790pub struct CoverageReportRequest {
791 pub run_id: String,
792 pub manifest: CoverageManifest,
793 pub raw_results: Vec<RawTestResult>,
794 pub generated_at: String,
795 #[serde(default, skip_serializing_if = "Option::is_none")]
796 pub coverage_model: Option<CoverageModelDeclaration>,
797 #[serde(skip_serializing_if = "Option::is_none")]
798 pub integrity: Option<Value>,
799 #[serde(default, deserialize_with = "deserialize_exit_code")]
800 pub test_exit_code: ExitCodeInput,
801}
802
803#[derive(Debug, Clone, PartialEq, Deserialize)]
804#[serde(rename_all = "camelCase", deny_unknown_fields)]
805pub struct ArchiveReportRequest {
806 pub archive_path: PathBuf,
807 pub run_id: String,
808 pub generated_at: String,
809 #[serde(skip_serializing_if = "Option::is_none")]
810 pub integrity: Option<Value>,
811 #[serde(default, deserialize_with = "deserialize_exit_code")]
812 pub test_exit_code: ExitCodeInput,
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Default)]
816pub enum ExitCodeInput {
817 #[default]
818 Missing,
819 Present(Option<i32>),
820}
821
822fn deserialize_exit_code<'de, D>(deserializer: D) -> Result<ExitCodeInput, D::Error>
823where
824 D: serde::Deserializer<'de>,
825{
826 Option::<i32>::deserialize(deserializer).map(ExitCodeInput::Present)
827}
828
829#[derive(Debug, Clone, PartialEq, Eq)]
830pub enum ReportError {
831 Analysis(AnalysisError),
832 DecisionAnalysis {
833 decision_id: String,
834 error: AnalysisError,
835 },
836 InvalidEvent(String),
837 InvalidServerRecord(String),
838 InvalidArchive(String),
839 MissingManifest,
840 InvalidJson {
841 path: String,
842 reason: String,
843 },
844 ScopeMismatch {
845 expected: String,
846 actual: String,
847 },
848 NoEvidence(String),
849}
850
851impl From<AnalysisError> for ReportError {
852 fn from(value: AnalysisError) -> Self {
853 Self::Analysis(value)
854 }
855}
856
857#[derive(Clone, Default)]
858struct OrderedVectors {
859 values: Vec<McdcVector>,
860 indexes: HashMap<String, usize>,
861}
862
863impl OrderedVectors {
864 fn insert(&mut self, vector: &McdcVector) -> usize {
865 let key = vector_key(vector);
866 if let Some(index) = self.indexes.get(&key) {
867 return *index;
868 }
869 let index = self.values.len();
870 self.values.push(vector.clone());
871 self.indexes.insert(key, index);
872 index
873 }
874}
875
876#[derive(Clone)]
877struct MutableObservation {
878 vector: McdcVector,
879 tests: Vec<u32>,
882 phases: Vec<u32>,
883 explicit_phases: Vec<u32>,
884}
885
886#[derive(Clone)]
887struct MutableTest {
888 id: String,
889 name: String,
890 file: Option<String>,
891 title: Option<String>,
892 retries: BTreeSet<usize>,
893 attempts: BTreeMap<usize, TestAttempt>,
894 unstarted: bool,
895 runner_reported_flaky: bool,
896 provenance: TestProvenance,
897 role: String,
898 attribution: String,
899 hits: Vec<usize>,
902 decisions: BTreeMap<String, OrderedVectors>,
903}
904
905#[derive(Clone)]
906struct MutablePhase {
907 phase: CoveragePhase,
908 test: Id,
909 hits: Vec<usize>,
910 decisions: BTreeMap<String, OrderedVectors>,
911 browser_events: usize,
912 server_events: usize,
913 explicit_events: usize,
914 inferred_events: usize,
915 explicit_browser_events: usize,
916 inferred_browser_events: usize,
917 explicit_server_events: usize,
918 inferred_server_events: usize,
919}
920
921fn vector_key(vector: &McdcVector) -> String {
922 let mut key = String::with_capacity(vector.values.len() + 2);
923 for value in &vector.values {
924 key.push(match value {
925 None => '-',
926 Some(false) => 'F',
927 Some(true) => 'T',
928 });
929 }
930 key.push(':');
931 key.push(if vector.outcome { 'T' } else { 'F' });
932 key
933}
934
935fn sorted<T: Clone + Ord>(values: &BTreeSet<T>) -> Vec<T> {
936 values.iter().cloned().collect()
937}
938
939fn record_attempt(test: &mut MutableTest, raw: &RawTestResult) {
940 let (Some(retry), Some(raw_status)) = (raw.retry, raw.status.as_ref()) else {
941 return;
942 };
943 let previous = test.attempts.get(&retry);
944 let status =
947 if raw_status == "unknown" || previous.is_some_and(|attempt| attempt.status == "failed") {
948 previous.map_or_else(|| raw_status.clone(), |attempt| attempt.status.clone())
949 } else {
950 raw_status.clone()
951 };
952 let expected_status = raw
953 .expected_status
954 .clone()
955 .or_else(|| previous.and_then(|attempt| attempt.expected_status.clone()));
956 test.attempts.insert(
957 retry,
958 TestAttempt {
959 retry,
960 status,
961 expected_status,
962 },
963 );
964}
965
966fn effective_attempt_status(attempt: &TestAttempt) -> &str {
967 if attempt.expected_status.as_deref() == Some("failed") {
968 match attempt.status.as_str() {
969 "failed" => "passed",
970 "passed" => "failed",
971 status => status,
972 }
973 } else {
974 attempt.status.as_str()
975 }
976}
977
978fn test_outcome(test: &MutableTest) -> String {
979 let Some(terminal) = test.attempts.values().next_back() else {
980 return if test.unstarted {
981 "unstarted".into()
982 } else {
983 "unknown".into()
984 };
985 };
986 let terminal_status = effective_attempt_status(terminal);
987 if terminal_status == "passed"
988 && (test.runner_reported_flaky
989 || test
990 .attempts
991 .values()
992 .take(test.attempts.len().saturating_sub(1))
993 .any(|attempt| effective_attempt_status(attempt) != "passed"))
994 {
995 "flaky".into()
996 } else {
997 terminal_status.into()
998 }
999}
1000
1001fn raw_test_id(raw: &RawTestResult) -> &str {
1002 raw.test_id.as_deref().unwrap_or(&raw.test)
1003}
1004
1005pub fn passing_coverage_results(raw_results: &[RawTestResult]) -> Vec<&RawTestResult> {
1009 let mut attempts: BTreeMap<(String, usize), (BTreeSet<String>, bool)> = BTreeMap::new();
1010 for raw in raw_results {
1011 let Some(retry) = raw.retry else {
1012 continue;
1013 };
1014 let entry = attempts
1015 .entry((raw_test_id(raw).into(), retry))
1016 .or_default();
1017 if let Some(status) = &raw.status {
1018 entry.0.insert(status.clone());
1019 }
1020 entry.1 |= raw.expected_status.as_deref() == Some("failed");
1021 }
1022 let mut terminal_retries = BTreeMap::<String, usize>::new();
1023 for (test, retry) in attempts.keys() {
1024 terminal_retries
1025 .entry(test.clone())
1026 .and_modify(|value| *value = (*value).max(*retry))
1027 .or_insert(*retry);
1028 }
1029 let accepted = terminal_retries
1030 .into_iter()
1031 .filter_map(|(test, retry)| {
1032 let (statuses, expected_failure) = attempts.get(&(test.clone(), retry))?;
1033 (statuses.contains("passed") && !expected_failure).then_some((test, retry))
1034 })
1035 .collect::<BTreeSet<_>>();
1036 raw_results
1037 .iter()
1038 .filter(|raw| {
1039 raw.retry
1040 .is_some_and(|retry| accepted.contains(&(raw_test_id(raw).into(), retry)))
1041 })
1042 .collect()
1043}
1044
1045pub fn failed_coverage_results(raw_results: &[RawTestResult]) -> Vec<&RawTestResult> {
1046 let mut attempts = BTreeMap::<(String, usize), Vec<&RawTestResult>>::new();
1047 for raw in raw_results {
1048 if let Some(retry) = raw.retry {
1049 attempts
1050 .entry((raw_test_id(raw).to_owned(), retry))
1051 .or_default()
1052 .push(raw);
1053 }
1054 }
1055 let failed = attempts
1056 .into_iter()
1057 .filter_map(|(identity, records)| {
1058 let authoritative = records
1063 .iter()
1064 .copied()
1065 .filter(|raw| raw.expected_status.is_some())
1066 .collect::<Vec<_>>();
1067 let authoritative = if authoritative.is_empty() {
1068 records
1069 } else {
1070 authoritative
1071 };
1072 authoritative
1073 .iter()
1074 .any(
1075 |raw| match (raw.status.as_deref(), raw.expected_status.as_deref()) {
1076 (Some("failed"), Some("failed")) => false,
1077 (Some("passed"), Some("failed")) => true,
1078 (Some("failed"), _) => true,
1079 _ => false,
1080 },
1081 )
1082 .then_some(identity)
1083 })
1084 .collect::<BTreeSet<_>>();
1085 raw_results
1086 .iter()
1087 .filter(|raw| {
1088 raw.retry
1089 .is_some_and(|retry| failed.contains(&(raw_test_id(raw).into(), retry)))
1090 })
1091 .collect()
1092}
1093
1094pub(crate) fn javascript_coverage_model() -> CoverageModelDeclaration {
1095 CoverageModelDeclaration {
1096 language: "javascript".into(),
1097 variant: "masking-short-circuit".into(),
1098 name: "coverage-completeness-v2".into(),
1099 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(),
1100 measured: [
1101 "executable source lines",
1102 "executable statements",
1103 "function entries",
1104 "true and false outcomes of if, ternary, while, do/while, and classic for decisions",
1105 "true and false outcomes of every atomic condition in those decisions",
1106 "masking MC/DC independence for every atomic condition in those decisions",
1107 "short-circuit and right-evaluated selections for &&, ||, and ?? value expressions, including JSX",
1108 "short-circuit and evaluated alternatives for logical assignments and optional chains",
1109 "provided and default-evaluated parameter and destructuring values",
1110 "try success and catch entry",
1111 "zero and entered for-in/for-of loops",
1112 "entered switch cases, defaults, and implicit no-match alternatives",
1113 ]
1114 .into_iter()
1115 .map(str::to_owned)
1116 .collect(),
1117 not_measured: [
1118 "all input values or semantic input partitions",
1119 "all execution paths or ordering/concurrency interleavings",
1120 "destructuring defaults in classic for initializers (reported as blockers when discovered)",
1121 "the internal statements and decisions of runtime-generated eval/Function source",
1122 "mutation score or assertion fault-detection strength",
1123 ]
1124 .into_iter()
1125 .map(str::to_owned)
1126 .collect(),
1127 }
1128}
1129
1130pub fn coverage_summary_for_file(
1139 view: &CoverageView,
1140 file: &str,
1141) -> Result<CoverageSummary, ReportError> {
1142 let decisions = view
1143 .decisions
1144 .iter()
1145 .filter(|decision| decision.meta.file == file)
1146 .cloned()
1147 .collect::<Vec<_>>();
1148 let points = view
1149 .points
1150 .iter()
1151 .filter(|point| point.meta.file == file)
1152 .cloned()
1153 .collect::<Vec<_>>();
1154 let branches = view
1155 .branches
1156 .iter()
1157 .filter(|branch| branch.meta.file == file)
1158 .cloned()
1159 .collect::<Vec<_>>();
1160 let lines = view
1161 .lines
1162 .iter()
1163 .filter(|line| line.file == file)
1164 .cloned()
1165 .collect::<Vec<_>>();
1166 summary_for_results(&decisions, &points, &branches, &lines, None)
1167}
1168
1169pub(crate) fn entries_in_kind_rows(view: &CoverageView) -> BTreeSet<String> {
1176 let with_tests = view
1177 .tests
1178 .iter()
1179 .filter(|test| test.role == "test")
1180 .map(|test| test.provenance.kind.as_str())
1181 .collect::<BTreeSet<_>>();
1182 view.tests
1183 .iter()
1184 .filter(|test| {
1185 coverage_is_its_own(&test.attribution)
1186 && (test.role == "test"
1187 || (test.role == "setup" && with_tests.contains(test.provenance.kind.as_str())))
1188 })
1189 .map(|test| test.id.clone())
1190 .collect()
1191}
1192
1193pub(crate) fn summary_for_tests(view: &CoverageView) -> Result<CoverageSummary, ReportError> {
1196 let selected = entries_in_kind_rows(view);
1197 summary_for_results(
1198 &view.decisions,
1199 &view.points,
1200 &view.branches,
1201 &view.lines,
1202 Some(&selected),
1203 )
1204}
1205
1206fn summary_for_results(
1207 decisions: &[DecisionResult],
1208 points: &[PointResult],
1209 branches: &[BranchResult],
1210 lines: &[LineResult],
1211 test_ids: Option<&BTreeSet<String>>,
1212) -> Result<CoverageSummary, ReportError> {
1213 let test_ids = test_ids.map(|ids| {
1215 ids.iter()
1216 .map(String::as_str)
1217 .collect::<crate::interned::FastSet<&str>>()
1218 });
1219 let includes = |tests: &[Id], covered: bool| {
1220 test_ids.as_ref().map_or(covered, |selected| {
1221 tests.iter().any(|test| selected.contains(test.as_str()))
1222 })
1223 };
1224 let input = CoverageCoreInput {
1225 decisions: decisions
1226 .iter()
1227 .map(|decision| DecisionCoverage {
1228 condition_count: decision.meta.conditions.len(),
1229 vectors: decision
1230 .vector_observations
1231 .iter()
1232 .filter(|observation| includes(&observation.tests, true))
1233 .map(|observation| observation.vector.clone())
1234 .collect(),
1235 })
1236 .collect(),
1237 points: points
1238 .iter()
1239 .map(|point| PointCoverage {
1240 kind: point.meta.kind.clone(),
1241 covered: includes(&point.tests, point.covered),
1242 })
1243 .collect(),
1244 branches: branches
1245 .iter()
1246 .map(|branch| BranchCoverage {
1247 kind: branch.meta.kind.clone(),
1248 alternatives: branch
1249 .alternatives
1250 .iter()
1251 .map(|alternative| includes(&alternative.tests, alternative.covered))
1252 .collect(),
1253 })
1254 .collect(),
1255 lines: lines
1258 .iter()
1259 .filter(|line| line.measured)
1260 .map(|line| includes(&line.tests, line.covered))
1261 .collect(),
1262 };
1263 Ok(analyze_core(&input)?.summary)
1264}
1265
1266pub fn coverage_summary_for_tests(
1271 view: &CoverageView,
1272 test_ids: &BTreeSet<String>,
1273) -> Result<CoverageSummary, ReportError> {
1274 summary_for_results(
1275 &view.decisions,
1276 &view.points,
1277 &view.branches,
1278 &view.lines,
1279 Some(test_ids),
1280 )
1281}
1282
1283pub fn blocking_limitation(limitation: &Value) -> bool {
1286 limitation
1287 .get("blocking")
1288 .and_then(Value::as_bool)
1289 .unwrap_or(true)
1290}
1291
1292fn hash_slot<'m, V: Default, S: std::hash::BuildHasher>(
1293 map: &'m mut HashMap<String, V, S>,
1294 key: &str,
1295) -> &'m mut V {
1296 if !map.contains_key(key) {
1297 map.insert(key.to_owned(), V::default());
1298 }
1299 map.get_mut(key).expect("slot was inserted")
1300}
1301
1302fn tree_slot<'m, V: Default>(map: &'m mut BTreeMap<String, V>, key: &str) -> &'m mut V {
1303 if !map.contains_key(key) {
1304 map.insert(key.to_owned(), V::default());
1305 }
1306 map.get_mut(key).expect("slot was inserted")
1307}
1308
1309fn correlate_event<'a>(
1311 event: &'a RuntimeEvent,
1312 ordered_phases: &'a [CoveragePhase],
1313) -> Option<&'a str> {
1314 event
1315 .phase_id
1316 .as_deref()
1317 .or_else(|| phase_at(ordered_phases, event.timestamp_ms))
1318}
1319
1320fn phase_at(ordered_phases: &[CoveragePhase], timestamp_ms: i64) -> Option<&str> {
1328 let started = ordered_phases
1329 .iter()
1330 .take_while(|phase| phase.started_at_ms <= timestamp_ms)
1331 .count();
1332 let last = ordered_phases[..started].last()?;
1333 let handed_over = last.started_at_ms == timestamp_ms
1334 && started >= 2
1335 && ordered_phases[started - 2].ended_at_ms == Some(timestamp_ms)
1336 && ordered_phases[started - 2].started_at_ms < timestamp_ms;
1337 Some(
1338 if handed_over {
1339 &ordered_phases[started - 2]
1340 } else {
1341 last
1342 }
1343 .id
1344 .as_str(),
1345 )
1346}
1347
1348#[derive(Default)]
1352struct HitIds {
1353 names: Vec<Id>,
1354 numbers: FastMap<String, usize>,
1355}
1356
1357impl HitIds {
1358 fn number(&mut self, name: &str, ids: &mut Interner) -> usize {
1359 if let Some(number) = self.numbers.get(name) {
1360 return *number;
1361 }
1362 let number = self.names.len();
1363 self.names.push(ids.id(name));
1364 self.numbers.insert(name.to_owned(), number);
1365 number
1366 }
1367
1368 fn ranks(&self) -> Vec<u32> {
1371 let mut order = (0..self.names.len()).collect::<Vec<_>>();
1372 order.sort_unstable_by(|left, right| self.names[*left].cmp(&self.names[*right]));
1373 let mut rank = vec![0_u32; self.names.len()];
1374 for (position, number) in order.into_iter().enumerate() {
1375 rank[number] = position as u32;
1376 }
1377 rank
1378 }
1379
1380 fn sorted_by(&self, mut numbers: Vec<usize>, rank: &[u32]) -> Vec<Id> {
1382 numbers.sort_unstable_by_key(|number| rank[*number]);
1383 numbers.dedup();
1384 numbers
1385 .into_iter()
1386 .map(|number| self.names[number].clone())
1387 .collect()
1388 }
1389
1390 #[cfg(test)]
1391 fn sorted(&self, numbers: BTreeSet<usize>) -> Vec<Id> {
1392 let mut names = numbers
1393 .into_iter()
1394 .map(|n| self.names[n].clone())
1395 .collect::<Vec<_>>();
1396 names.sort_unstable();
1397 names
1398 }
1399}
1400
1401#[derive(Default)]
1404struct References {
1405 names: Vec<Id>,
1406 numbers: FastMap<Id, u32>,
1407 last: Option<u32>,
1408 by_obligation: Vec<Vec<u32>>,
1409}
1410
1411impl References {
1412 fn number(&mut self, name: &str, ids: &mut Interner) -> u32 {
1415 if let Some(number) = self.last
1416 && self.names[number as usize] == name
1417 {
1418 return number;
1419 }
1420 let number = match self.numbers.get(name) {
1421 Some(number) => *number,
1422 None => {
1423 let number = self.names.len() as u32;
1424 let id = ids.id(name);
1425 self.names.push(id.clone());
1426 self.numbers.insert(id, number);
1427 number
1428 }
1429 };
1430 self.last = Some(number);
1431 number
1432 }
1433
1434 fn add(&mut self, obligation: usize, name: &str, ids: &mut Interner) {
1435 let number = self.number(name, ids);
1436 if self.by_obligation.len() <= obligation {
1437 self.by_obligation.resize_with(obligation + 1, Vec::new);
1438 }
1439 let reached = &mut self.by_obligation[obligation];
1440 if reached.last() != Some(&number) {
1441 reached.push(number);
1442 }
1443 }
1444
1445 fn into_relation(self, hits: &HitIds) -> Relation {
1446 let mut order = (0..self.names.len()).collect::<Vec<_>>();
1447 order.sort_unstable_by(|left, right| self.names[*left].cmp(&self.names[*right]));
1448 let mut rank = vec![0_u32; self.names.len()];
1449 for (position, number) in order.into_iter().enumerate() {
1450 rank[number] = position as u32;
1451 }
1452 let mut relation = Relation {
1453 names: self.names,
1454 rank,
1455 by_hit: HashMap::new(),
1456 none: Reached::default(),
1457 };
1458 for (obligation, reached) in self.by_obligation.into_iter().enumerate() {
1459 if !reached.is_empty() {
1460 let reached = relation.reached(reached);
1461 relation
1462 .by_hit
1463 .insert(hits.names[obligation].to_string(), reached);
1464 }
1465 }
1466 relation
1467 }
1468}
1469
1470struct Relation {
1473 names: Vec<Id>,
1474 rank: Vec<u32>,
1476 by_hit: HashMap<String, Reached>,
1477 none: Reached,
1478}
1479
1480impl Relation {
1481 fn of(&self, id: &str) -> &Reached {
1483 self.by_hit.get(id).unwrap_or(&self.none)
1484 }
1485
1486 fn reached(&self, mut numbers: Vec<u32>) -> Reached {
1489 numbers.sort_unstable_by_key(|number| self.rank[*number as usize]);
1490 numbers.dedup();
1491 Reached {
1492 names: numbers
1493 .iter()
1494 .map(|number| self.names[*number as usize].clone())
1495 .collect(),
1496 numbers,
1497 }
1498 }
1499}
1500
1501#[derive(Default)]
1504struct Reached {
1505 names: Vec<Id>,
1506 numbers: Vec<u32>,
1507}
1508
1509struct TestTraits<'t> {
1511 setup: bool,
1512 background: bool,
1513 runner: &'t str,
1514 kind: &'t str,
1515}
1516
1517impl<'t> TestTraits<'t> {
1518 fn of(test: &'t MutableTest) -> Self {
1519 Self {
1520 setup: test.role == "setup",
1521 background: test.role == "background",
1522 runner: &test.provenance.runner,
1523 kind: &test.provenance.kind,
1524 }
1525 }
1526}
1527
1528fn confidence_from(
1533 tests: &Reached,
1534 phases: &Reached,
1535 explicit: &Reached,
1536 test_traits: &[Option<TestTraits<'_>>],
1537 phase_kinds: &[Option<&str>],
1538 explicit_kinds: &[Option<&str>],
1539) -> CoverageConfidence {
1540 let mut has_test = false;
1541 let mut setup_roles = true;
1542 let mut background_roles = true;
1543 let mut runners = BTreeSet::new();
1544 let mut kinds = BTreeSet::new();
1545 for number in &tests.numbers {
1546 if let Some(test) = &test_traits[*number as usize] {
1547 has_test = true;
1548 setup_roles &= test.setup;
1549 background_roles &= test.background;
1550 runners.insert(test.runner);
1551 kinds.insert(test.kind);
1552 }
1553 }
1554 let mut has_phase = false;
1555 let mut setup_phases = true;
1556 let mut background_phases = true;
1557 for number in &phases.numbers {
1558 if let Some(kind) = phase_kinds[*number as usize] {
1559 has_phase = true;
1560 setup_phases &= kind == "setup";
1561 background_phases &= kind == "background";
1562 }
1563 }
1564 let has_action = explicit
1565 .numbers
1566 .iter()
1567 .any(|number| explicit_kinds[*number as usize] == Some("action"));
1568 let level = if tests.names.is_empty() {
1569 "unexecuted"
1570 } else if has_action {
1571 "action"
1572 } else {
1573 "executed"
1574 };
1575 CoverageConfidence {
1576 level: level.into(),
1577 setup_only: if has_phase {
1578 setup_phases
1579 } else {
1580 has_test && setup_roles
1581 },
1582 background_only: if has_phase {
1583 background_phases
1584 } else {
1585 has_test && background_roles
1586 },
1587 asserted: false,
1588 tests: tests.names.clone(),
1589 asserted_tests: vec![],
1590 runners: runners.into_iter().map(str::to_owned).collect(),
1591 e2e: kinds.contains("e2e"),
1592 kinds: kinds.into_iter().map(str::to_owned).collect(),
1593 }
1594}
1595
1596pub fn create_coverage_view(
1597 manifest: &CoverageManifest,
1598 raw_results: &[RawTestResult],
1599 generated_at: &str,
1600) -> Result<CoverageView, ReportError> {
1601 create_coverage_view_with_model(
1602 manifest,
1603 &raw_results.iter().collect::<Vec<_>>(),
1604 generated_at,
1605 &javascript_coverage_model(),
1606 &mut Interner::default(),
1607 )
1608}
1609
1610fn create_coverage_view_with_model(
1611 manifest: &CoverageManifest,
1612 raw_results: &[&RawTestResult],
1613 generated_at: &str,
1614 coverage_model: &CoverageModelDeclaration,
1615 ids: &mut Interner,
1616) -> Result<CoverageView, ReportError> {
1617 let mut decision_metadata = manifest.decisions.clone();
1618 let decision_indexes = decision_metadata
1619 .iter()
1620 .enumerate()
1621 .map(|(index, meta)| (meta.id.clone(), index))
1622 .collect::<HashMap<_, _>>();
1623 let manifest_files = manifest
1624 .decisions
1625 .iter()
1626 .map(|meta| meta.file.clone())
1627 .chain(manifest.points.iter().map(|meta| meta.file.clone()))
1628 .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
1629 .chain(
1630 manifest
1631 .scope
1632 .as_ref()
1633 .and_then(|scope| scope.get("entries"))
1634 .and_then(Value::as_array)
1635 .into_iter()
1636 .flatten()
1637 .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
1638 .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
1639 )
1640 .collect::<BTreeSet<_>>();
1641 let mut vectors_by_decision = FastMap::<String, Vec<MutableObservation>>::default();
1642 let mut vector_indexes = FastMap::<String, FastMap<String, usize>>::default();
1643 let mut tests_by_decision = FastMap::<String, Vec<u32>>::default();
1645 let mut tests_by_hit = References::default();
1646 let mut hit_ids = HitIds::default();
1647 let mut tests_by_id = FastMap::<Id, MutableTest>::default();
1648 let mut test_order = Vec::<Id>::new();
1649 let mut phases_by_id = FastMap::<Id, MutablePhase>::default();
1650 let mut phases_by_hit = References::default();
1651 let mut explicit_phases_by_hit = References::default();
1652
1653 for raw in raw_results {
1654 let id = ids.id(raw_test_id(raw));
1655 if !tests_by_id.contains_key(&id) {
1656 test_order.push(id.clone());
1657 tests_by_id.insert(
1658 id.clone(),
1659 MutableTest {
1660 id: id.to_string(),
1661 name: raw.test.clone(),
1662 file: raw.test_file.clone(),
1663 title: raw.title.clone(),
1664 retries: raw.retry.into_iter().collect(),
1665 attempts: BTreeMap::new(),
1666 unstarted: raw.status.as_deref() == Some("unstarted"),
1667 runner_reported_flaky: raw.flaky,
1668 provenance: raw.provenance.clone(),
1669 role: raw.role.clone(),
1670 attribution: raw.attribution.clone(),
1671 hits: Vec::new(),
1672 decisions: BTreeMap::new(),
1673 },
1674 );
1675 }
1676 let test = tests_by_id.get_mut(&id).expect("test was inserted");
1677 test.attribution = least_exact(&test.attribution, &raw.attribution);
1682 test.unstarted |= raw.status.as_deref() == Some("unstarted");
1683 if let Some(retry) = raw.retry {
1684 test.retries.insert(retry);
1685 }
1686 test.runner_reported_flaky |= raw.flaky;
1687 record_attempt(test, raw);
1688
1689 let mut ordered_phases = raw.phases.clone();
1690 ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1691 for phase in &ordered_phases {
1692 phases_by_id.insert(
1693 ids.id(&phase.id),
1694 MutablePhase {
1695 phase: phase.clone(),
1696 test: id.clone(),
1697 hits: Vec::new(),
1698 decisions: BTreeMap::new(),
1699 browser_events: 0,
1700 server_events: 0,
1701 explicit_events: 0,
1702 inferred_events: 0,
1703 explicit_browser_events: 0,
1704 inferred_browser_events: 0,
1705 explicit_server_events: 0,
1706 inferred_server_events: 0,
1707 },
1708 );
1709 }
1710
1711 let correlate = |event: &RuntimeEvent| {
1712 event
1713 .phase_id
1714 .clone()
1715 .or_else(|| phase_at(&ordered_phases, event.timestamp_ms).map(str::to_owned))
1716 };
1717
1718 let snapshots = raw
1719 .runtime
1720 .iter()
1721 .map(|snapshot| (snapshot, false))
1722 .chain(raw.browser.iter().map(|snapshot| (snapshot, true)));
1723 for (snapshot, browser) in snapshots {
1724 for decision in &snapshot.decisions {
1725 let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1726 if manifest_files.contains(&decision.meta.file) {
1727 return Err(ReportError::InvalidServerRecord(format!(
1728 "decision {} is absent from the frozen manifest",
1729 decision.meta.id
1730 )));
1731 }
1732 continue;
1733 };
1734 if decision_metadata[index] != decision.meta {
1735 return Err(ReportError::InvalidServerRecord(format!(
1736 "decision {} metadata differs from the frozen manifest",
1737 decision.meta.id
1738 )));
1739 }
1740 for vector in &decision.vectors {
1741 let key = vector_key(vector);
1742 let indexes = hash_slot(&mut vector_indexes, &decision.meta.id);
1743 let observations = hash_slot(&mut vectors_by_decision, &decision.meta.id);
1744 let observation_index = *indexes.entry(key).or_insert_with(|| {
1745 observations.push(MutableObservation {
1746 vector: vector.clone(),
1747 tests: Vec::new(),
1748 phases: Vec::new(),
1749 explicit_phases: Vec::new(),
1750 });
1751 observations.len() - 1
1752 });
1753 observations[observation_index]
1754 .tests
1755 .push(tests_by_hit.number(&id, ids));
1756 tree_slot(
1757 &mut tests_by_id.get_mut(&id).expect("registered test").decisions,
1758 &decision.meta.id,
1759 )
1760 .insert(vector);
1761 }
1762 if !decision.vectors.is_empty() {
1763 hash_slot(&mut tests_by_decision, &decision.meta.id)
1764 .push(tests_by_hit.number(&id, ids));
1765 }
1766 }
1767 let test_hits = &mut tests_by_id.get_mut(&id).expect("registered test").hits;
1768 for hit in &snapshot.hits {
1769 let hit = hit_ids.number(hit, ids);
1770 tests_by_hit.add(hit, &id, ids);
1771 test_hits.push(hit);
1772 }
1773 for event in &snapshot.events {
1774 let explicit = event.phase_id.is_some();
1775 let Some(phase_id) = correlate_event(event, &ordered_phases) else {
1776 continue;
1777 };
1778 let Some(phase) = phases_by_id.get_mut(phase_id) else {
1779 continue;
1780 };
1781 if event.environment == "browser" {
1782 phase.browser_events += 1;
1783 if explicit {
1784 phase.explicit_browser_events += 1;
1785 } else {
1786 phase.inferred_browser_events += 1;
1787 }
1788 } else {
1789 phase.server_events += 1;
1790 if explicit {
1791 phase.explicit_server_events += 1;
1792 } else {
1793 phase.inferred_server_events += 1;
1794 }
1795 }
1796 if explicit {
1797 phase.explicit_events += 1;
1798 } else {
1799 phase.inferred_events += 1;
1800 }
1801 if event.event_type == "hit" {
1802 let hit = hit_ids.number(&event.id, ids);
1803 phase.hits.push(hit);
1804 phases_by_hit.add(hit, phase_id, ids);
1805 if explicit {
1806 explicit_phases_by_hit.add(hit, phase_id, ids);
1807 }
1808 } else if event.event_type == "decision" {
1809 let vector = event
1810 .vector
1811 .as_ref()
1812 .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1813 tree_slot(&mut phase.decisions, &event.id).insert(vector);
1814 if let Some(index) = vector_indexes
1815 .get(&event.id)
1816 .and_then(|indexes| indexes.get(&vector_key(vector)))
1817 .copied()
1818 && let Some(observation) = vectors_by_decision
1819 .get_mut(&event.id)
1820 .and_then(|observations| observations.get_mut(index))
1821 {
1822 observation.phases.push(phases_by_hit.number(phase_id, ids));
1823 if explicit {
1824 observation
1825 .explicit_phases
1826 .push(explicit_phases_by_hit.number(phase_id, ids));
1827 }
1828 }
1829 } else {
1830 return Err(ReportError::InvalidEvent(event.event_type.clone()));
1831 }
1832 }
1833 if let Some(phase_id) = snapshot.phase_id.as_deref()
1837 && let Some(phase) = phases_by_id.get_mut(phase_id)
1838 {
1839 let observed = snapshot.hits.len()
1840 + snapshot
1841 .decisions
1842 .iter()
1843 .map(|decision| decision.vectors.len())
1844 .sum::<usize>();
1845 if browser {
1846 phase.browser_events += observed;
1847 phase.explicit_browser_events += observed;
1848 } else {
1849 phase.server_events += observed;
1850 phase.explicit_server_events += observed;
1851 }
1852 phase.explicit_events += observed;
1853 for hit in &snapshot.hits {
1854 let hit = hit_ids.number(hit, ids);
1855 phase.hits.push(hit);
1856 phases_by_hit.add(hit, phase_id, ids);
1857 explicit_phases_by_hit.add(hit, phase_id, ids);
1858 }
1859 for decision in &snapshot.decisions {
1860 let id = &decision.meta.id;
1861 for vector in &decision.vectors {
1862 tree_slot(&mut phase.decisions, id).insert(vector);
1863 if let Some(index) = vector_indexes
1864 .get(id)
1865 .and_then(|indexes| indexes.get(&vector_key(vector)))
1866 .copied()
1867 && let Some(observation) = vectors_by_decision
1868 .get_mut(id)
1869 .and_then(|observations| observations.get_mut(index))
1870 {
1871 observation.phases.push(phases_by_hit.number(phase_id, ids));
1872 observation
1873 .explicit_phases
1874 .push(explicit_phases_by_hit.number(phase_id, ids));
1875 }
1876 }
1877 }
1878 }
1879 }
1880
1881 for record in &raw.server {
1882 let (record_id, decision) = if record.record_type == "decision" {
1883 let meta = record
1884 .meta
1885 .as_ref()
1886 .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1887 let vector = record
1888 .vector
1889 .as_ref()
1890 .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1891 let Some(index) = decision_indexes.get(&meta.id).copied() else {
1892 if manifest_files.contains(&meta.file) {
1893 return Err(ReportError::InvalidServerRecord(format!(
1894 "decision {} is absent from the frozen manifest",
1895 meta.id
1896 )));
1897 }
1898 continue;
1899 };
1900 if decision_metadata[index] != *meta {
1901 return Err(ReportError::InvalidServerRecord(format!(
1902 "decision {} metadata differs from the frozen manifest",
1903 meta.id
1904 )));
1905 }
1906 let key = vector_key(vector);
1907 let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1908 let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1909 let index = *indexes.entry(key).or_insert_with(|| {
1910 observations.push(MutableObservation {
1911 vector: vector.clone(),
1912 tests: Vec::new(),
1913 phases: Vec::new(),
1914 explicit_phases: Vec::new(),
1915 });
1916 observations.len() - 1
1917 });
1918 observations[index]
1919 .tests
1920 .push(tests_by_hit.number(&id, ids));
1921 tests_by_id
1922 .get_mut(&id)
1923 .expect("registered test")
1924 .decisions
1925 .entry(meta.id.clone())
1926 .or_default()
1927 .insert(vector);
1928 hash_slot(&mut tests_by_decision, &meta.id).push(tests_by_hit.number(&id, ids));
1929 (meta.id.clone(), Some(vector.clone()))
1930 } else if record.record_type == "hit" {
1931 let hit = record
1932 .id
1933 .as_ref()
1934 .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1935 let number = hit_ids.number(hit, ids);
1936 tests_by_hit.add(number, &id, ids);
1937 tests_by_id
1938 .get_mut(&id)
1939 .expect("registered test")
1940 .hits
1941 .push(number);
1942 (hit.clone(), None)
1943 } else {
1944 return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1945 };
1946 let Some(timestamp_ms) = record.timestamp_ms else {
1947 continue;
1948 };
1949 let event = RuntimeEvent {
1950 event_type: record.record_type.clone(),
1951 id: record_id,
1952 vector: decision,
1953 timestamp_ms,
1954 statement_id: record.statement_id.clone(),
1955 phase_id: record.phase_id.clone(),
1956 environment: "server".into(),
1957 };
1958 let explicit = event.phase_id.is_some();
1959 let phase_id = correlate(&event);
1960 let Some(phase_id) = phase_id else { continue };
1961 let Some(phase) = phases_by_id.get_mut(phase_id.as_str()) else {
1962 continue;
1963 };
1964 phase.server_events += 1;
1965 if explicit {
1966 phase.explicit_events += 1;
1967 phase.explicit_server_events += 1;
1968 } else {
1969 phase.inferred_events += 1;
1970 phase.inferred_server_events += 1;
1971 }
1972 if event.event_type == "hit" {
1973 let hit = hit_ids.number(&event.id, ids);
1974 phase.hits.push(hit);
1975 phases_by_hit.add(hit, &phase_id, ids);
1976 if explicit {
1977 explicit_phases_by_hit.add(hit, &phase_id, ids);
1978 }
1979 } else if let Some(vector) = &event.vector {
1980 phase
1981 .decisions
1982 .entry(event.id.clone())
1983 .or_default()
1984 .insert(vector);
1985 if let Some(index) = vector_indexes
1986 .get(&event.id)
1987 .and_then(|indexes| indexes.get(&vector_key(vector)))
1988 .copied()
1989 && let Some(observation) = vectors_by_decision
1990 .get_mut(&event.id)
1991 .and_then(|observations| observations.get_mut(index))
1992 {
1993 observation
1994 .phases
1995 .push(phases_by_hit.number(&phase_id, ids));
1996 if explicit {
1997 observation
1998 .explicit_phases
1999 .push(explicit_phases_by_hit.number(&phase_id, ids));
2000 }
2001 }
2002 }
2003 }
2004 }
2005
2006 decision_metadata.sort_by(|left, right| {
2011 left.file
2012 .cmp(&right.file)
2013 .then(left.line.cmp(&right.line))
2014 .then(left.column.cmp(&right.column))
2015 });
2016 let tests_by_hit = tests_by_hit.into_relation(&hit_ids);
2017 let phases_by_hit = phases_by_hit.into_relation(&hit_ids);
2018 let explicit_phases_by_hit = explicit_phases_by_hit.into_relation(&hit_ids);
2019 let mut declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
2023 for branch in &manifest.branches {
2024 if !branch.alternatives.is_empty()
2025 && branch
2026 .alternatives
2027 .iter()
2028 .all(|alternative| declined.contains(&alternative.id))
2029 {
2030 declined.insert(&branch.id);
2031 }
2032 }
2033 let test_traits = tests_by_hit
2037 .names
2038 .iter()
2039 .map(|name| tests_by_id.get(name).map(TestTraits::of))
2040 .collect::<Vec<_>>();
2041 let phase_kind = |relation: &Relation| {
2042 relation
2043 .names
2044 .iter()
2045 .map(|name| {
2046 phases_by_id
2047 .get(name)
2048 .map(|phase| phase.phase.kind.as_str())
2049 })
2050 .collect::<Vec<_>>()
2051 };
2052 let phase_kinds = phase_kind(&phases_by_hit);
2053 let explicit_kinds = phase_kind(&explicit_phases_by_hit);
2054 let confidence = |tests: &Reached, phases: &Reached, explicit: &Reached| {
2055 confidence_from(
2056 tests,
2057 phases,
2058 explicit,
2059 &test_traits,
2060 &phase_kinds,
2061 &explicit_kinds,
2062 )
2063 };
2064 let declared_branches = manifest
2070 .branches
2071 .iter()
2072 .map(|branch| branch.id.as_str())
2073 .collect::<HashSet<_>>();
2074 let mut outcome_branches = Vec::new();
2075 let mut decisions = Vec::with_capacity(decision_metadata.len());
2076 for meta in decision_metadata {
2077 let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
2078 let outcome_id = format!("{}:outcome", meta.id);
2079 if !declared_branches.contains(outcome_id.as_str()) {
2080 let alternatives = [true, false]
2081 .into_iter()
2082 .map(|outcome| {
2083 let (mut tests, mut phases, mut explicit) =
2084 (Vec::new(), Vec::new(), Vec::new());
2085 for observation in mutable
2086 .iter()
2087 .filter(|observation| observation.vector.outcome == outcome)
2088 {
2089 tests.extend(&observation.tests);
2090 phases.extend(&observation.phases);
2091 explicit.extend(&observation.explicit_phases);
2092 }
2093 let tests = tests_by_hit.reached(tests);
2094 let phases = phases_by_hit.reached(phases);
2095 let explicit = explicit_phases_by_hit.reached(explicit);
2096 AlternativeResult {
2097 id: format!("{outcome_id}:{outcome}"),
2098 label: outcome.to_string(),
2099 covered: !tests.names.is_empty(),
2100 confidence: confidence(&tests, &phases, &explicit),
2101 tests: tests.names,
2102 phases: phases.names,
2103 }
2104 })
2105 .collect::<Vec<_>>();
2106 outcome_branches.push(BranchResult {
2107 covered: alternatives.iter().all(|alternative| alternative.covered),
2108 meta: BranchMeta {
2109 id: outcome_id,
2110 kind: meta.kind.clone(),
2111 file: meta.file.clone(),
2112 line: meta.line,
2113 column: meta.column,
2114 source: meta.source.clone(),
2115 alternatives: alternatives
2116 .iter()
2117 .map(|alternative| BranchAlternativeMeta {
2118 id: alternative.id.clone(),
2119 label: alternative.label.clone(),
2120 })
2121 .collect(),
2122 },
2123 alternatives,
2124 });
2125 }
2126 let mut observations = Vec::with_capacity(mutable.len());
2127 let mut observed_phases = Vec::new();
2128 let mut observed_explicit = Vec::new();
2129 for observation in mutable {
2130 let tests = tests_by_hit.reached(observation.tests);
2131 let phases = phases_by_hit.reached(observation.phases);
2132 let explicit = explicit_phases_by_hit.reached(observation.explicit_phases);
2133 observed_phases.extend(&phases.numbers);
2134 observed_explicit.extend(&explicit.numbers);
2135 observations.push(VectorObservation {
2136 vector: observation.vector,
2137 confidence: confidence(&tests, &phases, &explicit),
2138 tests: tests.names,
2139 phases: phases.names,
2140 explicit_phases: explicit.names,
2141 });
2142 }
2143 let vectors = observations
2144 .iter()
2145 .map(|observation| observation.vector.clone())
2146 .collect::<Vec<_>>();
2147 let witnesses =
2148 find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
2149 ReportError::DecisionAnalysis {
2150 decision_id: meta.id.clone(),
2151 error,
2152 }
2153 })?;
2154 let mut conditions = Vec::with_capacity(meta.conditions.len());
2155 for (index, source) in meta.conditions.iter().enumerate() {
2156 let witness = witnesses[index].map(|witness| {
2157 [
2158 vectors[witness.first].clone(),
2159 vectors[witness.second].clone(),
2160 ]
2161 });
2162 let witness_tests = witnesses[index].map(|witness| {
2163 [
2164 observations[witness.first].tests.clone(),
2165 observations[witness.second].tests.clone(),
2166 ]
2167 });
2168 let assertion_covered = false;
2169 conditions.push(ConditionResult {
2170 index,
2171 source: source.clone(),
2172 covered: witness.is_some(),
2173 assertion_covered,
2174 witness,
2175 witness_tests,
2176 });
2177 }
2178 let decision_tests =
2179 tests_by_hit.reached(tests_by_decision.remove(&meta.id).unwrap_or_default());
2180 let decision_confidence = confidence(
2181 &decision_tests,
2182 &phases_by_hit.reached(observed_phases),
2183 &explicit_phases_by_hit.reached(observed_explicit),
2184 );
2185 decisions.push(DecisionResult {
2186 executed: !vectors.is_empty(),
2187 covered: conditions.iter().all(|condition| condition.covered),
2188 meta,
2189 vectors,
2190 vector_observations: observations,
2191 conditions,
2192 tests: decision_tests.names,
2193 confidence: decision_confidence,
2194 });
2195 }
2196
2197 let points = manifest
2198 .points
2199 .iter()
2200 .cloned()
2201 .map(|meta| {
2202 let tests = tests_by_hit.of(&meta.id);
2203 let phases = phases_by_hit.of(&meta.id);
2204 let explicit = explicit_phases_by_hit.of(&meta.id);
2205 PointResult {
2206 measured: !declined.contains(&meta.id),
2207 covered: tests_by_hit.by_hit.contains_key(&meta.id),
2208 confidence: confidence(tests, phases, explicit),
2209 meta,
2210 tests: tests.names.clone(),
2211 phases: phases.names.clone(),
2212 }
2213 })
2214 .collect::<Vec<_>>();
2215
2216 let branches = manifest
2217 .branches
2218 .iter()
2219 .cloned()
2220 .map(|mut meta| {
2221 meta.alternatives
2223 .retain(|alternative| !declined.contains(&alternative.id));
2224 let alternatives = meta
2225 .alternatives
2226 .iter()
2227 .map(|alternative| {
2228 let tests = tests_by_hit.of(&alternative.id);
2229 let phases = phases_by_hit.of(&alternative.id);
2230 let explicit = explicit_phases_by_hit.of(&alternative.id);
2231 AlternativeResult {
2232 id: alternative.id.clone(),
2233 label: alternative.label.clone(),
2234 covered: tests_by_hit.by_hit.contains_key(&alternative.id),
2235 tests: tests.names.clone(),
2236 phases: phases.names.clone(),
2237 confidence: confidence(tests, phases, explicit),
2238 }
2239 })
2240 .collect::<Vec<_>>();
2241 BranchResult {
2242 covered: alternatives.iter().all(|alternative| alternative.covered),
2243 meta,
2244 alternatives,
2245 }
2246 })
2247 .chain(outcome_branches)
2248 .collect::<Vec<_>>();
2249
2250 let mut line_aggregates = BTreeMap::<SourceLine, LineAggregate>::new();
2261 for point in &points {
2262 let aggregate = line_aggregates
2263 .entry(SourceLine {
2264 file: ids.id(&point.meta.file),
2265 line: point.meta.line,
2266 })
2267 .or_default();
2268 if declined.contains(&point.meta.id) {
2269 continue;
2270 }
2271 aggregate.measured = true;
2272 aggregate.covered |= point.covered;
2273 aggregate
2274 .tests
2275 .extend(&tests_by_hit.of(&point.meta.id).numbers);
2276 aggregate
2277 .phases
2278 .extend(&phases_by_hit.of(&point.meta.id).numbers);
2279 aggregate
2280 .explicit_phases
2281 .extend(&explicit_phases_by_hit.of(&point.meta.id).numbers);
2282 }
2283 let lines = line_aggregates
2284 .into_iter()
2285 .map(|(location, aggregate)| {
2286 let LineAggregate {
2287 covered,
2288 measured,
2289 tests,
2290 phases,
2291 explicit_phases,
2292 } = aggregate;
2293 let tests = tests_by_hit.reached(tests);
2294 let phases = phases_by_hit.reached(phases);
2295 let explicit = explicit_phases_by_hit.reached(explicit_phases);
2296 let traits = tests
2297 .numbers
2298 .iter()
2299 .filter_map(|number| test_traits[*number as usize].as_ref())
2300 .collect::<Vec<_>>();
2301 let runners = traits
2302 .iter()
2303 .map(|traits| traits.runner)
2304 .collect::<BTreeSet<_>>();
2305 let kinds = traits
2306 .iter()
2307 .map(|traits| traits.kind)
2308 .collect::<BTreeSet<_>>();
2309 LineResult {
2310 file: location.file.to_string(),
2311 line: location.line,
2312 covered,
2313 measured,
2314 confidence: confidence(&tests, &phases, &explicit),
2315 tests: tests.names,
2316 runners: runners.iter().map(|runner| (*runner).to_owned()).collect(),
2317 exclusive_kind: (kinds.len() == 1).then(|| (*kinds.first().unwrap()).to_owned()),
2318 phases: phases.names,
2319 kinds: kinds.iter().map(|kind| (*kind).to_owned()).collect(),
2320 }
2321 })
2322 .collect::<Vec<_>>();
2323 drop(test_traits);
2324
2325 let hit_ranks = hit_ids.ranks();
2329 let mut point_files = manifest
2330 .points
2331 .iter()
2332 .map(|point| ids.id(&point.file))
2333 .collect::<Vec<_>>();
2334 point_files.sort_unstable();
2335 point_files.dedup();
2336 let mut hit_locations = vec![None; hit_ids.names.len()];
2337 for point in &manifest.points {
2338 if let Some(number) = hit_ids.numbers.get(point.id.as_str()) {
2339 let file = point_files
2340 .binary_search_by(|file| file.as_str().cmp(&point.file))
2341 .expect("every point file is listed") as u32;
2342 hit_locations[*number] = Some((file, point.line));
2343 }
2344 }
2345 let lines_of = |hits: &[usize]| {
2346 let mut located = hits
2347 .iter()
2348 .filter_map(|hit| hit_locations[*hit])
2349 .collect::<Vec<_>>();
2350 located.sort_unstable();
2351 located.dedup();
2352 located
2353 .into_iter()
2354 .map(|(file, line)| SourceLine {
2355 file: point_files[file as usize].clone(),
2356 line,
2357 })
2358 .collect::<Vec<_>>()
2359 };
2360 test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
2361 let tests = test_order
2362 .into_iter()
2363 .map(|id| {
2364 let test = tests_by_id.remove(&id).expect("test order references test");
2365 let outcome = test_outcome(&test);
2366 let lines = lines_of(&test.hits);
2367 let hits = hit_ids.sorted_by(test.hits, &hit_ranks);
2368 TestCoverageResult {
2369 id: test.id,
2370 name: test.name,
2371 file: test.file,
2372 title: test.title,
2373 retries: test.retries.into_iter().collect(),
2374 attempts: test.attempts.into_values().collect(),
2375 outcome,
2376 provenance: test.provenance,
2377 role: test.role,
2378 attribution: test.attribution,
2379 hits,
2380 decisions: test
2381 .decisions
2382 .into_iter()
2383 .map(|(id, vectors)| TestDecisionResult {
2384 id,
2385 vectors: vectors.values,
2386 })
2387 .collect(),
2388 lines,
2389 }
2390 })
2391 .collect::<Vec<_>>();
2392
2393 let mut test_files = BTreeMap::<
2394 String,
2395 (
2396 BTreeSet<String>,
2397 BTreeSet<String>,
2398 BTreeSet<String>,
2399 BTreeSet<SourceLine>,
2400 ),
2401 >::new();
2402 for test in &tests {
2403 let aggregate = test_files
2404 .entry(
2405 test.file
2406 .clone()
2407 .unwrap_or_else(|| "(unknown test file)".into()),
2408 )
2409 .or_default();
2410 aggregate.0.insert(test.id.clone());
2411 aggregate.1.insert(test.provenance.runner.clone());
2412 aggregate.2.insert(test.provenance.kind.clone());
2413 aggregate.3.extend(test.lines.iter().cloned());
2414 }
2415 let test_files = test_files
2416 .into_iter()
2417 .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
2418 file,
2419 tests: sorted(&tests),
2420 runners: sorted(&runners),
2421 kinds: sorted(&kinds),
2422 lines: lines.into_iter().collect(),
2423 })
2424 .collect::<Vec<_>>();
2425
2426 let mut phases = phases_by_id.into_values().collect::<Vec<_>>();
2427 phases.sort_by(|left, right| {
2428 left.phase
2429 .started_at_ms
2430 .cmp(&right.phase.started_at_ms)
2431 .then(left.phase.id.cmp(&right.phase.id))
2432 });
2433 let phases = phases
2434 .into_iter()
2435 .map(|phase| {
2436 let lines = lines_of(&phase.hits);
2437 let hits = hit_ids.sorted_by(phase.hits, &hit_ranks);
2438 PhaseResult {
2439 phase: phase.phase,
2440 test: phase.test,
2441 hits,
2442 decisions: phase
2443 .decisions
2444 .into_iter()
2445 .map(|(id, vectors)| TestDecisionResult {
2446 id,
2447 vectors: vectors.values,
2448 })
2449 .collect(),
2450 lines,
2451 browser_events: phase.browser_events,
2452 server_events: phase.server_events,
2453 explicit_events: phase.explicit_events,
2454 inferred_events: phase.inferred_events,
2455 explicit_browser_events: phase.explicit_browser_events,
2456 inferred_browser_events: phase.inferred_browser_events,
2457 explicit_server_events: phase.explicit_server_events,
2458 inferred_server_events: phase.inferred_server_events,
2459 }
2460 })
2461 .collect::<Vec<_>>();
2462
2463 let total_obligations = decisions.len() + points.len() + branches.len();
2464 let (decisions, points, branches) = if declined.is_empty() {
2465 (decisions, points, branches)
2466 } else {
2467 (
2468 decisions
2469 .into_iter()
2470 .filter(|result| !declined.contains(&result.meta.id))
2471 .collect::<Vec<_>>(),
2472 points
2473 .into_iter()
2474 .filter(|result| !declined.contains(&result.meta.id))
2475 .collect::<Vec<_>>(),
2476 branches
2477 .into_iter()
2478 .filter(|result| !declined.contains(&result.meta.id))
2479 .collect::<Vec<_>>(),
2480 )
2481 };
2482 let measured_obligations = decisions.len() + points.len() + branches.len();
2483 let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
2484 if total_obligations > measured_obligations {
2485 summary.unmeasured_obligations = Some(total_obligations - measured_obligations);
2486 summary.exact_fraction_pct = Some(if total_obligations == 0 {
2487 100.0
2488 } else {
2489 (measured_obligations as f64) * 100.0 / (total_obligations as f64)
2490 });
2491 }
2492 if manifest.limitations.iter().any(blocking_limitation) {
2499 summary.coverage_complete = false;
2500 summary.completeness_blocked = Some(true);
2501 }
2502
2503 let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
2504 let values = tests
2505 .iter()
2506 .map(|test| {
2507 if field == "kind" {
2508 test.provenance.kind.clone()
2509 } else {
2510 test.provenance.runner.clone()
2511 }
2512 })
2513 .collect::<BTreeSet<_>>();
2514 values
2515 .into_iter()
2516 .map(|value| {
2517 let in_dimension = |test: &&TestCoverageResult| {
2518 if field == "kind" {
2519 test.provenance.kind == value
2520 } else {
2521 test.provenance.runner == value
2522 }
2523 };
2524 let selected = tests
2531 .iter()
2532 .filter(in_dimension)
2533 .filter(|test| coverage_is_its_own(&test.attribution))
2534 .map(|test| test.id.clone())
2535 .collect::<BTreeSet<_>>();
2536 let counted = tests
2537 .iter()
2538 .filter(in_dimension)
2539 .map(|test| test.id.clone())
2540 .collect::<BTreeSet<_>>();
2541 Ok(DimensionCoverage {
2542 kind: (field == "kind").then(|| value.clone()),
2543 runner: (field == "runner").then(|| value.clone()),
2544 tests: tests
2547 .iter()
2548 .filter(|test| counted.contains(&test.id) && test.role == "test")
2549 .count(),
2550 setups: tests
2551 .iter()
2552 .filter(|test| counted.contains(&test.id) && test.role == "setup")
2553 .count(),
2554 attributed: tests
2555 .iter()
2556 .filter(|test| selected.contains(&test.id) && test.role == "test")
2557 .count(),
2558 summary: summary_for_results(
2559 &decisions,
2560 &points,
2561 &branches,
2562 &lines,
2563 Some(&selected),
2564 )?,
2565 })
2566 })
2567 .collect()
2568 };
2569
2570 Ok(CoverageView {
2571 generated_at: generated_at.into(),
2572 variant: coverage_model.variant.clone(),
2573 scope: manifest.scope.clone(),
2574 model: CoverageModel {
2575 language: coverage_model.language.clone(),
2576 name: coverage_model.name.clone(),
2577 completeness_meaning: coverage_model.completeness_meaning.clone(),
2578 measured: coverage_model.measured.clone(),
2579 not_measured: coverage_model.not_measured.clone(),
2580 },
2581 integrity: None,
2582 limitations: manifest.limitations.clone(),
2583 transport: None,
2584 summary,
2585 coverage_by_kind: dimension_coverage("kind")?,
2586 coverage_by_runner: dimension_coverage("runner")?,
2587 decisions,
2588 points,
2589 branches,
2590 tests,
2591 test_files,
2592 phases,
2593 lines,
2594 })
2595}
2596
2597pub fn analyze_coverage_results(
2598 request: &CoverageReportRequest,
2599) -> Result<CoverageReport, ReportError> {
2600 if let Some(scope) = request
2601 .raw_results
2602 .iter()
2603 .filter_map(|raw| raw.scope.as_ref())
2604 .find(|scope| scope.run_id != request.run_id)
2605 {
2606 return Err(ReportError::ScopeMismatch {
2607 expected: request.run_id.clone(),
2608 actual: scope.run_id.clone(),
2609 });
2610 }
2611 if request.raw_results.is_empty() {
2612 return Err(ReportError::NoEvidence(request.run_id.clone()));
2613 }
2614 let default_model = javascript_coverage_model();
2615 let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
2616 PersistedCoverageModel::from_declaration(coverage_model).map_err(|reason| {
2617 ReportError::InvalidJson {
2618 path: "coverage-model.json".into(),
2619 reason: reason.into(),
2620 }
2621 })?;
2622 let all = request.raw_results.iter().collect::<Vec<_>>();
2627 let passing = passing_coverage_results(&request.raw_results);
2628 let failing = failed_coverage_results(&request.raw_results);
2629 let build = |results: &[&RawTestResult]| {
2630 create_coverage_view_with_model(
2631 &request.manifest,
2632 results,
2633 &request.generated_at,
2634 coverage_model,
2635 &mut Interner::default(),
2636 )
2637 };
2638 let (view, passed, failed) = std::thread::scope(|scope| {
2639 let passed = scope.spawn(|| build(&passing));
2640 let failed = scope.spawn(|| build(&failing));
2641 let view = build(&all);
2642 (
2643 view,
2644 passed
2645 .join()
2646 .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
2647 failed
2648 .join()
2649 .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
2650 )
2651 });
2652 let (view, passed, failed) = (view?, passed?, failed?);
2653 let execution = match request.test_exit_code {
2654 ExitCodeInput::Missing => None,
2655 ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
2656 valid: test_exit_code == Some(0),
2657 test_exit_code,
2658 }),
2659 };
2660 let mut view = view;
2661 let mut passed = passed;
2662 let mut failed = failed;
2663 if let Some(integrity) = &request.integrity {
2664 view.integrity = Some(integrity.clone());
2665 passed.integrity = Some(integrity.clone());
2666 failed.integrity = Some(integrity.clone());
2667 }
2668 Ok(CoverageReport {
2669 view,
2670 execution,
2671 filters: CoverageFilters { passed, failed },
2672 })
2673}
2674
2675fn parse_entry<T: for<'de> Deserialize<'de>>(
2676 entry: &EvidenceArchiveEntry,
2677) -> Result<T, ReportError> {
2678 serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
2679 path: entry.path.clone(),
2680 reason: error.to_string(),
2681 })
2682}
2683
2684fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
2685 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2686) -> Result<Vec<T>, ReportError> {
2687 let mut records = Vec::new();
2688 for entry in entries {
2689 let Some(contents) = entry.contents.strip_suffix(b"\n") else {
2690 return Err(ReportError::InvalidJson {
2691 path: entry.path.clone(),
2692 reason: "recognized JSONL evidence must end with a newline".into(),
2693 });
2694 };
2695 if contents.is_empty() {
2696 return Err(ReportError::InvalidJson {
2697 path: entry.path.clone(),
2698 reason: "recognized JSONL evidence must contain at least one record".into(),
2699 });
2700 }
2701 for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
2702 if line.is_empty() {
2703 return Err(ReportError::InvalidJson {
2704 path: entry.path.clone(),
2705 reason: format!("blank JSONL record at line {}", index + 1),
2706 });
2707 }
2708 records.push(serde_json::from_slice(line).map_err(|error| {
2709 ReportError::InvalidJson {
2710 path: entry.path.clone(),
2711 reason: format!("invalid JSONL record at line {}: {error}", index + 1),
2712 }
2713 })?);
2714 }
2715 }
2716 Ok(records)
2717}
2718
2719struct TolerantJsonLines<T> {
2725 records: Vec<T>,
2726 corrupt_records: usize,
2727 corrupt_files: usize,
2728}
2729
2730fn parse_server_json_lines<'a, T: for<'de> Deserialize<'de>>(
2731 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2732) -> TolerantJsonLines<T> {
2733 let mut parsed = TolerantJsonLines {
2734 records: Vec::new(),
2735 corrupt_records: 0,
2736 corrupt_files: 0,
2737 };
2738 for entry in entries {
2739 let mut corrupt_here = 0;
2740 for line in entry.contents.split(|byte| *byte == b'\n') {
2741 if line.is_empty() {
2742 continue;
2743 }
2744 match serde_json::from_slice(line) {
2745 Ok(record) => parsed.records.push(record),
2746 Err(_) => corrupt_here += 1,
2747 }
2748 }
2749 if corrupt_here > 0 {
2750 parsed.corrupt_records += corrupt_here;
2751 parsed.corrupt_files += 1;
2752 }
2753 }
2754 parsed
2755}
2756
2757fn is_mcdc_result(path: &str) -> bool {
2758 path == "mcdc.json" || path.ends_with("/mcdc.json")
2759}
2760
2761fn is_mcdc_journal(path: &str) -> bool {
2762 path == "mcdc.jsonl" || path.ends_with(".mcdc.jsonl")
2763}
2764
2765pub fn analyze_coverage_archive(
2766 request: &ArchiveReportRequest,
2767) -> Result<CoverageReport, ReportError> {
2768 let entries = read_archive(Path::new(&request.archive_path))
2769 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2770 let manifest = entries
2771 .iter()
2772 .find(|entry| entry.path == "manifest.json")
2773 .ok_or(ReportError::MissingManifest)
2774 .and_then(parse_entry::<CoverageManifest>)?;
2775 let mut raw_results = entries
2776 .iter()
2777 .filter(|entry| is_mcdc_result(&entry.path))
2778 .map(parse_entry::<RawTestResult>)
2779 .collect::<Result<Vec<_>, _>>()?;
2780 let journal_results = parse_json_lines::<RawTestResult>(
2781 entries.iter().filter(|entry| is_mcdc_journal(&entry.path)),
2782 )?;
2783 raw_results.extend(journal_results);
2784
2785 let scoped = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2786 entry.path.starts_with("server/")
2787 && !entry.path.starts_with("server/background/")
2788 && entry.path.ends_with(".jsonl")
2789 }));
2790 let scoped_records = scoped.records;
2791 let mut by_scope = std::collections::HashMap::<&ExecutionScope, usize>::new();
2796 for (index, raw) in raw_results.iter().enumerate() {
2797 if let Some(scope) = &raw.scope {
2798 by_scope.entry(scope).or_insert(index);
2799 }
2800 }
2801 let by_scope: std::collections::HashMap<ExecutionScope, usize> = by_scope
2802 .into_iter()
2803 .map(|(scope, index)| (scope.clone(), index))
2804 .collect();
2805 let mut seen = std::collections::HashMap::<usize, std::collections::HashSet<Vec<u8>>>::new();
2806 for record in &scoped_records {
2807 let Some(scope) = &record.scope else { continue };
2808 let Some(&index) = by_scope.get(scope) else {
2809 continue;
2810 };
2811 let raw = &mut raw_results[index];
2812 let known = seen.entry(index).or_insert_with(|| {
2813 raw.server
2814 .iter()
2815 .filter_map(|existing| serde_json::to_vec(existing).ok())
2816 .collect()
2817 });
2818 let Ok(key) = serde_json::to_vec(record) else {
2819 if !raw.server.contains(record) {
2820 raw.server.push(record.clone());
2821 }
2822 continue;
2823 };
2824 if known.insert(key) {
2825 raw.server.push(record.clone());
2826 }
2827 }
2828
2829 let background = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2830 entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2831 }));
2832 let background_records = background.records;
2833 if !background_records.is_empty() {
2834 raw_results.push(RawTestResult {
2835 test_id: Some(format!("background:{}", request.run_id)),
2836 scope: None,
2837 test: "Background / unattributed".into(),
2838 test_file: None,
2839 title: Some("Background / unattributed".into()),
2840 retry: None,
2841 status: Some("unknown".into()),
2842 expected_status: None,
2843 flaky: false,
2844 provenance: TestProvenance {
2845 runner: "background".into(),
2846 kind: "background".into(),
2847 project: None,
2848 source: "explicit".into(),
2849 },
2850 role: "background".into(),
2851 attribution: ATTRIBUTION_EXACT.into(),
2852 phases: vec![],
2853 runtime: vec![],
2854 browser: vec![],
2855 server: background_records.clone(),
2856 });
2857 }
2858
2859 let execution =
2862 parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2863 entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2864 }));
2865 let execution_events = execution.records;
2866 let count_event = |name: &str| {
2867 execution_events
2868 .iter()
2869 .filter(|event| event.kind() == name)
2870 .count()
2871 };
2872 let transport = TransportStats {
2873 processes: count_event("process"),
2874 child_launches: count_event("child-launch"),
2875 remote_launches: count_event("remote-launch"),
2876 workspace_capabilities: count_event("workspace-capability"),
2877 guest_processes: count_event("guest-process"),
2878 scoped_server_records: scoped_records.len(),
2879 background_server_records: background_records.len(),
2880 corrupt_records: scoped.corrupt_records
2881 + background.corrupt_records
2882 + execution.corrupt_records,
2883 corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
2884 };
2885 let frontend = entries
2886 .iter()
2887 .find(|entry| entry.path == "frontend.json")
2888 .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
2889 .and_then(parse_entry::<FrontendRunDeclaration>)?;
2890 let persisted = entries
2891 .iter()
2892 .find(|entry| entry.path == "coverage-model.json")
2893 .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
2894 .and_then(parse_entry::<PersistedCoverageModel>)?;
2895 let coverage_model =
2896 persisted
2897 .into_declaration()
2898 .map_err(|reason| ReportError::InvalidJson {
2899 path: "coverage-model.json".into(),
2900 reason: reason.into(),
2901 })?;
2902 if frontend.language != coverage_model.language {
2903 return Err(ReportError::InvalidArchive(format!(
2904 "frontend language {} differs from coverage model language {}",
2905 frontend.language, coverage_model.language
2906 )));
2907 }
2908 separate_repeated_executions(&mut raw_results);
2909 release_leaked_phases(&mut raw_results);
2910 let normalized = CoverageReportRequest {
2911 run_id: request.run_id.clone(),
2912 manifest,
2913 raw_results,
2914 generated_at: request.generated_at.clone(),
2915 coverage_model: Some(coverage_model),
2916 integrity: request.integrity.clone(),
2917 test_exit_code: request.test_exit_code.clone(),
2918 };
2919 drop(entries);
2923 analyze_frontend_request(&frontend, &normalized, transport)
2924}
2925
2926fn separate_repeated_executions(raw_results: &mut [RawTestResult]) {
2936 let mut declared = BTreeSet::<String>::new();
2937 let mut executions = BTreeMap::<String, usize>::new();
2938 for raw in raw_results.iter_mut() {
2939 let repeated = raw.phases.iter().any(|phase| declared.contains(&phase.id));
2940 if !repeated {
2941 declared.extend(raw.phases.iter().map(|phase| phase.id.clone()));
2942 continue;
2943 }
2944 let mut renamed = BTreeMap::<String, String>::new();
2945 for phase in &raw.phases {
2946 let count = executions.entry(phase.id.clone()).or_insert(1);
2947 let fresh = loop {
2948 *count += 1;
2949 let candidate = format!("{}~{}", phase.id, count);
2950 if !declared.contains(&candidate) {
2951 break candidate;
2952 }
2953 };
2954 renamed.insert(phase.id.clone(), fresh);
2955 }
2956 let rename = |phase: &mut Option<String>| {
2957 if let Some(fresh) = phase.as_ref().and_then(|id| renamed.get(id)) {
2958 *phase = Some(fresh.clone());
2959 }
2960 };
2961 for phase in &mut raw.phases {
2962 phase.id = renamed[&phase.id].clone();
2963 rename(&mut phase.caused_by_phase_id);
2964 }
2965 for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
2966 rename(&mut snapshot.phase_id);
2967 for event in &mut snapshot.events {
2968 rename(&mut event.phase_id);
2969 }
2970 }
2971 for record in &mut raw.server {
2972 rename(&mut record.phase_id);
2973 }
2974 declared.extend(renamed.into_values());
2975 }
2976}
2977
2978fn release_leaked_phases(raw_results: &mut [RawTestResult]) {
2988 let declared = raw_results
2989 .iter()
2990 .flat_map(|raw| raw.phases.iter().map(|phase| phase.id.clone()))
2991 .collect::<BTreeSet<_>>();
2992 for raw in raw_results.iter_mut() {
2993 let own = raw
2994 .phases
2995 .iter()
2996 .map(|phase| phase.id.clone())
2997 .collect::<BTreeSet<_>>();
2998 let leaked = |phase: &mut Option<String>| {
2999 if phase
3000 .as_ref()
3001 .is_some_and(|id| !own.contains(id) && declared.contains(id))
3002 {
3003 *phase = None;
3004 }
3005 };
3006 for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
3007 leaked(&mut snapshot.phase_id);
3008 for event in &mut snapshot.events {
3009 leaked(&mut event.phase_id);
3010 }
3011 }
3012 for record in &mut raw.server {
3013 leaked(&mut record.phase_id);
3014 }
3015 }
3016}
3017
3018pub fn analyze_frontend_request(
3023 frontend: &FrontendRunDeclaration,
3024 normalized: &CoverageReportRequest,
3025 transport: TransportStats,
3026) -> Result<CoverageReport, ReportError> {
3027 let mut report = crate::frontend_protocol::analyze_frontend_results(frontend, normalized)
3028 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
3029 report.view.transport = Some(transport.clone());
3030 report.filters.passed.transport = Some(transport.clone());
3031 report.filters.failed.transport = Some(transport);
3032 Ok(report)
3033}
3034
3035impl TransportStats {
3036 pub fn none() -> Self {
3039 Self {
3040 processes: 0,
3041 child_launches: 0,
3042 remote_launches: 0,
3043 workspace_capabilities: 0,
3044 guest_processes: 0,
3045 scoped_server_records: 0,
3046 background_server_records: 0,
3047 corrupt_records: 0,
3048 corrupt_files: 0,
3049 }
3050 }
3051}
3052
3053#[cfg(test)]
3054mod tests {
3055 use std::{
3056 fs,
3057 sync::atomic::{AtomicU64, Ordering},
3058 time::SystemTime,
3059 };
3060
3061 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
3062
3063 use super::*;
3064
3065 #[test]
3066 fn a_view_holds_one_copy_of_each_id_it_names() {
3067 let root = std::env::temp_dir().join(format!(
3073 "supercov-shared-ids-{}-{}",
3074 std::process::id(),
3075 std::time::SystemTime::now()
3076 .duration_since(std::time::UNIX_EPOCH)
3077 .unwrap()
3078 .as_nanos()
3079 ));
3080 let directory = crate::run_store::create_analyzable_test_run(&root, "shared");
3081 let run = crate::run_store::discover_runs(&root)
3082 .unwrap()
3083 .runs
3084 .remove(0);
3085 let report = crate::run_store::analyze_stored_run(&run).unwrap();
3086 let _ = directory;
3087 for view in [&report.view, &report.filters.passed] {
3088 let mentions = view
3089 .points
3090 .iter()
3091 .flat_map(|point| point.tests.iter().chain(&point.confidence.tests))
3092 .chain(view.lines.iter().flat_map(|line| &line.tests))
3093 .collect::<Vec<_>>();
3094 assert!(
3095 mentions.len() >= 2,
3096 "the fixture names its test in several places"
3097 );
3098 let first = mentions[0];
3099 for mention in &mentions {
3100 assert_eq!(*mention, first);
3101 assert!(
3102 mention.same_allocation(first),
3103 "every mention shares one copy"
3104 );
3105 }
3106 }
3107 fs::remove_dir_all(root).unwrap();
3108 }
3109
3110 #[test]
3111 fn references_numbered_as_they_arrive_come_out_as_the_sets_inserted_directly() {
3112 let arrivals = [
3115 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3116 ("py:statement:a", "h11/tests/test_x.py::test_b[2-50]"),
3117 ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3118 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3119 ("py:statement:a", "h11/tests/test_x.py::test_b[10-50]"),
3120 ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3121 ("py:statement:c", "python-phase:9"),
3122 ("py:statement:c", "python-phase:10"),
3123 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3124 ];
3125 let mut ids = Interner::default();
3126 let mut hits = HitIds::default();
3127 let mut numbered = References::default();
3128 hits.number("unused", &mut ids);
3130 let mut direct = HashMap::<String, BTreeSet<String>>::new();
3131 for (obligation, name) in arrivals {
3132 let hit = hits.number(obligation, &mut ids);
3133 numbered.add(hit, name, &mut ids);
3134 direct
3135 .entry(obligation.to_owned())
3136 .or_default()
3137 .insert(name.to_owned());
3138 }
3139 let numbered = numbered
3140 .into_relation(&hits)
3141 .by_hit
3142 .into_iter()
3143 .map(|(obligation, reached)| {
3144 let names = reached
3145 .names
3146 .into_iter()
3147 .map(String::from)
3148 .collect::<Vec<_>>();
3149 let mut sorted_names = names.clone();
3150 sorted_names.sort();
3151 sorted_names.dedup();
3152 assert_eq!(
3153 names, sorted_names,
3154 "a relation lists its ids once, in order"
3155 );
3156 (obligation, names.into_iter().collect())
3157 })
3158 .collect::<HashMap<_, BTreeSet<String>>>();
3159 assert_eq!(numbered, direct);
3160 assert_eq!(
3161 hits.sorted((0..hits.names.len()).collect()),
3162 [
3163 "py:statement:a",
3164 "py:statement:b",
3165 "py:statement:c",
3166 "unused"
3167 ]
3168 .map(Id::from)
3169 );
3170 }
3171
3172 static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
3173
3174 fn point(id: &str, line: usize) -> PointMeta {
3175 PointMeta {
3176 id: id.into(),
3177 kind: PointKind::Statement,
3178 file: "src/app.js".into(),
3179 line,
3180 column: 0,
3181 source: "work();".into(),
3182 label: None,
3183 }
3184 }
3185
3186 fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
3187 RawTestResult {
3188 test_id: Some(id.into()),
3189 scope: None,
3190 test: id.into(),
3191 test_file: Some("tests/app.test.js".into()),
3192 title: None,
3193 retry: Some(retry),
3194 status: Some(status.into()),
3195 expected_status: None,
3196 flaky: false,
3197 provenance: TestProvenance {
3198 runner: "node:test".into(),
3199 kind: "unit".into(),
3200 project: None,
3201 source: "runner-default".into(),
3202 },
3203 role: "test".into(),
3204 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
3205 phases: vec![],
3206 runtime: vec![RuntimeSnapshot {
3207 decisions: vec![],
3208 hits: hits.iter().map(|hit| (*hit).into()).collect(),
3209 events: vec![],
3210 logicals: vec![],
3211 phase_id: None,
3212 }],
3213 browser: vec![],
3214 server: vec![],
3215 }
3216 }
3217
3218 #[test]
3219 fn declined_obligations_are_unmeasured_never_uncovered() {
3220 let point = |id: &str, line: usize| PointMeta {
3224 id: id.into(),
3225 kind: PointKind::Statement,
3226 file: "src/app.js".into(),
3227 line,
3228 column: 0,
3229 source: "work();".into(),
3230 label: None,
3231 };
3232 let measured_only = CoverageManifest {
3233 decisions: vec![],
3234 points: vec![point("measured", 1)],
3235 branches: vec![],
3236 limitations: vec![],
3237 unmeasured: Vec::new(),
3238 scope: None,
3239 assertion_sites: Vec::new(),
3240 };
3241 let with_declined = CoverageManifest {
3242 decisions: vec![],
3243 points: vec![point("measured", 1), point("declined", 2)],
3244 branches: vec![],
3245 limitations: vec![],
3246 unmeasured: vec!["declined".into()],
3247 scope: None,
3248 assertion_sites: Vec::new(),
3249 };
3250
3251 let baseline =
3252 create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
3253 let view =
3254 create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
3255
3256 assert_eq!(
3258 view.summary.statements.total, baseline.summary.statements.total,
3259 "a declined obligation stayed in the covered/uncovered denominator"
3260 );
3261 assert_eq!(view.summary.unmeasured_obligations, Some(1));
3262 assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
3263
3264 assert_eq!(
3268 view.summary.lines.total, baseline.summary.lines.total,
3269 "a declined obligation kept its line in the line denominator"
3270 );
3271 let declined_line = view
3272 .lines
3273 .iter()
3274 .find(|line| line.line == 2)
3275 .expect("the declined line stays addressable");
3276 assert!(!declined_line.measured);
3277 assert!(!declined_line.covered);
3278
3279 assert_eq!(baseline.summary.unmeasured_obligations, None);
3282 assert_eq!(baseline.summary.exact_fraction_pct, None);
3283 let encoded = serde_json::to_string(&baseline.summary).unwrap();
3284 assert!(
3285 !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
3286 "a fully measured summary must serialize unchanged: {encoded}"
3287 );
3288 }
3289
3290 #[test]
3291 fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
3292 let point = |id: &str| PointMeta {
3296 id: id.into(),
3297 kind: PointKind::Statement,
3298 file: "src/app.js".into(),
3299 line: 1,
3300 column: 0,
3301 source: "work();".into(),
3302 label: None,
3303 };
3304 let manifest = CoverageManifest {
3305 decisions: vec![],
3306 points: vec![point("declined")],
3307 branches: vec![],
3308 limitations: vec![],
3309 unmeasured: vec!["declined".into()],
3310 scope: None,
3311 assertion_sites: Vec::new(),
3312 };
3313 let view = create_coverage_view(
3314 &manifest,
3315 &[raw("test", 0, "passed", &["declined"])],
3316 "time",
3317 )
3318 .unwrap();
3319
3320 assert_eq!(view.summary.lines.total, 0);
3321 assert_eq!(view.summary.lines.covered, 0);
3322 let line = view.lines.first().expect("the line stays addressable");
3323 assert!(!line.measured);
3324 assert!(!line.covered, "an unmeasured line is not covered either");
3325 }
3326
3327 #[test]
3328 fn report_retains_unexecuted_manifest_conditions() {
3329 let manifest = CoverageManifest {
3330 unmeasured: Vec::new(),
3331 decisions: vec![DecisionMeta {
3332 id: "decision".into(),
3333 file: "src/app.js".into(),
3334 line: 1,
3335 column: 0,
3336 source: "left && right".into(),
3337 conditions: vec!["left".into(), "right".into()],
3338 kind: "if".into(),
3339 }],
3340 points: vec![],
3341 branches: vec![],
3342 limitations: vec![],
3343 scope: None,
3344 assertion_sites: Vec::new(),
3345 };
3346 let view =
3347 create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
3348 assert_eq!(view.summary.conditions, 2);
3349 assert_eq!(view.summary.covered_conditions, 0);
3350 assert_eq!(view.decisions[0].conditions.len(), 2);
3351 }
3352
3353 #[test]
3354 fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
3355 let manifest = CoverageManifest {
3356 unmeasured: Vec::new(),
3357 decisions: vec![DecisionMeta {
3358 id: "application-decision".into(),
3359 file: "src/app.js".into(),
3360 line: 1,
3361 column: 0,
3362 source: "left && right".into(),
3363 conditions: vec!["left".into(), "right".into()],
3364 kind: "if".into(),
3365 }],
3366 points: vec![],
3367 branches: vec![],
3368 limitations: vec![],
3369 scope: None,
3370 assertion_sites: Vec::new(),
3371 };
3372 let mut attempt = raw("test", 0, "passed", &[]);
3373 attempt.runtime[0].decisions.push(DecisionSnapshot {
3374 meta: DecisionMeta {
3375 id: "synthetic-fixture".into(),
3376 file: "fixtures/generated.js".into(),
3377 line: 1,
3378 column: 0,
3379 source: "a && b && c".into(),
3380 conditions: vec!["a".into(), "b".into(), "c".into()],
3381 kind: "if".into(),
3382 },
3383 vectors: vec![McdcVector {
3384 values: vec![Some(true), Some(true), Some(true)],
3385 outcome: true,
3386 }],
3387 });
3388
3389 let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3390 assert_eq!(view.decisions.len(), 1);
3391 assert_eq!(view.decisions[0].meta.id, "application-decision");
3392 assert!(view.decisions[0].vectors.is_empty());
3393 }
3394
3395 #[test]
3396 fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
3397 let expected = DecisionMeta {
3398 id: "application-decision".into(),
3399 file: "src/app.js".into(),
3400 line: 1,
3401 column: 0,
3402 source: "left && right".into(),
3403 conditions: vec!["left".into(), "right".into()],
3404 kind: "if".into(),
3405 };
3406 let manifest = CoverageManifest {
3407 unmeasured: Vec::new(),
3408 decisions: vec![expected.clone()],
3409 points: vec![],
3410 branches: vec![],
3411 limitations: vec![],
3412 scope: Some(serde_json::json!({
3413 "entries": [{ "file": "src/empty.js", "status": "included" }]
3414 })),
3415 assertion_sites: Vec::new(),
3416 };
3417 let vector = McdcVector {
3418 values: vec![Some(true), Some(true)],
3419 outcome: true,
3420 };
3421
3422 let mut unknown = raw("test", 0, "passed", &[]);
3423 unknown.runtime[0].decisions.push(DecisionSnapshot {
3424 meta: DecisionMeta {
3425 id: "unknown-decision".into(),
3426 file: "src/empty.js".into(),
3427 ..expected.clone()
3428 },
3429 vectors: vec![vector.clone()],
3430 });
3431 assert!(matches!(
3432 create_coverage_view(&manifest, &[unknown], "time"),
3433 Err(ReportError::InvalidServerRecord(reason))
3434 if reason.contains("absent from the frozen manifest")
3435 ));
3436
3437 let mut changed = raw("test", 0, "passed", &[]);
3438 changed.runtime[0].decisions.push(DecisionSnapshot {
3439 meta: DecisionMeta {
3440 source: "left || right".into(),
3441 ..expected
3442 },
3443 vectors: vec![vector],
3444 });
3445 assert!(matches!(
3446 create_coverage_view(&manifest, &[changed], "time"),
3447 Err(ReportError::InvalidServerRecord(reason))
3448 if reason.contains("differs from the frozen manifest")
3449 ));
3450 }
3451
3452 #[test]
3453 fn neither_timestamp_nor_explicit_phase_links_award_assertion_credit() {
3454 let manifest = CoverageManifest {
3455 unmeasured: Vec::new(),
3456 decisions: vec![],
3457 points: vec![point("hit", 1)],
3458 branches: vec![],
3459 limitations: vec![],
3460 scope: None,
3461 assertion_sites: Vec::new(),
3462 };
3463 let phase = CoveragePhase {
3464 id: "assertion".into(),
3465 kind: "assertion".into(),
3466 operation: "equal".into(),
3467 source: None,
3468 caused_by_phase_id: None,
3469 started_at_ms: 100,
3470 ended_at_ms: Some(120),
3471 status: Some("passed".into()),
3472 error: None,
3473 };
3474 let mut attempt = raw("test", 0, "passed", &["hit"]);
3475 attempt.phases.push(phase.clone());
3476 attempt.runtime[0].events.push(RuntimeEvent {
3477 event_type: "hit".into(),
3478 id: "hit".into(),
3479 vector: None,
3480 timestamp_ms: 110,
3481 phase_id: None,
3482 statement_id: None,
3483 environment: "server".into(),
3484 });
3485 let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
3486 assert_eq!(inferred.points[0].confidence.level, "executed");
3487 assert!(!inferred.points[0].confidence.asserted);
3488
3489 attempt.runtime[0].events[0].phase_id = Some(phase.id);
3490 let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3491 assert_eq!(explicit.points[0].confidence.level, "executed");
3492 assert!(!explicit.points[0].confidence.asserted);
3493 }
3494
3495 #[test]
3496 fn an_event_in_the_millisecond_one_phase_ended_and_the_next_began_is_the_first_ones() {
3497 let phase = |id: &str, started_at_ms: i64, ended_at_ms: i64| CoveragePhase {
3498 id: id.into(),
3499 kind: "action".into(),
3500 operation: id.into(),
3501 source: None,
3502 caused_by_phase_id: None,
3503 started_at_ms,
3504 ended_at_ms: Some(ended_at_ms),
3505 status: Some("passed".into()),
3506 error: None,
3507 };
3508 let handed_over = [phase("goto", 100, 200), phase("expect", 200, 214)];
3513 assert_eq!(phase_at(&handed_over, 199), Some("goto"));
3514 assert_eq!(phase_at(&handed_over, 200), Some("goto"));
3515 assert_eq!(phase_at(&handed_over, 201), Some("expect"));
3516 assert_eq!(phase_at(&handed_over, 99), None);
3517 let apart = [phase("goto", 100, 150), phase("click", 200, 214)];
3519 assert_eq!(phase_at(&apart, 200), Some("click"));
3520 let inside = [phase("step", 100, 300), phase("expect", 200, 220)];
3522 assert_eq!(phase_at(&inside, 200), Some("expect"));
3523 let instant = [phase("first", 200, 200), phase("second", 200, 210)];
3525 assert_eq!(phase_at(&instant, 200), Some("second"));
3526 }
3527
3528 #[test]
3529 fn verified_view_uses_only_the_terminal_successful_attempt() {
3530 let manifest = CoverageManifest {
3531 unmeasured: Vec::new(),
3532 decisions: vec![],
3533 points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
3534 branches: vec![],
3535 limitations: vec![],
3536 scope: None,
3537 assertion_sites: Vec::new(),
3538 };
3539 let failed = raw("flaky", 0, "failed", &["failed"]);
3540 let mut passed = raw("flaky", 1, "passed", &["passed"]);
3541 passed.flaky = true;
3542 let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
3543 expected.expected_status = Some("failed".into());
3544 let request = CoverageReportRequest {
3545 run_id: "run".into(),
3546 manifest,
3547 raw_results: vec![failed, passed, expected],
3548 generated_at: "time".into(),
3549 coverage_model: None,
3550 integrity: None,
3551 test_exit_code: ExitCodeInput::Missing,
3552 };
3553 let report = analyze_coverage_results(&request).unwrap();
3554 assert!(!report.filters.passed.points[0].covered);
3555 assert!(report.filters.passed.points[1].covered);
3556 assert!(!report.filters.passed.points[2].covered);
3557 assert!(report.filters.failed.points[0].covered);
3558 assert_eq!(
3559 report
3560 .view
3561 .tests
3562 .iter()
3563 .find(|test| test.name == "expected-failure")
3564 .unwrap()
3565 .outcome,
3566 "failed"
3567 );
3568 assert_eq!(report.view.tests[1].outcome, "flaky");
3569 }
3570
3571 #[test]
3572 fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
3573 let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
3574 expected.expected_status = Some("failed".into());
3575 let companion = raw("expected-failure", 0, "failed", &["expected"]);
3576 let request = CoverageReportRequest {
3577 run_id: "run".into(),
3578 manifest: CoverageManifest {
3579 unmeasured: Vec::new(),
3580 decisions: vec![],
3581 points: vec![point("expected", 1)],
3582 branches: vec![],
3583 limitations: vec![],
3584 scope: None,
3585 assertion_sites: Vec::new(),
3586 },
3587 raw_results: vec![companion, expected],
3588 generated_at: "time".into(),
3589 coverage_model: None,
3590 integrity: None,
3591 test_exit_code: ExitCodeInput::Missing,
3592 };
3593 let report = analyze_coverage_results(&request).unwrap();
3594 assert_eq!(report.view.tests[0].outcome, "passed");
3595 assert!(!report.filters.passed.points[0].covered);
3596 assert!(!report.filters.failed.points[0].covered);
3597 }
3598
3599 #[test]
3600 fn selected_but_unstarted_test_is_not_an_invented_attempt() {
3601 let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
3602 unstarted.scope = None;
3603 unstarted.retry = None;
3604 unstarted.runtime.clear();
3605 let request = CoverageReportRequest {
3606 run_id: "run".into(),
3607 manifest: CoverageManifest {
3608 unmeasured: Vec::new(),
3609 decisions: vec![],
3610 points: vec![],
3611 branches: vec![],
3612 limitations: vec![],
3613 scope: None,
3614 assertion_sites: Vec::new(),
3615 },
3616 raw_results: vec![unstarted],
3617 generated_at: "time".into(),
3618 coverage_model: None,
3619 integrity: None,
3620 test_exit_code: ExitCodeInput::Present(Some(100)),
3621 };
3622 let report = analyze_coverage_results(&request).unwrap();
3623 assert_eq!(report.view.tests[0].outcome, "unstarted");
3624 assert!(report.view.tests[0].attempts.is_empty());
3625 assert!(report.filters.passed.tests.is_empty());
3626 assert!(report.filters.failed.tests.is_empty());
3627 }
3628
3629 fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
3630 let nonce = SystemTime::now()
3631 .duration_since(SystemTime::UNIX_EPOCH)
3632 .unwrap()
3633 .as_nanos();
3634 let root = std::env::temp_dir().join(format!(
3635 "supercov-rust-report-{}-{nonce}-{}",
3636 std::process::id(),
3637 ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
3638 ));
3639 fs::create_dir_all(&root).unwrap();
3640 let path = root.join("evidence.raw.gz");
3641 if !entries
3642 .iter()
3643 .any(|entry| entry.path == "coverage-model.json")
3644 {
3645 entries.push(EvidenceArchiveEntry {
3646 path: "coverage-model.json".into(),
3647 contents: serde_json::to_vec(
3648 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
3649 .unwrap(),
3650 )
3651 .unwrap(),
3652 });
3653 }
3654 if !entries.iter().any(|entry| entry.path == "frontend.json") {
3655 entries.push(EvidenceArchiveEntry {
3656 path: "frontend.json".into(),
3657 contents: serde_json::to_vec(&serde_json::json!({
3658 "protocolVersion": 2,
3659 "frontendId": "javascript",
3660 "frontendVersion": "fixture-v1",
3661 "language": "javascript",
3662 "structuralSource": "owned-probes",
3663 "runners": [{
3664 "runner": "node:test",
3665 "executionModel": "serial-in-process",
3666 "attribution": {
3667 "run": "exact",
3668 "worker": "unavailable",
3669 "test": "exact",
3670 "retry": "exact",
3671 "phase": "exact",
3672 "action": "exact",
3673 "assertion": "exact"
3674 },
3675 "limitations": [{
3676 "id": "fixture-worker-unavailable",
3677 "scopes": ["worker"],
3678 "reason": "The fixture does not require worker identity"
3679 }]
3680 }],
3681 "structuralLimitations": []
3682 }))
3683 .unwrap(),
3684 });
3685 }
3686 write_archive(entries, &path).unwrap();
3687 path
3688 }
3689
3690 #[test]
3691 fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
3692 let manifest = CoverageManifest {
3693 unmeasured: Vec::new(),
3694 decisions: vec![],
3695 points: vec![point("background-hit", 1), point("test-hit", 2)],
3696 branches: vec![],
3697 limitations: vec![],
3698 scope: None,
3699 assertion_sites: Vec::new(),
3700 };
3701 let unattributed = |axis: &str| {
3704 serde_json::json!({
3705 "id": format!("background-no-{axis}"),
3706 "scopes": [axis],
3707 "reason": format!("Runner background did not expose exact {axis} identity for every result")
3708 })
3709 };
3710 let frontend = serde_json::json!({
3711 "protocolVersion": 2,
3712 "frontendId": "javascript",
3713 "frontendVersion": "fixture-v1",
3714 "language": "javascript",
3715 "structuralSource": "owned-probes",
3716 "runners": [
3717 {
3718 "runner": "node:test",
3719 "executionModel": "serial-in-process",
3720 "attribution": {
3721 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
3722 "phase": "exact", "action": "exact", "assertion": "exact"
3723 },
3724 "limitations": [{
3725 "id": "fixture-worker-unavailable",
3726 "scopes": ["worker"],
3727 "reason": "The fixture does not require worker identity"
3728 }]
3729 },
3730 {
3731 "runner": "background",
3732 "executionModel": "parallel-unattributed",
3733 "attribution": {
3734 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
3735 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
3736 },
3737 "limitations": [
3738 unattributed("worker"), unattributed("test"), unattributed("retry"),
3739 unattributed("phase"), unattributed("action"), unattributed("assertion")
3740 ]
3741 }
3742 ],
3743 "structuralLimitations": []
3744 });
3745 let path = archive(vec![
3746 EvidenceArchiveEntry {
3747 path: "manifest.json".into(),
3748 contents: serde_json::to_vec(&manifest).unwrap(),
3749 },
3750 EvidenceArchiveEntry {
3751 path: "frontend.json".into(),
3752 contents: serde_json::to_vec(&frontend).unwrap(),
3753 },
3754 EvidenceArchiveEntry {
3755 path: "playwright-worker-1.mcdc.jsonl".into(),
3756 contents: {
3757 let mut contents =
3758 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
3759 .unwrap();
3760 contents.extend_from_slice(b"\npartial-final-line");
3761 contents
3762 },
3763 },
3764 EvidenceArchiveEntry {
3765 path: "server/background/worker.jsonl".into(),
3766 contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
3767 },
3768 ]);
3769 let result = analyze_coverage_archive(&ArchiveReportRequest {
3770 archive_path: path.clone(),
3771 run_id: "run".into(),
3772 generated_at: "time".into(),
3773 integrity: None,
3774 test_exit_code: ExitCodeInput::Missing,
3775 });
3776 assert!(matches!(
3777 result,
3778 Err(ReportError::InvalidJson { path, .. })
3779 if path == "playwright-worker-1.mcdc.jsonl"
3780 ));
3781 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3782 }
3783
3784 #[test]
3785 fn a_torn_background_record_is_counted_not_fatal() {
3786 let manifest = CoverageManifest {
3791 unmeasured: Vec::new(),
3792 decisions: vec![],
3793 points: vec![],
3794 branches: vec![],
3795 limitations: vec![],
3796 scope: None,
3797 assertion_sites: Vec::new(),
3798 };
3799 let unattributed = |axis: &str| {
3802 serde_json::json!({
3803 "id": format!("background-no-{axis}"),
3804 "scopes": [axis],
3805 "reason": format!("Runner background did not expose exact {axis} identity for every result")
3806 })
3807 };
3808 let frontend = serde_json::json!({
3809 "protocolVersion": 2,
3810 "frontendId": "javascript",
3811 "frontendVersion": "fixture-v1",
3812 "language": "javascript",
3813 "structuralSource": "owned-probes",
3814 "runners": [
3815 {
3816 "runner": "node:test",
3817 "executionModel": "serial-in-process",
3818 "attribution": {
3819 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
3820 "phase": "exact", "action": "exact", "assertion": "exact"
3821 },
3822 "limitations": [{
3823 "id": "fixture-worker-unavailable",
3824 "scopes": ["worker"],
3825 "reason": "The fixture does not require worker identity"
3826 }]
3827 },
3828 {
3829 "runner": "background",
3830 "executionModel": "parallel-unattributed",
3831 "attribution": {
3832 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
3833 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
3834 },
3835 "limitations": [
3836 unattributed("worker"), unattributed("test"), unattributed("retry"),
3837 unattributed("phase"), unattributed("action"), unattributed("assertion")
3838 ]
3839 }
3840 ],
3841 "structuralLimitations": []
3842 });
3843 let path = archive(vec![
3844 EvidenceArchiveEntry {
3845 path: "manifest.json".into(),
3846 contents: serde_json::to_vec(&manifest).unwrap(),
3847 },
3848 EvidenceArchiveEntry {
3849 path: "frontend.json".into(),
3850 contents: serde_json::to_vec(&frontend).unwrap(),
3851 },
3852 EvidenceArchiveEntry {
3853 path: "playwright-worker-1.mcdc.jsonl".into(),
3854 contents: {
3855 let mut contents =
3856 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
3857 .unwrap();
3858 contents.push(b'\n');
3859 contents
3860 },
3861 },
3862 EvidenceArchiveEntry {
3863 path: "execution.37360-1.168.jsonl".into(),
3864 contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
3865 },
3866 EvidenceArchiveEntry {
3867 path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
3868 contents: concat!(
3869 "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
3870 "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
3871 "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
3872 )
3873 .as_bytes()
3874 .to_vec(),
3875 },
3876 ]);
3877 let report = analyze_coverage_archive(&ArchiveReportRequest {
3878 archive_path: path.clone(),
3879 run_id: "run".into(),
3880 generated_at: "time".into(),
3881 integrity: None,
3882 test_exit_code: ExitCodeInput::Missing,
3883 })
3884 .expect("a torn evidence line must not fail the whole run");
3885 let transport = report.view.transport.expect("transport stats");
3886 assert_eq!(transport.corrupt_records, 2);
3887 assert_eq!(transport.corrupt_files, 2);
3888 assert_eq!(transport.background_server_records, 2);
3889 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3890 }
3891
3892 #[test]
3893 fn archive_analysis_rejects_cross_run_evidence() {
3894 let manifest = CoverageManifest {
3895 unmeasured: Vec::new(),
3896 decisions: vec![],
3897 points: vec![],
3898 branches: vec![],
3899 limitations: vec![],
3900 scope: None,
3901 assertion_sites: Vec::new(),
3902 };
3903 let mut result = raw("test", 0, "passed", &[]);
3904 result.scope = Some(ExecutionScope {
3905 version: 1,
3906 run_id: "other-run".into(),
3907 worker_id: "worker".into(),
3908 test_id: "test".into(),
3909 test_key: "key".into(),
3910 retry: 0,
3911 attempt_id: "attempt".into(),
3912 });
3913 let path = archive(vec![
3914 EvidenceArchiveEntry {
3915 path: "manifest.json".into(),
3916 contents: serde_json::to_vec(&manifest).unwrap(),
3917 },
3918 EvidenceArchiveEntry {
3919 path: "worker/mcdc.json".into(),
3920 contents: serde_json::to_vec(&result).unwrap(),
3921 },
3922 ]);
3923 assert!(matches!(
3924 analyze_coverage_archive(&ArchiveReportRequest {
3925 archive_path: path.clone(),
3926 run_id: "run".into(),
3927 generated_at: "time".into(),
3928 integrity: None,
3929 test_exit_code: ExitCodeInput::Missing,
3930 }),
3931 Err(ReportError::InvalidArchive(reason))
3932 if reason.contains("expected=run actual=other-run")
3933 ));
3934 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3935 }
3936
3937 #[test]
3938 fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
3939 let vectors: Value = serde_json::from_str(include_str!(
3940 "../test-assets/coverage-model-v1/vectors.json"
3941 ))
3942 .unwrap();
3943 for value in vectors["valid"].as_array().unwrap() {
3944 let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
3945 model.into_declaration().unwrap();
3946 }
3947 for vector in vectors["invalid"].as_array().unwrap() {
3948 let value = vector["value"].clone();
3949 if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
3950 assert!(
3951 model.into_declaration().is_err(),
3952 "accepted invalid model vector: {}",
3953 vector["reason"]
3954 );
3955 }
3956 }
3957
3958 let manifest = CoverageManifest {
3959 unmeasured: Vec::new(),
3960 decisions: vec![],
3961 points: vec![],
3962 branches: vec![],
3963 limitations: vec![],
3964 scope: None,
3965 assertion_sites: Vec::new(),
3966 };
3967 let path = archive(vec![
3968 EvidenceArchiveEntry {
3969 path: "coverage-model.json".into(),
3970 contents: serde_json::to_vec(&PersistedCoverageModel {
3971 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
3972 language: "rust".into(),
3973 variant: "rust-source-v1".into(),
3974 name: "Rust source coverage".into(),
3975 completeness_meaning: "Every Rust obligation was satisfied.".into(),
3976 measured: vec!["Rust statements".into()],
3977 not_measured: vec![],
3978 })
3979 .unwrap(),
3980 },
3981 EvidenceArchiveEntry {
3982 path: "manifest.json".into(),
3983 contents: serde_json::to_vec(&manifest).unwrap(),
3984 },
3985 ]);
3986 assert!(matches!(
3987 analyze_coverage_archive(&ArchiveReportRequest {
3988 archive_path: path.clone(),
3989 run_id: "run".into(),
3990 generated_at: "time".into(),
3991 integrity: None,
3992 test_exit_code: ExitCodeInput::Missing,
3993 }),
3994 Err(ReportError::InvalidArchive(reason))
3995 if reason.contains("javascript differs from coverage model language rust")
3996 ));
3997 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3998 }
3999
4000 #[test]
4001 fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
4002 let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
4003 "runId": "run",
4004 "manifest": { "decisions": [], "points": [], "branches": [] },
4005 "rawResults": [{
4006 "test": "test",
4007 "status": "passed",
4008 "browser": [],
4009 "server": []
4010 }],
4011 "generatedAt": "time",
4012 "testExitCode": null
4013 }))
4014 .unwrap();
4015 let report = analyze_coverage_results(&request).unwrap();
4016 assert_eq!(
4017 report.execution,
4018 Some(ExecutionResult {
4019 test_exit_code: None,
4020 valid: false,
4021 })
4022 );
4023 }
4024 #[test]
4025 fn a_phase_leaked_from_another_test_is_untagged_and_an_unknown_one_is_kept() {
4026 let record = |test: &str, phases: &[&str], events: &[(&str, &str)]| -> RawTestResult {
4027 serde_json::from_value(serde_json::json!({
4028 "test": test,
4029 "phases": phases.iter().map(|id| serde_json::json!({
4030 "id": id, "kind": "assertion", "operation": "assert.ok", "startedAtMs": 0
4031 })).collect::<Vec<_>>(),
4032 "runtime": [{
4033 "events": events.iter().map(|(id, phase)| serde_json::json!({
4034 "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
4035 })).collect::<Vec<_>>()
4036 }]
4037 }))
4038 .unwrap()
4039 };
4040 let mut results = vec![
4041 record(
4042 "upload",
4043 &["a-0:assertion:1"],
4044 &[("own", "a-0:assertion:1")],
4045 ),
4046 record(
4048 "form data",
4049 &["b-0:assertion:1"],
4050 &[
4051 ("late", "a-0:assertion:1"),
4052 ("mine", "b-0:assertion:1"),
4053 ("corrupt", "z-0:assertion:9"),
4054 ],
4055 ),
4056 ];
4057 release_leaked_phases(&mut results);
4058 let phases = |raw: &RawTestResult| {
4059 raw.runtime[0]
4060 .events
4061 .iter()
4062 .map(|event| (event.id.clone(), event.phase_id.clone()))
4063 .collect::<Vec<_>>()
4064 };
4065 assert_eq!(
4066 phases(&results[0]),
4067 [("own".into(), Some("a-0:assertion:1".into()))]
4068 );
4069 assert_eq!(
4070 phases(&results[1]),
4071 [
4072 ("late".into(), None),
4073 ("mine".into(), Some("b-0:assertion:1".into())),
4074 ("corrupt".into(), Some("z-0:assertion:9".into())),
4076 ]
4077 );
4078 }
4079
4080 #[test]
4081 fn a_test_run_twice_under_one_identity_keeps_both_executions_apart() {
4082 let record = |status: &str, events: &[(&str, &str)]| -> RawTestResult {
4085 serde_json::from_value(serde_json::json!({
4086 "test": "parse",
4087 "testId": "parse",
4088 "retry": 0,
4089 "status": status,
4090 "phases": [
4091 { "id": "t-0:assertion:1", "kind": "assertion", "operation": "expect", "startedAtMs": 0 },
4092 { "id": "t-0:assertion:2", "kind": "assertion", "operation": "expect", "startedAtMs": 0, "causedByPhaseId": "t-0:assertion:1" }
4093 ],
4094 "runtime": [{
4095 "events": events.iter().map(|(id, phase)| serde_json::json!({
4096 "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
4097 })).collect::<Vec<_>>()
4098 }]
4099 }))
4100 .unwrap()
4101 };
4102 let mut results = vec![
4103 record("failed", &[("node", "t-0:assertion:1")]),
4104 record("passed", &[("edge", "t-0:assertion:2")]),
4105 record("passed", &[("third", "t-0:assertion:1")]),
4106 ];
4107 separate_repeated_executions(&mut results);
4108 let ids = |raw: &RawTestResult| {
4109 raw.phases
4110 .iter()
4111 .map(|phase| (phase.id.clone(), phase.caused_by_phase_id.clone()))
4112 .collect::<Vec<_>>()
4113 };
4114 let events = |raw: &RawTestResult| {
4115 raw.runtime[0]
4116 .events
4117 .iter()
4118 .map(|event| event.phase_id.clone().unwrap())
4119 .collect::<Vec<_>>()
4120 };
4121 assert_eq!(
4122 ids(&results[0]),
4123 [
4124 ("t-0:assertion:1".into(), None),
4125 ("t-0:assertion:2".into(), Some("t-0:assertion:1".into()))
4126 ]
4127 );
4128 assert_eq!(
4129 ids(&results[1]),
4130 [
4131 ("t-0:assertion:1~2".into(), None),
4132 ("t-0:assertion:2~2".into(), Some("t-0:assertion:1~2".into()))
4133 ]
4134 );
4135 assert_eq!(events(&results[1]), ["t-0:assertion:2~2"]);
4136 assert_eq!(events(&results[2]), ["t-0:assertion:1~3"]);
4137
4138 let mut test = MutableTest {
4139 id: "parse".into(),
4140 name: "parse".into(),
4141 file: None,
4142 title: None,
4143 retries: BTreeSet::new(),
4144 attempts: BTreeMap::new(),
4145 unstarted: false,
4146 runner_reported_flaky: false,
4147 provenance: TestProvenance::default(),
4148 role: "test".into(),
4149 attribution: "exact".into(),
4150 hits: Vec::new(),
4151 decisions: BTreeMap::new(),
4152 };
4153 for raw in &results {
4154 record_attempt(&mut test, raw);
4155 }
4156 assert_eq!(
4157 test_outcome(&test),
4158 "failed",
4159 "a failed execution fails the attempt"
4160 );
4161 }
4162
4163 #[test]
4164 fn a_dimension_is_summarised_over_the_tests_it_can_describe() {
4165 let exact = |id: &str, kind: &str| TestCoverageResult {
4171 id: id.into(),
4172 name: id.into(),
4173 file: None,
4174 title: None,
4175 retries: vec![],
4176 attempts: vec![],
4177 outcome: "passed".into(),
4178 provenance: TestProvenance {
4179 runner: "go-test".into(),
4180 kind: kind.into(),
4181 project: None,
4182 source: String::new(),
4183 },
4184 role: "test".into(),
4185 attribution: ATTRIBUTION_EXACT.into(),
4186 hits: vec![],
4187 decisions: vec![],
4188 lines: vec![],
4189 };
4190 let mut run_wide = exact("parallel", "unit");
4191 run_wide.attribution = ATTRIBUTION_RUN_WIDE.into();
4192
4193 assert!(coverage_is_its_own(&exact("serial", "unit").attribution));
4202 assert!(coverage_is_its_own(ATTRIBUTION_PARTIAL));
4203 assert!(!coverage_is_its_own(&run_wide.attribution));
4204
4205 assert!(coverage_is_complete(ATTRIBUTION_EXACT));
4206 assert!(!coverage_is_complete(ATTRIBUTION_PARTIAL));
4207 assert!(!coverage_is_complete(ATTRIBUTION_RUN_WIDE));
4208
4209 for (left, right, merged) in [
4212 (ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL, ATTRIBUTION_PARTIAL),
4213 (ATTRIBUTION_PARTIAL, ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL),
4214 (
4215 ATTRIBUTION_PARTIAL,
4216 ATTRIBUTION_RUN_WIDE,
4217 ATTRIBUTION_RUN_WIDE,
4218 ),
4219 (ATTRIBUTION_EXACT, ATTRIBUTION_EXACT, ATTRIBUTION_EXACT),
4220 ] {
4221 assert_eq!(least_exact(left, right), merged);
4222 }
4223
4224 assert!(coverage_is_its_own(""));
4227 assert!(coverage_is_complete(""));
4228 }
4229}