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