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
1169fn summary_for_results(
1170 decisions: &[DecisionResult],
1171 points: &[PointResult],
1172 branches: &[BranchResult],
1173 lines: &[LineResult],
1174 test_ids: Option<&BTreeSet<String>>,
1175) -> Result<CoverageSummary, ReportError> {
1176 let test_ids = test_ids.map(|ids| {
1178 ids.iter()
1179 .map(String::as_str)
1180 .collect::<crate::interned::FastSet<&str>>()
1181 });
1182 let includes = |tests: &[Id], covered: bool| {
1183 test_ids.as_ref().map_or(covered, |selected| {
1184 tests.iter().any(|test| selected.contains(test.as_str()))
1185 })
1186 };
1187 let input = CoverageCoreInput {
1188 decisions: decisions
1189 .iter()
1190 .map(|decision| DecisionCoverage {
1191 condition_count: decision.meta.conditions.len(),
1192 vectors: decision
1193 .vector_observations
1194 .iter()
1195 .filter(|observation| includes(&observation.tests, true))
1196 .map(|observation| observation.vector.clone())
1197 .collect(),
1198 })
1199 .collect(),
1200 points: points
1201 .iter()
1202 .map(|point| PointCoverage {
1203 kind: point.meta.kind.clone(),
1204 covered: includes(&point.tests, point.covered),
1205 })
1206 .collect(),
1207 branches: branches
1208 .iter()
1209 .map(|branch| BranchCoverage {
1210 kind: branch.meta.kind.clone(),
1211 alternatives: branch
1212 .alternatives
1213 .iter()
1214 .map(|alternative| includes(&alternative.tests, alternative.covered))
1215 .collect(),
1216 })
1217 .collect(),
1218 lines: lines
1221 .iter()
1222 .filter(|line| line.measured)
1223 .map(|line| includes(&line.tests, line.covered))
1224 .collect(),
1225 };
1226 Ok(analyze_core(&input)?.summary)
1227}
1228
1229pub fn coverage_summary_for_tests(
1234 view: &CoverageView,
1235 test_ids: &BTreeSet<String>,
1236) -> Result<CoverageSummary, ReportError> {
1237 summary_for_results(
1238 &view.decisions,
1239 &view.points,
1240 &view.branches,
1241 &view.lines,
1242 Some(test_ids),
1243 )
1244}
1245
1246pub fn blocking_limitation(limitation: &Value) -> bool {
1249 limitation
1250 .get("blocking")
1251 .and_then(Value::as_bool)
1252 .unwrap_or(true)
1253}
1254
1255fn hash_slot<'m, V: Default, S: std::hash::BuildHasher>(
1256 map: &'m mut HashMap<String, V, S>,
1257 key: &str,
1258) -> &'m mut V {
1259 if !map.contains_key(key) {
1260 map.insert(key.to_owned(), V::default());
1261 }
1262 map.get_mut(key).expect("slot was inserted")
1263}
1264
1265fn tree_slot<'m, V: Default>(map: &'m mut BTreeMap<String, V>, key: &str) -> &'m mut V {
1266 if !map.contains_key(key) {
1267 map.insert(key.to_owned(), V::default());
1268 }
1269 map.get_mut(key).expect("slot was inserted")
1270}
1271
1272fn correlate_event<'a>(
1274 event: &'a RuntimeEvent,
1275 ordered_phases: &'a [CoveragePhase],
1276) -> Option<&'a str> {
1277 event.phase_id.as_deref().or_else(|| {
1278 ordered_phases
1279 .iter()
1280 .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1281 .last()
1282 .map(|phase| phase.id.as_str())
1283 })
1284}
1285
1286#[derive(Default)]
1290struct HitIds {
1291 names: Vec<Id>,
1292 numbers: FastMap<String, usize>,
1293}
1294
1295impl HitIds {
1296 fn number(&mut self, name: &str, ids: &mut Interner) -> usize {
1297 if let Some(number) = self.numbers.get(name) {
1298 return *number;
1299 }
1300 let number = self.names.len();
1301 self.names.push(ids.id(name));
1302 self.numbers.insert(name.to_owned(), number);
1303 number
1304 }
1305
1306 fn ranks(&self) -> Vec<u32> {
1309 let mut order = (0..self.names.len()).collect::<Vec<_>>();
1310 order.sort_unstable_by(|left, right| self.names[*left].cmp(&self.names[*right]));
1311 let mut rank = vec![0_u32; self.names.len()];
1312 for (position, number) in order.into_iter().enumerate() {
1313 rank[number] = position as u32;
1314 }
1315 rank
1316 }
1317
1318 fn sorted_by(&self, mut numbers: Vec<usize>, rank: &[u32]) -> Vec<Id> {
1320 numbers.sort_unstable_by_key(|number| rank[*number]);
1321 numbers.dedup();
1322 numbers
1323 .into_iter()
1324 .map(|number| self.names[number].clone())
1325 .collect()
1326 }
1327
1328 #[cfg(test)]
1329 fn sorted(&self, numbers: BTreeSet<usize>) -> Vec<Id> {
1330 let mut names = numbers
1331 .into_iter()
1332 .map(|n| self.names[n].clone())
1333 .collect::<Vec<_>>();
1334 names.sort_unstable();
1335 names
1336 }
1337}
1338
1339#[derive(Default)]
1342struct References {
1343 names: Vec<Id>,
1344 numbers: FastMap<Id, u32>,
1345 last: Option<u32>,
1346 by_obligation: Vec<Vec<u32>>,
1347}
1348
1349impl References {
1350 fn number(&mut self, name: &str, ids: &mut Interner) -> u32 {
1353 if let Some(number) = self.last
1354 && self.names[number as usize] == name
1355 {
1356 return number;
1357 }
1358 let number = match self.numbers.get(name) {
1359 Some(number) => *number,
1360 None => {
1361 let number = self.names.len() as u32;
1362 let id = ids.id(name);
1363 self.names.push(id.clone());
1364 self.numbers.insert(id, number);
1365 number
1366 }
1367 };
1368 self.last = Some(number);
1369 number
1370 }
1371
1372 fn add(&mut self, obligation: usize, name: &str, ids: &mut Interner) {
1373 let number = self.number(name, ids);
1374 if self.by_obligation.len() <= obligation {
1375 self.by_obligation.resize_with(obligation + 1, Vec::new);
1376 }
1377 let reached = &mut self.by_obligation[obligation];
1378 if reached.last() != Some(&number) {
1379 reached.push(number);
1380 }
1381 }
1382
1383 fn into_relation(self, hits: &HitIds) -> Relation {
1384 let mut order = (0..self.names.len()).collect::<Vec<_>>();
1385 order.sort_unstable_by(|left, right| self.names[*left].cmp(&self.names[*right]));
1386 let mut rank = vec![0_u32; self.names.len()];
1387 for (position, number) in order.into_iter().enumerate() {
1388 rank[number] = position as u32;
1389 }
1390 let mut relation = Relation {
1391 names: self.names,
1392 rank,
1393 by_hit: HashMap::new(),
1394 none: Reached::default(),
1395 };
1396 for (obligation, reached) in self.by_obligation.into_iter().enumerate() {
1397 if !reached.is_empty() {
1398 let reached = relation.reached(reached);
1399 relation
1400 .by_hit
1401 .insert(hits.names[obligation].to_string(), reached);
1402 }
1403 }
1404 relation
1405 }
1406}
1407
1408struct Relation {
1411 names: Vec<Id>,
1412 rank: Vec<u32>,
1414 by_hit: HashMap<String, Reached>,
1415 none: Reached,
1416}
1417
1418impl Relation {
1419 fn of(&self, id: &str) -> &Reached {
1421 self.by_hit.get(id).unwrap_or(&self.none)
1422 }
1423
1424 fn reached(&self, mut numbers: Vec<u32>) -> Reached {
1427 numbers.sort_unstable_by_key(|number| self.rank[*number as usize]);
1428 numbers.dedup();
1429 Reached {
1430 names: numbers
1431 .iter()
1432 .map(|number| self.names[*number as usize].clone())
1433 .collect(),
1434 numbers,
1435 }
1436 }
1437}
1438
1439#[derive(Default)]
1442struct Reached {
1443 names: Vec<Id>,
1444 numbers: Vec<u32>,
1445}
1446
1447struct TestTraits<'t> {
1449 setup: bool,
1450 background: bool,
1451 runner: &'t str,
1452 kind: &'t str,
1453}
1454
1455impl<'t> TestTraits<'t> {
1456 fn of(test: &'t MutableTest) -> Self {
1457 Self {
1458 setup: test.role == "setup",
1459 background: test.role == "background",
1460 runner: &test.provenance.runner,
1461 kind: &test.provenance.kind,
1462 }
1463 }
1464}
1465
1466fn confidence_from(
1471 tests: &Reached,
1472 phases: &Reached,
1473 explicit: &Reached,
1474 test_traits: &[Option<TestTraits<'_>>],
1475 phase_kinds: &[Option<&str>],
1476 explicit_kinds: &[Option<&str>],
1477) -> CoverageConfidence {
1478 let mut has_test = false;
1479 let mut setup_roles = true;
1480 let mut background_roles = true;
1481 let mut runners = BTreeSet::new();
1482 let mut kinds = BTreeSet::new();
1483 for number in &tests.numbers {
1484 if let Some(test) = &test_traits[*number as usize] {
1485 has_test = true;
1486 setup_roles &= test.setup;
1487 background_roles &= test.background;
1488 runners.insert(test.runner);
1489 kinds.insert(test.kind);
1490 }
1491 }
1492 let mut has_phase = false;
1493 let mut setup_phases = true;
1494 let mut background_phases = true;
1495 for number in &phases.numbers {
1496 if let Some(kind) = phase_kinds[*number as usize] {
1497 has_phase = true;
1498 setup_phases &= kind == "setup";
1499 background_phases &= kind == "background";
1500 }
1501 }
1502 let has_action = explicit
1503 .numbers
1504 .iter()
1505 .any(|number| explicit_kinds[*number as usize] == Some("action"));
1506 let level = if tests.names.is_empty() {
1507 "unexecuted"
1508 } else if has_action {
1509 "action"
1510 } else {
1511 "executed"
1512 };
1513 CoverageConfidence {
1514 level: level.into(),
1515 setup_only: if has_phase {
1516 setup_phases
1517 } else {
1518 has_test && setup_roles
1519 },
1520 background_only: if has_phase {
1521 background_phases
1522 } else {
1523 has_test && background_roles
1524 },
1525 asserted: false,
1526 tests: tests.names.clone(),
1527 asserted_tests: vec![],
1528 runners: runners.into_iter().map(str::to_owned).collect(),
1529 e2e: kinds.contains("e2e"),
1530 kinds: kinds.into_iter().map(str::to_owned).collect(),
1531 }
1532}
1533
1534pub fn create_coverage_view(
1535 manifest: &CoverageManifest,
1536 raw_results: &[RawTestResult],
1537 generated_at: &str,
1538) -> Result<CoverageView, ReportError> {
1539 create_coverage_view_with_model(
1540 manifest,
1541 &raw_results.iter().collect::<Vec<_>>(),
1542 generated_at,
1543 &javascript_coverage_model(),
1544 &mut Interner::default(),
1545 )
1546}
1547
1548fn create_coverage_view_with_model(
1549 manifest: &CoverageManifest,
1550 raw_results: &[&RawTestResult],
1551 generated_at: &str,
1552 coverage_model: &CoverageModelDeclaration,
1553 ids: &mut Interner,
1554) -> Result<CoverageView, ReportError> {
1555 let mut decision_metadata = manifest.decisions.clone();
1556 let decision_indexes = decision_metadata
1557 .iter()
1558 .enumerate()
1559 .map(|(index, meta)| (meta.id.clone(), index))
1560 .collect::<HashMap<_, _>>();
1561 let manifest_files = manifest
1562 .decisions
1563 .iter()
1564 .map(|meta| meta.file.clone())
1565 .chain(manifest.points.iter().map(|meta| meta.file.clone()))
1566 .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
1567 .chain(
1568 manifest
1569 .scope
1570 .as_ref()
1571 .and_then(|scope| scope.get("entries"))
1572 .and_then(Value::as_array)
1573 .into_iter()
1574 .flatten()
1575 .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
1576 .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
1577 )
1578 .collect::<BTreeSet<_>>();
1579 let mut vectors_by_decision = FastMap::<String, Vec<MutableObservation>>::default();
1580 let mut vector_indexes = FastMap::<String, FastMap<String, usize>>::default();
1581 let mut tests_by_decision = FastMap::<String, Vec<u32>>::default();
1583 let mut tests_by_hit = References::default();
1584 let mut hit_ids = HitIds::default();
1585 let mut tests_by_id = FastMap::<Id, MutableTest>::default();
1586 let mut test_order = Vec::<Id>::new();
1587 let mut phases_by_id = FastMap::<Id, MutablePhase>::default();
1588 let mut phases_by_hit = References::default();
1589 let mut explicit_phases_by_hit = References::default();
1590
1591 for raw in raw_results {
1592 let id = ids.id(raw_test_id(raw));
1593 if !tests_by_id.contains_key(&id) {
1594 test_order.push(id.clone());
1595 tests_by_id.insert(
1596 id.clone(),
1597 MutableTest {
1598 id: id.to_string(),
1599 name: raw.test.clone(),
1600 file: raw.test_file.clone(),
1601 title: raw.title.clone(),
1602 retries: raw.retry.into_iter().collect(),
1603 attempts: BTreeMap::new(),
1604 unstarted: raw.status.as_deref() == Some("unstarted"),
1605 runner_reported_flaky: raw.flaky,
1606 provenance: raw.provenance.clone(),
1607 role: raw.role.clone(),
1608 attribution: raw.attribution.clone(),
1609 hits: Vec::new(),
1610 decisions: BTreeMap::new(),
1611 },
1612 );
1613 }
1614 let test = tests_by_id.get_mut(&id).expect("test was inserted");
1615 test.attribution = least_exact(&test.attribution, &raw.attribution);
1620 test.unstarted |= raw.status.as_deref() == Some("unstarted");
1621 if let Some(retry) = raw.retry {
1622 test.retries.insert(retry);
1623 }
1624 test.runner_reported_flaky |= raw.flaky;
1625 record_attempt(test, raw);
1626
1627 let mut ordered_phases = raw.phases.clone();
1628 ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1629 for phase in &ordered_phases {
1630 phases_by_id.insert(
1631 ids.id(&phase.id),
1632 MutablePhase {
1633 phase: phase.clone(),
1634 test: id.clone(),
1635 hits: Vec::new(),
1636 decisions: BTreeMap::new(),
1637 browser_events: 0,
1638 server_events: 0,
1639 explicit_events: 0,
1640 inferred_events: 0,
1641 explicit_browser_events: 0,
1642 inferred_browser_events: 0,
1643 explicit_server_events: 0,
1644 inferred_server_events: 0,
1645 },
1646 );
1647 }
1648
1649 let correlate = |event: &RuntimeEvent| {
1650 event.phase_id.clone().or_else(|| {
1651 ordered_phases
1652 .iter()
1653 .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1654 .last()
1655 .map(|phase| phase.id.clone())
1656 })
1657 };
1658
1659 let snapshots = raw
1660 .runtime
1661 .iter()
1662 .map(|snapshot| (snapshot, false))
1663 .chain(raw.browser.iter().map(|snapshot| (snapshot, true)));
1664 for (snapshot, browser) in snapshots {
1665 for decision in &snapshot.decisions {
1666 let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1667 if manifest_files.contains(&decision.meta.file) {
1668 return Err(ReportError::InvalidServerRecord(format!(
1669 "decision {} is absent from the frozen manifest",
1670 decision.meta.id
1671 )));
1672 }
1673 continue;
1674 };
1675 if decision_metadata[index] != decision.meta {
1676 return Err(ReportError::InvalidServerRecord(format!(
1677 "decision {} metadata differs from the frozen manifest",
1678 decision.meta.id
1679 )));
1680 }
1681 for vector in &decision.vectors {
1682 let key = vector_key(vector);
1683 let indexes = hash_slot(&mut vector_indexes, &decision.meta.id);
1684 let observations = hash_slot(&mut vectors_by_decision, &decision.meta.id);
1685 let observation_index = *indexes.entry(key).or_insert_with(|| {
1686 observations.push(MutableObservation {
1687 vector: vector.clone(),
1688 tests: Vec::new(),
1689 phases: Vec::new(),
1690 explicit_phases: Vec::new(),
1691 });
1692 observations.len() - 1
1693 });
1694 observations[observation_index]
1695 .tests
1696 .push(tests_by_hit.number(&id, ids));
1697 tree_slot(
1698 &mut tests_by_id.get_mut(&id).expect("registered test").decisions,
1699 &decision.meta.id,
1700 )
1701 .insert(vector);
1702 }
1703 if !decision.vectors.is_empty() {
1704 hash_slot(&mut tests_by_decision, &decision.meta.id)
1705 .push(tests_by_hit.number(&id, ids));
1706 }
1707 }
1708 let test_hits = &mut tests_by_id.get_mut(&id).expect("registered test").hits;
1709 for hit in &snapshot.hits {
1710 let hit = hit_ids.number(hit, ids);
1711 tests_by_hit.add(hit, &id, ids);
1712 test_hits.push(hit);
1713 }
1714 for event in &snapshot.events {
1715 let explicit = event.phase_id.is_some();
1716 let Some(phase_id) = correlate_event(event, &ordered_phases) else {
1717 continue;
1718 };
1719 let Some(phase) = phases_by_id.get_mut(phase_id) else {
1720 continue;
1721 };
1722 if event.environment == "browser" {
1723 phase.browser_events += 1;
1724 if explicit {
1725 phase.explicit_browser_events += 1;
1726 } else {
1727 phase.inferred_browser_events += 1;
1728 }
1729 } else {
1730 phase.server_events += 1;
1731 if explicit {
1732 phase.explicit_server_events += 1;
1733 } else {
1734 phase.inferred_server_events += 1;
1735 }
1736 }
1737 if explicit {
1738 phase.explicit_events += 1;
1739 } else {
1740 phase.inferred_events += 1;
1741 }
1742 if event.event_type == "hit" {
1743 let hit = hit_ids.number(&event.id, ids);
1744 phase.hits.push(hit);
1745 phases_by_hit.add(hit, phase_id, ids);
1746 if explicit {
1747 explicit_phases_by_hit.add(hit, phase_id, ids);
1748 }
1749 } else if event.event_type == "decision" {
1750 let vector = event
1751 .vector
1752 .as_ref()
1753 .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1754 tree_slot(&mut phase.decisions, &event.id).insert(vector);
1755 if let Some(index) = vector_indexes
1756 .get(&event.id)
1757 .and_then(|indexes| indexes.get(&vector_key(vector)))
1758 .copied()
1759 && let Some(observation) = vectors_by_decision
1760 .get_mut(&event.id)
1761 .and_then(|observations| observations.get_mut(index))
1762 {
1763 observation.phases.push(phases_by_hit.number(phase_id, ids));
1764 if explicit {
1765 observation
1766 .explicit_phases
1767 .push(explicit_phases_by_hit.number(phase_id, ids));
1768 }
1769 }
1770 } else {
1771 return Err(ReportError::InvalidEvent(event.event_type.clone()));
1772 }
1773 }
1774 if let Some(phase_id) = snapshot.phase_id.as_deref()
1778 && let Some(phase) = phases_by_id.get_mut(phase_id)
1779 {
1780 let observed = snapshot.hits.len()
1781 + snapshot
1782 .decisions
1783 .iter()
1784 .map(|decision| decision.vectors.len())
1785 .sum::<usize>();
1786 if browser {
1787 phase.browser_events += observed;
1788 phase.explicit_browser_events += observed;
1789 } else {
1790 phase.server_events += observed;
1791 phase.explicit_server_events += observed;
1792 }
1793 phase.explicit_events += observed;
1794 for hit in &snapshot.hits {
1795 let hit = hit_ids.number(hit, ids);
1796 phase.hits.push(hit);
1797 phases_by_hit.add(hit, phase_id, ids);
1798 explicit_phases_by_hit.add(hit, phase_id, ids);
1799 }
1800 for decision in &snapshot.decisions {
1801 let id = &decision.meta.id;
1802 for vector in &decision.vectors {
1803 tree_slot(&mut phase.decisions, id).insert(vector);
1804 if let Some(index) = vector_indexes
1805 .get(id)
1806 .and_then(|indexes| indexes.get(&vector_key(vector)))
1807 .copied()
1808 && let Some(observation) = vectors_by_decision
1809 .get_mut(id)
1810 .and_then(|observations| observations.get_mut(index))
1811 {
1812 observation.phases.push(phases_by_hit.number(phase_id, ids));
1813 observation
1814 .explicit_phases
1815 .push(explicit_phases_by_hit.number(phase_id, ids));
1816 }
1817 }
1818 }
1819 }
1820 }
1821
1822 for record in &raw.server {
1823 let (record_id, decision) = if record.record_type == "decision" {
1824 let meta = record
1825 .meta
1826 .as_ref()
1827 .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1828 let vector = record
1829 .vector
1830 .as_ref()
1831 .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1832 let Some(index) = decision_indexes.get(&meta.id).copied() else {
1833 if manifest_files.contains(&meta.file) {
1834 return Err(ReportError::InvalidServerRecord(format!(
1835 "decision {} is absent from the frozen manifest",
1836 meta.id
1837 )));
1838 }
1839 continue;
1840 };
1841 if decision_metadata[index] != *meta {
1842 return Err(ReportError::InvalidServerRecord(format!(
1843 "decision {} metadata differs from the frozen manifest",
1844 meta.id
1845 )));
1846 }
1847 let key = vector_key(vector);
1848 let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1849 let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1850 let index = *indexes.entry(key).or_insert_with(|| {
1851 observations.push(MutableObservation {
1852 vector: vector.clone(),
1853 tests: Vec::new(),
1854 phases: Vec::new(),
1855 explicit_phases: Vec::new(),
1856 });
1857 observations.len() - 1
1858 });
1859 observations[index]
1860 .tests
1861 .push(tests_by_hit.number(&id, ids));
1862 tests_by_id
1863 .get_mut(&id)
1864 .expect("registered test")
1865 .decisions
1866 .entry(meta.id.clone())
1867 .or_default()
1868 .insert(vector);
1869 hash_slot(&mut tests_by_decision, &meta.id).push(tests_by_hit.number(&id, ids));
1870 (meta.id.clone(), Some(vector.clone()))
1871 } else if record.record_type == "hit" {
1872 let hit = record
1873 .id
1874 .as_ref()
1875 .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1876 let number = hit_ids.number(hit, ids);
1877 tests_by_hit.add(number, &id, ids);
1878 tests_by_id
1879 .get_mut(&id)
1880 .expect("registered test")
1881 .hits
1882 .push(number);
1883 (hit.clone(), None)
1884 } else {
1885 return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1886 };
1887 let Some(timestamp_ms) = record.timestamp_ms else {
1888 continue;
1889 };
1890 let event = RuntimeEvent {
1891 event_type: record.record_type.clone(),
1892 id: record_id,
1893 vector: decision,
1894 timestamp_ms,
1895 statement_id: record.statement_id.clone(),
1896 phase_id: record.phase_id.clone(),
1897 environment: "server".into(),
1898 };
1899 let explicit = event.phase_id.is_some();
1900 let phase_id = correlate(&event);
1901 let Some(phase_id) = phase_id else { continue };
1902 let Some(phase) = phases_by_id.get_mut(phase_id.as_str()) else {
1903 continue;
1904 };
1905 phase.server_events += 1;
1906 if explicit {
1907 phase.explicit_events += 1;
1908 phase.explicit_server_events += 1;
1909 } else {
1910 phase.inferred_events += 1;
1911 phase.inferred_server_events += 1;
1912 }
1913 if event.event_type == "hit" {
1914 let hit = hit_ids.number(&event.id, ids);
1915 phase.hits.push(hit);
1916 phases_by_hit.add(hit, &phase_id, ids);
1917 if explicit {
1918 explicit_phases_by_hit.add(hit, &phase_id, ids);
1919 }
1920 } else if let Some(vector) = &event.vector {
1921 phase
1922 .decisions
1923 .entry(event.id.clone())
1924 .or_default()
1925 .insert(vector);
1926 if let Some(index) = vector_indexes
1927 .get(&event.id)
1928 .and_then(|indexes| indexes.get(&vector_key(vector)))
1929 .copied()
1930 && let Some(observation) = vectors_by_decision
1931 .get_mut(&event.id)
1932 .and_then(|observations| observations.get_mut(index))
1933 {
1934 observation
1935 .phases
1936 .push(phases_by_hit.number(&phase_id, ids));
1937 if explicit {
1938 observation
1939 .explicit_phases
1940 .push(explicit_phases_by_hit.number(&phase_id, ids));
1941 }
1942 }
1943 }
1944 }
1945 }
1946
1947 decision_metadata.sort_by(|left, right| {
1952 left.file
1953 .cmp(&right.file)
1954 .then(left.line.cmp(&right.line))
1955 .then(left.column.cmp(&right.column))
1956 });
1957 let tests_by_hit = tests_by_hit.into_relation(&hit_ids);
1958 let phases_by_hit = phases_by_hit.into_relation(&hit_ids);
1959 let explicit_phases_by_hit = explicit_phases_by_hit.into_relation(&hit_ids);
1960 let mut declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
1964 for branch in &manifest.branches {
1965 if !branch.alternatives.is_empty()
1966 && branch
1967 .alternatives
1968 .iter()
1969 .all(|alternative| declined.contains(&alternative.id))
1970 {
1971 declined.insert(&branch.id);
1972 }
1973 }
1974 let test_traits = tests_by_hit
1978 .names
1979 .iter()
1980 .map(|name| tests_by_id.get(name).map(TestTraits::of))
1981 .collect::<Vec<_>>();
1982 let phase_kind = |relation: &Relation| {
1983 relation
1984 .names
1985 .iter()
1986 .map(|name| {
1987 phases_by_id
1988 .get(name)
1989 .map(|phase| phase.phase.kind.as_str())
1990 })
1991 .collect::<Vec<_>>()
1992 };
1993 let phase_kinds = phase_kind(&phases_by_hit);
1994 let explicit_kinds = phase_kind(&explicit_phases_by_hit);
1995 let confidence = |tests: &Reached, phases: &Reached, explicit: &Reached| {
1996 confidence_from(
1997 tests,
1998 phases,
1999 explicit,
2000 &test_traits,
2001 &phase_kinds,
2002 &explicit_kinds,
2003 )
2004 };
2005 let declared_branches = manifest
2011 .branches
2012 .iter()
2013 .map(|branch| branch.id.as_str())
2014 .collect::<HashSet<_>>();
2015 let mut outcome_branches = Vec::new();
2016 let mut decisions = Vec::with_capacity(decision_metadata.len());
2017 for meta in decision_metadata {
2018 let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
2019 let outcome_id = format!("{}:outcome", meta.id);
2020 if !declared_branches.contains(outcome_id.as_str()) {
2021 let alternatives = [true, false]
2022 .into_iter()
2023 .map(|outcome| {
2024 let (mut tests, mut phases, mut explicit) =
2025 (Vec::new(), Vec::new(), Vec::new());
2026 for observation in mutable
2027 .iter()
2028 .filter(|observation| observation.vector.outcome == outcome)
2029 {
2030 tests.extend(&observation.tests);
2031 phases.extend(&observation.phases);
2032 explicit.extend(&observation.explicit_phases);
2033 }
2034 let tests = tests_by_hit.reached(tests);
2035 let phases = phases_by_hit.reached(phases);
2036 let explicit = explicit_phases_by_hit.reached(explicit);
2037 AlternativeResult {
2038 id: format!("{outcome_id}:{outcome}"),
2039 label: outcome.to_string(),
2040 covered: !tests.names.is_empty(),
2041 confidence: confidence(&tests, &phases, &explicit),
2042 tests: tests.names,
2043 phases: phases.names,
2044 }
2045 })
2046 .collect::<Vec<_>>();
2047 outcome_branches.push(BranchResult {
2048 covered: alternatives.iter().all(|alternative| alternative.covered),
2049 meta: BranchMeta {
2050 id: outcome_id,
2051 kind: meta.kind.clone(),
2052 file: meta.file.clone(),
2053 line: meta.line,
2054 column: meta.column,
2055 source: meta.source.clone(),
2056 alternatives: alternatives
2057 .iter()
2058 .map(|alternative| BranchAlternativeMeta {
2059 id: alternative.id.clone(),
2060 label: alternative.label.clone(),
2061 })
2062 .collect(),
2063 },
2064 alternatives,
2065 });
2066 }
2067 let mut observations = Vec::with_capacity(mutable.len());
2068 let mut observed_phases = Vec::new();
2069 let mut observed_explicit = Vec::new();
2070 for observation in mutable {
2071 let tests = tests_by_hit.reached(observation.tests);
2072 let phases = phases_by_hit.reached(observation.phases);
2073 let explicit = explicit_phases_by_hit.reached(observation.explicit_phases);
2074 observed_phases.extend(&phases.numbers);
2075 observed_explicit.extend(&explicit.numbers);
2076 observations.push(VectorObservation {
2077 vector: observation.vector,
2078 confidence: confidence(&tests, &phases, &explicit),
2079 tests: tests.names,
2080 phases: phases.names,
2081 explicit_phases: explicit.names,
2082 });
2083 }
2084 let vectors = observations
2085 .iter()
2086 .map(|observation| observation.vector.clone())
2087 .collect::<Vec<_>>();
2088 let witnesses =
2089 find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
2090 ReportError::DecisionAnalysis {
2091 decision_id: meta.id.clone(),
2092 error,
2093 }
2094 })?;
2095 let mut conditions = Vec::with_capacity(meta.conditions.len());
2096 for (index, source) in meta.conditions.iter().enumerate() {
2097 let witness = witnesses[index].map(|witness| {
2098 [
2099 vectors[witness.first].clone(),
2100 vectors[witness.second].clone(),
2101 ]
2102 });
2103 let witness_tests = witnesses[index].map(|witness| {
2104 [
2105 observations[witness.first].tests.clone(),
2106 observations[witness.second].tests.clone(),
2107 ]
2108 });
2109 let assertion_covered = false;
2110 conditions.push(ConditionResult {
2111 index,
2112 source: source.clone(),
2113 covered: witness.is_some(),
2114 assertion_covered,
2115 witness,
2116 witness_tests,
2117 });
2118 }
2119 let decision_tests =
2120 tests_by_hit.reached(tests_by_decision.remove(&meta.id).unwrap_or_default());
2121 let decision_confidence = confidence(
2122 &decision_tests,
2123 &phases_by_hit.reached(observed_phases),
2124 &explicit_phases_by_hit.reached(observed_explicit),
2125 );
2126 decisions.push(DecisionResult {
2127 executed: !vectors.is_empty(),
2128 covered: conditions.iter().all(|condition| condition.covered),
2129 meta,
2130 vectors,
2131 vector_observations: observations,
2132 conditions,
2133 tests: decision_tests.names,
2134 confidence: decision_confidence,
2135 });
2136 }
2137
2138 let points = manifest
2139 .points
2140 .iter()
2141 .cloned()
2142 .map(|meta| {
2143 let tests = tests_by_hit.of(&meta.id);
2144 let phases = phases_by_hit.of(&meta.id);
2145 let explicit = explicit_phases_by_hit.of(&meta.id);
2146 PointResult {
2147 measured: !declined.contains(&meta.id),
2148 covered: tests_by_hit.by_hit.contains_key(&meta.id),
2149 confidence: confidence(tests, phases, explicit),
2150 meta,
2151 tests: tests.names.clone(),
2152 phases: phases.names.clone(),
2153 }
2154 })
2155 .collect::<Vec<_>>();
2156
2157 let branches = manifest
2158 .branches
2159 .iter()
2160 .cloned()
2161 .map(|mut meta| {
2162 meta.alternatives
2164 .retain(|alternative| !declined.contains(&alternative.id));
2165 let alternatives = meta
2166 .alternatives
2167 .iter()
2168 .map(|alternative| {
2169 let tests = tests_by_hit.of(&alternative.id);
2170 let phases = phases_by_hit.of(&alternative.id);
2171 let explicit = explicit_phases_by_hit.of(&alternative.id);
2172 AlternativeResult {
2173 id: alternative.id.clone(),
2174 label: alternative.label.clone(),
2175 covered: tests_by_hit.by_hit.contains_key(&alternative.id),
2176 tests: tests.names.clone(),
2177 phases: phases.names.clone(),
2178 confidence: confidence(tests, phases, explicit),
2179 }
2180 })
2181 .collect::<Vec<_>>();
2182 BranchResult {
2183 covered: alternatives.iter().all(|alternative| alternative.covered),
2184 meta,
2185 alternatives,
2186 }
2187 })
2188 .chain(outcome_branches)
2189 .collect::<Vec<_>>();
2190
2191 let mut line_aggregates = BTreeMap::<SourceLine, LineAggregate>::new();
2202 for point in &points {
2203 let aggregate = line_aggregates
2204 .entry(SourceLine {
2205 file: ids.id(&point.meta.file),
2206 line: point.meta.line,
2207 })
2208 .or_default();
2209 if declined.contains(&point.meta.id) {
2210 continue;
2211 }
2212 aggregate.measured = true;
2213 aggregate.covered |= point.covered;
2214 aggregate
2215 .tests
2216 .extend(&tests_by_hit.of(&point.meta.id).numbers);
2217 aggregate
2218 .phases
2219 .extend(&phases_by_hit.of(&point.meta.id).numbers);
2220 aggregate
2221 .explicit_phases
2222 .extend(&explicit_phases_by_hit.of(&point.meta.id).numbers);
2223 }
2224 let lines = line_aggregates
2225 .into_iter()
2226 .map(|(location, aggregate)| {
2227 let LineAggregate {
2228 covered,
2229 measured,
2230 tests,
2231 phases,
2232 explicit_phases,
2233 } = aggregate;
2234 let tests = tests_by_hit.reached(tests);
2235 let phases = phases_by_hit.reached(phases);
2236 let explicit = explicit_phases_by_hit.reached(explicit_phases);
2237 let traits = tests
2238 .numbers
2239 .iter()
2240 .filter_map(|number| test_traits[*number as usize].as_ref())
2241 .collect::<Vec<_>>();
2242 let runners = traits
2243 .iter()
2244 .map(|traits| traits.runner)
2245 .collect::<BTreeSet<_>>();
2246 let kinds = traits
2247 .iter()
2248 .map(|traits| traits.kind)
2249 .collect::<BTreeSet<_>>();
2250 LineResult {
2251 file: location.file.to_string(),
2252 line: location.line,
2253 covered,
2254 measured,
2255 confidence: confidence(&tests, &phases, &explicit),
2256 tests: tests.names,
2257 runners: runners.iter().map(|runner| (*runner).to_owned()).collect(),
2258 exclusive_kind: (kinds.len() == 1).then(|| (*kinds.first().unwrap()).to_owned()),
2259 phases: phases.names,
2260 kinds: kinds.iter().map(|kind| (*kind).to_owned()).collect(),
2261 }
2262 })
2263 .collect::<Vec<_>>();
2264 drop(test_traits);
2265
2266 let hit_ranks = hit_ids.ranks();
2270 let mut point_files = manifest
2271 .points
2272 .iter()
2273 .map(|point| ids.id(&point.file))
2274 .collect::<Vec<_>>();
2275 point_files.sort_unstable();
2276 point_files.dedup();
2277 let mut hit_locations = vec![None; hit_ids.names.len()];
2278 for point in &manifest.points {
2279 if let Some(number) = hit_ids.numbers.get(point.id.as_str()) {
2280 let file = point_files
2281 .binary_search_by(|file| file.as_str().cmp(&point.file))
2282 .expect("every point file is listed") as u32;
2283 hit_locations[*number] = Some((file, point.line));
2284 }
2285 }
2286 let lines_of = |hits: &[usize]| {
2287 let mut located = hits
2288 .iter()
2289 .filter_map(|hit| hit_locations[*hit])
2290 .collect::<Vec<_>>();
2291 located.sort_unstable();
2292 located.dedup();
2293 located
2294 .into_iter()
2295 .map(|(file, line)| SourceLine {
2296 file: point_files[file as usize].clone(),
2297 line,
2298 })
2299 .collect::<Vec<_>>()
2300 };
2301 test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
2302 let tests = test_order
2303 .into_iter()
2304 .map(|id| {
2305 let test = tests_by_id.remove(&id).expect("test order references test");
2306 let outcome = test_outcome(&test);
2307 let lines = lines_of(&test.hits);
2308 let hits = hit_ids.sorted_by(test.hits, &hit_ranks);
2309 TestCoverageResult {
2310 id: test.id,
2311 name: test.name,
2312 file: test.file,
2313 title: test.title,
2314 retries: test.retries.into_iter().collect(),
2315 attempts: test.attempts.into_values().collect(),
2316 outcome,
2317 provenance: test.provenance,
2318 role: test.role,
2319 attribution: test.attribution,
2320 hits,
2321 decisions: test
2322 .decisions
2323 .into_iter()
2324 .map(|(id, vectors)| TestDecisionResult {
2325 id,
2326 vectors: vectors.values,
2327 })
2328 .collect(),
2329 lines,
2330 }
2331 })
2332 .collect::<Vec<_>>();
2333
2334 let mut test_files = BTreeMap::<
2335 String,
2336 (
2337 BTreeSet<String>,
2338 BTreeSet<String>,
2339 BTreeSet<String>,
2340 BTreeSet<SourceLine>,
2341 ),
2342 >::new();
2343 for test in &tests {
2344 let aggregate = test_files
2345 .entry(
2346 test.file
2347 .clone()
2348 .unwrap_or_else(|| "(unknown test file)".into()),
2349 )
2350 .or_default();
2351 aggregate.0.insert(test.id.clone());
2352 aggregate.1.insert(test.provenance.runner.clone());
2353 aggregate.2.insert(test.provenance.kind.clone());
2354 aggregate.3.extend(test.lines.iter().cloned());
2355 }
2356 let test_files = test_files
2357 .into_iter()
2358 .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
2359 file,
2360 tests: sorted(&tests),
2361 runners: sorted(&runners),
2362 kinds: sorted(&kinds),
2363 lines: lines.into_iter().collect(),
2364 })
2365 .collect::<Vec<_>>();
2366
2367 let mut phases = phases_by_id.into_values().collect::<Vec<_>>();
2368 phases.sort_by(|left, right| {
2369 left.phase
2370 .started_at_ms
2371 .cmp(&right.phase.started_at_ms)
2372 .then(left.phase.id.cmp(&right.phase.id))
2373 });
2374 let phases = phases
2375 .into_iter()
2376 .map(|phase| {
2377 let lines = lines_of(&phase.hits);
2378 let hits = hit_ids.sorted_by(phase.hits, &hit_ranks);
2379 PhaseResult {
2380 phase: phase.phase,
2381 test: phase.test,
2382 hits,
2383 decisions: phase
2384 .decisions
2385 .into_iter()
2386 .map(|(id, vectors)| TestDecisionResult {
2387 id,
2388 vectors: vectors.values,
2389 })
2390 .collect(),
2391 lines,
2392 browser_events: phase.browser_events,
2393 server_events: phase.server_events,
2394 explicit_events: phase.explicit_events,
2395 inferred_events: phase.inferred_events,
2396 explicit_browser_events: phase.explicit_browser_events,
2397 inferred_browser_events: phase.inferred_browser_events,
2398 explicit_server_events: phase.explicit_server_events,
2399 inferred_server_events: phase.inferred_server_events,
2400 }
2401 })
2402 .collect::<Vec<_>>();
2403
2404 let total_obligations = decisions.len() + points.len() + branches.len();
2405 let (decisions, points, branches) = if declined.is_empty() {
2406 (decisions, points, branches)
2407 } else {
2408 (
2409 decisions
2410 .into_iter()
2411 .filter(|result| !declined.contains(&result.meta.id))
2412 .collect::<Vec<_>>(),
2413 points
2414 .into_iter()
2415 .filter(|result| !declined.contains(&result.meta.id))
2416 .collect::<Vec<_>>(),
2417 branches
2418 .into_iter()
2419 .filter(|result| !declined.contains(&result.meta.id))
2420 .collect::<Vec<_>>(),
2421 )
2422 };
2423 let measured_obligations = decisions.len() + points.len() + branches.len();
2424 let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
2425 if total_obligations > measured_obligations {
2426 summary.unmeasured_obligations = Some(total_obligations - measured_obligations);
2427 summary.exact_fraction_pct = Some(if total_obligations == 0 {
2428 100.0
2429 } else {
2430 (measured_obligations as f64) * 100.0 / (total_obligations as f64)
2431 });
2432 }
2433 if manifest.limitations.iter().any(blocking_limitation) {
2440 summary.coverage_complete = false;
2441 summary.completeness_blocked = Some(true);
2442 }
2443
2444 let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
2445 let values = tests
2446 .iter()
2447 .map(|test| {
2448 if field == "kind" {
2449 test.provenance.kind.clone()
2450 } else {
2451 test.provenance.runner.clone()
2452 }
2453 })
2454 .collect::<BTreeSet<_>>();
2455 values
2456 .into_iter()
2457 .map(|value| {
2458 let in_dimension = |test: &&TestCoverageResult| {
2459 if field == "kind" {
2460 test.provenance.kind == value
2461 } else {
2462 test.provenance.runner == value
2463 }
2464 };
2465 let selected = tests
2472 .iter()
2473 .filter(in_dimension)
2474 .filter(|test| coverage_is_its_own(&test.attribution))
2475 .map(|test| test.id.clone())
2476 .collect::<BTreeSet<_>>();
2477 let counted = tests
2478 .iter()
2479 .filter(in_dimension)
2480 .map(|test| test.id.clone())
2481 .collect::<BTreeSet<_>>();
2482 Ok(DimensionCoverage {
2483 kind: (field == "kind").then(|| value.clone()),
2484 runner: (field == "runner").then(|| value.clone()),
2485 tests: tests
2488 .iter()
2489 .filter(|test| counted.contains(&test.id) && test.role == "test")
2490 .count(),
2491 setups: tests
2492 .iter()
2493 .filter(|test| counted.contains(&test.id) && test.role == "setup")
2494 .count(),
2495 attributed: tests
2496 .iter()
2497 .filter(|test| selected.contains(&test.id) && test.role == "test")
2498 .count(),
2499 summary: summary_for_results(
2500 &decisions,
2501 &points,
2502 &branches,
2503 &lines,
2504 Some(&selected),
2505 )?,
2506 })
2507 })
2508 .collect()
2509 };
2510
2511 Ok(CoverageView {
2512 generated_at: generated_at.into(),
2513 variant: coverage_model.variant.clone(),
2514 scope: manifest.scope.clone(),
2515 model: CoverageModel {
2516 language: coverage_model.language.clone(),
2517 name: coverage_model.name.clone(),
2518 completeness_meaning: coverage_model.completeness_meaning.clone(),
2519 measured: coverage_model.measured.clone(),
2520 not_measured: coverage_model.not_measured.clone(),
2521 },
2522 integrity: None,
2523 limitations: manifest.limitations.clone(),
2524 transport: None,
2525 summary,
2526 coverage_by_kind: dimension_coverage("kind")?,
2527 coverage_by_runner: dimension_coverage("runner")?,
2528 decisions,
2529 points,
2530 branches,
2531 tests,
2532 test_files,
2533 phases,
2534 lines,
2535 })
2536}
2537
2538pub fn analyze_coverage_results(
2539 request: &CoverageReportRequest,
2540) -> Result<CoverageReport, ReportError> {
2541 if let Some(scope) = request
2542 .raw_results
2543 .iter()
2544 .filter_map(|raw| raw.scope.as_ref())
2545 .find(|scope| scope.run_id != request.run_id)
2546 {
2547 return Err(ReportError::ScopeMismatch {
2548 expected: request.run_id.clone(),
2549 actual: scope.run_id.clone(),
2550 });
2551 }
2552 if request.raw_results.is_empty() {
2553 return Err(ReportError::NoEvidence(request.run_id.clone()));
2554 }
2555 let default_model = javascript_coverage_model();
2556 let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
2557 PersistedCoverageModel::from_declaration(coverage_model).map_err(|reason| {
2558 ReportError::InvalidJson {
2559 path: "coverage-model.json".into(),
2560 reason: reason.into(),
2561 }
2562 })?;
2563 let all = request.raw_results.iter().collect::<Vec<_>>();
2568 let passing = passing_coverage_results(&request.raw_results);
2569 let failing = failed_coverage_results(&request.raw_results);
2570 let build = |results: &[&RawTestResult]| {
2571 create_coverage_view_with_model(
2572 &request.manifest,
2573 results,
2574 &request.generated_at,
2575 coverage_model,
2576 &mut Interner::default(),
2577 )
2578 };
2579 let (view, passed, failed) = std::thread::scope(|scope| {
2580 let passed = scope.spawn(|| build(&passing));
2581 let failed = scope.spawn(|| build(&failing));
2582 let view = build(&all);
2583 (
2584 view,
2585 passed
2586 .join()
2587 .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
2588 failed
2589 .join()
2590 .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
2591 )
2592 });
2593 let (view, passed, failed) = (view?, passed?, failed?);
2594 let execution = match request.test_exit_code {
2595 ExitCodeInput::Missing => None,
2596 ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
2597 valid: test_exit_code == Some(0),
2598 test_exit_code,
2599 }),
2600 };
2601 let mut view = view;
2602 let mut passed = passed;
2603 let mut failed = failed;
2604 if let Some(integrity) = &request.integrity {
2605 view.integrity = Some(integrity.clone());
2606 passed.integrity = Some(integrity.clone());
2607 failed.integrity = Some(integrity.clone());
2608 }
2609 Ok(CoverageReport {
2610 view,
2611 execution,
2612 filters: CoverageFilters { passed, failed },
2613 })
2614}
2615
2616fn parse_entry<T: for<'de> Deserialize<'de>>(
2617 entry: &EvidenceArchiveEntry,
2618) -> Result<T, ReportError> {
2619 serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
2620 path: entry.path.clone(),
2621 reason: error.to_string(),
2622 })
2623}
2624
2625fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
2626 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2627) -> Result<Vec<T>, ReportError> {
2628 let mut records = Vec::new();
2629 for entry in entries {
2630 let Some(contents) = entry.contents.strip_suffix(b"\n") else {
2631 return Err(ReportError::InvalidJson {
2632 path: entry.path.clone(),
2633 reason: "recognized JSONL evidence must end with a newline".into(),
2634 });
2635 };
2636 if contents.is_empty() {
2637 return Err(ReportError::InvalidJson {
2638 path: entry.path.clone(),
2639 reason: "recognized JSONL evidence must contain at least one record".into(),
2640 });
2641 }
2642 for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
2643 if line.is_empty() {
2644 return Err(ReportError::InvalidJson {
2645 path: entry.path.clone(),
2646 reason: format!("blank JSONL record at line {}", index + 1),
2647 });
2648 }
2649 records.push(serde_json::from_slice(line).map_err(|error| {
2650 ReportError::InvalidJson {
2651 path: entry.path.clone(),
2652 reason: format!("invalid JSONL record at line {}: {error}", index + 1),
2653 }
2654 })?);
2655 }
2656 }
2657 Ok(records)
2658}
2659
2660struct TolerantJsonLines<T> {
2666 records: Vec<T>,
2667 corrupt_records: usize,
2668 corrupt_files: usize,
2669}
2670
2671fn parse_server_json_lines<'a, T: for<'de> Deserialize<'de>>(
2672 entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2673) -> TolerantJsonLines<T> {
2674 let mut parsed = TolerantJsonLines {
2675 records: Vec::new(),
2676 corrupt_records: 0,
2677 corrupt_files: 0,
2678 };
2679 for entry in entries {
2680 let mut corrupt_here = 0;
2681 for line in entry.contents.split(|byte| *byte == b'\n') {
2682 if line.is_empty() {
2683 continue;
2684 }
2685 match serde_json::from_slice(line) {
2686 Ok(record) => parsed.records.push(record),
2687 Err(_) => corrupt_here += 1,
2688 }
2689 }
2690 if corrupt_here > 0 {
2691 parsed.corrupt_records += corrupt_here;
2692 parsed.corrupt_files += 1;
2693 }
2694 }
2695 parsed
2696}
2697
2698fn is_mcdc_result(path: &str) -> bool {
2699 path == "mcdc.json" || path.ends_with("/mcdc.json")
2700}
2701
2702fn is_mcdc_journal(path: &str) -> bool {
2703 path == "mcdc.jsonl" || path.ends_with(".mcdc.jsonl")
2704}
2705
2706pub fn analyze_coverage_archive(
2707 request: &ArchiveReportRequest,
2708) -> Result<CoverageReport, ReportError> {
2709 let entries = read_archive(Path::new(&request.archive_path))
2710 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2711 let manifest = entries
2712 .iter()
2713 .find(|entry| entry.path == "manifest.json")
2714 .ok_or(ReportError::MissingManifest)
2715 .and_then(parse_entry::<CoverageManifest>)?;
2716 let mut raw_results = entries
2717 .iter()
2718 .filter(|entry| is_mcdc_result(&entry.path))
2719 .map(parse_entry::<RawTestResult>)
2720 .collect::<Result<Vec<_>, _>>()?;
2721 let journal_results = parse_json_lines::<RawTestResult>(
2722 entries.iter().filter(|entry| is_mcdc_journal(&entry.path)),
2723 )?;
2724 raw_results.extend(journal_results);
2725
2726 let scoped = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2727 entry.path.starts_with("server/")
2728 && !entry.path.starts_with("server/background/")
2729 && entry.path.ends_with(".jsonl")
2730 }));
2731 let scoped_records = scoped.records;
2732 let mut by_scope = std::collections::HashMap::<&ExecutionScope, usize>::new();
2737 for (index, raw) in raw_results.iter().enumerate() {
2738 if let Some(scope) = &raw.scope {
2739 by_scope.entry(scope).or_insert(index);
2740 }
2741 }
2742 let by_scope: std::collections::HashMap<ExecutionScope, usize> = by_scope
2743 .into_iter()
2744 .map(|(scope, index)| (scope.clone(), index))
2745 .collect();
2746 let mut seen = std::collections::HashMap::<usize, std::collections::HashSet<Vec<u8>>>::new();
2747 for record in &scoped_records {
2748 let Some(scope) = &record.scope else { continue };
2749 let Some(&index) = by_scope.get(scope) else {
2750 continue;
2751 };
2752 let raw = &mut raw_results[index];
2753 let known = seen.entry(index).or_insert_with(|| {
2754 raw.server
2755 .iter()
2756 .filter_map(|existing| serde_json::to_vec(existing).ok())
2757 .collect()
2758 });
2759 let Ok(key) = serde_json::to_vec(record) else {
2760 if !raw.server.contains(record) {
2761 raw.server.push(record.clone());
2762 }
2763 continue;
2764 };
2765 if known.insert(key) {
2766 raw.server.push(record.clone());
2767 }
2768 }
2769
2770 let background = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2771 entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2772 }));
2773 let background_records = background.records;
2774 if !background_records.is_empty() {
2775 raw_results.push(RawTestResult {
2776 test_id: Some(format!("background:{}", request.run_id)),
2777 scope: None,
2778 test: "Background / unattributed".into(),
2779 test_file: None,
2780 title: Some("Background / unattributed".into()),
2781 retry: None,
2782 status: Some("unknown".into()),
2783 expected_status: None,
2784 flaky: false,
2785 provenance: TestProvenance {
2786 runner: "background".into(),
2787 kind: "background".into(),
2788 project: None,
2789 source: "explicit".into(),
2790 },
2791 role: "background".into(),
2792 attribution: ATTRIBUTION_EXACT.into(),
2793 phases: vec![],
2794 runtime: vec![],
2795 browser: vec![],
2796 server: background_records.clone(),
2797 });
2798 }
2799
2800 let execution =
2803 parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2804 entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2805 }));
2806 let execution_events = execution.records;
2807 let count_event = |name: &str| {
2808 execution_events
2809 .iter()
2810 .filter(|event| event.kind() == name)
2811 .count()
2812 };
2813 let transport = TransportStats {
2814 processes: count_event("process"),
2815 child_launches: count_event("child-launch"),
2816 remote_launches: count_event("remote-launch"),
2817 workspace_capabilities: count_event("workspace-capability"),
2818 guest_processes: count_event("guest-process"),
2819 scoped_server_records: scoped_records.len(),
2820 background_server_records: background_records.len(),
2821 corrupt_records: scoped.corrupt_records
2822 + background.corrupt_records
2823 + execution.corrupt_records,
2824 corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
2825 };
2826 let frontend = entries
2827 .iter()
2828 .find(|entry| entry.path == "frontend.json")
2829 .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
2830 .and_then(parse_entry::<FrontendRunDeclaration>)?;
2831 let persisted = entries
2832 .iter()
2833 .find(|entry| entry.path == "coverage-model.json")
2834 .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
2835 .and_then(parse_entry::<PersistedCoverageModel>)?;
2836 let coverage_model =
2837 persisted
2838 .into_declaration()
2839 .map_err(|reason| ReportError::InvalidJson {
2840 path: "coverage-model.json".into(),
2841 reason: reason.into(),
2842 })?;
2843 if frontend.language != coverage_model.language {
2844 return Err(ReportError::InvalidArchive(format!(
2845 "frontend language {} differs from coverage model language {}",
2846 frontend.language, coverage_model.language
2847 )));
2848 }
2849 separate_repeated_executions(&mut raw_results);
2850 release_leaked_phases(&mut raw_results);
2851 let normalized = CoverageReportRequest {
2852 run_id: request.run_id.clone(),
2853 manifest,
2854 raw_results,
2855 generated_at: request.generated_at.clone(),
2856 coverage_model: Some(coverage_model),
2857 integrity: request.integrity.clone(),
2858 test_exit_code: request.test_exit_code.clone(),
2859 };
2860 drop(entries);
2864 analyze_frontend_request(&frontend, &normalized, transport)
2865}
2866
2867fn separate_repeated_executions(raw_results: &mut [RawTestResult]) {
2877 let mut declared = BTreeSet::<String>::new();
2878 let mut executions = BTreeMap::<String, usize>::new();
2879 for raw in raw_results.iter_mut() {
2880 let repeated = raw.phases.iter().any(|phase| declared.contains(&phase.id));
2881 if !repeated {
2882 declared.extend(raw.phases.iter().map(|phase| phase.id.clone()));
2883 continue;
2884 }
2885 let mut renamed = BTreeMap::<String, String>::new();
2886 for phase in &raw.phases {
2887 let count = executions.entry(phase.id.clone()).or_insert(1);
2888 let fresh = loop {
2889 *count += 1;
2890 let candidate = format!("{}~{}", phase.id, count);
2891 if !declared.contains(&candidate) {
2892 break candidate;
2893 }
2894 };
2895 renamed.insert(phase.id.clone(), fresh);
2896 }
2897 let rename = |phase: &mut Option<String>| {
2898 if let Some(fresh) = phase.as_ref().and_then(|id| renamed.get(id)) {
2899 *phase = Some(fresh.clone());
2900 }
2901 };
2902 for phase in &mut raw.phases {
2903 phase.id = renamed[&phase.id].clone();
2904 rename(&mut phase.caused_by_phase_id);
2905 }
2906 for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
2907 rename(&mut snapshot.phase_id);
2908 for event in &mut snapshot.events {
2909 rename(&mut event.phase_id);
2910 }
2911 }
2912 for record in &mut raw.server {
2913 rename(&mut record.phase_id);
2914 }
2915 declared.extend(renamed.into_values());
2916 }
2917}
2918
2919fn release_leaked_phases(raw_results: &mut [RawTestResult]) {
2929 let declared = raw_results
2930 .iter()
2931 .flat_map(|raw| raw.phases.iter().map(|phase| phase.id.clone()))
2932 .collect::<BTreeSet<_>>();
2933 for raw in raw_results.iter_mut() {
2934 let own = raw
2935 .phases
2936 .iter()
2937 .map(|phase| phase.id.clone())
2938 .collect::<BTreeSet<_>>();
2939 let leaked = |phase: &mut Option<String>| {
2940 if phase
2941 .as_ref()
2942 .is_some_and(|id| !own.contains(id) && declared.contains(id))
2943 {
2944 *phase = None;
2945 }
2946 };
2947 for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
2948 leaked(&mut snapshot.phase_id);
2949 for event in &mut snapshot.events {
2950 leaked(&mut event.phase_id);
2951 }
2952 }
2953 for record in &mut raw.server {
2954 leaked(&mut record.phase_id);
2955 }
2956 }
2957}
2958
2959pub fn analyze_frontend_request(
2964 frontend: &FrontendRunDeclaration,
2965 normalized: &CoverageReportRequest,
2966 transport: TransportStats,
2967) -> Result<CoverageReport, ReportError> {
2968 let mut report = crate::frontend_protocol::analyze_frontend_results(frontend, normalized)
2969 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2970 report.view.transport = Some(transport.clone());
2971 report.filters.passed.transport = Some(transport.clone());
2972 report.filters.failed.transport = Some(transport);
2973 Ok(report)
2974}
2975
2976impl TransportStats {
2977 pub fn none() -> Self {
2980 Self {
2981 processes: 0,
2982 child_launches: 0,
2983 remote_launches: 0,
2984 workspace_capabilities: 0,
2985 guest_processes: 0,
2986 scoped_server_records: 0,
2987 background_server_records: 0,
2988 corrupt_records: 0,
2989 corrupt_files: 0,
2990 }
2991 }
2992}
2993
2994#[cfg(test)]
2995mod tests {
2996 use std::{
2997 fs,
2998 sync::atomic::{AtomicU64, Ordering},
2999 time::SystemTime,
3000 };
3001
3002 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
3003
3004 use super::*;
3005
3006 #[test]
3007 fn a_view_holds_one_copy_of_each_id_it_names() {
3008 let root = std::env::temp_dir().join(format!(
3014 "supercov-shared-ids-{}-{}",
3015 std::process::id(),
3016 std::time::SystemTime::now()
3017 .duration_since(std::time::UNIX_EPOCH)
3018 .unwrap()
3019 .as_nanos()
3020 ));
3021 let directory = crate::run_store::create_analyzable_test_run(&root, "shared");
3022 let run = crate::run_store::discover_runs(&root)
3023 .unwrap()
3024 .runs
3025 .remove(0);
3026 let report = crate::run_store::analyze_stored_run(&run).unwrap();
3027 let _ = directory;
3028 for view in [&report.view, &report.filters.passed] {
3029 let mentions = view
3030 .points
3031 .iter()
3032 .flat_map(|point| point.tests.iter().chain(&point.confidence.tests))
3033 .chain(view.lines.iter().flat_map(|line| &line.tests))
3034 .collect::<Vec<_>>();
3035 assert!(
3036 mentions.len() >= 2,
3037 "the fixture names its test in several places"
3038 );
3039 let first = mentions[0];
3040 for mention in &mentions {
3041 assert_eq!(*mention, first);
3042 assert!(
3043 mention.same_allocation(first),
3044 "every mention shares one copy"
3045 );
3046 }
3047 }
3048 fs::remove_dir_all(root).unwrap();
3049 }
3050
3051 #[test]
3052 fn references_numbered_as_they_arrive_come_out_as_the_sets_inserted_directly() {
3053 let arrivals = [
3056 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3057 ("py:statement:a", "h11/tests/test_x.py::test_b[2-50]"),
3058 ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3059 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3060 ("py:statement:a", "h11/tests/test_x.py::test_b[10-50]"),
3061 ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3062 ("py:statement:c", "python-phase:9"),
3063 ("py:statement:c", "python-phase:10"),
3064 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3065 ];
3066 let mut ids = Interner::default();
3067 let mut hits = HitIds::default();
3068 let mut numbered = References::default();
3069 hits.number("unused", &mut ids);
3071 let mut direct = HashMap::<String, BTreeSet<String>>::new();
3072 for (obligation, name) in arrivals {
3073 let hit = hits.number(obligation, &mut ids);
3074 numbered.add(hit, name, &mut ids);
3075 direct
3076 .entry(obligation.to_owned())
3077 .or_default()
3078 .insert(name.to_owned());
3079 }
3080 let numbered = numbered
3081 .into_relation(&hits)
3082 .by_hit
3083 .into_iter()
3084 .map(|(obligation, reached)| {
3085 let names = reached
3086 .names
3087 .into_iter()
3088 .map(String::from)
3089 .collect::<Vec<_>>();
3090 let mut sorted_names = names.clone();
3091 sorted_names.sort();
3092 sorted_names.dedup();
3093 assert_eq!(
3094 names, sorted_names,
3095 "a relation lists its ids once, in order"
3096 );
3097 (obligation, names.into_iter().collect())
3098 })
3099 .collect::<HashMap<_, BTreeSet<String>>>();
3100 assert_eq!(numbered, direct);
3101 assert_eq!(
3102 hits.sorted((0..hits.names.len()).collect()),
3103 [
3104 "py:statement:a",
3105 "py:statement:b",
3106 "py:statement:c",
3107 "unused"
3108 ]
3109 .map(Id::from)
3110 );
3111 }
3112
3113 static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
3114
3115 fn point(id: &str, line: usize) -> PointMeta {
3116 PointMeta {
3117 id: id.into(),
3118 kind: PointKind::Statement,
3119 file: "src/app.js".into(),
3120 line,
3121 column: 0,
3122 source: "work();".into(),
3123 label: None,
3124 }
3125 }
3126
3127 fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
3128 RawTestResult {
3129 test_id: Some(id.into()),
3130 scope: None,
3131 test: id.into(),
3132 test_file: Some("tests/app.test.js".into()),
3133 title: None,
3134 retry: Some(retry),
3135 status: Some(status.into()),
3136 expected_status: None,
3137 flaky: false,
3138 provenance: TestProvenance {
3139 runner: "node:test".into(),
3140 kind: "unit".into(),
3141 project: None,
3142 source: "runner-default".into(),
3143 },
3144 role: "test".into(),
3145 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
3146 phases: vec![],
3147 runtime: vec![RuntimeSnapshot {
3148 decisions: vec![],
3149 hits: hits.iter().map(|hit| (*hit).into()).collect(),
3150 events: vec![],
3151 logicals: vec![],
3152 phase_id: None,
3153 }],
3154 browser: vec![],
3155 server: vec![],
3156 }
3157 }
3158
3159 #[test]
3160 fn declined_obligations_are_unmeasured_never_uncovered() {
3161 let point = |id: &str, line: usize| PointMeta {
3165 id: id.into(),
3166 kind: PointKind::Statement,
3167 file: "src/app.js".into(),
3168 line,
3169 column: 0,
3170 source: "work();".into(),
3171 label: None,
3172 };
3173 let measured_only = CoverageManifest {
3174 decisions: vec![],
3175 points: vec![point("measured", 1)],
3176 branches: vec![],
3177 limitations: vec![],
3178 unmeasured: Vec::new(),
3179 scope: None,
3180 assertion_sites: Vec::new(),
3181 };
3182 let with_declined = CoverageManifest {
3183 decisions: vec![],
3184 points: vec![point("measured", 1), point("declined", 2)],
3185 branches: vec![],
3186 limitations: vec![],
3187 unmeasured: vec!["declined".into()],
3188 scope: None,
3189 assertion_sites: Vec::new(),
3190 };
3191
3192 let baseline =
3193 create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
3194 let view =
3195 create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
3196
3197 assert_eq!(
3199 view.summary.statements.total, baseline.summary.statements.total,
3200 "a declined obligation stayed in the covered/uncovered denominator"
3201 );
3202 assert_eq!(view.summary.unmeasured_obligations, Some(1));
3203 assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
3204
3205 assert_eq!(
3209 view.summary.lines.total, baseline.summary.lines.total,
3210 "a declined obligation kept its line in the line denominator"
3211 );
3212 let declined_line = view
3213 .lines
3214 .iter()
3215 .find(|line| line.line == 2)
3216 .expect("the declined line stays addressable");
3217 assert!(!declined_line.measured);
3218 assert!(!declined_line.covered);
3219
3220 assert_eq!(baseline.summary.unmeasured_obligations, None);
3223 assert_eq!(baseline.summary.exact_fraction_pct, None);
3224 let encoded = serde_json::to_string(&baseline.summary).unwrap();
3225 assert!(
3226 !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
3227 "a fully measured summary must serialize unchanged: {encoded}"
3228 );
3229 }
3230
3231 #[test]
3232 fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
3233 let point = |id: &str| PointMeta {
3237 id: id.into(),
3238 kind: PointKind::Statement,
3239 file: "src/app.js".into(),
3240 line: 1,
3241 column: 0,
3242 source: "work();".into(),
3243 label: None,
3244 };
3245 let manifest = CoverageManifest {
3246 decisions: vec![],
3247 points: vec![point("declined")],
3248 branches: vec![],
3249 limitations: vec![],
3250 unmeasured: vec!["declined".into()],
3251 scope: None,
3252 assertion_sites: Vec::new(),
3253 };
3254 let view = create_coverage_view(
3255 &manifest,
3256 &[raw("test", 0, "passed", &["declined"])],
3257 "time",
3258 )
3259 .unwrap();
3260
3261 assert_eq!(view.summary.lines.total, 0);
3262 assert_eq!(view.summary.lines.covered, 0);
3263 let line = view.lines.first().expect("the line stays addressable");
3264 assert!(!line.measured);
3265 assert!(!line.covered, "an unmeasured line is not covered either");
3266 }
3267
3268 #[test]
3269 fn report_retains_unexecuted_manifest_conditions() {
3270 let manifest = CoverageManifest {
3271 unmeasured: Vec::new(),
3272 decisions: vec![DecisionMeta {
3273 id: "decision".into(),
3274 file: "src/app.js".into(),
3275 line: 1,
3276 column: 0,
3277 source: "left && right".into(),
3278 conditions: vec!["left".into(), "right".into()],
3279 kind: "if".into(),
3280 }],
3281 points: vec![],
3282 branches: vec![],
3283 limitations: vec![],
3284 scope: None,
3285 assertion_sites: Vec::new(),
3286 };
3287 let view =
3288 create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
3289 assert_eq!(view.summary.conditions, 2);
3290 assert_eq!(view.summary.covered_conditions, 0);
3291 assert_eq!(view.decisions[0].conditions.len(), 2);
3292 }
3293
3294 #[test]
3295 fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
3296 let manifest = CoverageManifest {
3297 unmeasured: Vec::new(),
3298 decisions: vec![DecisionMeta {
3299 id: "application-decision".into(),
3300 file: "src/app.js".into(),
3301 line: 1,
3302 column: 0,
3303 source: "left && right".into(),
3304 conditions: vec!["left".into(), "right".into()],
3305 kind: "if".into(),
3306 }],
3307 points: vec![],
3308 branches: vec![],
3309 limitations: vec![],
3310 scope: None,
3311 assertion_sites: Vec::new(),
3312 };
3313 let mut attempt = raw("test", 0, "passed", &[]);
3314 attempt.runtime[0].decisions.push(DecisionSnapshot {
3315 meta: DecisionMeta {
3316 id: "synthetic-fixture".into(),
3317 file: "fixtures/generated.js".into(),
3318 line: 1,
3319 column: 0,
3320 source: "a && b && c".into(),
3321 conditions: vec!["a".into(), "b".into(), "c".into()],
3322 kind: "if".into(),
3323 },
3324 vectors: vec![McdcVector {
3325 values: vec![Some(true), Some(true), Some(true)],
3326 outcome: true,
3327 }],
3328 });
3329
3330 let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3331 assert_eq!(view.decisions.len(), 1);
3332 assert_eq!(view.decisions[0].meta.id, "application-decision");
3333 assert!(view.decisions[0].vectors.is_empty());
3334 }
3335
3336 #[test]
3337 fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
3338 let expected = DecisionMeta {
3339 id: "application-decision".into(),
3340 file: "src/app.js".into(),
3341 line: 1,
3342 column: 0,
3343 source: "left && right".into(),
3344 conditions: vec!["left".into(), "right".into()],
3345 kind: "if".into(),
3346 };
3347 let manifest = CoverageManifest {
3348 unmeasured: Vec::new(),
3349 decisions: vec![expected.clone()],
3350 points: vec![],
3351 branches: vec![],
3352 limitations: vec![],
3353 scope: Some(serde_json::json!({
3354 "entries": [{ "file": "src/empty.js", "status": "included" }]
3355 })),
3356 assertion_sites: Vec::new(),
3357 };
3358 let vector = McdcVector {
3359 values: vec![Some(true), Some(true)],
3360 outcome: true,
3361 };
3362
3363 let mut unknown = raw("test", 0, "passed", &[]);
3364 unknown.runtime[0].decisions.push(DecisionSnapshot {
3365 meta: DecisionMeta {
3366 id: "unknown-decision".into(),
3367 file: "src/empty.js".into(),
3368 ..expected.clone()
3369 },
3370 vectors: vec![vector.clone()],
3371 });
3372 assert!(matches!(
3373 create_coverage_view(&manifest, &[unknown], "time"),
3374 Err(ReportError::InvalidServerRecord(reason))
3375 if reason.contains("absent from the frozen manifest")
3376 ));
3377
3378 let mut changed = raw("test", 0, "passed", &[]);
3379 changed.runtime[0].decisions.push(DecisionSnapshot {
3380 meta: DecisionMeta {
3381 source: "left || right".into(),
3382 ..expected
3383 },
3384 vectors: vec![vector],
3385 });
3386 assert!(matches!(
3387 create_coverage_view(&manifest, &[changed], "time"),
3388 Err(ReportError::InvalidServerRecord(reason))
3389 if reason.contains("differs from the frozen manifest")
3390 ));
3391 }
3392
3393 #[test]
3394 fn neither_timestamp_nor_explicit_phase_links_award_assertion_credit() {
3395 let manifest = CoverageManifest {
3396 unmeasured: Vec::new(),
3397 decisions: vec![],
3398 points: vec![point("hit", 1)],
3399 branches: vec![],
3400 limitations: vec![],
3401 scope: None,
3402 assertion_sites: Vec::new(),
3403 };
3404 let phase = CoveragePhase {
3405 id: "assertion".into(),
3406 kind: "assertion".into(),
3407 operation: "equal".into(),
3408 source: None,
3409 caused_by_phase_id: None,
3410 started_at_ms: 100,
3411 ended_at_ms: Some(120),
3412 status: Some("passed".into()),
3413 error: None,
3414 };
3415 let mut attempt = raw("test", 0, "passed", &["hit"]);
3416 attempt.phases.push(phase.clone());
3417 attempt.runtime[0].events.push(RuntimeEvent {
3418 event_type: "hit".into(),
3419 id: "hit".into(),
3420 vector: None,
3421 timestamp_ms: 110,
3422 phase_id: None,
3423 statement_id: None,
3424 environment: "server".into(),
3425 });
3426 let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
3427 assert_eq!(inferred.points[0].confidence.level, "executed");
3428 assert!(!inferred.points[0].confidence.asserted);
3429
3430 attempt.runtime[0].events[0].phase_id = Some(phase.id);
3431 let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3432 assert_eq!(explicit.points[0].confidence.level, "executed");
3433 assert!(!explicit.points[0].confidence.asserted);
3434 }
3435
3436 #[test]
3437 fn verified_view_uses_only_the_terminal_successful_attempt() {
3438 let manifest = CoverageManifest {
3439 unmeasured: Vec::new(),
3440 decisions: vec![],
3441 points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
3442 branches: vec![],
3443 limitations: vec![],
3444 scope: None,
3445 assertion_sites: Vec::new(),
3446 };
3447 let failed = raw("flaky", 0, "failed", &["failed"]);
3448 let mut passed = raw("flaky", 1, "passed", &["passed"]);
3449 passed.flaky = true;
3450 let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
3451 expected.expected_status = Some("failed".into());
3452 let request = CoverageReportRequest {
3453 run_id: "run".into(),
3454 manifest,
3455 raw_results: vec![failed, passed, expected],
3456 generated_at: "time".into(),
3457 coverage_model: None,
3458 integrity: None,
3459 test_exit_code: ExitCodeInput::Missing,
3460 };
3461 let report = analyze_coverage_results(&request).unwrap();
3462 assert!(!report.filters.passed.points[0].covered);
3463 assert!(report.filters.passed.points[1].covered);
3464 assert!(!report.filters.passed.points[2].covered);
3465 assert!(report.filters.failed.points[0].covered);
3466 assert_eq!(
3467 report
3468 .view
3469 .tests
3470 .iter()
3471 .find(|test| test.name == "expected-failure")
3472 .unwrap()
3473 .outcome,
3474 "failed"
3475 );
3476 assert_eq!(report.view.tests[1].outcome, "flaky");
3477 }
3478
3479 #[test]
3480 fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
3481 let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
3482 expected.expected_status = Some("failed".into());
3483 let companion = raw("expected-failure", 0, "failed", &["expected"]);
3484 let request = CoverageReportRequest {
3485 run_id: "run".into(),
3486 manifest: CoverageManifest {
3487 unmeasured: Vec::new(),
3488 decisions: vec![],
3489 points: vec![point("expected", 1)],
3490 branches: vec![],
3491 limitations: vec![],
3492 scope: None,
3493 assertion_sites: Vec::new(),
3494 },
3495 raw_results: vec![companion, expected],
3496 generated_at: "time".into(),
3497 coverage_model: None,
3498 integrity: None,
3499 test_exit_code: ExitCodeInput::Missing,
3500 };
3501 let report = analyze_coverage_results(&request).unwrap();
3502 assert_eq!(report.view.tests[0].outcome, "passed");
3503 assert!(!report.filters.passed.points[0].covered);
3504 assert!(!report.filters.failed.points[0].covered);
3505 }
3506
3507 #[test]
3508 fn selected_but_unstarted_test_is_not_an_invented_attempt() {
3509 let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
3510 unstarted.scope = None;
3511 unstarted.retry = None;
3512 unstarted.runtime.clear();
3513 let request = CoverageReportRequest {
3514 run_id: "run".into(),
3515 manifest: CoverageManifest {
3516 unmeasured: Vec::new(),
3517 decisions: vec![],
3518 points: vec![],
3519 branches: vec![],
3520 limitations: vec![],
3521 scope: None,
3522 assertion_sites: Vec::new(),
3523 },
3524 raw_results: vec![unstarted],
3525 generated_at: "time".into(),
3526 coverage_model: None,
3527 integrity: None,
3528 test_exit_code: ExitCodeInput::Present(Some(100)),
3529 };
3530 let report = analyze_coverage_results(&request).unwrap();
3531 assert_eq!(report.view.tests[0].outcome, "unstarted");
3532 assert!(report.view.tests[0].attempts.is_empty());
3533 assert!(report.filters.passed.tests.is_empty());
3534 assert!(report.filters.failed.tests.is_empty());
3535 }
3536
3537 fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
3538 let nonce = SystemTime::now()
3539 .duration_since(SystemTime::UNIX_EPOCH)
3540 .unwrap()
3541 .as_nanos();
3542 let root = std::env::temp_dir().join(format!(
3543 "supercov-rust-report-{}-{nonce}-{}",
3544 std::process::id(),
3545 ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
3546 ));
3547 fs::create_dir_all(&root).unwrap();
3548 let path = root.join("evidence.raw.gz");
3549 if !entries
3550 .iter()
3551 .any(|entry| entry.path == "coverage-model.json")
3552 {
3553 entries.push(EvidenceArchiveEntry {
3554 path: "coverage-model.json".into(),
3555 contents: serde_json::to_vec(
3556 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
3557 .unwrap(),
3558 )
3559 .unwrap(),
3560 });
3561 }
3562 if !entries.iter().any(|entry| entry.path == "frontend.json") {
3563 entries.push(EvidenceArchiveEntry {
3564 path: "frontend.json".into(),
3565 contents: serde_json::to_vec(&serde_json::json!({
3566 "protocolVersion": 2,
3567 "frontendId": "javascript",
3568 "frontendVersion": "fixture-v1",
3569 "language": "javascript",
3570 "structuralSource": "owned-probes",
3571 "runners": [{
3572 "runner": "node:test",
3573 "executionModel": "serial-in-process",
3574 "attribution": {
3575 "run": "exact",
3576 "worker": "unavailable",
3577 "test": "exact",
3578 "retry": "exact",
3579 "phase": "exact",
3580 "action": "exact",
3581 "assertion": "exact"
3582 },
3583 "limitations": [{
3584 "id": "fixture-worker-unavailable",
3585 "scopes": ["worker"],
3586 "reason": "The fixture does not require worker identity"
3587 }]
3588 }],
3589 "structuralLimitations": []
3590 }))
3591 .unwrap(),
3592 });
3593 }
3594 write_archive(entries, &path).unwrap();
3595 path
3596 }
3597
3598 #[test]
3599 fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
3600 let manifest = CoverageManifest {
3601 unmeasured: Vec::new(),
3602 decisions: vec![],
3603 points: vec![point("background-hit", 1), point("test-hit", 2)],
3604 branches: vec![],
3605 limitations: vec![],
3606 scope: None,
3607 assertion_sites: Vec::new(),
3608 };
3609 let unattributed = |axis: &str| {
3612 serde_json::json!({
3613 "id": format!("background-no-{axis}"),
3614 "scopes": [axis],
3615 "reason": format!("Runner background did not expose exact {axis} identity for every result")
3616 })
3617 };
3618 let frontend = serde_json::json!({
3619 "protocolVersion": 2,
3620 "frontendId": "javascript",
3621 "frontendVersion": "fixture-v1",
3622 "language": "javascript",
3623 "structuralSource": "owned-probes",
3624 "runners": [
3625 {
3626 "runner": "node:test",
3627 "executionModel": "serial-in-process",
3628 "attribution": {
3629 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
3630 "phase": "exact", "action": "exact", "assertion": "exact"
3631 },
3632 "limitations": [{
3633 "id": "fixture-worker-unavailable",
3634 "scopes": ["worker"],
3635 "reason": "The fixture does not require worker identity"
3636 }]
3637 },
3638 {
3639 "runner": "background",
3640 "executionModel": "parallel-unattributed",
3641 "attribution": {
3642 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
3643 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
3644 },
3645 "limitations": [
3646 unattributed("worker"), unattributed("test"), unattributed("retry"),
3647 unattributed("phase"), unattributed("action"), unattributed("assertion")
3648 ]
3649 }
3650 ],
3651 "structuralLimitations": []
3652 });
3653 let path = archive(vec![
3654 EvidenceArchiveEntry {
3655 path: "manifest.json".into(),
3656 contents: serde_json::to_vec(&manifest).unwrap(),
3657 },
3658 EvidenceArchiveEntry {
3659 path: "frontend.json".into(),
3660 contents: serde_json::to_vec(&frontend).unwrap(),
3661 },
3662 EvidenceArchiveEntry {
3663 path: "playwright-worker-1.mcdc.jsonl".into(),
3664 contents: {
3665 let mut contents =
3666 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
3667 .unwrap();
3668 contents.extend_from_slice(b"\npartial-final-line");
3669 contents
3670 },
3671 },
3672 EvidenceArchiveEntry {
3673 path: "server/background/worker.jsonl".into(),
3674 contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
3675 },
3676 ]);
3677 let result = analyze_coverage_archive(&ArchiveReportRequest {
3678 archive_path: path.clone(),
3679 run_id: "run".into(),
3680 generated_at: "time".into(),
3681 integrity: None,
3682 test_exit_code: ExitCodeInput::Missing,
3683 });
3684 assert!(matches!(
3685 result,
3686 Err(ReportError::InvalidJson { path, .. })
3687 if path == "playwright-worker-1.mcdc.jsonl"
3688 ));
3689 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3690 }
3691
3692 #[test]
3693 fn a_torn_background_record_is_counted_not_fatal() {
3694 let manifest = CoverageManifest {
3699 unmeasured: Vec::new(),
3700 decisions: vec![],
3701 points: vec![],
3702 branches: vec![],
3703 limitations: vec![],
3704 scope: None,
3705 assertion_sites: Vec::new(),
3706 };
3707 let unattributed = |axis: &str| {
3710 serde_json::json!({
3711 "id": format!("background-no-{axis}"),
3712 "scopes": [axis],
3713 "reason": format!("Runner background did not expose exact {axis} identity for every result")
3714 })
3715 };
3716 let frontend = serde_json::json!({
3717 "protocolVersion": 2,
3718 "frontendId": "javascript",
3719 "frontendVersion": "fixture-v1",
3720 "language": "javascript",
3721 "structuralSource": "owned-probes",
3722 "runners": [
3723 {
3724 "runner": "node:test",
3725 "executionModel": "serial-in-process",
3726 "attribution": {
3727 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
3728 "phase": "exact", "action": "exact", "assertion": "exact"
3729 },
3730 "limitations": [{
3731 "id": "fixture-worker-unavailable",
3732 "scopes": ["worker"],
3733 "reason": "The fixture does not require worker identity"
3734 }]
3735 },
3736 {
3737 "runner": "background",
3738 "executionModel": "parallel-unattributed",
3739 "attribution": {
3740 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
3741 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
3742 },
3743 "limitations": [
3744 unattributed("worker"), unattributed("test"), unattributed("retry"),
3745 unattributed("phase"), unattributed("action"), unattributed("assertion")
3746 ]
3747 }
3748 ],
3749 "structuralLimitations": []
3750 });
3751 let path = archive(vec![
3752 EvidenceArchiveEntry {
3753 path: "manifest.json".into(),
3754 contents: serde_json::to_vec(&manifest).unwrap(),
3755 },
3756 EvidenceArchiveEntry {
3757 path: "frontend.json".into(),
3758 contents: serde_json::to_vec(&frontend).unwrap(),
3759 },
3760 EvidenceArchiveEntry {
3761 path: "playwright-worker-1.mcdc.jsonl".into(),
3762 contents: {
3763 let mut contents =
3764 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
3765 .unwrap();
3766 contents.push(b'\n');
3767 contents
3768 },
3769 },
3770 EvidenceArchiveEntry {
3771 path: "execution.37360-1.168.jsonl".into(),
3772 contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
3773 },
3774 EvidenceArchiveEntry {
3775 path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
3776 contents: concat!(
3777 "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
3778 "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
3779 "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
3780 )
3781 .as_bytes()
3782 .to_vec(),
3783 },
3784 ]);
3785 let report = analyze_coverage_archive(&ArchiveReportRequest {
3786 archive_path: path.clone(),
3787 run_id: "run".into(),
3788 generated_at: "time".into(),
3789 integrity: None,
3790 test_exit_code: ExitCodeInput::Missing,
3791 })
3792 .expect("a torn evidence line must not fail the whole run");
3793 let transport = report.view.transport.expect("transport stats");
3794 assert_eq!(transport.corrupt_records, 2);
3795 assert_eq!(transport.corrupt_files, 2);
3796 assert_eq!(transport.background_server_records, 2);
3797 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3798 }
3799
3800 #[test]
3801 fn archive_analysis_rejects_cross_run_evidence() {
3802 let manifest = CoverageManifest {
3803 unmeasured: Vec::new(),
3804 decisions: vec![],
3805 points: vec![],
3806 branches: vec![],
3807 limitations: vec![],
3808 scope: None,
3809 assertion_sites: Vec::new(),
3810 };
3811 let mut result = raw("test", 0, "passed", &[]);
3812 result.scope = Some(ExecutionScope {
3813 version: 1,
3814 run_id: "other-run".into(),
3815 worker_id: "worker".into(),
3816 test_id: "test".into(),
3817 test_key: "key".into(),
3818 retry: 0,
3819 attempt_id: "attempt".into(),
3820 });
3821 let path = archive(vec![
3822 EvidenceArchiveEntry {
3823 path: "manifest.json".into(),
3824 contents: serde_json::to_vec(&manifest).unwrap(),
3825 },
3826 EvidenceArchiveEntry {
3827 path: "worker/mcdc.json".into(),
3828 contents: serde_json::to_vec(&result).unwrap(),
3829 },
3830 ]);
3831 assert!(matches!(
3832 analyze_coverage_archive(&ArchiveReportRequest {
3833 archive_path: path.clone(),
3834 run_id: "run".into(),
3835 generated_at: "time".into(),
3836 integrity: None,
3837 test_exit_code: ExitCodeInput::Missing,
3838 }),
3839 Err(ReportError::InvalidArchive(reason))
3840 if reason.contains("expected=run actual=other-run")
3841 ));
3842 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3843 }
3844
3845 #[test]
3846 fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
3847 let vectors: Value = serde_json::from_str(include_str!(
3848 "../test-assets/coverage-model-v1/vectors.json"
3849 ))
3850 .unwrap();
3851 for value in vectors["valid"].as_array().unwrap() {
3852 let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
3853 model.into_declaration().unwrap();
3854 }
3855 for vector in vectors["invalid"].as_array().unwrap() {
3856 let value = vector["value"].clone();
3857 if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
3858 assert!(
3859 model.into_declaration().is_err(),
3860 "accepted invalid model vector: {}",
3861 vector["reason"]
3862 );
3863 }
3864 }
3865
3866 let manifest = CoverageManifest {
3867 unmeasured: Vec::new(),
3868 decisions: vec![],
3869 points: vec![],
3870 branches: vec![],
3871 limitations: vec![],
3872 scope: None,
3873 assertion_sites: Vec::new(),
3874 };
3875 let path = archive(vec![
3876 EvidenceArchiveEntry {
3877 path: "coverage-model.json".into(),
3878 contents: serde_json::to_vec(&PersistedCoverageModel {
3879 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
3880 language: "rust".into(),
3881 variant: "rust-source-v1".into(),
3882 name: "Rust source coverage".into(),
3883 completeness_meaning: "Every Rust obligation was satisfied.".into(),
3884 measured: vec!["Rust statements".into()],
3885 not_measured: vec![],
3886 })
3887 .unwrap(),
3888 },
3889 EvidenceArchiveEntry {
3890 path: "manifest.json".into(),
3891 contents: serde_json::to_vec(&manifest).unwrap(),
3892 },
3893 ]);
3894 assert!(matches!(
3895 analyze_coverage_archive(&ArchiveReportRequest {
3896 archive_path: path.clone(),
3897 run_id: "run".into(),
3898 generated_at: "time".into(),
3899 integrity: None,
3900 test_exit_code: ExitCodeInput::Missing,
3901 }),
3902 Err(ReportError::InvalidArchive(reason))
3903 if reason.contains("javascript differs from coverage model language rust")
3904 ));
3905 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3906 }
3907
3908 #[test]
3909 fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
3910 let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
3911 "runId": "run",
3912 "manifest": { "decisions": [], "points": [], "branches": [] },
3913 "rawResults": [{
3914 "test": "test",
3915 "status": "passed",
3916 "browser": [],
3917 "server": []
3918 }],
3919 "generatedAt": "time",
3920 "testExitCode": null
3921 }))
3922 .unwrap();
3923 let report = analyze_coverage_results(&request).unwrap();
3924 assert_eq!(
3925 report.execution,
3926 Some(ExecutionResult {
3927 test_exit_code: None,
3928 valid: false,
3929 })
3930 );
3931 }
3932 #[test]
3933 fn a_phase_leaked_from_another_test_is_untagged_and_an_unknown_one_is_kept() {
3934 let record = |test: &str, phases: &[&str], events: &[(&str, &str)]| -> RawTestResult {
3935 serde_json::from_value(serde_json::json!({
3936 "test": test,
3937 "phases": phases.iter().map(|id| serde_json::json!({
3938 "id": id, "kind": "assertion", "operation": "assert.ok", "startedAtMs": 0
3939 })).collect::<Vec<_>>(),
3940 "runtime": [{
3941 "events": events.iter().map(|(id, phase)| serde_json::json!({
3942 "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
3943 })).collect::<Vec<_>>()
3944 }]
3945 }))
3946 .unwrap()
3947 };
3948 let mut results = vec![
3949 record(
3950 "upload",
3951 &["a-0:assertion:1"],
3952 &[("own", "a-0:assertion:1")],
3953 ),
3954 record(
3956 "form data",
3957 &["b-0:assertion:1"],
3958 &[
3959 ("late", "a-0:assertion:1"),
3960 ("mine", "b-0:assertion:1"),
3961 ("corrupt", "z-0:assertion:9"),
3962 ],
3963 ),
3964 ];
3965 release_leaked_phases(&mut results);
3966 let phases = |raw: &RawTestResult| {
3967 raw.runtime[0]
3968 .events
3969 .iter()
3970 .map(|event| (event.id.clone(), event.phase_id.clone()))
3971 .collect::<Vec<_>>()
3972 };
3973 assert_eq!(
3974 phases(&results[0]),
3975 [("own".into(), Some("a-0:assertion:1".into()))]
3976 );
3977 assert_eq!(
3978 phases(&results[1]),
3979 [
3980 ("late".into(), None),
3981 ("mine".into(), Some("b-0:assertion:1".into())),
3982 ("corrupt".into(), Some("z-0:assertion:9".into())),
3984 ]
3985 );
3986 }
3987
3988 #[test]
3989 fn a_test_run_twice_under_one_identity_keeps_both_executions_apart() {
3990 let record = |status: &str, events: &[(&str, &str)]| -> RawTestResult {
3993 serde_json::from_value(serde_json::json!({
3994 "test": "parse",
3995 "testId": "parse",
3996 "retry": 0,
3997 "status": status,
3998 "phases": [
3999 { "id": "t-0:assertion:1", "kind": "assertion", "operation": "expect", "startedAtMs": 0 },
4000 { "id": "t-0:assertion:2", "kind": "assertion", "operation": "expect", "startedAtMs": 0, "causedByPhaseId": "t-0:assertion:1" }
4001 ],
4002 "runtime": [{
4003 "events": events.iter().map(|(id, phase)| serde_json::json!({
4004 "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
4005 })).collect::<Vec<_>>()
4006 }]
4007 }))
4008 .unwrap()
4009 };
4010 let mut results = vec![
4011 record("failed", &[("node", "t-0:assertion:1")]),
4012 record("passed", &[("edge", "t-0:assertion:2")]),
4013 record("passed", &[("third", "t-0:assertion:1")]),
4014 ];
4015 separate_repeated_executions(&mut results);
4016 let ids = |raw: &RawTestResult| {
4017 raw.phases
4018 .iter()
4019 .map(|phase| (phase.id.clone(), phase.caused_by_phase_id.clone()))
4020 .collect::<Vec<_>>()
4021 };
4022 let events = |raw: &RawTestResult| {
4023 raw.runtime[0]
4024 .events
4025 .iter()
4026 .map(|event| event.phase_id.clone().unwrap())
4027 .collect::<Vec<_>>()
4028 };
4029 assert_eq!(
4030 ids(&results[0]),
4031 [
4032 ("t-0:assertion:1".into(), None),
4033 ("t-0:assertion:2".into(), Some("t-0:assertion:1".into()))
4034 ]
4035 );
4036 assert_eq!(
4037 ids(&results[1]),
4038 [
4039 ("t-0:assertion:1~2".into(), None),
4040 ("t-0:assertion:2~2".into(), Some("t-0:assertion:1~2".into()))
4041 ]
4042 );
4043 assert_eq!(events(&results[1]), ["t-0:assertion:2~2"]);
4044 assert_eq!(events(&results[2]), ["t-0:assertion:1~3"]);
4045
4046 let mut test = MutableTest {
4047 id: "parse".into(),
4048 name: "parse".into(),
4049 file: None,
4050 title: None,
4051 retries: BTreeSet::new(),
4052 attempts: BTreeMap::new(),
4053 unstarted: false,
4054 runner_reported_flaky: false,
4055 provenance: TestProvenance::default(),
4056 role: "test".into(),
4057 attribution: "exact".into(),
4058 hits: Vec::new(),
4059 decisions: BTreeMap::new(),
4060 };
4061 for raw in &results {
4062 record_attempt(&mut test, raw);
4063 }
4064 assert_eq!(
4065 test_outcome(&test),
4066 "failed",
4067 "a failed execution fails the attempt"
4068 );
4069 }
4070
4071 #[test]
4072 fn a_dimension_is_summarised_over_the_tests_it_can_describe() {
4073 let exact = |id: &str, kind: &str| TestCoverageResult {
4079 id: id.into(),
4080 name: id.into(),
4081 file: None,
4082 title: None,
4083 retries: vec![],
4084 attempts: vec![],
4085 outcome: "passed".into(),
4086 provenance: TestProvenance {
4087 runner: "go-test".into(),
4088 kind: kind.into(),
4089 project: None,
4090 source: String::new(),
4091 },
4092 role: "test".into(),
4093 attribution: ATTRIBUTION_EXACT.into(),
4094 hits: vec![],
4095 decisions: vec![],
4096 lines: vec![],
4097 };
4098 let mut run_wide = exact("parallel", "unit");
4099 run_wide.attribution = ATTRIBUTION_RUN_WIDE.into();
4100
4101 assert!(coverage_is_its_own(&exact("serial", "unit").attribution));
4110 assert!(coverage_is_its_own(ATTRIBUTION_PARTIAL));
4111 assert!(!coverage_is_its_own(&run_wide.attribution));
4112
4113 assert!(coverage_is_complete(ATTRIBUTION_EXACT));
4114 assert!(!coverage_is_complete(ATTRIBUTION_PARTIAL));
4115 assert!(!coverage_is_complete(ATTRIBUTION_RUN_WIDE));
4116
4117 for (left, right, merged) in [
4120 (ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL, ATTRIBUTION_PARTIAL),
4121 (ATTRIBUTION_PARTIAL, ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL),
4122 (
4123 ATTRIBUTION_PARTIAL,
4124 ATTRIBUTION_RUN_WIDE,
4125 ATTRIBUTION_RUN_WIDE,
4126 ),
4127 (ATTRIBUTION_EXACT, ATTRIBUTION_EXACT, ATTRIBUTION_EXACT),
4128 ] {
4129 assert_eq!(least_exact(left, right), merged);
4130 }
4131
4132 assert!(coverage_is_its_own(""));
4135 assert!(coverage_is_complete(""));
4136 }
4137}