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