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