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, 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 for record in &scoped_records {
2733 let Some(scope) = &record.scope else { continue };
2734 let Some(raw) = raw_results
2735 .iter_mut()
2736 .find(|raw| raw.scope.as_ref() == Some(scope))
2737 else {
2738 continue;
2739 };
2740 if !raw.server.contains(record) {
2741 raw.server.push(record.clone());
2742 }
2743 }
2744
2745 let background = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2746 entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2747 }));
2748 let background_records = background.records;
2749 if !background_records.is_empty() {
2750 raw_results.push(RawTestResult {
2751 test_id: Some(format!("background:{}", request.run_id)),
2752 scope: None,
2753 test: "Background / unattributed".into(),
2754 test_file: None,
2755 title: Some("Background / unattributed".into()),
2756 retry: None,
2757 status: Some("unknown".into()),
2758 expected_status: None,
2759 flaky: false,
2760 provenance: TestProvenance {
2761 runner: "background".into(),
2762 kind: "background".into(),
2763 project: None,
2764 source: "explicit".into(),
2765 },
2766 role: "background".into(),
2767 attribution: ATTRIBUTION_EXACT.into(),
2768 phases: vec![],
2769 runtime: vec![],
2770 browser: vec![],
2771 server: background_records.clone(),
2772 });
2773 }
2774
2775 let execution =
2778 parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2779 entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2780 }));
2781 let execution_events = execution.records;
2782 let count_event = |name: &str| {
2783 execution_events
2784 .iter()
2785 .filter(|event| event.kind() == name)
2786 .count()
2787 };
2788 let transport = TransportStats {
2789 processes: count_event("process"),
2790 child_launches: count_event("child-launch"),
2791 remote_launches: count_event("remote-launch"),
2792 workspace_capabilities: count_event("workspace-capability"),
2793 guest_processes: count_event("guest-process"),
2794 scoped_server_records: scoped_records.len(),
2795 background_server_records: background_records.len(),
2796 corrupt_records: scoped.corrupt_records
2797 + background.corrupt_records
2798 + execution.corrupt_records,
2799 corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
2800 };
2801 let frontend = entries
2802 .iter()
2803 .find(|entry| entry.path == "frontend.json")
2804 .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
2805 .and_then(parse_entry::<FrontendRunDeclaration>)?;
2806 let persisted = entries
2807 .iter()
2808 .find(|entry| entry.path == "coverage-model.json")
2809 .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
2810 .and_then(parse_entry::<PersistedCoverageModel>)?;
2811 let coverage_model =
2812 persisted
2813 .into_declaration()
2814 .map_err(|reason| ReportError::InvalidJson {
2815 path: "coverage-model.json".into(),
2816 reason: reason.into(),
2817 })?;
2818 if frontend.language != coverage_model.language {
2819 return Err(ReportError::InvalidArchive(format!(
2820 "frontend language {} differs from coverage model language {}",
2821 frontend.language, coverage_model.language
2822 )));
2823 }
2824 separate_repeated_executions(&mut raw_results);
2825 release_leaked_phases(&mut raw_results);
2826 let normalized = CoverageReportRequest {
2827 run_id: request.run_id.clone(),
2828 manifest,
2829 raw_results,
2830 generated_at: request.generated_at.clone(),
2831 coverage_model: Some(coverage_model),
2832 integrity: request.integrity.clone(),
2833 test_exit_code: request.test_exit_code.clone(),
2834 };
2835 drop(entries);
2839 analyze_frontend_request(&frontend, &normalized, transport)
2840}
2841
2842fn separate_repeated_executions(raw_results: &mut [RawTestResult]) {
2852 let mut declared = BTreeSet::<String>::new();
2853 let mut executions = BTreeMap::<String, usize>::new();
2854 for raw in raw_results.iter_mut() {
2855 let repeated = raw.phases.iter().any(|phase| declared.contains(&phase.id));
2856 if !repeated {
2857 declared.extend(raw.phases.iter().map(|phase| phase.id.clone()));
2858 continue;
2859 }
2860 let mut renamed = BTreeMap::<String, String>::new();
2861 for phase in &raw.phases {
2862 let count = executions.entry(phase.id.clone()).or_insert(1);
2863 let fresh = loop {
2864 *count += 1;
2865 let candidate = format!("{}~{}", phase.id, count);
2866 if !declared.contains(&candidate) {
2867 break candidate;
2868 }
2869 };
2870 renamed.insert(phase.id.clone(), fresh);
2871 }
2872 let rename = |phase: &mut Option<String>| {
2873 if let Some(fresh) = phase.as_ref().and_then(|id| renamed.get(id)) {
2874 *phase = Some(fresh.clone());
2875 }
2876 };
2877 for phase in &mut raw.phases {
2878 phase.id = renamed[&phase.id].clone();
2879 rename(&mut phase.caused_by_phase_id);
2880 }
2881 for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
2882 rename(&mut snapshot.phase_id);
2883 for event in &mut snapshot.events {
2884 rename(&mut event.phase_id);
2885 }
2886 }
2887 for record in &mut raw.server {
2888 rename(&mut record.phase_id);
2889 }
2890 declared.extend(renamed.into_values());
2891 }
2892}
2893
2894fn release_leaked_phases(raw_results: &mut [RawTestResult]) {
2904 let declared = raw_results
2905 .iter()
2906 .flat_map(|raw| raw.phases.iter().map(|phase| phase.id.clone()))
2907 .collect::<BTreeSet<_>>();
2908 for raw in raw_results.iter_mut() {
2909 let own = raw
2910 .phases
2911 .iter()
2912 .map(|phase| phase.id.clone())
2913 .collect::<BTreeSet<_>>();
2914 let leaked = |phase: &mut Option<String>| {
2915 if phase
2916 .as_ref()
2917 .is_some_and(|id| !own.contains(id) && declared.contains(id))
2918 {
2919 *phase = None;
2920 }
2921 };
2922 for snapshot in raw.runtime.iter_mut().chain(raw.browser.iter_mut()) {
2923 leaked(&mut snapshot.phase_id);
2924 for event in &mut snapshot.events {
2925 leaked(&mut event.phase_id);
2926 }
2927 }
2928 for record in &mut raw.server {
2929 leaked(&mut record.phase_id);
2930 }
2931 }
2932}
2933
2934pub fn analyze_frontend_request(
2939 frontend: &FrontendRunDeclaration,
2940 normalized: &CoverageReportRequest,
2941 transport: TransportStats,
2942) -> Result<CoverageReport, ReportError> {
2943 let mut report = crate::frontend_protocol::analyze_frontend_results(frontend, normalized)
2944 .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2945 report.view.transport = Some(transport.clone());
2946 report.filters.passed.transport = Some(transport.clone());
2947 report.filters.failed.transport = Some(transport);
2948 Ok(report)
2949}
2950
2951impl TransportStats {
2952 pub fn none() -> Self {
2955 Self {
2956 processes: 0,
2957 child_launches: 0,
2958 remote_launches: 0,
2959 workspace_capabilities: 0,
2960 guest_processes: 0,
2961 scoped_server_records: 0,
2962 background_server_records: 0,
2963 corrupt_records: 0,
2964 corrupt_files: 0,
2965 }
2966 }
2967}
2968
2969#[cfg(test)]
2970mod tests {
2971 use std::{
2972 fs,
2973 sync::atomic::{AtomicU64, Ordering},
2974 time::SystemTime,
2975 };
2976
2977 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
2978
2979 use super::*;
2980
2981 #[test]
2982 fn a_view_holds_one_copy_of_each_id_it_names() {
2983 let root = std::env::temp_dir().join(format!(
2989 "supercov-shared-ids-{}-{}",
2990 std::process::id(),
2991 std::time::SystemTime::now()
2992 .duration_since(std::time::UNIX_EPOCH)
2993 .unwrap()
2994 .as_nanos()
2995 ));
2996 let directory = crate::run_store::create_analyzable_test_run(&root, "shared");
2997 let run = crate::run_store::discover_runs(&root)
2998 .unwrap()
2999 .runs
3000 .remove(0);
3001 let report = crate::run_store::analyze_stored_run(&run).unwrap();
3002 let _ = directory;
3003 for view in [&report.view, &report.filters.passed] {
3004 let mentions = view
3005 .points
3006 .iter()
3007 .flat_map(|point| point.tests.iter().chain(&point.confidence.tests))
3008 .chain(view.lines.iter().flat_map(|line| &line.tests))
3009 .collect::<Vec<_>>();
3010 assert!(
3011 mentions.len() >= 2,
3012 "the fixture names its test in several places"
3013 );
3014 let first = mentions[0];
3015 for mention in &mentions {
3016 assert_eq!(*mention, first);
3017 assert!(
3018 mention.same_allocation(first),
3019 "every mention shares one copy"
3020 );
3021 }
3022 }
3023 fs::remove_dir_all(root).unwrap();
3024 }
3025
3026 #[test]
3027 fn references_numbered_as_they_arrive_come_out_as_the_sets_inserted_directly() {
3028 let arrivals = [
3031 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3032 ("py:statement:a", "h11/tests/test_x.py::test_b[2-50]"),
3033 ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3034 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3035 ("py:statement:a", "h11/tests/test_x.py::test_b[10-50]"),
3036 ("py:statement:b", "h11/tests/test_x.py::test_a[1-50]"),
3037 ("py:statement:c", "python-phase:9"),
3038 ("py:statement:c", "python-phase:10"),
3039 ("py:statement:b", "h11/tests/test_x.py::test_b[2-50]"),
3040 ];
3041 let mut ids = Interner::default();
3042 let mut hits = HitIds::default();
3043 let mut numbered = References::default();
3044 hits.number("unused", &mut ids);
3046 let mut direct = HashMap::<String, BTreeSet<String>>::new();
3047 for (obligation, name) in arrivals {
3048 let hit = hits.number(obligation, &mut ids);
3049 numbered.add(hit, name, &mut ids);
3050 direct
3051 .entry(obligation.to_owned())
3052 .or_default()
3053 .insert(name.to_owned());
3054 }
3055 let numbered = numbered
3056 .into_relation(&hits)
3057 .by_hit
3058 .into_iter()
3059 .map(|(obligation, reached)| {
3060 let names = reached
3061 .names
3062 .into_iter()
3063 .map(String::from)
3064 .collect::<Vec<_>>();
3065 let mut sorted_names = names.clone();
3066 sorted_names.sort();
3067 sorted_names.dedup();
3068 assert_eq!(
3069 names, sorted_names,
3070 "a relation lists its ids once, in order"
3071 );
3072 (obligation, names.into_iter().collect())
3073 })
3074 .collect::<HashMap<_, BTreeSet<String>>>();
3075 assert_eq!(numbered, direct);
3076 assert_eq!(
3077 hits.sorted((0..hits.names.len()).collect()),
3078 [
3079 "py:statement:a",
3080 "py:statement:b",
3081 "py:statement:c",
3082 "unused"
3083 ]
3084 .map(Id::from)
3085 );
3086 }
3087
3088 static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
3089
3090 fn point(id: &str, line: usize) -> PointMeta {
3091 PointMeta {
3092 id: id.into(),
3093 kind: PointKind::Statement,
3094 file: "src/app.js".into(),
3095 line,
3096 column: 0,
3097 source: "work();".into(),
3098 label: None,
3099 }
3100 }
3101
3102 fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
3103 RawTestResult {
3104 test_id: Some(id.into()),
3105 scope: None,
3106 test: id.into(),
3107 test_file: Some("tests/app.test.js".into()),
3108 title: None,
3109 retry: Some(retry),
3110 status: Some(status.into()),
3111 expected_status: None,
3112 flaky: false,
3113 provenance: TestProvenance {
3114 runner: "node:test".into(),
3115 kind: "unit".into(),
3116 project: None,
3117 source: "runner-default".into(),
3118 },
3119 role: "test".into(),
3120 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
3121 phases: vec![],
3122 runtime: vec![RuntimeSnapshot {
3123 decisions: vec![],
3124 hits: hits.iter().map(|hit| (*hit).into()).collect(),
3125 events: vec![],
3126 logicals: vec![],
3127 phase_id: None,
3128 }],
3129 browser: vec![],
3130 server: vec![],
3131 }
3132 }
3133
3134 #[test]
3135 fn declined_obligations_are_unmeasured_never_uncovered() {
3136 let point = |id: &str, line: usize| PointMeta {
3140 id: id.into(),
3141 kind: PointKind::Statement,
3142 file: "src/app.js".into(),
3143 line,
3144 column: 0,
3145 source: "work();".into(),
3146 label: None,
3147 };
3148 let measured_only = CoverageManifest {
3149 decisions: vec![],
3150 points: vec![point("measured", 1)],
3151 branches: vec![],
3152 limitations: vec![],
3153 unmeasured: Vec::new(),
3154 scope: None,
3155 assertion_sites: Vec::new(),
3156 };
3157 let with_declined = CoverageManifest {
3158 decisions: vec![],
3159 points: vec![point("measured", 1), point("declined", 2)],
3160 branches: vec![],
3161 limitations: vec![],
3162 unmeasured: vec!["declined".into()],
3163 scope: None,
3164 assertion_sites: Vec::new(),
3165 };
3166
3167 let baseline =
3168 create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
3169 let view =
3170 create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
3171
3172 assert_eq!(
3174 view.summary.statements.total, baseline.summary.statements.total,
3175 "a declined obligation stayed in the covered/uncovered denominator"
3176 );
3177 assert_eq!(view.summary.unmeasured_obligations, Some(1));
3178 assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
3179
3180 assert_eq!(
3184 view.summary.lines.total, baseline.summary.lines.total,
3185 "a declined obligation kept its line in the line denominator"
3186 );
3187 let declined_line = view
3188 .lines
3189 .iter()
3190 .find(|line| line.line == 2)
3191 .expect("the declined line stays addressable");
3192 assert!(!declined_line.measured);
3193 assert!(!declined_line.covered);
3194
3195 assert_eq!(baseline.summary.unmeasured_obligations, None);
3198 assert_eq!(baseline.summary.exact_fraction_pct, None);
3199 let encoded = serde_json::to_string(&baseline.summary).unwrap();
3200 assert!(
3201 !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
3202 "a fully measured summary must serialize unchanged: {encoded}"
3203 );
3204 }
3205
3206 #[test]
3207 fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
3208 let point = |id: &str| PointMeta {
3212 id: id.into(),
3213 kind: PointKind::Statement,
3214 file: "src/app.js".into(),
3215 line: 1,
3216 column: 0,
3217 source: "work();".into(),
3218 label: None,
3219 };
3220 let manifest = CoverageManifest {
3221 decisions: vec![],
3222 points: vec![point("declined")],
3223 branches: vec![],
3224 limitations: vec![],
3225 unmeasured: vec!["declined".into()],
3226 scope: None,
3227 assertion_sites: Vec::new(),
3228 };
3229 let view = create_coverage_view(
3230 &manifest,
3231 &[raw("test", 0, "passed", &["declined"])],
3232 "time",
3233 )
3234 .unwrap();
3235
3236 assert_eq!(view.summary.lines.total, 0);
3237 assert_eq!(view.summary.lines.covered, 0);
3238 let line = view.lines.first().expect("the line stays addressable");
3239 assert!(!line.measured);
3240 assert!(!line.covered, "an unmeasured line is not covered either");
3241 }
3242
3243 #[test]
3244 fn report_retains_unexecuted_manifest_conditions() {
3245 let manifest = CoverageManifest {
3246 unmeasured: Vec::new(),
3247 decisions: vec![DecisionMeta {
3248 id: "decision".into(),
3249 file: "src/app.js".into(),
3250 line: 1,
3251 column: 0,
3252 source: "left && right".into(),
3253 conditions: vec!["left".into(), "right".into()],
3254 kind: "if".into(),
3255 }],
3256 points: vec![],
3257 branches: vec![],
3258 limitations: vec![],
3259 scope: None,
3260 assertion_sites: Vec::new(),
3261 };
3262 let view =
3263 create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
3264 assert_eq!(view.summary.conditions, 2);
3265 assert_eq!(view.summary.covered_conditions, 0);
3266 assert_eq!(view.decisions[0].conditions.len(), 2);
3267 }
3268
3269 #[test]
3270 fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
3271 let manifest = CoverageManifest {
3272 unmeasured: Vec::new(),
3273 decisions: vec![DecisionMeta {
3274 id: "application-decision".into(),
3275 file: "src/app.js".into(),
3276 line: 1,
3277 column: 0,
3278 source: "left && right".into(),
3279 conditions: vec!["left".into(), "right".into()],
3280 kind: "if".into(),
3281 }],
3282 points: vec![],
3283 branches: vec![],
3284 limitations: vec![],
3285 scope: None,
3286 assertion_sites: Vec::new(),
3287 };
3288 let mut attempt = raw("test", 0, "passed", &[]);
3289 attempt.runtime[0].decisions.push(DecisionSnapshot {
3290 meta: DecisionMeta {
3291 id: "synthetic-fixture".into(),
3292 file: "fixtures/generated.js".into(),
3293 line: 1,
3294 column: 0,
3295 source: "a && b && c".into(),
3296 conditions: vec!["a".into(), "b".into(), "c".into()],
3297 kind: "if".into(),
3298 },
3299 vectors: vec![McdcVector {
3300 values: vec![Some(true), Some(true), Some(true)],
3301 outcome: true,
3302 }],
3303 });
3304
3305 let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3306 assert_eq!(view.decisions.len(), 1);
3307 assert_eq!(view.decisions[0].meta.id, "application-decision");
3308 assert!(view.decisions[0].vectors.is_empty());
3309 }
3310
3311 #[test]
3312 fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
3313 let expected = DecisionMeta {
3314 id: "application-decision".into(),
3315 file: "src/app.js".into(),
3316 line: 1,
3317 column: 0,
3318 source: "left && right".into(),
3319 conditions: vec!["left".into(), "right".into()],
3320 kind: "if".into(),
3321 };
3322 let manifest = CoverageManifest {
3323 unmeasured: Vec::new(),
3324 decisions: vec![expected.clone()],
3325 points: vec![],
3326 branches: vec![],
3327 limitations: vec![],
3328 scope: Some(serde_json::json!({
3329 "entries": [{ "file": "src/empty.js", "status": "included" }]
3330 })),
3331 assertion_sites: Vec::new(),
3332 };
3333 let vector = McdcVector {
3334 values: vec![Some(true), Some(true)],
3335 outcome: true,
3336 };
3337
3338 let mut unknown = raw("test", 0, "passed", &[]);
3339 unknown.runtime[0].decisions.push(DecisionSnapshot {
3340 meta: DecisionMeta {
3341 id: "unknown-decision".into(),
3342 file: "src/empty.js".into(),
3343 ..expected.clone()
3344 },
3345 vectors: vec![vector.clone()],
3346 });
3347 assert!(matches!(
3348 create_coverage_view(&manifest, &[unknown], "time"),
3349 Err(ReportError::InvalidServerRecord(reason))
3350 if reason.contains("absent from the frozen manifest")
3351 ));
3352
3353 let mut changed = raw("test", 0, "passed", &[]);
3354 changed.runtime[0].decisions.push(DecisionSnapshot {
3355 meta: DecisionMeta {
3356 source: "left || right".into(),
3357 ..expected
3358 },
3359 vectors: vec![vector],
3360 });
3361 assert!(matches!(
3362 create_coverage_view(&manifest, &[changed], "time"),
3363 Err(ReportError::InvalidServerRecord(reason))
3364 if reason.contains("differs from the frozen manifest")
3365 ));
3366 }
3367
3368 #[test]
3369 fn neither_timestamp_nor_explicit_phase_links_award_assertion_credit() {
3370 let manifest = CoverageManifest {
3371 unmeasured: Vec::new(),
3372 decisions: vec![],
3373 points: vec![point("hit", 1)],
3374 branches: vec![],
3375 limitations: vec![],
3376 scope: None,
3377 assertion_sites: Vec::new(),
3378 };
3379 let phase = CoveragePhase {
3380 id: "assertion".into(),
3381 kind: "assertion".into(),
3382 operation: "equal".into(),
3383 source: None,
3384 caused_by_phase_id: None,
3385 started_at_ms: 100,
3386 ended_at_ms: Some(120),
3387 status: Some("passed".into()),
3388 error: None,
3389 };
3390 let mut attempt = raw("test", 0, "passed", &["hit"]);
3391 attempt.phases.push(phase.clone());
3392 attempt.runtime[0].events.push(RuntimeEvent {
3393 event_type: "hit".into(),
3394 id: "hit".into(),
3395 vector: None,
3396 timestamp_ms: 110,
3397 phase_id: None,
3398 statement_id: None,
3399 environment: "server".into(),
3400 });
3401 let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
3402 assert_eq!(inferred.points[0].confidence.level, "executed");
3403 assert!(!inferred.points[0].confidence.asserted);
3404
3405 attempt.runtime[0].events[0].phase_id = Some(phase.id);
3406 let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
3407 assert_eq!(explicit.points[0].confidence.level, "executed");
3408 assert!(!explicit.points[0].confidence.asserted);
3409 }
3410
3411 #[test]
3412 fn verified_view_uses_only_the_terminal_successful_attempt() {
3413 let manifest = CoverageManifest {
3414 unmeasured: Vec::new(),
3415 decisions: vec![],
3416 points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
3417 branches: vec![],
3418 limitations: vec![],
3419 scope: None,
3420 assertion_sites: Vec::new(),
3421 };
3422 let failed = raw("flaky", 0, "failed", &["failed"]);
3423 let mut passed = raw("flaky", 1, "passed", &["passed"]);
3424 passed.flaky = true;
3425 let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
3426 expected.expected_status = Some("failed".into());
3427 let request = CoverageReportRequest {
3428 run_id: "run".into(),
3429 manifest,
3430 raw_results: vec![failed, passed, expected],
3431 generated_at: "time".into(),
3432 coverage_model: None,
3433 integrity: None,
3434 test_exit_code: ExitCodeInput::Missing,
3435 };
3436 let report = analyze_coverage_results(&request).unwrap();
3437 assert!(!report.filters.passed.points[0].covered);
3438 assert!(report.filters.passed.points[1].covered);
3439 assert!(!report.filters.passed.points[2].covered);
3440 assert!(report.filters.failed.points[0].covered);
3441 assert_eq!(
3442 report
3443 .view
3444 .tests
3445 .iter()
3446 .find(|test| test.name == "expected-failure")
3447 .unwrap()
3448 .outcome,
3449 "failed"
3450 );
3451 assert_eq!(report.view.tests[1].outcome, "flaky");
3452 }
3453
3454 #[test]
3455 fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
3456 let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
3457 expected.expected_status = Some("failed".into());
3458 let companion = raw("expected-failure", 0, "failed", &["expected"]);
3459 let request = CoverageReportRequest {
3460 run_id: "run".into(),
3461 manifest: CoverageManifest {
3462 unmeasured: Vec::new(),
3463 decisions: vec![],
3464 points: vec![point("expected", 1)],
3465 branches: vec![],
3466 limitations: vec![],
3467 scope: None,
3468 assertion_sites: Vec::new(),
3469 },
3470 raw_results: vec![companion, expected],
3471 generated_at: "time".into(),
3472 coverage_model: None,
3473 integrity: None,
3474 test_exit_code: ExitCodeInput::Missing,
3475 };
3476 let report = analyze_coverage_results(&request).unwrap();
3477 assert_eq!(report.view.tests[0].outcome, "passed");
3478 assert!(!report.filters.passed.points[0].covered);
3479 assert!(!report.filters.failed.points[0].covered);
3480 }
3481
3482 #[test]
3483 fn selected_but_unstarted_test_is_not_an_invented_attempt() {
3484 let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
3485 unstarted.scope = None;
3486 unstarted.retry = None;
3487 unstarted.runtime.clear();
3488 let request = CoverageReportRequest {
3489 run_id: "run".into(),
3490 manifest: CoverageManifest {
3491 unmeasured: Vec::new(),
3492 decisions: vec![],
3493 points: vec![],
3494 branches: vec![],
3495 limitations: vec![],
3496 scope: None,
3497 assertion_sites: Vec::new(),
3498 },
3499 raw_results: vec![unstarted],
3500 generated_at: "time".into(),
3501 coverage_model: None,
3502 integrity: None,
3503 test_exit_code: ExitCodeInput::Present(Some(100)),
3504 };
3505 let report = analyze_coverage_results(&request).unwrap();
3506 assert_eq!(report.view.tests[0].outcome, "unstarted");
3507 assert!(report.view.tests[0].attempts.is_empty());
3508 assert!(report.filters.passed.tests.is_empty());
3509 assert!(report.filters.failed.tests.is_empty());
3510 }
3511
3512 fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
3513 let nonce = SystemTime::now()
3514 .duration_since(SystemTime::UNIX_EPOCH)
3515 .unwrap()
3516 .as_nanos();
3517 let root = std::env::temp_dir().join(format!(
3518 "supercov-rust-report-{}-{nonce}-{}",
3519 std::process::id(),
3520 ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
3521 ));
3522 fs::create_dir_all(&root).unwrap();
3523 let path = root.join("evidence.raw.gz");
3524 if !entries
3525 .iter()
3526 .any(|entry| entry.path == "coverage-model.json")
3527 {
3528 entries.push(EvidenceArchiveEntry {
3529 path: "coverage-model.json".into(),
3530 contents: serde_json::to_vec(
3531 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
3532 .unwrap(),
3533 )
3534 .unwrap(),
3535 });
3536 }
3537 if !entries.iter().any(|entry| entry.path == "frontend.json") {
3538 entries.push(EvidenceArchiveEntry {
3539 path: "frontend.json".into(),
3540 contents: serde_json::to_vec(&serde_json::json!({
3541 "protocolVersion": 2,
3542 "frontendId": "javascript",
3543 "frontendVersion": "fixture-v1",
3544 "language": "javascript",
3545 "structuralSource": "owned-probes",
3546 "runners": [{
3547 "runner": "node:test",
3548 "executionModel": "serial-in-process",
3549 "attribution": {
3550 "run": "exact",
3551 "worker": "unavailable",
3552 "test": "exact",
3553 "retry": "exact",
3554 "phase": "exact",
3555 "action": "exact",
3556 "assertion": "exact"
3557 },
3558 "limitations": [{
3559 "id": "fixture-worker-unavailable",
3560 "scopes": ["worker"],
3561 "reason": "The fixture does not require worker identity"
3562 }]
3563 }],
3564 "structuralLimitations": []
3565 }))
3566 .unwrap(),
3567 });
3568 }
3569 write_archive(entries, &path).unwrap();
3570 path
3571 }
3572
3573 #[test]
3574 fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
3575 let manifest = CoverageManifest {
3576 unmeasured: Vec::new(),
3577 decisions: vec![],
3578 points: vec![point("background-hit", 1), point("test-hit", 2)],
3579 branches: vec![],
3580 limitations: vec![],
3581 scope: None,
3582 assertion_sites: Vec::new(),
3583 };
3584 let unattributed = |axis: &str| {
3587 serde_json::json!({
3588 "id": format!("background-no-{axis}"),
3589 "scopes": [axis],
3590 "reason": format!("Runner background did not expose exact {axis} identity for every result")
3591 })
3592 };
3593 let frontend = serde_json::json!({
3594 "protocolVersion": 2,
3595 "frontendId": "javascript",
3596 "frontendVersion": "fixture-v1",
3597 "language": "javascript",
3598 "structuralSource": "owned-probes",
3599 "runners": [
3600 {
3601 "runner": "node:test",
3602 "executionModel": "serial-in-process",
3603 "attribution": {
3604 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
3605 "phase": "exact", "action": "exact", "assertion": "exact"
3606 },
3607 "limitations": [{
3608 "id": "fixture-worker-unavailable",
3609 "scopes": ["worker"],
3610 "reason": "The fixture does not require worker identity"
3611 }]
3612 },
3613 {
3614 "runner": "background",
3615 "executionModel": "parallel-unattributed",
3616 "attribution": {
3617 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
3618 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
3619 },
3620 "limitations": [
3621 unattributed("worker"), unattributed("test"), unattributed("retry"),
3622 unattributed("phase"), unattributed("action"), unattributed("assertion")
3623 ]
3624 }
3625 ],
3626 "structuralLimitations": []
3627 });
3628 let path = archive(vec![
3629 EvidenceArchiveEntry {
3630 path: "manifest.json".into(),
3631 contents: serde_json::to_vec(&manifest).unwrap(),
3632 },
3633 EvidenceArchiveEntry {
3634 path: "frontend.json".into(),
3635 contents: serde_json::to_vec(&frontend).unwrap(),
3636 },
3637 EvidenceArchiveEntry {
3638 path: "playwright-worker-1.mcdc.jsonl".into(),
3639 contents: {
3640 let mut contents =
3641 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
3642 .unwrap();
3643 contents.extend_from_slice(b"\npartial-final-line");
3644 contents
3645 },
3646 },
3647 EvidenceArchiveEntry {
3648 path: "server/background/worker.jsonl".into(),
3649 contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
3650 },
3651 ]);
3652 let result = analyze_coverage_archive(&ArchiveReportRequest {
3653 archive_path: path.clone(),
3654 run_id: "run".into(),
3655 generated_at: "time".into(),
3656 integrity: None,
3657 test_exit_code: ExitCodeInput::Missing,
3658 });
3659 assert!(matches!(
3660 result,
3661 Err(ReportError::InvalidJson { path, .. })
3662 if path == "playwright-worker-1.mcdc.jsonl"
3663 ));
3664 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3665 }
3666
3667 #[test]
3668 fn a_torn_background_record_is_counted_not_fatal() {
3669 let manifest = CoverageManifest {
3674 unmeasured: Vec::new(),
3675 decisions: vec![],
3676 points: vec![],
3677 branches: vec![],
3678 limitations: vec![],
3679 scope: None,
3680 assertion_sites: Vec::new(),
3681 };
3682 let unattributed = |axis: &str| {
3685 serde_json::json!({
3686 "id": format!("background-no-{axis}"),
3687 "scopes": [axis],
3688 "reason": format!("Runner background did not expose exact {axis} identity for every result")
3689 })
3690 };
3691 let frontend = serde_json::json!({
3692 "protocolVersion": 2,
3693 "frontendId": "javascript",
3694 "frontendVersion": "fixture-v1",
3695 "language": "javascript",
3696 "structuralSource": "owned-probes",
3697 "runners": [
3698 {
3699 "runner": "node:test",
3700 "executionModel": "serial-in-process",
3701 "attribution": {
3702 "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
3703 "phase": "exact", "action": "exact", "assertion": "exact"
3704 },
3705 "limitations": [{
3706 "id": "fixture-worker-unavailable",
3707 "scopes": ["worker"],
3708 "reason": "The fixture does not require worker identity"
3709 }]
3710 },
3711 {
3712 "runner": "background",
3713 "executionModel": "parallel-unattributed",
3714 "attribution": {
3715 "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
3716 "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
3717 },
3718 "limitations": [
3719 unattributed("worker"), unattributed("test"), unattributed("retry"),
3720 unattributed("phase"), unattributed("action"), unattributed("assertion")
3721 ]
3722 }
3723 ],
3724 "structuralLimitations": []
3725 });
3726 let path = archive(vec![
3727 EvidenceArchiveEntry {
3728 path: "manifest.json".into(),
3729 contents: serde_json::to_vec(&manifest).unwrap(),
3730 },
3731 EvidenceArchiveEntry {
3732 path: "frontend.json".into(),
3733 contents: serde_json::to_vec(&frontend).unwrap(),
3734 },
3735 EvidenceArchiveEntry {
3736 path: "playwright-worker-1.mcdc.jsonl".into(),
3737 contents: {
3738 let mut contents =
3739 serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
3740 .unwrap();
3741 contents.push(b'\n');
3742 contents
3743 },
3744 },
3745 EvidenceArchiveEntry {
3746 path: "execution.37360-1.168.jsonl".into(),
3747 contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
3748 },
3749 EvidenceArchiveEntry {
3750 path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
3751 contents: concat!(
3752 "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
3753 "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
3754 "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
3755 )
3756 .as_bytes()
3757 .to_vec(),
3758 },
3759 ]);
3760 let report = analyze_coverage_archive(&ArchiveReportRequest {
3761 archive_path: path.clone(),
3762 run_id: "run".into(),
3763 generated_at: "time".into(),
3764 integrity: None,
3765 test_exit_code: ExitCodeInput::Missing,
3766 })
3767 .expect("a torn evidence line must not fail the whole run");
3768 let transport = report.view.transport.expect("transport stats");
3769 assert_eq!(transport.corrupt_records, 2);
3770 assert_eq!(transport.corrupt_files, 2);
3771 assert_eq!(transport.background_server_records, 2);
3772 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3773 }
3774
3775 #[test]
3776 fn archive_analysis_rejects_cross_run_evidence() {
3777 let manifest = CoverageManifest {
3778 unmeasured: Vec::new(),
3779 decisions: vec![],
3780 points: vec![],
3781 branches: vec![],
3782 limitations: vec![],
3783 scope: None,
3784 assertion_sites: Vec::new(),
3785 };
3786 let mut result = raw("test", 0, "passed", &[]);
3787 result.scope = Some(ExecutionScope {
3788 version: 1,
3789 run_id: "other-run".into(),
3790 worker_id: "worker".into(),
3791 test_id: "test".into(),
3792 test_key: "key".into(),
3793 retry: 0,
3794 attempt_id: "attempt".into(),
3795 });
3796 let path = archive(vec![
3797 EvidenceArchiveEntry {
3798 path: "manifest.json".into(),
3799 contents: serde_json::to_vec(&manifest).unwrap(),
3800 },
3801 EvidenceArchiveEntry {
3802 path: "worker/mcdc.json".into(),
3803 contents: serde_json::to_vec(&result).unwrap(),
3804 },
3805 ]);
3806 assert!(matches!(
3807 analyze_coverage_archive(&ArchiveReportRequest {
3808 archive_path: path.clone(),
3809 run_id: "run".into(),
3810 generated_at: "time".into(),
3811 integrity: None,
3812 test_exit_code: ExitCodeInput::Missing,
3813 }),
3814 Err(ReportError::InvalidArchive(reason))
3815 if reason.contains("expected=run actual=other-run")
3816 ));
3817 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3818 }
3819
3820 #[test]
3821 fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
3822 let vectors: Value = serde_json::from_str(include_str!(
3823 "../test-assets/coverage-model-v1/vectors.json"
3824 ))
3825 .unwrap();
3826 for value in vectors["valid"].as_array().unwrap() {
3827 let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
3828 model.into_declaration().unwrap();
3829 }
3830 for vector in vectors["invalid"].as_array().unwrap() {
3831 let value = vector["value"].clone();
3832 if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
3833 assert!(
3834 model.into_declaration().is_err(),
3835 "accepted invalid model vector: {}",
3836 vector["reason"]
3837 );
3838 }
3839 }
3840
3841 let manifest = CoverageManifest {
3842 unmeasured: Vec::new(),
3843 decisions: vec![],
3844 points: vec![],
3845 branches: vec![],
3846 limitations: vec![],
3847 scope: None,
3848 assertion_sites: Vec::new(),
3849 };
3850 let path = archive(vec![
3851 EvidenceArchiveEntry {
3852 path: "coverage-model.json".into(),
3853 contents: serde_json::to_vec(&PersistedCoverageModel {
3854 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
3855 language: "rust".into(),
3856 variant: "rust-source-v1".into(),
3857 name: "Rust source coverage".into(),
3858 completeness_meaning: "Every Rust obligation was satisfied.".into(),
3859 measured: vec!["Rust statements".into()],
3860 not_measured: vec![],
3861 })
3862 .unwrap(),
3863 },
3864 EvidenceArchiveEntry {
3865 path: "manifest.json".into(),
3866 contents: serde_json::to_vec(&manifest).unwrap(),
3867 },
3868 ]);
3869 assert!(matches!(
3870 analyze_coverage_archive(&ArchiveReportRequest {
3871 archive_path: path.clone(),
3872 run_id: "run".into(),
3873 generated_at: "time".into(),
3874 integrity: None,
3875 test_exit_code: ExitCodeInput::Missing,
3876 }),
3877 Err(ReportError::InvalidArchive(reason))
3878 if reason.contains("javascript differs from coverage model language rust")
3879 ));
3880 fs::remove_dir_all(path.parent().unwrap()).unwrap();
3881 }
3882
3883 #[test]
3884 fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
3885 let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
3886 "runId": "run",
3887 "manifest": { "decisions": [], "points": [], "branches": [] },
3888 "rawResults": [{
3889 "test": "test",
3890 "status": "passed",
3891 "browser": [],
3892 "server": []
3893 }],
3894 "generatedAt": "time",
3895 "testExitCode": null
3896 }))
3897 .unwrap();
3898 let report = analyze_coverage_results(&request).unwrap();
3899 assert_eq!(
3900 report.execution,
3901 Some(ExecutionResult {
3902 test_exit_code: None,
3903 valid: false,
3904 })
3905 );
3906 }
3907 #[test]
3908 fn a_phase_leaked_from_another_test_is_untagged_and_an_unknown_one_is_kept() {
3909 let record = |test: &str, phases: &[&str], events: &[(&str, &str)]| -> RawTestResult {
3910 serde_json::from_value(serde_json::json!({
3911 "test": test,
3912 "phases": phases.iter().map(|id| serde_json::json!({
3913 "id": id, "kind": "assertion", "operation": "assert.ok", "startedAtMs": 0
3914 })).collect::<Vec<_>>(),
3915 "runtime": [{
3916 "events": events.iter().map(|(id, phase)| serde_json::json!({
3917 "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
3918 })).collect::<Vec<_>>()
3919 }]
3920 }))
3921 .unwrap()
3922 };
3923 let mut results = vec![
3924 record(
3925 "upload",
3926 &["a-0:assertion:1"],
3927 &[("own", "a-0:assertion:1")],
3928 ),
3929 record(
3931 "form data",
3932 &["b-0:assertion:1"],
3933 &[
3934 ("late", "a-0:assertion:1"),
3935 ("mine", "b-0:assertion:1"),
3936 ("corrupt", "z-0:assertion:9"),
3937 ],
3938 ),
3939 ];
3940 release_leaked_phases(&mut results);
3941 let phases = |raw: &RawTestResult| {
3942 raw.runtime[0]
3943 .events
3944 .iter()
3945 .map(|event| (event.id.clone(), event.phase_id.clone()))
3946 .collect::<Vec<_>>()
3947 };
3948 assert_eq!(
3949 phases(&results[0]),
3950 [("own".into(), Some("a-0:assertion:1".into()))]
3951 );
3952 assert_eq!(
3953 phases(&results[1]),
3954 [
3955 ("late".into(), None),
3956 ("mine".into(), Some("b-0:assertion:1".into())),
3957 ("corrupt".into(), Some("z-0:assertion:9".into())),
3959 ]
3960 );
3961 }
3962
3963 #[test]
3964 fn a_test_run_twice_under_one_identity_keeps_both_executions_apart() {
3965 let record = |status: &str, events: &[(&str, &str)]| -> RawTestResult {
3968 serde_json::from_value(serde_json::json!({
3969 "test": "parse",
3970 "testId": "parse",
3971 "retry": 0,
3972 "status": status,
3973 "phases": [
3974 { "id": "t-0:assertion:1", "kind": "assertion", "operation": "expect", "startedAtMs": 0 },
3975 { "id": "t-0:assertion:2", "kind": "assertion", "operation": "expect", "startedAtMs": 0, "causedByPhaseId": "t-0:assertion:1" }
3976 ],
3977 "runtime": [{
3978 "events": events.iter().map(|(id, phase)| serde_json::json!({
3979 "type": "hit", "id": id, "timestampMs": 0, "phaseId": phase, "environment": "server"
3980 })).collect::<Vec<_>>()
3981 }]
3982 }))
3983 .unwrap()
3984 };
3985 let mut results = vec![
3986 record("failed", &[("node", "t-0:assertion:1")]),
3987 record("passed", &[("edge", "t-0:assertion:2")]),
3988 record("passed", &[("third", "t-0:assertion:1")]),
3989 ];
3990 separate_repeated_executions(&mut results);
3991 let ids = |raw: &RawTestResult| {
3992 raw.phases
3993 .iter()
3994 .map(|phase| (phase.id.clone(), phase.caused_by_phase_id.clone()))
3995 .collect::<Vec<_>>()
3996 };
3997 let events = |raw: &RawTestResult| {
3998 raw.runtime[0]
3999 .events
4000 .iter()
4001 .map(|event| event.phase_id.clone().unwrap())
4002 .collect::<Vec<_>>()
4003 };
4004 assert_eq!(
4005 ids(&results[0]),
4006 [
4007 ("t-0:assertion:1".into(), None),
4008 ("t-0:assertion:2".into(), Some("t-0:assertion:1".into()))
4009 ]
4010 );
4011 assert_eq!(
4012 ids(&results[1]),
4013 [
4014 ("t-0:assertion:1~2".into(), None),
4015 ("t-0:assertion:2~2".into(), Some("t-0:assertion:1~2".into()))
4016 ]
4017 );
4018 assert_eq!(events(&results[1]), ["t-0:assertion:2~2"]);
4019 assert_eq!(events(&results[2]), ["t-0:assertion:1~3"]);
4020
4021 let mut test = MutableTest {
4022 id: "parse".into(),
4023 name: "parse".into(),
4024 file: None,
4025 title: None,
4026 retries: BTreeSet::new(),
4027 attempts: BTreeMap::new(),
4028 unstarted: false,
4029 runner_reported_flaky: false,
4030 provenance: TestProvenance::default(),
4031 role: "test".into(),
4032 attribution: "exact".into(),
4033 hits: Vec::new(),
4034 decisions: BTreeMap::new(),
4035 };
4036 for raw in &results {
4037 record_attempt(&mut test, raw);
4038 }
4039 assert_eq!(
4040 test_outcome(&test),
4041 "failed",
4042 "a failed execution fails the attempt"
4043 );
4044 }
4045
4046 #[test]
4047 fn a_dimension_is_summarised_over_the_tests_it_can_describe() {
4048 let exact = |id: &str, kind: &str| TestCoverageResult {
4054 id: id.into(),
4055 name: id.into(),
4056 file: None,
4057 title: None,
4058 retries: vec![],
4059 attempts: vec![],
4060 outcome: "passed".into(),
4061 provenance: TestProvenance {
4062 runner: "go-test".into(),
4063 kind: kind.into(),
4064 project: None,
4065 source: String::new(),
4066 },
4067 role: "test".into(),
4068 attribution: ATTRIBUTION_EXACT.into(),
4069 hits: vec![],
4070 decisions: vec![],
4071 lines: vec![],
4072 };
4073 let mut run_wide = exact("parallel", "unit");
4074 run_wide.attribution = ATTRIBUTION_RUN_WIDE.into();
4075
4076 assert!(coverage_is_its_own(&exact("serial", "unit").attribution));
4085 assert!(coverage_is_its_own(ATTRIBUTION_PARTIAL));
4086 assert!(!coverage_is_its_own(&run_wide.attribution));
4087
4088 assert!(coverage_is_complete(ATTRIBUTION_EXACT));
4089 assert!(!coverage_is_complete(ATTRIBUTION_PARTIAL));
4090 assert!(!coverage_is_complete(ATTRIBUTION_RUN_WIDE));
4091
4092 for (left, right, merged) in [
4095 (ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL, ATTRIBUTION_PARTIAL),
4096 (ATTRIBUTION_PARTIAL, ATTRIBUTION_EXACT, ATTRIBUTION_PARTIAL),
4097 (
4098 ATTRIBUTION_PARTIAL,
4099 ATTRIBUTION_RUN_WIDE,
4100 ATTRIBUTION_RUN_WIDE,
4101 ),
4102 (ATTRIBUTION_EXACT, ATTRIBUTION_EXACT, ATTRIBUTION_EXACT),
4103 ] {
4104 assert_eq!(least_exact(left, right), merged);
4105 }
4106
4107 assert!(coverage_is_its_own(""));
4110 assert!(coverage_is_complete(""));
4111 }
4112}