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