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