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