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