1use std::collections::{BTreeMap, BTreeSet, HashMap};
8
9use serde::Serialize;
10use supercov_contracts::COVERAGE_MODEL_SCHEMA_VERSION;
11
12use crate::{
13 coverage_analysis::{
14 CoverageCount, CoverageSummary, McdcVector, find_witnesses_for_conditions,
15 },
16 coverage_report::{
17 CoverageModel, CoverageReport, CoverageView, TransportStats, coverage_summary_for_tests,
18 },
19 query_index::{QueryIndex, QueryIndexError, QueryIndexSection},
20};
21
22pub const SECTION_STRING_BYTES: u32 = 1;
23pub const SECTION_STRINGS: u32 = 2;
24pub const SECTION_STRING_RELATIONS: u32 = 3;
25pub const SECTION_VIEW_SUMMARIES: u32 = 10;
26pub const SECTION_FILE_GAPS: u32 = 11;
27pub const SECTION_DECISION_GAPS: u32 = 12;
28pub const SECTION_DIMENSIONS: u32 = 13;
29pub const SECTION_PROJECTIONS: u32 = 14;
30pub const SECTION_SCOPE_ENTRIES: u32 = 15;
31pub const SECTION_CONFIDENCE: u32 = 16;
32pub const SECTION_LINES: u32 = 17;
33pub const SECTION_TEST_SUMMARIES: u32 = 18;
34pub const SECTION_PHASE_SUMMARIES: u32 = 19;
35pub const SECTION_ANCHORS: u32 = 20;
36pub const SECTION_TEST_RETRIES: u32 = 21;
37pub const SECTION_TEST_ATTEMPTS: u32 = 22;
38pub const SECTION_TEST_LINES: u32 = 23;
39pub const SECTION_TEST_HITS: u32 = 24;
40pub const SECTION_TEST_DECISIONS: u32 = 25;
41pub const SECTION_TEST_VECTORS: u32 = 26;
42pub const SECTION_VECTOR_VALUES: u32 = 27;
43pub const SECTION_HIT_METADATA: u32 = 28;
44pub const SECTION_DECISION_METADATA: u32 = 29;
45pub const SECTION_DECISION_DETAILS: u32 = 30;
46pub const SECTION_DECISION_VECTOR_OBSERVATIONS: u32 = 31;
47pub const SECTION_DECISION_CONDITIONS: u32 = 32;
48pub const SECTION_LIMITATIONS: u32 = 33;
49pub const SECTION_COVERAGE_MODEL: u32 = 34;
50
51const STRING_RECORD_SIZE: usize = 16;
52const SUMMARY_RECORD_SIZE: usize = 176;
53const FILE_GAP_RECORD_SIZE: usize = 176;
54const DECISION_GAP_RECORD_SIZE: usize = 96;
55const DIMENSION_RECORD_SIZE: usize = 192;
56const PROJECTION_RECORD_SIZE: usize = 536;
57const SCOPE_ENTRY_RECORD_SIZE: usize = 96;
58const CONFIDENCE_RECORD_SIZE: usize = 96;
59const LINE_RECORD_SIZE: usize = 80;
60const TEST_SUMMARY_RECORD_SIZE: usize = 64;
61const PHASE_SUMMARY_RECORD_SIZE: usize = 64;
62const ANCHOR_RECORD_SIZE: usize = 64;
63const TEST_RETRY_RECORD_SIZE: usize = 16;
64const TEST_ATTEMPT_RECORD_SIZE: usize = 24;
65const TEST_LINE_RECORD_SIZE: usize = 24;
66const TEST_HIT_RECORD_SIZE: usize = 16;
67const TEST_DECISION_RECORD_SIZE: usize = 32;
68const TEST_VECTOR_RECORD_SIZE: usize = 24;
69const HIT_METADATA_RECORD_SIZE: usize = 64;
70const DECISION_METADATA_RECORD_SIZE: usize = 64;
71const DECISION_DETAIL_RECORD_SIZE: usize = 64;
72const DECISION_VECTOR_OBSERVATION_RECORD_SIZE: usize = 64;
73const DECISION_CONDITION_RECORD_SIZE: usize = 64;
74const LIMITATION_RECORD_SIZE: usize = 64;
75const COVERAGE_MODEL_RECORD_SIZE: usize = 48;
76const NO_STRING: u32 = u32::MAX;
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
79#[serde(rename_all = "lowercase")]
80pub enum CoverageViewId {
81 All = 0,
82 Passed = 1,
83 Failed = 2,
84}
85
86impl TryFrom<u8> for CoverageViewId {
87 type Error = CoverageIndexError;
88
89 fn try_from(value: u8) -> Result<Self, Self::Error> {
90 match value {
91 0 => Ok(Self::All),
92 1 => Ok(Self::Passed),
93 2 => Ok(Self::Failed),
94 _ => Err(CoverageIndexError::InvalidRecord("coverage view")),
95 }
96 }
97}
98
99#[derive(Debug)]
100pub enum CoverageIndexError {
101 Container(QueryIndexError),
102 InvalidRecord(&'static str),
103 InvalidUtf8,
104 SizeOverflow,
105}
106
107impl From<QueryIndexError> for CoverageIndexError {
108 fn from(value: QueryIndexError) -> Self {
109 Self::Container(value)
110 }
111}
112
113impl std::fmt::Display for CoverageIndexError {
114 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Self::Container(error) => write!(formatter, "{error}"),
117 Self::InvalidRecord(reason) => write!(formatter, "invalid coverage index: {reason}"),
118 Self::InvalidUtf8 => write!(formatter, "invalid UTF-8 in coverage index"),
119 Self::SizeOverflow => write!(formatter, "coverage index exceeds format limits"),
120 }
121 }
122}
123
124impl std::error::Error for CoverageIndexError {}
125
126fn put_u32(bytes: &mut [u8], offset: usize, value: u32) {
127 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
128}
129
130fn put_u64(bytes: &mut [u8], offset: usize, value: u64) {
131 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
132}
133
134fn get_u32(bytes: &[u8], offset: usize) -> Result<u32, CoverageIndexError> {
135 Ok(u32::from_le_bytes(
136 bytes
137 .get(offset..offset + 4)
138 .and_then(|value| value.try_into().ok())
139 .ok_or(CoverageIndexError::InvalidRecord("truncated u32"))?,
140 ))
141}
142
143fn get_u64(bytes: &[u8], offset: usize) -> Result<u64, CoverageIndexError> {
144 Ok(u64::from_le_bytes(
145 bytes
146 .get(offset..offset + 8)
147 .and_then(|value| value.try_into().ok())
148 .ok_or(CoverageIndexError::InvalidRecord("truncated u64"))?,
149 ))
150}
151
152fn usize_u64(value: usize) -> Result<u64, CoverageIndexError> {
153 u64::try_from(value).map_err(|_| CoverageIndexError::SizeOverflow)
154}
155
156fn usize_u32(value: usize) -> Result<u32, CoverageIndexError> {
157 u32::try_from(value).map_err(|_| CoverageIndexError::SizeOverflow)
158}
159
160#[derive(Default)]
161struct StringTable {
162 ids: HashMap<String, u32>,
163 strings: Vec<String>,
164}
165
166#[derive(Default)]
167struct StringRelations {
168 values: Vec<u32>,
169 interned: HashMap<Vec<u32>, (u64, u64)>,
173}
174
175impl StringRelations {
176 fn push(
185 &mut self,
186 values: impl IntoIterator<Item = String>,
187 strings: &mut StringTable,
188 ) -> Result<(u64, u64), CoverageIndexError> {
189 let mut run = Vec::new();
190 for value in values {
191 run.push(strings.intern(&value)?);
192 }
193 if let Some(found) = self.interned.get(&run) {
194 return Ok(*found);
195 }
196 let offset = usize_u64(self.values.len())?;
197 let count = usize_u64(run.len())?;
198 self.values.extend_from_slice(&run);
199 self.interned.insert(run, (offset, count));
200 Ok((offset, count))
201 }
202
203 fn section(self) -> Result<QueryIndexSection, CoverageIndexError> {
204 let mut bytes = Vec::with_capacity(self.values.len() * 4);
205 for value in self.values {
206 bytes.extend_from_slice(&value.to_le_bytes());
207 }
208 Ok(QueryIndexSection {
209 kind: SECTION_STRING_RELATIONS,
210 record_size: 4,
211 count: usize_u64(bytes.len() / 4)?,
212 bytes,
213 })
214 }
215}
216
217impl StringTable {
218 fn intern(&mut self, value: &str) -> Result<u32, CoverageIndexError> {
219 if let Some(id) = self.ids.get(value) {
220 return Ok(*id);
221 }
222 let id = usize_u32(self.strings.len())?;
223 self.ids.insert(value.into(), id);
224 self.strings.push(value.into());
225 Ok(id)
226 }
227
228 fn sections(self) -> Result<[QueryIndexSection; 2], CoverageIndexError> {
229 let mut blob = Vec::new();
230 let mut records = Vec::with_capacity(self.strings.len() * STRING_RECORD_SIZE);
231 for string in self.strings {
232 let offset = usize_u64(blob.len())?;
233 let value = string.as_bytes();
234 let length = usize_u32(value.len())?;
235 blob.extend_from_slice(value);
236 let mut record = [0_u8; STRING_RECORD_SIZE];
237 put_u64(&mut record, 0, offset);
238 put_u32(&mut record, 8, length);
239 records.extend_from_slice(&record);
240 }
241 Ok([
242 QueryIndexSection {
243 kind: SECTION_STRING_BYTES,
244 record_size: 0,
245 count: usize_u64(blob.len())?,
246 bytes: blob,
247 },
248 QueryIndexSection {
249 kind: SECTION_STRINGS,
250 record_size: STRING_RECORD_SIZE as u32,
251 count: usize_u64(records.len() / STRING_RECORD_SIZE)?,
252 bytes: records,
253 },
254 ])
255 }
256}
257
258fn put_count(
259 bytes: &mut [u8],
260 offset: usize,
261 count: &CoverageCount,
262) -> Result<(), CoverageIndexError> {
263 put_u64(bytes, offset, usize_u64(count.covered)?);
264 put_u64(bytes, offset + 8, usize_u64(count.total)?);
265 Ok(())
266}
267
268fn summary_record(
269 id: CoverageViewId,
270 view: &CoverageView,
271 strings: &mut StringTable,
272) -> Result<[u8; SUMMARY_RECORD_SIZE], CoverageIndexError> {
273 let mut record = [0_u8; SUMMARY_RECORD_SIZE];
274 record[0] = id as u8;
275 record[1] = u8::from(view.summary.coverage_complete);
276 record[2] = match view.summary.completeness_blocked {
277 None => 0,
278 Some(false) => 1,
279 Some(true) => 2,
280 };
281 put_u32(&mut record, 4, strings.intern(&view.generated_at)?);
282 put_u32(&mut record, 8, strings.intern(&view.variant)?);
283 let values = [
284 view.summary.decisions,
285 view.summary.executed_decisions,
286 view.summary.covered_decisions,
287 view.summary.conditions,
288 view.summary.covered_conditions,
289 ];
290 for (index, value) in values.into_iter().enumerate() {
291 put_u64(&mut record, 16 + index * 8, usize_u64(value)?);
292 }
293 for (index, count) in [
294 &view.summary.lines,
295 &view.summary.statements,
296 &view.summary.functions,
297 &view.summary.branches,
298 &view.summary.decision_outcomes,
299 &view.summary.condition_outcomes,
300 &view.summary.value_selections,
301 ]
302 .into_iter()
303 .enumerate()
304 {
305 put_count(&mut record, 56 + index * 16, count)?;
306 }
307 Ok(record)
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
311#[serde(rename_all = "camelCase")]
312pub struct IndexedFileGap {
313 #[serde(skip)]
314 pub view: CoverageViewId,
315 pub file: String,
316 pub uncovered_lines: usize,
317 pub uncovered_statements: usize,
318 pub uncovered_functions: usize,
319 pub missing_branches: usize,
320 pub missing_mcdc_conditions: usize,
321 pub measurement_limitations: usize,
322 pub limitation_kinds: Vec<String>,
323 pub covered_by_other_tests: IndexedGapDimensions,
324 pub uncovered_everywhere: IndexedGapDimensions,
325 pub score: usize,
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
329#[serde(rename_all = "camelCase")]
330pub struct IndexedGapDimensions {
331 pub lines: usize,
332 pub statements: usize,
333 pub functions: usize,
334 pub branches: usize,
335 pub mcdc_conditions: usize,
336}
337
338#[derive(Debug, Clone, PartialEq, Serialize)]
339#[serde(rename_all = "camelCase")]
340pub struct IndexedCoverageSnapshot {
341 pub all_summary: CoverageSummary,
342 pub passed_summary: CoverageSummary,
343 pub failed_summary: CoverageSummary,
344 pub all_files: Vec<IndexedFileGap>,
345 pub passed_files: Vec<IndexedFileGap>,
346 pub failed_files: Vec<IndexedFileGap>,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
350#[serde(rename_all = "camelCase")]
351pub struct IndexedDecisionGap {
352 #[serde(skip)]
353 pub view: CoverageViewId,
354 #[serde(skip)]
355 pub file: String,
356 pub id: String,
357 pub line: usize,
358 pub column: usize,
359 pub kind: String,
360 pub conditions: usize,
361 pub missing_conditions: usize,
362 pub source: String,
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366pub enum CoverageDimension {
367 Kind = 0,
368 Runner = 1,
369}
370
371#[derive(Debug, Clone, PartialEq, Serialize)]
372#[serde(rename_all = "camelCase")]
373pub struct IndexedDimensionCoverage {
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub kind: Option<String>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub runner: Option<String>,
378 pub tests: usize,
379 pub setups: usize,
380 pub attributed: usize,
384 pub summary: CoverageSummary,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
388#[serde(rename_all = "camelCase")]
389pub struct IndexedAttribution {
390 pub browser_explicit: usize,
391 pub browser_fallback: usize,
392 pub server_explicit: usize,
393 pub server_fallback: usize,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
397#[serde(rename_all = "camelCase")]
398pub struct IndexedOutcomeCounts {
399 pub passed: usize,
400 pub failed: usize,
401 pub flaky: usize,
402 pub skipped: usize,
403 pub timed_out: usize,
404 pub interrupted: usize,
405 pub unknown: usize,
406 pub unstarted: usize,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
410#[serde(rename_all = "camelCase")]
411pub struct IndexedMeasurementKinds {
412 #[serde(rename = "dynamic-code")]
413 pub dynamic_code: usize,
414 #[serde(rename = "semantic-safety")]
415 pub semantic_safety: usize,
416 #[serde(rename = "source-scope")]
417 pub source_scope: usize,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
421#[serde(rename_all = "camelCase")]
422pub struct IndexedMeasurement {
423 pub complete: bool,
424 pub limitations: usize,
425 pub evidence_corruptions: usize,
426 pub blocking: usize,
427 pub declared: usize,
430 pub files: usize,
431 pub by_kind: IndexedMeasurementKinds,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
435#[serde(rename_all = "camelCase")]
436pub struct IndexedConfidenceLines {
437 pub unexecuted: usize,
438 pub executed: usize,
439 pub action: usize,
440 pub asserted: usize,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
444#[serde(rename_all = "camelCase")]
445pub struct IndexedSummaryConfidence {
446 pub lines: IndexedConfidenceLines,
447 pub assertion_covered_mcdc_conditions: usize,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
451#[serde(rename_all = "camelCase")]
452pub struct IndexedCoverageModel {
453 pub schema_version: u32,
454 pub variant: String,
455 pub name: String,
456 pub completeness_meaning: String,
457 pub measured: Vec<String>,
458 pub not_measured: Vec<String>,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
462#[serde(rename_all = "camelCase")]
463pub struct IndexedSourceScope {
464 pub kind: String,
465 pub language: String,
466 pub model: String,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub mode: Option<String>,
469 pub roots: Vec<String>,
470 #[serde(skip_serializing_if = "Option::is_none")]
471 pub unit: Option<String>,
472 #[serde(skip_serializing_if = "Option::is_none")]
473 pub measurement_complete: Option<bool>,
474 pub included: usize,
475 pub excluded: usize,
476 pub ambiguous: usize,
477}
478
479#[derive(Debug, Clone, PartialEq)]
480pub struct IndexedProjection {
481 pub view: CoverageViewId,
482 pub kind: Option<String>,
483 pub runner: Option<String>,
484 pub generated_at: String,
485 pub summary: CoverageSummary,
486 pub measurement: IndexedMeasurement,
487 pub attribution: IndexedAttribution,
488 pub transport: Option<TransportStats>,
489 pub empty_evidence_tests: usize,
490 pub first_empty_evidence_test: Option<String>,
491 pub confidence: IndexedSummaryConfidence,
492 pub files_with_gaps: usize,
493 pub files_with_coverage_gaps: usize,
494 pub tests: usize,
495 pub setups: usize,
496 pub test_outcomes: IndexedOutcomeCounts,
497 pub source_scope: Option<IndexedSourceScope>,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
501#[serde(rename_all = "camelCase")]
502pub struct IndexedScopeEntry {
503 pub file: String,
504 pub status: String,
505 pub reason: String,
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub package_root: Option<String>,
508 pub measurement_limitations: usize,
509 pub limitation_kinds: Vec<String>,
510}
511
512#[derive(Debug, Clone, PartialEq)]
513pub struct IndexedLine {
514 pub file: String,
515 pub line: usize,
516 pub covered: bool,
517 pub measured: bool,
521 pub tests: Vec<String>,
522 pub phases: Vec<String>,
523 pub confidence: crate::coverage_report::CoverageConfidence,
524}
525
526#[derive(Debug, Clone, PartialEq)]
527pub struct IndexedTestSummary {
528 pub id: String,
529 pub name: String,
530 pub file: Option<String>,
531 pub title: Option<String>,
532 pub outcome: String,
533 pub role: String,
534 pub attribution: String,
538 pub provenance: crate::coverage_report::TestProvenance,
539}
540
541#[derive(Debug, Clone, PartialEq)]
542pub struct IndexedPhaseSummary {
543 pub id: String,
544 pub kind: String,
545 pub operation: String,
546 pub source: Option<String>,
547 pub test: String,
548 pub status: Option<String>,
549 pub caused_by_phase_id: Option<String>,
550 pub lines: usize,
551 pub decisions: usize,
552}
553
554#[derive(Debug, Clone, PartialEq)]
555pub struct IndexedAnchor {
556 pub kind: String,
557 pub id: String,
558 pub file: String,
559 pub line: usize,
560 pub column: usize,
561 pub covered: bool,
562 pub conditions: Option<usize>,
563 pub covered_conditions: Option<usize>,
564 pub tests: Vec<String>,
565}
566
567#[derive(Debug, Clone, PartialEq)]
568pub struct IndexedTestDetail {
569 pub summary: IndexedTestSummary,
570 pub retries: Vec<usize>,
571 pub attempts: Vec<crate::coverage_report::TestAttempt>,
572 pub hits: Vec<String>,
573 pub decisions: Vec<crate::coverage_report::TestDecisionResult>,
574 pub lines: Vec<crate::coverage_report::SourceLine>,
575}
576
577#[derive(Debug, Clone, PartialEq, Eq)]
578pub struct IndexedHitMetadata {
579 pub id: String,
580 pub obligation: String,
581 pub branch_kind: Option<String>,
582 pub file: String,
583 pub line: usize,
584 pub column: usize,
585 pub label: Option<String>,
586 pub alternative: Option<String>,
587 pub parent_id: Option<String>,
588 pub source: String,
589 pub tests: Vec<String>,
590}
591
592#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
593pub struct IndexedLimitation {
594 pub id: String,
595 pub kind: String,
596 pub file: String,
597 pub line: usize,
598 pub column: usize,
599 pub source: String,
600 pub reason: String,
601 pub blocking: bool,
604}
605
606#[derive(Default)]
607struct MutableFileGap {
608 uncovered_lines: usize,
609 uncovered_statements: usize,
610 uncovered_functions: usize,
611 missing_branches: usize,
612 missing_mcdc_conditions: usize,
613 measurement_limitations: usize,
614 limitation_mask: u32,
615 covered_by_other_tests: [usize; 5],
616 uncovered_everywhere: [usize; 5],
617}
618
619fn limitation_kind(value: &serde_json::Value) -> Option<(&str, &str)> {
620 Some((value.get("file")?.as_str()?, value.get("kind")?.as_str()?))
621}
622
623fn includes_selected(tests: &[String], selected: Option<&BTreeSet<String>>, covered: bool) -> bool {
624 selected.map_or(covered, |selected| {
625 tests.iter().any(|test| selected.contains(test))
626 })
627}
628
629fn classify(
630 gap: &mut MutableFileGap,
631 dimension: usize,
632 selected: Option<&BTreeSet<String>>,
633 covered_overall: bool,
634) {
635 if selected.is_some() && covered_overall {
636 gap.covered_by_other_tests[dimension] += 1;
637 } else {
638 gap.uncovered_everywhere[dimension] += 1;
639 }
640}
641
642fn file_gaps(
643 view: &CoverageView,
644 selected: Option<&BTreeSet<String>>,
645) -> Result<Vec<(String, MutableFileGap)>, CoverageIndexError> {
646 let mut files = BTreeMap::<String, MutableFileGap>::new();
647 for line in &view.lines {
648 let gap = files.entry(line.file.clone()).or_default();
649 if !includes_selected(&line.tests, selected, line.covered) {
650 gap.uncovered_lines += 1;
651 classify(gap, 0, selected, line.covered);
652 }
653 }
654 for point in &view.points {
655 let gap = files.entry(point.meta.file.clone()).or_default();
656 if !includes_selected(&point.tests, selected, point.covered) {
657 match point.meta.kind {
658 crate::coverage_analysis::PointKind::Statement => {
659 gap.uncovered_statements += 1;
660 classify(gap, 1, selected, point.covered);
661 }
662 crate::coverage_analysis::PointKind::Function => {
663 gap.uncovered_functions += 1;
664 classify(gap, 2, selected, point.covered);
665 }
666 }
667 }
668 }
669 for branch in &view.branches {
670 let gap = files.entry(branch.meta.file.clone()).or_default();
671 for alternative in &branch.alternatives {
672 if !includes_selected(&alternative.tests, selected, alternative.covered) {
673 gap.missing_branches += 1;
674 classify(gap, 3, selected, alternative.covered);
675 }
676 }
677 }
678 for decision in &view.decisions {
679 let gap = files.entry(decision.meta.file.clone()).or_default();
680 let selected_vectors = decision
681 .vector_observations
682 .iter()
683 .filter(|observation| includes_selected(&observation.tests, selected, true))
684 .map(|observation| observation.vector.clone())
685 .collect::<Vec<_>>();
686 let witnesses =
687 find_witnesses_for_conditions(&selected_vectors, decision.meta.conditions.len())
688 .map_err(|_| CoverageIndexError::InvalidRecord("MC/DC vector width"))?;
689 for (index, witness) in witnesses.into_iter().enumerate() {
690 if witness.is_none() {
691 gap.missing_mcdc_conditions += 1;
692 classify(gap, 4, selected, decision.conditions[index].covered);
693 }
694 }
695 }
696 for limitation in &view.limitations {
697 let Some((file, kind)) = limitation_kind(limitation) else {
698 continue;
699 };
700 let gap = files.entry(file.into()).or_default();
701 gap.measurement_limitations += 1;
702 gap.limitation_mask |= match kind {
703 "dynamic-code" => 1,
704 "semantic-safety" => 2,
705 "source-scope" => 4,
706 _ => 8,
707 };
708 }
709 Ok(files.into_iter().collect())
710}
711
712fn file_gap_record(
713 view_id: CoverageViewId,
714 file: &str,
715 gap: &MutableFileGap,
716 kind: Option<&str>,
717 runner: Option<&str>,
718 strings: &mut StringTable,
719) -> Result<[u8; FILE_GAP_RECORD_SIZE], CoverageIndexError> {
720 let mut record = [0_u8; FILE_GAP_RECORD_SIZE];
721 record[0] = view_id as u8;
722 put_u32(&mut record, 4, strings.intern(file)?);
723 for (index, value) in [
724 gap.uncovered_lines,
725 gap.uncovered_statements,
726 gap.uncovered_functions,
727 gap.missing_branches,
728 gap.missing_mcdc_conditions,
729 gap.measurement_limitations,
730 ]
731 .into_iter()
732 .enumerate()
733 {
734 put_u64(&mut record, 8 + index * 8, usize_u64(value)?);
735 }
736 put_u32(&mut record, 56, gap.limitation_mask);
737 let score = gap.uncovered_lines
738 + gap.uncovered_functions * 2
739 + gap.missing_branches * 2
740 + gap.missing_mcdc_conditions * 3
741 + gap.measurement_limitations * 3;
742 put_u64(&mut record, 64, usize_u64(score)?);
743 put_u32(
744 &mut record,
745 72,
746 kind.map_or(Ok(NO_STRING), |value| strings.intern(value))?,
747 );
748 put_u32(
749 &mut record,
750 76,
751 runner.map_or(Ok(NO_STRING), |value| strings.intern(value))?,
752 );
753 for (index, value) in gap.covered_by_other_tests.into_iter().enumerate() {
754 put_u64(&mut record, 80 + index * 8, usize_u64(value)?);
755 }
756 for (index, value) in gap.uncovered_everywhere.into_iter().enumerate() {
757 put_u64(&mut record, 120 + index * 8, usize_u64(value)?);
758 }
759 Ok(record)
760}
761
762fn projections(view: &CoverageView) -> Vec<(Option<String>, Option<String>, BTreeSet<String>)> {
763 let kinds = view
764 .tests
765 .iter()
766 .map(|test| test.provenance.kind.clone())
767 .collect::<BTreeSet<_>>();
768 let runners = view
769 .tests
770 .iter()
771 .map(|test| test.provenance.runner.clone())
772 .collect::<BTreeSet<_>>();
773 let mut selectors = Vec::new();
774 for kind in &kinds {
775 selectors.push((Some(kind.clone()), None));
776 }
777 for runner in &runners {
778 selectors.push((None, Some(runner.clone())));
779 }
780 for kind in &kinds {
781 for runner in &runners {
782 selectors.push((Some(kind.clone()), Some(runner.clone())));
783 }
784 }
785 selectors
786 .into_iter()
787 .filter_map(|(kind, runner)| {
788 let selected = view
789 .tests
790 .iter()
791 .filter(|test| {
792 kind.as_ref()
793 .is_none_or(|value| test.provenance.kind == *value)
794 && runner
795 .as_ref()
796 .is_none_or(|value| test.provenance.runner == *value)
797 })
798 .map(|test| test.id.clone())
799 .collect::<BTreeSet<_>>();
800 (!selected.is_empty()).then_some((kind, runner, selected))
801 })
802 .collect()
803}
804
805fn decision_gap_record(
806 view_id: CoverageViewId,
807 decision: &crate::coverage_report::DecisionResult,
808 selected: Option<&BTreeSet<String>>,
809 kind: Option<&str>,
810 runner: Option<&str>,
811 strings: &mut StringTable,
812) -> Result<[u8; DECISION_GAP_RECORD_SIZE], CoverageIndexError> {
813 let vectors = decision
814 .vector_observations
815 .iter()
816 .filter(|observation| includes_selected(&observation.tests, selected, true))
817 .map(|observation| observation.vector.clone())
818 .collect::<Vec<_>>();
819 let witnesses = find_witnesses_for_conditions(&vectors, decision.meta.conditions.len())
820 .map_err(|_| CoverageIndexError::InvalidRecord("MC/DC vector width"))?;
821 let mut record = [0_u8; DECISION_GAP_RECORD_SIZE];
822 record[0] = view_id as u8;
823 put_u32(
824 &mut record,
825 4,
826 kind.map_or(Ok(NO_STRING), |value| strings.intern(value))?,
827 );
828 put_u32(
829 &mut record,
830 8,
831 runner.map_or(Ok(NO_STRING), |value| strings.intern(value))?,
832 );
833 put_u32(&mut record, 12, strings.intern(&decision.meta.id)?);
834 put_u32(&mut record, 16, strings.intern(&decision.meta.file)?);
835 put_u32(&mut record, 20, strings.intern(&decision.meta.kind)?);
836 put_u32(
837 &mut record,
838 24,
839 strings.intern(
840 &decision
841 .meta
842 .source
843 .split_whitespace()
844 .collect::<Vec<_>>()
845 .join(" "),
846 )?,
847 );
848 put_u64(&mut record, 32, usize_u64(decision.meta.line)?);
849 put_u64(&mut record, 40, usize_u64(decision.meta.column)?);
850 put_u64(&mut record, 48, usize_u64(decision.meta.conditions.len())?);
851 put_u64(
852 &mut record,
853 56,
854 usize_u64(witnesses.iter().filter(|witness| witness.is_none()).count())?,
855 );
856 Ok(record)
857}
858
859fn put_summary_payload(
860 record: &mut [u8],
861 flags_offset: usize,
862 base: usize,
863 summary: &CoverageSummary,
864) -> Result<(), CoverageIndexError> {
865 record[flags_offset] = u8::from(summary.coverage_complete);
866 record[flags_offset + 1] = match summary.completeness_blocked {
867 None => 0,
868 Some(false) => 1,
869 Some(true) => 2,
870 };
871 for (index, value) in [
872 summary.decisions,
873 summary.executed_decisions,
874 summary.covered_decisions,
875 summary.conditions,
876 summary.covered_conditions,
877 ]
878 .into_iter()
879 .enumerate()
880 {
881 put_u64(record, base + index * 8, usize_u64(value)?);
882 }
883 for (index, count) in [
884 &summary.lines,
885 &summary.statements,
886 &summary.functions,
887 &summary.branches,
888 &summary.decision_outcomes,
889 &summary.condition_outcomes,
890 &summary.value_selections,
891 ]
892 .into_iter()
893 .enumerate()
894 {
895 put_count(record, base + 40 + index * 16, count)?;
896 }
897 Ok(())
898}
899
900fn dimension_record(
901 view_id: CoverageViewId,
902 dimension: CoverageDimension,
903 value: &crate::coverage_report::DimensionCoverage,
904 strings: &mut StringTable,
905) -> Result<[u8; DIMENSION_RECORD_SIZE], CoverageIndexError> {
906 let mut record = [0_u8; DIMENSION_RECORD_SIZE];
907 record[0] = view_id as u8;
908 record[1] = dimension as u8;
909 let name = match dimension {
910 CoverageDimension::Kind => value.kind.as_deref(),
911 CoverageDimension::Runner => value.runner.as_deref(),
912 }
913 .ok_or(CoverageIndexError::InvalidRecord("dimension name"))?;
914 put_u32(&mut record, 4, strings.intern(name)?);
915 put_u64(&mut record, 8, usize_u64(value.tests)?);
916 put_u64(&mut record, 16, usize_u64(value.setups)?);
917 put_u64(&mut record, 184, usize_u64(value.attributed)?);
921 put_summary_payload(&mut record, 24, 32, &value.summary)?;
922 Ok(record)
923}
924
925#[derive(Debug, Clone, Copy, PartialEq, Eq)]
926enum ScopeKind {
927 SourceDiscovery = 1,
928 Compiler = 2,
929}
930
931struct ScopeProjection<'a> {
932 kind: ScopeKind,
933 language: &'a str,
934 model: &'a str,
935 mode: Option<&'a str>,
936 roots: Vec<String>,
937 unit: Option<&'a str>,
938 measurement_complete: Option<bool>,
939 entries: Option<&'a Vec<serde_json::Value>>,
940}
941
942fn scope_projection<'a>(
943 scope: Option<&'a serde_json::Value>,
944 coverage_language: &'a str,
945 coverage_model: &'a str,
946) -> Result<Option<ScopeProjection<'a>>, CoverageIndexError> {
947 let Some(scope) = scope else {
948 return Ok(None);
949 };
950 let object = scope
951 .as_object()
952 .ok_or(CoverageIndexError::InvalidRecord("coverage scope"))?;
953 if let Some(mode) = object.get("mode") {
954 let mode = mode
955 .as_str()
956 .ok_or(CoverageIndexError::InvalidRecord("source-scope mode"))?;
957 let roots = object
958 .get("roots")
959 .and_then(serde_json::Value::as_array)
960 .ok_or(CoverageIndexError::InvalidRecord("source-scope roots"))?
961 .iter()
962 .map(|root| {
963 root.as_str()
964 .map(str::to_owned)
965 .ok_or(CoverageIndexError::InvalidRecord("source-scope root"))
966 })
967 .collect::<Result<Vec<_>, _>>()?;
968 let entries = object
969 .get("entries")
970 .and_then(serde_json::Value::as_array)
971 .ok_or(CoverageIndexError::InvalidRecord("source-scope entries"))?;
972 return Ok(Some(ScopeProjection {
973 kind: ScopeKind::SourceDiscovery,
974 language: coverage_language,
975 model: coverage_model,
976 mode: Some(mode),
977 roots,
978 unit: None,
979 measurement_complete: None,
980 entries: Some(entries),
981 }));
982 }
983
984 let language = object
985 .get("language")
986 .and_then(serde_json::Value::as_str)
987 .ok_or(CoverageIndexError::InvalidRecord("compiler-scope language"))?;
988 let model = object
989 .get("model")
990 .and_then(serde_json::Value::as_str)
991 .ok_or(CoverageIndexError::InvalidRecord("compiler-scope model"))?;
992 let unit = object
993 .get("crate")
994 .and_then(serde_json::Value::as_str)
995 .ok_or(CoverageIndexError::InvalidRecord("compiler-scope unit"))?;
996 let measurement_complete = object
997 .get("measurementComplete")
998 .and_then(serde_json::Value::as_bool)
999 .ok_or(CoverageIndexError::InvalidRecord(
1000 "compiler-scope measurement completeness",
1001 ))?;
1002 Ok(Some(ScopeProjection {
1003 kind: ScopeKind::Compiler,
1004 language,
1005 model,
1006 mode: None,
1007 roots: Vec::new(),
1008 unit: Some(unit),
1009 measurement_complete: Some(measurement_complete),
1010 entries: None,
1011 }))
1012}
1013
1014fn projection_record(
1015 view_id: CoverageViewId,
1016 view: &CoverageView,
1017 selected: Option<&BTreeSet<String>>,
1018 kind: Option<&str>,
1019 runner: Option<&str>,
1020 strings: &mut StringTable,
1021 relations: &mut StringRelations,
1022) -> Result<[u8; PROJECTION_RECORD_SIZE], CoverageIndexError> {
1023 let mut record = [0_u8; PROJECTION_RECORD_SIZE];
1024 record[0] = view_id as u8;
1025 put_u32(
1026 &mut record,
1027 4,
1028 kind.map_or(Ok(NO_STRING), |value| strings.intern(value))?,
1029 );
1030 put_u32(
1031 &mut record,
1032 8,
1033 runner.map_or(Ok(NO_STRING), |value| strings.intern(value))?,
1034 );
1035 put_u32(&mut record, 12, strings.intern(&view.generated_at)?);
1036
1037 let scope = scope_projection(view.scope.as_ref(), &view.model.language, &view.model.name)?;
1038 record[2] = u8::from(scope.is_some());
1039 record[3] = scope.as_ref().map_or(0, |scope| scope.kind as u8);
1040 put_u32(
1041 &mut record,
1042 16,
1043 scope
1044 .as_ref()
1045 .and_then(|scope| scope.mode)
1046 .map_or(Ok(NO_STRING), |value| strings.intern(value))?,
1047 );
1048 let roots = scope
1049 .as_ref()
1050 .map_or_else(Vec::new, |scope| scope.roots.clone());
1051 let (roots_offset, roots_count) = relations.push(roots, strings)?;
1052 put_u64(&mut record, 24, roots_offset);
1053 put_u32(
1054 &mut record,
1055 32,
1056 u32::try_from(roots_count).map_err(|_| CoverageIndexError::SizeOverflow)?,
1057 );
1058
1059 let summary = selected.map_or_else(
1060 || Ok(view.summary.clone()),
1061 |ids| {
1062 coverage_summary_for_tests(view, ids)
1063 .map_err(|_| CoverageIndexError::InvalidRecord("projection summary"))
1064 },
1065 )?;
1066 put_summary_payload(&mut record, 36, 40, &summary)?;
1067
1068 let mut limitation_kinds = [0_usize; 3];
1069 let mut limitation_files = BTreeSet::new();
1070 for limitation in &view.limitations {
1071 if let Some((file, kind)) = limitation_kind(limitation) {
1072 limitation_files.insert(file);
1073 match kind {
1074 "dynamic-code" => limitation_kinds[0] += 1,
1075 "semantic-safety" => limitation_kinds[1] += 1,
1076 "source-scope" => limitation_kinds[2] += 1,
1077 _ => {}
1078 }
1079 }
1080 }
1081 let corrupt_records = view
1082 .transport
1083 .as_ref()
1084 .map_or(0, |value| value.corrupt_records);
1085 let corrupt_files = view
1086 .transport
1087 .as_ref()
1088 .map_or(0, |value| value.corrupt_files);
1089 let declared = view
1092 .limitations
1093 .iter()
1094 .filter(|limitation| !crate::coverage_report::blocking_limitation(limitation))
1095 .count();
1096 for (offset, value) in [
1097 (192, view.limitations.len()),
1098 (200, corrupt_records),
1099 (208, view.limitations.len() - declared + corrupt_records),
1100 (528, declared),
1101 (216, limitation_files.len() + corrupt_files),
1102 (224, limitation_kinds[0]),
1103 (232, limitation_kinds[1]),
1104 (240, limitation_kinds[2]),
1105 ] {
1106 put_u64(&mut record, offset, usize_u64(value)?);
1107 }
1108
1109 let phases = view
1110 .phases
1111 .iter()
1112 .filter(|phase| selected.is_none_or(|selected| selected.contains(&phase.test)));
1113 let mut attribution = [0_usize; 4];
1114 for phase in phases {
1115 attribution[0] += phase.explicit_browser_events;
1116 attribution[1] += phase.inferred_browser_events;
1117 attribution[2] += phase.explicit_server_events;
1118 attribution[3] += phase.inferred_server_events;
1119 }
1120 for (index, value) in attribution.into_iter().enumerate() {
1121 put_u64(&mut record, 248 + index * 8, usize_u64(value)?);
1122 }
1123
1124 let confidence_levels = ["unexecuted", "executed", "action", "asserted"];
1125 for (index, level) in confidence_levels.into_iter().enumerate() {
1126 put_u64(
1127 &mut record,
1128 280 + index * 8,
1129 usize_u64(
1130 view.lines
1131 .iter()
1132 .filter(|line| line.confidence.level == level)
1133 .count(),
1134 )?,
1135 );
1136 }
1137 put_u64(
1138 &mut record,
1139 312,
1140 usize_u64(
1141 view.decisions
1142 .iter()
1143 .flat_map(|decision| &decision.conditions)
1144 .filter(|condition| condition.assertion_covered)
1145 .count(),
1146 )?,
1147 );
1148
1149 let gaps = file_gaps(view, selected)?;
1150 put_u64(
1151 &mut record,
1152 320,
1153 usize_u64(
1154 gaps.iter()
1155 .filter(|(_, gap)| {
1156 gap.uncovered_lines > 0
1157 || gap.uncovered_statements > 0
1158 || gap.uncovered_functions > 0
1159 || gap.missing_branches > 0
1160 || gap.missing_mcdc_conditions > 0
1161 || gap.measurement_limitations > 0
1162 })
1163 .count(),
1164 )?,
1165 );
1166 put_u64(
1167 &mut record,
1168 328,
1169 usize_u64(
1170 gaps.iter()
1171 .filter(|(_, gap)| {
1172 gap.uncovered_lines > 0
1173 || gap.uncovered_statements > 0
1174 || gap.uncovered_functions > 0
1175 || gap.missing_branches > 0
1176 || gap.missing_mcdc_conditions > 0
1177 })
1178 .count(),
1179 )?,
1180 );
1181
1182 let selected_tests = view
1183 .tests
1184 .iter()
1185 .filter(|test| selected.is_none_or(|selected| selected.contains(&test.id)))
1186 .collect::<Vec<_>>();
1187 put_u64(
1188 &mut record,
1189 336,
1190 usize_u64(
1191 selected_tests
1192 .iter()
1193 .filter(|test| test.role == "test")
1194 .count(),
1195 )?,
1196 );
1197 put_u64(
1198 &mut record,
1199 344,
1200 usize_u64(
1201 selected_tests
1202 .iter()
1203 .filter(|test| test.role == "setup")
1204 .count(),
1205 )?,
1206 );
1207 for (index, outcome) in [
1208 "passed",
1209 "failed",
1210 "flaky",
1211 "skipped",
1212 "timedOut",
1213 "interrupted",
1214 "unknown",
1215 ]
1216 .into_iter()
1217 .enumerate()
1218 {
1219 put_u64(
1220 &mut record,
1221 352 + index * 8,
1222 usize_u64(
1223 selected_tests
1224 .iter()
1225 .filter(|test| test.role == "test" && test.outcome == outcome)
1226 .count(),
1227 )?,
1228 );
1229 }
1230 put_u64(
1231 &mut record,
1232 520,
1233 usize_u64(
1234 selected_tests
1235 .iter()
1236 .filter(|test| test.role == "test" && test.outcome == "unstarted")
1237 .count(),
1238 )?,
1239 );
1240
1241 if let Some(transport) = &view.transport {
1242 record[1] = 1;
1243 for (index, value) in [
1244 transport.processes,
1245 transport.child_launches,
1246 transport.remote_launches,
1247 transport.workspace_capabilities,
1248 transport.scoped_server_records,
1249 transport.background_server_records,
1250 transport.corrupt_records,
1251 transport.corrupt_files,
1252 ]
1253 .into_iter()
1254 .enumerate()
1255 {
1256 put_u64(&mut record, 408 + index * 8, usize_u64(value)?);
1257 }
1258 }
1259
1260 let phase_tests = view
1261 .phases
1262 .iter()
1263 .map(|phase| phase.test.as_str())
1264 .collect::<BTreeSet<_>>();
1265 let empty_tests = selected_tests
1266 .iter()
1267 .filter(|test| {
1268 test.role == "test"
1269 && test.lines.is_empty()
1270 && test.hits.is_empty()
1271 && test.decisions.is_empty()
1272 && phase_tests.contains(test.id.as_str())
1273 })
1274 .collect::<Vec<_>>();
1275 put_u64(&mut record, 472, usize_u64(empty_tests.len())?);
1276 put_u32(
1277 &mut record,
1278 20,
1279 empty_tests
1280 .first()
1281 .map_or(Ok(NO_STRING), |test| strings.intern(&test.name))?,
1282 );
1283
1284 let entries = scope.as_ref().and_then(|scope| scope.entries);
1285 for (index, status) in ["included", "excluded", "ambiguous"]
1286 .into_iter()
1287 .enumerate()
1288 {
1289 put_u64(
1290 &mut record,
1291 480 + index * 8,
1292 usize_u64(entries.map_or(0, |entries| {
1293 entries
1294 .iter()
1295 .filter(|entry| {
1296 entry.get("status").and_then(serde_json::Value::as_str) == Some(status)
1297 })
1298 .count()
1299 }))?,
1300 );
1301 }
1302 put_u32(
1303 &mut record,
1304 504,
1305 scope
1306 .as_ref()
1307 .map_or(Ok(NO_STRING), |scope| strings.intern(scope.language))?,
1308 );
1309 put_u32(
1310 &mut record,
1311 508,
1312 scope
1313 .as_ref()
1314 .map_or(Ok(NO_STRING), |scope| strings.intern(scope.model))?,
1315 );
1316 put_u32(
1317 &mut record,
1318 512,
1319 scope
1320 .as_ref()
1321 .and_then(|scope| scope.unit)
1322 .map_or(Ok(NO_STRING), |value| strings.intern(value))?,
1323 );
1324 if let Some(measurement_complete) = scope.as_ref().and_then(|scope| scope.measurement_complete)
1325 {
1326 record[516] = 1;
1327 record[517] = u8::from(measurement_complete);
1328 }
1329 Ok(record)
1330}
1331
1332fn scope_entry_records(
1333 view_id: CoverageViewId,
1334 view: &CoverageView,
1335 strings: &mut StringTable,
1336) -> Result<Vec<[u8; SCOPE_ENTRY_RECORD_SIZE]>, CoverageIndexError> {
1337 let Some(scope) = &view.scope else {
1338 return Ok(Vec::new());
1339 };
1340 if scope.get("mode").is_none() {
1341 return Ok(Vec::new());
1342 }
1343 let entries = scope
1344 .get("entries")
1345 .and_then(serde_json::Value::as_array)
1346 .ok_or(CoverageIndexError::InvalidRecord("source-scope entries"))?;
1347 let mut limitations = BTreeMap::<&str, (usize, u32)>::new();
1348 for limitation in &view.limitations {
1349 let Some((file, kind)) = limitation_kind(limitation) else {
1350 return Err(CoverageIndexError::InvalidRecord("coverage limitation"));
1351 };
1352 let value = limitations.entry(file).or_default();
1353 value.0 += 1;
1354 value.1 |= match kind {
1355 "dynamic-code" => 1,
1356 "semantic-safety" => 2,
1357 "source-scope" => 4,
1358 _ => {
1359 return Err(CoverageIndexError::InvalidRecord(
1360 "coverage limitation kind",
1361 ));
1362 }
1363 };
1364 }
1365 entries
1366 .iter()
1367 .map(|entry| {
1368 let file = entry
1369 .get("file")
1370 .and_then(serde_json::Value::as_str)
1371 .ok_or(CoverageIndexError::InvalidRecord("source-scope file"))?;
1372 let status = entry
1373 .get("status")
1374 .and_then(serde_json::Value::as_str)
1375 .ok_or(CoverageIndexError::InvalidRecord("source-scope status"))?;
1376 let reason = entry
1377 .get("reason")
1378 .and_then(serde_json::Value::as_str)
1379 .ok_or(CoverageIndexError::InvalidRecord("source-scope reason"))?;
1380 let package_root = entry
1381 .get("packageRoot")
1382 .map(|value| {
1383 value.as_str().ok_or(CoverageIndexError::InvalidRecord(
1384 "source-scope package root",
1385 ))
1386 })
1387 .transpose()?;
1388 let mut record = [0_u8; SCOPE_ENTRY_RECORD_SIZE];
1389 record[0] = view_id as u8;
1390 record[1] = match status {
1391 "included" => 0,
1392 "excluded" => 1,
1393 "ambiguous" => 2,
1394 _ => return Err(CoverageIndexError::InvalidRecord("source-scope status")),
1395 };
1396 put_u32(&mut record, 4, strings.intern(file)?);
1397 put_u32(&mut record, 8, strings.intern(reason)?);
1398 put_u32(
1399 &mut record,
1400 12,
1401 package_root.map_or(Ok(NO_STRING), |value| strings.intern(value))?,
1402 );
1403 let (count, mask) = limitations.get(file).copied().unwrap_or_default();
1404 put_u64(&mut record, 16, usize_u64(count)?);
1405 put_u32(&mut record, 24, mask);
1406 Ok(record)
1407 })
1408 .collect()
1409}
1410
1411fn optional_string_id(
1412 value: Option<&str>,
1413 strings: &mut StringTable,
1414) -> Result<u32, CoverageIndexError> {
1415 value.map_or(Ok(NO_STRING), |value| strings.intern(value))
1416}
1417
1418fn confidence_record(
1419 confidence: &crate::coverage_report::CoverageConfidence,
1420 strings: &mut StringTable,
1421 relations: &mut StringRelations,
1422) -> Result<[u8; CONFIDENCE_RECORD_SIZE], CoverageIndexError> {
1423 let mut record = [0_u8; CONFIDENCE_RECORD_SIZE];
1424 record[0] = match confidence.level.as_str() {
1425 "unexecuted" => 0,
1426 "executed" => 1,
1427 "action" => 2,
1428 "asserted" => 3,
1429 _ => return Err(CoverageIndexError::InvalidRecord("confidence level")),
1430 };
1431 record[1] = u8::from(confidence.setup_only)
1432 | (u8::from(confidence.background_only) << 1)
1433 | (u8::from(confidence.asserted) << 2)
1434 | (u8::from(confidence.e2e) << 3);
1435 for (index, values) in [
1436 confidence.tests.clone(),
1437 confidence.asserted_tests.clone(),
1438 confidence.runners.clone(),
1439 confidence.kinds.clone(),
1440 ]
1441 .into_iter()
1442 .enumerate()
1443 {
1444 let (offset, count) = relations.push(values, strings)?;
1445 put_u64(&mut record, 8 + index * 16, offset);
1446 put_u64(&mut record, 16 + index * 16, count);
1447 }
1448 Ok(record)
1449}
1450
1451fn line_record(
1452 view_id: CoverageViewId,
1453 line: &crate::coverage_report::LineResult,
1454 confidence_index: usize,
1455 strings: &mut StringTable,
1456 relations: &mut StringRelations,
1457) -> Result<[u8; LINE_RECORD_SIZE], CoverageIndexError> {
1458 let mut record = [0_u8; LINE_RECORD_SIZE];
1459 record[0] = view_id as u8;
1460 record[1] = u8::from(line.covered);
1461 record[2] = u8::from(!line.measured);
1464 put_u32(&mut record, 4, strings.intern(&line.file)?);
1465 put_u64(&mut record, 8, usize_u64(line.line)?);
1466 let (tests_offset, tests_count) = relations.push(line.tests.clone(), strings)?;
1467 put_u64(&mut record, 16, tests_offset);
1468 put_u64(&mut record, 24, tests_count);
1469 let (phases_offset, phases_count) = relations.push(line.phases.clone(), strings)?;
1470 put_u64(&mut record, 32, phases_offset);
1471 put_u64(&mut record, 40, phases_count);
1472 put_u64(&mut record, 48, usize_u64(confidence_index)?);
1473 Ok(record)
1474}
1475
1476fn test_summary_record(
1477 view_id: CoverageViewId,
1478 test: &crate::coverage_report::TestCoverageResult,
1479 strings: &mut StringTable,
1480) -> Result<[u8; TEST_SUMMARY_RECORD_SIZE], CoverageIndexError> {
1481 let mut record = [0_u8; TEST_SUMMARY_RECORD_SIZE];
1482 record[0] = view_id as u8;
1483 record[1] = match test.role.as_str() {
1484 "test" => 0,
1485 "setup" => 1,
1486 "background" => 2,
1487 _ => return Err(CoverageIndexError::InvalidRecord("test role")),
1488 };
1489 record[3] = match test.attribution.as_str() {
1493 crate::coverage_report::ATTRIBUTION_EXACT => 0,
1494 crate::coverage_report::ATTRIBUTION_RUN_WIDE => 1,
1495 crate::coverage_report::ATTRIBUTION_PARTIAL => 2,
1496 _ => return Err(CoverageIndexError::InvalidRecord("test attribution")),
1497 };
1498 record[2] = match test.outcome.as_str() {
1499 "passed" => 0,
1500 "failed" => 1,
1501 "flaky" => 2,
1502 "skipped" => 3,
1503 "timedOut" => 4,
1504 "interrupted" => 5,
1505 "unknown" => 6,
1506 "unstarted" => 7,
1507 _ => return Err(CoverageIndexError::InvalidRecord("test outcome")),
1508 };
1509 put_u32(&mut record, 4, strings.intern(&test.id)?);
1510 put_u32(&mut record, 8, strings.intern(&test.name)?);
1511 put_u32(
1512 &mut record,
1513 12,
1514 optional_string_id(test.file.as_deref(), strings)?,
1515 );
1516 put_u32(
1517 &mut record,
1518 16,
1519 optional_string_id(test.title.as_deref(), strings)?,
1520 );
1521 put_u32(&mut record, 20, strings.intern(&test.provenance.runner)?);
1522 put_u32(&mut record, 24, strings.intern(&test.provenance.kind)?);
1523 put_u32(
1524 &mut record,
1525 28,
1526 optional_string_id(test.provenance.project.as_deref(), strings)?,
1527 );
1528 put_u32(&mut record, 32, strings.intern(&test.provenance.source)?);
1529 Ok(record)
1530}
1531
1532fn phase_summary_record(
1533 view_id: CoverageViewId,
1534 phase: &crate::coverage_report::PhaseResult,
1535 strings: &mut StringTable,
1536) -> Result<[u8; PHASE_SUMMARY_RECORD_SIZE], CoverageIndexError> {
1537 let mut record = [0_u8; PHASE_SUMMARY_RECORD_SIZE];
1538 record[0] = view_id as u8;
1539 put_u32(&mut record, 4, strings.intern(&phase.phase.id)?);
1540 put_u32(&mut record, 8, strings.intern(&phase.phase.kind)?);
1541 put_u32(&mut record, 12, strings.intern(&phase.phase.operation)?);
1542 put_u32(
1543 &mut record,
1544 16,
1545 optional_string_id(phase.phase.source.as_deref(), strings)?,
1546 );
1547 put_u32(&mut record, 20, strings.intern(&phase.test)?);
1548 put_u32(
1549 &mut record,
1550 24,
1551 optional_string_id(phase.phase.status.as_deref(), strings)?,
1552 );
1553 put_u32(
1554 &mut record,
1555 28,
1556 optional_string_id(phase.phase.caused_by_phase_id.as_deref(), strings)?,
1557 );
1558 put_u64(&mut record, 32, usize_u64(phase.lines.len())?);
1559 put_u64(
1560 &mut record,
1561 40,
1562 usize_u64(
1563 phase
1564 .decisions
1565 .iter()
1566 .map(|decision| decision.vectors.len())
1567 .sum(),
1568 )?,
1569 );
1570 Ok(record)
1571}
1572
1573struct AnchorInput<'a> {
1574 view_id: CoverageViewId,
1575 kind: u8,
1576 id: &'a str,
1577 file: &'a str,
1578 line: usize,
1579 column: usize,
1580 covered: bool,
1581 conditions: Option<(usize, usize)>,
1582 tests: &'a [String],
1583}
1584
1585fn anchor_record(
1586 input: AnchorInput<'_>,
1587 strings: &mut StringTable,
1588 relations: &mut StringRelations,
1589) -> Result<[u8; ANCHOR_RECORD_SIZE], CoverageIndexError> {
1590 let mut record = [0_u8; ANCHOR_RECORD_SIZE];
1591 record[0] = input.view_id as u8;
1592 record[1] = input.kind;
1593 record[2] = u8::from(input.covered);
1594 put_u32(&mut record, 4, strings.intern(input.id)?);
1595 put_u32(&mut record, 8, strings.intern(input.file)?);
1596 put_u64(&mut record, 16, usize_u64(input.line)?);
1597 put_u64(&mut record, 24, usize_u64(input.column)?);
1598 if let Some((covered, total)) = input.conditions {
1599 put_u64(&mut record, 32, usize_u64(total)?);
1600 put_u64(&mut record, 40, usize_u64(covered)?);
1601 }
1602 let (tests_offset, tests_count) = relations.push(input.tests.iter().cloned(), strings)?;
1603 put_u64(&mut record, 48, tests_offset);
1604 put_u64(&mut record, 56, tests_count);
1605 Ok(record)
1606}
1607
1608fn test_retry_record(
1609 view_id: CoverageViewId,
1610 test_id: &str,
1611 retry: usize,
1612 strings: &mut StringTable,
1613) -> Result<[u8; TEST_RETRY_RECORD_SIZE], CoverageIndexError> {
1614 let mut record = [0_u8; TEST_RETRY_RECORD_SIZE];
1615 record[0] = view_id as u8;
1616 put_u32(&mut record, 4, strings.intern(test_id)?);
1617 put_u64(&mut record, 8, usize_u64(retry)?);
1618 Ok(record)
1619}
1620
1621fn test_attempt_record(
1622 view_id: CoverageViewId,
1623 test_id: &str,
1624 attempt: &crate::coverage_report::TestAttempt,
1625 strings: &mut StringTable,
1626) -> Result<[u8; TEST_ATTEMPT_RECORD_SIZE], CoverageIndexError> {
1627 let mut record = [0_u8; TEST_ATTEMPT_RECORD_SIZE];
1628 record[0] = view_id as u8;
1629 put_u32(&mut record, 4, strings.intern(test_id)?);
1630 put_u64(&mut record, 8, usize_u64(attempt.retry)?);
1631 put_u32(&mut record, 16, strings.intern(&attempt.status)?);
1632 put_u32(
1633 &mut record,
1634 20,
1635 optional_string_id(attempt.expected_status.as_deref(), strings)?,
1636 );
1637 Ok(record)
1638}
1639
1640fn test_line_record(
1641 view_id: CoverageViewId,
1642 test_id: &str,
1643 line: &crate::coverage_report::SourceLine,
1644 strings: &mut StringTable,
1645) -> Result<[u8; TEST_LINE_RECORD_SIZE], CoverageIndexError> {
1646 let mut record = [0_u8; TEST_LINE_RECORD_SIZE];
1647 record[0] = view_id as u8;
1648 put_u32(&mut record, 4, strings.intern(test_id)?);
1649 put_u32(&mut record, 8, strings.intern(&line.file)?);
1650 put_u64(&mut record, 16, usize_u64(line.line)?);
1651 Ok(record)
1652}
1653
1654fn test_hit_record(
1655 view_id: CoverageViewId,
1656 test_id: &str,
1657 hit: &str,
1658 strings: &mut StringTable,
1659) -> Result<[u8; TEST_HIT_RECORD_SIZE], CoverageIndexError> {
1660 let mut record = [0_u8; TEST_HIT_RECORD_SIZE];
1661 record[0] = view_id as u8;
1662 put_u32(&mut record, 4, strings.intern(test_id)?);
1663 put_u32(&mut record, 8, strings.intern(hit)?);
1664 Ok(record)
1665}
1666
1667fn test_vector_record(
1668 vector: &McdcVector,
1669 values: &mut Vec<u8>,
1670) -> Result<[u8; TEST_VECTOR_RECORD_SIZE], CoverageIndexError> {
1671 let mut record = [0_u8; TEST_VECTOR_RECORD_SIZE];
1672 record[0] = u8::from(vector.outcome);
1673 put_u64(&mut record, 8, usize_u64(values.len())?);
1674 put_u64(&mut record, 16, usize_u64(vector.values.len())?);
1675 values.extend(vector.values.iter().map(|value| match value {
1676 None => 0,
1677 Some(false) => 1,
1678 Some(true) => 2,
1679 }));
1680 Ok(record)
1681}
1682
1683fn test_decision_record(
1684 view_id: CoverageViewId,
1685 test_id: &str,
1686 decision_id: &str,
1687 vectors_offset: usize,
1688 vectors_count: usize,
1689 strings: &mut StringTable,
1690) -> Result<[u8; TEST_DECISION_RECORD_SIZE], CoverageIndexError> {
1691 let mut record = [0_u8; TEST_DECISION_RECORD_SIZE];
1692 record[0] = view_id as u8;
1693 put_u32(&mut record, 4, strings.intern(test_id)?);
1694 put_u32(&mut record, 8, strings.intern(decision_id)?);
1695 put_u64(&mut record, 16, usize_u64(vectors_offset)?);
1696 put_u64(&mut record, 24, usize_u64(vectors_count)?);
1697 Ok(record)
1698}
1699
1700struct HitMetadataInput<'a> {
1701 view_id: CoverageViewId,
1702 kind: u8,
1703 id: &'a str,
1704 file: &'a str,
1705 line: usize,
1706 column: usize,
1707 branch_kind: Option<&'a str>,
1708 label: Option<&'a str>,
1709 alternative: Option<&'a str>,
1710 source: &'a str,
1711 tests: &'a [String],
1712}
1713
1714fn hit_metadata_record(
1715 input: HitMetadataInput<'_>,
1716 strings: &mut StringTable,
1717 relations: &mut StringRelations,
1718) -> Result<[u8; HIT_METADATA_RECORD_SIZE], CoverageIndexError> {
1719 let mut record = [0_u8; HIT_METADATA_RECORD_SIZE];
1720 record[0] = input.view_id as u8;
1721 record[1] = input.kind;
1722 put_u32(&mut record, 4, strings.intern(input.id)?);
1723 put_u32(&mut record, 8, strings.intern(input.file)?);
1724 put_u64(&mut record, 16, usize_u64(input.line)?);
1725 put_u64(&mut record, 24, usize_u64(input.column)?);
1726 put_u32(
1727 &mut record,
1728 32,
1729 optional_string_id(input.branch_kind, strings)?,
1730 );
1731 put_u32(&mut record, 36, optional_string_id(input.label, strings)?);
1732 put_u32(
1733 &mut record,
1734 40,
1735 optional_string_id(input.alternative, strings)?,
1736 );
1737 put_u32(&mut record, 44, strings.intern(input.source)?);
1738 let (tests_offset, tests_count) = relations.push(input.tests.iter().cloned(), strings)?;
1739 put_u64(&mut record, 48, tests_offset);
1740 put_u64(&mut record, 56, tests_count);
1741 Ok(record)
1742}
1743
1744fn limitation_record(
1745 view_id: CoverageViewId,
1746 limitation: &serde_json::Value,
1747 strings: &mut StringTable,
1748) -> Result<[u8; LIMITATION_RECORD_SIZE], CoverageIndexError> {
1749 let field = |name| {
1750 limitation
1751 .get(name)
1752 .and_then(serde_json::Value::as_str)
1753 .ok_or(CoverageIndexError::InvalidRecord("coverage limitation"))
1754 };
1755 let number = |name| {
1756 limitation
1757 .get(name)
1758 .and_then(serde_json::Value::as_u64)
1759 .ok_or(CoverageIndexError::InvalidRecord("coverage limitation"))
1760 };
1761 let mut record = [0_u8; LIMITATION_RECORD_SIZE];
1762 record[0] = view_id as u8;
1763 record[1] = u8::from(crate::coverage_report::blocking_limitation(limitation));
1764 put_u32(&mut record, 4, strings.intern(field("id")?)?);
1765 put_u32(&mut record, 8, strings.intern(field("kind")?)?);
1766 put_u32(&mut record, 12, strings.intern(field("file")?)?);
1767 put_u32(&mut record, 16, strings.intern(field("source")?)?);
1768 put_u32(&mut record, 20, strings.intern(field("reason")?)?);
1769 put_u64(&mut record, 24, number("line")?);
1770 put_u64(&mut record, 32, number("column")?);
1771 Ok(record)
1772}
1773
1774fn decision_metadata_record(
1775 view_id: CoverageViewId,
1776 decision: &crate::coverage_report::DecisionResult,
1777 strings: &mut StringTable,
1778 relations: &mut StringRelations,
1779) -> Result<[u8; DECISION_METADATA_RECORD_SIZE], CoverageIndexError> {
1780 let mut record = [0_u8; DECISION_METADATA_RECORD_SIZE];
1781 record[0] = view_id as u8;
1782 put_u32(&mut record, 4, strings.intern(&decision.meta.id)?);
1783 put_u32(&mut record, 8, strings.intern(&decision.meta.file)?);
1784 put_u32(&mut record, 12, strings.intern(&decision.meta.source)?);
1785 put_u32(&mut record, 16, strings.intern(&decision.meta.kind)?);
1786 put_u64(&mut record, 24, usize_u64(decision.meta.line)?);
1787 put_u64(&mut record, 32, usize_u64(decision.meta.column)?);
1788 let (conditions_offset, conditions_count) =
1789 relations.push(decision.meta.conditions.clone(), strings)?;
1790 put_u64(&mut record, 40, conditions_offset);
1791 put_u64(&mut record, 48, conditions_count);
1792 Ok(record)
1793}
1794
1795fn decision_vector_observation_record(
1796 observation: &crate::coverage_report::VectorObservation,
1797 confidence_index: usize,
1798 vector_index: usize,
1799 strings: &mut StringTable,
1800 relations: &mut StringRelations,
1801) -> Result<[u8; DECISION_VECTOR_OBSERVATION_RECORD_SIZE], CoverageIndexError> {
1802 let mut record = [0_u8; DECISION_VECTOR_OBSERVATION_RECORD_SIZE];
1803 put_u64(&mut record, 0, usize_u64(confidence_index)?);
1804 put_u64(&mut record, 8, usize_u64(vector_index)?);
1805 for (offset, values) in [
1806 (16, observation.tests.clone()),
1807 (32, observation.phases.clone()),
1808 (48, observation.explicit_phases.clone()),
1809 ] {
1810 let (relation_offset, relation_count) = relations.push(values, strings)?;
1811 put_u64(&mut record, offset, relation_offset);
1812 put_u64(&mut record, offset + 8, relation_count);
1813 }
1814 Ok(record)
1815}
1816
1817fn decision_condition_record(
1818 condition: &crate::coverage_report::ConditionResult,
1819 witness_vectors: Option<(usize, usize)>,
1820 strings: &mut StringTable,
1821 relations: &mut StringRelations,
1822) -> Result<[u8; DECISION_CONDITION_RECORD_SIZE], CoverageIndexError> {
1823 let mut record = [0_u8; DECISION_CONDITION_RECORD_SIZE];
1824 record[0] = u8::from(condition.covered)
1825 | (u8::from(condition.assertion_covered) << 1)
1826 | (u8::from(condition.witness.is_some()) << 2);
1827 put_u32(&mut record, 4, strings.intern(&condition.source)?);
1828 put_u64(&mut record, 8, usize_u64(condition.index)?);
1829 if let Some((first, second)) = witness_vectors {
1830 put_u64(&mut record, 16, usize_u64(first)?);
1831 put_u64(&mut record, 24, usize_u64(second)?);
1832 }
1833 let witness_tests = condition.witness_tests.clone().unwrap_or_default();
1834 for (offset, values) in [
1835 (32, witness_tests[0].clone()),
1836 (48, witness_tests[1].clone()),
1837 ] {
1838 let (relation_offset, relation_count) = relations.push(values, strings)?;
1839 put_u64(&mut record, offset, relation_offset);
1840 put_u64(&mut record, offset + 8, relation_count);
1841 }
1842 Ok(record)
1843}
1844
1845struct DecisionDetailInput<'a> {
1846 view_id: CoverageViewId,
1847 decision: &'a crate::coverage_report::DecisionResult,
1848 confidence_index: usize,
1849 observations: (usize, usize),
1850 conditions: (usize, usize),
1851}
1852
1853fn decision_detail_record(
1854 input: DecisionDetailInput<'_>,
1855 strings: &mut StringTable,
1856 relations: &mut StringRelations,
1857) -> Result<[u8; DECISION_DETAIL_RECORD_SIZE], CoverageIndexError> {
1858 let mut record = [0_u8; DECISION_DETAIL_RECORD_SIZE];
1859 record[0] = input.view_id as u8;
1860 record[1] = u8::from(input.decision.executed) | (u8::from(input.decision.covered) << 1);
1861 put_u32(&mut record, 4, strings.intern(&input.decision.meta.id)?);
1862 put_u64(&mut record, 8, usize_u64(input.confidence_index)?);
1863 let (tests_offset, tests_count) = relations.push(input.decision.tests.clone(), strings)?;
1864 put_u64(&mut record, 16, tests_offset);
1865 put_u64(&mut record, 24, tests_count);
1866 put_u64(&mut record, 32, usize_u64(input.observations.0)?);
1867 put_u64(&mut record, 40, usize_u64(input.observations.1)?);
1868 put_u64(&mut record, 48, usize_u64(input.conditions.0)?);
1869 put_u64(&mut record, 56, usize_u64(input.conditions.1)?);
1870 Ok(record)
1871}
1872
1873fn coverage_model_record(
1874 variant: &str,
1875 model: &CoverageModel,
1876 strings: &mut StringTable,
1877 relations: &mut StringRelations,
1878) -> Result<[u8; COVERAGE_MODEL_RECORD_SIZE], CoverageIndexError> {
1879 let mut record = [0_u8; COVERAGE_MODEL_RECORD_SIZE];
1880 put_u32(&mut record, 0, strings.intern(variant)?);
1881 put_u32(&mut record, 4, strings.intern(&model.name)?);
1882 put_u32(&mut record, 8, strings.intern(&model.completeness_meaning)?);
1883 let (measured_offset, measured_count) = relations.push(model.measured.clone(), strings)?;
1884 put_u64(&mut record, 16, measured_offset);
1885 put_u64(&mut record, 24, measured_count);
1886 let (not_measured_offset, not_measured_count) =
1887 relations.push(model.not_measured.clone(), strings)?;
1888 put_u64(&mut record, 32, not_measured_offset);
1889 put_u64(&mut record, 40, not_measured_count);
1890 Ok(record)
1891}
1892
1893pub fn coverage_index_sections(
1894 report: &CoverageReport,
1895) -> Result<Vec<QueryIndexSection>, CoverageIndexError> {
1896 let views = [
1897 (CoverageViewId::All, &report.view),
1898 (CoverageViewId::Passed, &report.filters.passed),
1899 (CoverageViewId::Failed, &report.filters.failed),
1900 ];
1901 let mut strings = StringTable::default();
1902 let mut relations = StringRelations::default();
1903 let mut summaries = Vec::with_capacity(views.len() * SUMMARY_RECORD_SIZE);
1904 let mut gaps = Vec::new();
1905 let mut decision_gaps = Vec::new();
1906 let mut dimensions = Vec::new();
1907 let mut projection_records = Vec::new();
1908 let mut scope_entries = Vec::new();
1909 let mut confidence_records = Vec::new();
1910 let mut line_records = Vec::new();
1911 let mut test_summaries = Vec::new();
1912 let mut phase_summaries = Vec::new();
1913 let mut anchors = Vec::new();
1914 let mut test_retries = Vec::new();
1915 let mut test_attempts = Vec::new();
1916 let mut test_lines = Vec::new();
1917 let mut test_hits = Vec::new();
1918 let mut test_decisions = Vec::new();
1919 let mut test_vectors = Vec::new();
1920 let mut vector_values = Vec::new();
1921 let mut hit_metadata = Vec::new();
1922 let mut decision_metadata = Vec::new();
1923 let mut decision_details = Vec::new();
1924 let mut decision_vector_observations = Vec::new();
1925 let mut decision_conditions = Vec::new();
1926 let mut limitations = Vec::new();
1927 for (id, view) in views {
1928 summaries.extend_from_slice(&summary_record(id, view, &mut strings)?);
1929 projection_records.extend_from_slice(&projection_record(
1930 id,
1931 view,
1932 None,
1933 None,
1934 None,
1935 &mut strings,
1936 &mut relations,
1937 )?);
1938 for entry in scope_entry_records(id, view, &mut strings)? {
1939 scope_entries.extend_from_slice(&entry);
1940 }
1941 for limitation in &view.limitations {
1942 limitations.extend_from_slice(&limitation_record(id, limitation, &mut strings)?);
1943 }
1944 for line in &view.lines {
1945 let confidence_index = confidence_records.len() / CONFIDENCE_RECORD_SIZE;
1946 confidence_records.extend_from_slice(&confidence_record(
1947 &line.confidence,
1948 &mut strings,
1949 &mut relations,
1950 )?);
1951 line_records.extend_from_slice(&line_record(
1952 id,
1953 line,
1954 confidence_index,
1955 &mut strings,
1956 &mut relations,
1957 )?);
1958 }
1959 for test in &view.tests {
1960 test_summaries.extend_from_slice(&test_summary_record(id, test, &mut strings)?);
1961 for retry in &test.retries {
1962 test_retries.extend_from_slice(&test_retry_record(
1963 id,
1964 &test.id,
1965 *retry,
1966 &mut strings,
1967 )?);
1968 }
1969 for attempt in &test.attempts {
1970 test_attempts.extend_from_slice(&test_attempt_record(
1971 id,
1972 &test.id,
1973 attempt,
1974 &mut strings,
1975 )?);
1976 }
1977 for line in &test.lines {
1978 test_lines.extend_from_slice(&test_line_record(id, &test.id, line, &mut strings)?);
1979 }
1980 for hit in &test.hits {
1981 test_hits.extend_from_slice(&test_hit_record(id, &test.id, hit, &mut strings)?);
1982 }
1983 for decision in &test.decisions {
1984 let vectors_offset = test_vectors.len() / TEST_VECTOR_RECORD_SIZE;
1985 for vector in &decision.vectors {
1986 test_vectors
1987 .extend_from_slice(&test_vector_record(vector, &mut vector_values)?);
1988 }
1989 test_decisions.extend_from_slice(&test_decision_record(
1990 id,
1991 &test.id,
1992 &decision.id,
1993 vectors_offset,
1994 decision.vectors.len(),
1995 &mut strings,
1996 )?);
1997 }
1998 }
1999 for phase in &view.phases {
2000 phase_summaries.extend_from_slice(&phase_summary_record(id, phase, &mut strings)?);
2001 }
2002 for decision in &view.decisions {
2003 let confidence_index = confidence_records.len() / CONFIDENCE_RECORD_SIZE;
2004 confidence_records.extend_from_slice(&confidence_record(
2005 &decision.confidence,
2006 &mut strings,
2007 &mut relations,
2008 )?);
2009 let observations_offset =
2010 decision_vector_observations.len() / DECISION_VECTOR_OBSERVATION_RECORD_SIZE;
2011 for observation in &decision.vector_observations {
2012 let observation_confidence = confidence_records.len() / CONFIDENCE_RECORD_SIZE;
2013 confidence_records.extend_from_slice(&confidence_record(
2014 &observation.confidence,
2015 &mut strings,
2016 &mut relations,
2017 )?);
2018 let vector_index = test_vectors.len() / TEST_VECTOR_RECORD_SIZE;
2019 test_vectors.extend_from_slice(&test_vector_record(
2020 &observation.vector,
2021 &mut vector_values,
2022 )?);
2023 decision_vector_observations.extend_from_slice(
2024 &decision_vector_observation_record(
2025 observation,
2026 observation_confidence,
2027 vector_index,
2028 &mut strings,
2029 &mut relations,
2030 )?,
2031 );
2032 }
2033 let conditions_offset = decision_conditions.len() / DECISION_CONDITION_RECORD_SIZE;
2034 for condition in &decision.conditions {
2035 let witness_vectors = if let Some(witness) = &condition.witness {
2036 let first = test_vectors.len() / TEST_VECTOR_RECORD_SIZE;
2037 test_vectors
2038 .extend_from_slice(&test_vector_record(&witness[0], &mut vector_values)?);
2039 let second = test_vectors.len() / TEST_VECTOR_RECORD_SIZE;
2040 test_vectors
2041 .extend_from_slice(&test_vector_record(&witness[1], &mut vector_values)?);
2042 Some((first, second))
2043 } else {
2044 None
2045 };
2046 decision_conditions.extend_from_slice(&decision_condition_record(
2047 condition,
2048 witness_vectors,
2049 &mut strings,
2050 &mut relations,
2051 )?);
2052 }
2053 decision_details.extend_from_slice(&decision_detail_record(
2054 DecisionDetailInput {
2055 view_id: id,
2056 decision,
2057 confidence_index,
2058 observations: (observations_offset, decision.vector_observations.len()),
2059 conditions: (conditions_offset, decision.conditions.len()),
2060 },
2061 &mut strings,
2062 &mut relations,
2063 )?);
2064 decision_metadata.extend_from_slice(&decision_metadata_record(
2065 id,
2066 decision,
2067 &mut strings,
2068 &mut relations,
2069 )?);
2070 anchors.extend_from_slice(&anchor_record(
2071 AnchorInput {
2072 view_id: id,
2073 kind: 0,
2074 id: &decision.meta.id,
2075 file: &decision.meta.file,
2076 line: decision.meta.line,
2077 column: decision.meta.column,
2078 covered: decision.covered,
2079 conditions: Some((
2080 decision
2081 .conditions
2082 .iter()
2083 .filter(|condition| condition.covered)
2084 .count(),
2085 decision.conditions.len(),
2086 )),
2087 tests: &decision.tests,
2088 },
2089 &mut strings,
2090 &mut relations,
2091 )?);
2092 }
2093 for branch in &view.branches {
2094 anchors.extend_from_slice(&anchor_record(
2095 AnchorInput {
2096 view_id: id,
2097 kind: 1,
2098 id: &branch.meta.id,
2099 file: &branch.meta.file,
2100 line: branch.meta.line,
2101 column: branch.meta.column,
2102 covered: branch.covered,
2103 conditions: None,
2104 tests: &[],
2105 },
2106 &mut strings,
2107 &mut relations,
2108 )?);
2109 for alternative in &branch.alternatives {
2110 hit_metadata.extend_from_slice(&hit_metadata_record(
2111 HitMetadataInput {
2112 view_id: id,
2113 kind: 2,
2114 id: &alternative.id,
2115 file: &branch.meta.file,
2116 line: branch.meta.line,
2117 column: branch.meta.column,
2118 branch_kind: Some(&branch.meta.kind),
2119 label: Some(&branch.meta.id),
2120 alternative: Some(&alternative.label),
2121 source: &branch.meta.source,
2122 tests: &alternative.tests,
2123 },
2124 &mut strings,
2125 &mut relations,
2126 )?);
2127 }
2128 }
2129 for point in &view.points {
2130 anchors.extend_from_slice(&anchor_record(
2131 AnchorInput {
2132 view_id: id,
2133 kind: match point.meta.kind {
2134 crate::coverage_analysis::PointKind::Statement => 2,
2135 crate::coverage_analysis::PointKind::Function => 3,
2136 },
2137 id: &point.meta.id,
2138 file: &point.meta.file,
2139 line: point.meta.line,
2140 column: point.meta.column,
2141 covered: point.covered,
2142 conditions: None,
2143 tests: &point.tests,
2144 },
2145 &mut strings,
2146 &mut relations,
2147 )?);
2148 hit_metadata.extend_from_slice(&hit_metadata_record(
2149 HitMetadataInput {
2150 view_id: id,
2151 kind: match point.meta.kind {
2152 crate::coverage_analysis::PointKind::Statement => 0,
2153 crate::coverage_analysis::PointKind::Function => 1,
2154 },
2155 id: &point.meta.id,
2156 file: &point.meta.file,
2157 line: point.meta.line,
2158 column: point.meta.column,
2159 branch_kind: None,
2160 label: point.meta.label.as_deref(),
2161 alternative: None,
2162 source: &point.meta.source,
2163 tests: &point.tests,
2164 },
2165 &mut strings,
2166 &mut relations,
2167 )?);
2168 }
2169 for value in &view.coverage_by_kind {
2170 dimensions.extend_from_slice(&dimension_record(
2171 id,
2172 CoverageDimension::Kind,
2173 value,
2174 &mut strings,
2175 )?);
2176 }
2177 for value in &view.coverage_by_runner {
2178 dimensions.extend_from_slice(&dimension_record(
2179 id,
2180 CoverageDimension::Runner,
2181 value,
2182 &mut strings,
2183 )?);
2184 }
2185 for decision in &view.decisions {
2186 decision_gaps.extend_from_slice(&decision_gap_record(
2187 id,
2188 decision,
2189 None,
2190 None,
2191 None,
2192 &mut strings,
2193 )?);
2194 }
2195 for (file, gap) in file_gaps(view, None)? {
2196 gaps.extend_from_slice(&file_gap_record(id, &file, &gap, None, None, &mut strings)?);
2197 }
2198 for (kind, runner, selected) in projections(view) {
2199 projection_records.extend_from_slice(&projection_record(
2200 id,
2201 view,
2202 Some(&selected),
2203 kind.as_deref(),
2204 runner.as_deref(),
2205 &mut strings,
2206 &mut relations,
2207 )?);
2208 for decision in &view.decisions {
2209 decision_gaps.extend_from_slice(&decision_gap_record(
2210 id,
2211 decision,
2212 Some(&selected),
2213 kind.as_deref(),
2214 runner.as_deref(),
2215 &mut strings,
2216 )?);
2217 }
2218 for (file, gap) in file_gaps(view, Some(&selected))? {
2219 gaps.extend_from_slice(&file_gap_record(
2220 id,
2221 &file,
2222 &gap,
2223 kind.as_deref(),
2224 runner.as_deref(),
2225 &mut strings,
2226 )?);
2227 }
2228 }
2229 }
2230 let model = coverage_model_record(
2231 &report.view.variant,
2232 &report.view.model,
2233 &mut strings,
2234 &mut relations,
2235 )?;
2236 let [blob, string_records] = strings.sections()?;
2237 let string_relations = relations.section()?;
2238 Ok(vec![
2239 blob,
2240 string_records,
2241 string_relations,
2242 QueryIndexSection {
2243 kind: SECTION_VIEW_SUMMARIES,
2244 record_size: SUMMARY_RECORD_SIZE as u32,
2245 count: usize_u64(summaries.len() / SUMMARY_RECORD_SIZE)?,
2246 bytes: summaries,
2247 },
2248 QueryIndexSection {
2249 kind: SECTION_FILE_GAPS,
2250 record_size: FILE_GAP_RECORD_SIZE as u32,
2251 count: usize_u64(gaps.len() / FILE_GAP_RECORD_SIZE)?,
2252 bytes: gaps,
2253 },
2254 QueryIndexSection {
2255 kind: SECTION_DECISION_GAPS,
2256 record_size: DECISION_GAP_RECORD_SIZE as u32,
2257 count: usize_u64(decision_gaps.len() / DECISION_GAP_RECORD_SIZE)?,
2258 bytes: decision_gaps,
2259 },
2260 QueryIndexSection {
2261 kind: SECTION_DIMENSIONS,
2262 record_size: DIMENSION_RECORD_SIZE as u32,
2263 count: usize_u64(dimensions.len() / DIMENSION_RECORD_SIZE)?,
2264 bytes: dimensions,
2265 },
2266 QueryIndexSection {
2267 kind: SECTION_PROJECTIONS,
2268 record_size: PROJECTION_RECORD_SIZE as u32,
2269 count: usize_u64(projection_records.len() / PROJECTION_RECORD_SIZE)?,
2270 bytes: projection_records,
2271 },
2272 QueryIndexSection {
2273 kind: SECTION_SCOPE_ENTRIES,
2274 record_size: SCOPE_ENTRY_RECORD_SIZE as u32,
2275 count: usize_u64(scope_entries.len() / SCOPE_ENTRY_RECORD_SIZE)?,
2276 bytes: scope_entries,
2277 },
2278 QueryIndexSection {
2279 kind: SECTION_CONFIDENCE,
2280 record_size: CONFIDENCE_RECORD_SIZE as u32,
2281 count: usize_u64(confidence_records.len() / CONFIDENCE_RECORD_SIZE)?,
2282 bytes: confidence_records,
2283 },
2284 QueryIndexSection {
2285 kind: SECTION_LINES,
2286 record_size: LINE_RECORD_SIZE as u32,
2287 count: usize_u64(line_records.len() / LINE_RECORD_SIZE)?,
2288 bytes: line_records,
2289 },
2290 QueryIndexSection {
2291 kind: SECTION_TEST_SUMMARIES,
2292 record_size: TEST_SUMMARY_RECORD_SIZE as u32,
2293 count: usize_u64(test_summaries.len() / TEST_SUMMARY_RECORD_SIZE)?,
2294 bytes: test_summaries,
2295 },
2296 QueryIndexSection {
2297 kind: SECTION_PHASE_SUMMARIES,
2298 record_size: PHASE_SUMMARY_RECORD_SIZE as u32,
2299 count: usize_u64(phase_summaries.len() / PHASE_SUMMARY_RECORD_SIZE)?,
2300 bytes: phase_summaries,
2301 },
2302 QueryIndexSection {
2303 kind: SECTION_ANCHORS,
2304 record_size: ANCHOR_RECORD_SIZE as u32,
2305 count: usize_u64(anchors.len() / ANCHOR_RECORD_SIZE)?,
2306 bytes: anchors,
2307 },
2308 QueryIndexSection {
2309 kind: SECTION_TEST_RETRIES,
2310 record_size: TEST_RETRY_RECORD_SIZE as u32,
2311 count: usize_u64(test_retries.len() / TEST_RETRY_RECORD_SIZE)?,
2312 bytes: test_retries,
2313 },
2314 QueryIndexSection {
2315 kind: SECTION_TEST_ATTEMPTS,
2316 record_size: TEST_ATTEMPT_RECORD_SIZE as u32,
2317 count: usize_u64(test_attempts.len() / TEST_ATTEMPT_RECORD_SIZE)?,
2318 bytes: test_attempts,
2319 },
2320 QueryIndexSection {
2321 kind: SECTION_TEST_LINES,
2322 record_size: TEST_LINE_RECORD_SIZE as u32,
2323 count: usize_u64(test_lines.len() / TEST_LINE_RECORD_SIZE)?,
2324 bytes: test_lines,
2325 },
2326 QueryIndexSection {
2327 kind: SECTION_TEST_HITS,
2328 record_size: TEST_HIT_RECORD_SIZE as u32,
2329 count: usize_u64(test_hits.len() / TEST_HIT_RECORD_SIZE)?,
2330 bytes: test_hits,
2331 },
2332 QueryIndexSection {
2333 kind: SECTION_TEST_DECISIONS,
2334 record_size: TEST_DECISION_RECORD_SIZE as u32,
2335 count: usize_u64(test_decisions.len() / TEST_DECISION_RECORD_SIZE)?,
2336 bytes: test_decisions,
2337 },
2338 QueryIndexSection {
2339 kind: SECTION_TEST_VECTORS,
2340 record_size: TEST_VECTOR_RECORD_SIZE as u32,
2341 count: usize_u64(test_vectors.len() / TEST_VECTOR_RECORD_SIZE)?,
2342 bytes: test_vectors,
2343 },
2344 QueryIndexSection {
2345 kind: SECTION_VECTOR_VALUES,
2346 record_size: 1,
2347 count: usize_u64(vector_values.len())?,
2348 bytes: vector_values,
2349 },
2350 QueryIndexSection {
2351 kind: SECTION_HIT_METADATA,
2352 record_size: HIT_METADATA_RECORD_SIZE as u32,
2353 count: usize_u64(hit_metadata.len() / HIT_METADATA_RECORD_SIZE)?,
2354 bytes: hit_metadata,
2355 },
2356 QueryIndexSection {
2357 kind: SECTION_DECISION_METADATA,
2358 record_size: DECISION_METADATA_RECORD_SIZE as u32,
2359 count: usize_u64(decision_metadata.len() / DECISION_METADATA_RECORD_SIZE)?,
2360 bytes: decision_metadata,
2361 },
2362 QueryIndexSection {
2363 kind: SECTION_DECISION_DETAILS,
2364 record_size: DECISION_DETAIL_RECORD_SIZE as u32,
2365 count: usize_u64(decision_details.len() / DECISION_DETAIL_RECORD_SIZE)?,
2366 bytes: decision_details,
2367 },
2368 QueryIndexSection {
2369 kind: SECTION_DECISION_VECTOR_OBSERVATIONS,
2370 record_size: DECISION_VECTOR_OBSERVATION_RECORD_SIZE as u32,
2371 count: usize_u64(
2372 decision_vector_observations.len() / DECISION_VECTOR_OBSERVATION_RECORD_SIZE,
2373 )?,
2374 bytes: decision_vector_observations,
2375 },
2376 QueryIndexSection {
2377 kind: SECTION_DECISION_CONDITIONS,
2378 record_size: DECISION_CONDITION_RECORD_SIZE as u32,
2379 count: usize_u64(decision_conditions.len() / DECISION_CONDITION_RECORD_SIZE)?,
2380 bytes: decision_conditions,
2381 },
2382 QueryIndexSection {
2383 kind: SECTION_LIMITATIONS,
2384 record_size: LIMITATION_RECORD_SIZE as u32,
2385 count: usize_u64(limitations.len() / LIMITATION_RECORD_SIZE)?,
2386 bytes: limitations,
2387 },
2388 QueryIndexSection {
2389 kind: SECTION_COVERAGE_MODEL,
2390 record_size: COVERAGE_MODEL_RECORD_SIZE as u32,
2391 count: 1,
2392 bytes: model.to_vec(),
2393 },
2394 ])
2395}
2396
2397pub struct CoverageIndex<'a> {
2398 index: &'a QueryIndex,
2399}
2400
2401impl<'a> CoverageIndex<'a> {
2402 pub fn new(index: &'a QueryIndex) -> Result<Self, CoverageIndexError> {
2403 for (kind, size) in [
2404 (SECTION_STRINGS, STRING_RECORD_SIZE),
2405 (SECTION_VIEW_SUMMARIES, SUMMARY_RECORD_SIZE),
2406 (SECTION_FILE_GAPS, FILE_GAP_RECORD_SIZE),
2407 (SECTION_DECISION_GAPS, DECISION_GAP_RECORD_SIZE),
2408 (SECTION_DIMENSIONS, DIMENSION_RECORD_SIZE),
2409 (SECTION_PROJECTIONS, PROJECTION_RECORD_SIZE),
2410 (SECTION_SCOPE_ENTRIES, SCOPE_ENTRY_RECORD_SIZE),
2411 (SECTION_CONFIDENCE, CONFIDENCE_RECORD_SIZE),
2412 (SECTION_LINES, LINE_RECORD_SIZE),
2413 (SECTION_TEST_SUMMARIES, TEST_SUMMARY_RECORD_SIZE),
2414 (SECTION_PHASE_SUMMARIES, PHASE_SUMMARY_RECORD_SIZE),
2415 (SECTION_ANCHORS, ANCHOR_RECORD_SIZE),
2416 (SECTION_TEST_RETRIES, TEST_RETRY_RECORD_SIZE),
2417 (SECTION_TEST_ATTEMPTS, TEST_ATTEMPT_RECORD_SIZE),
2418 (SECTION_TEST_LINES, TEST_LINE_RECORD_SIZE),
2419 (SECTION_TEST_HITS, TEST_HIT_RECORD_SIZE),
2420 (SECTION_TEST_DECISIONS, TEST_DECISION_RECORD_SIZE),
2421 (SECTION_TEST_VECTORS, TEST_VECTOR_RECORD_SIZE),
2422 (SECTION_VECTOR_VALUES, 1),
2423 (SECTION_HIT_METADATA, HIT_METADATA_RECORD_SIZE),
2424 (SECTION_DECISION_METADATA, DECISION_METADATA_RECORD_SIZE),
2425 (SECTION_DECISION_DETAILS, DECISION_DETAIL_RECORD_SIZE),
2426 (
2427 SECTION_DECISION_VECTOR_OBSERVATIONS,
2428 DECISION_VECTOR_OBSERVATION_RECORD_SIZE,
2429 ),
2430 (SECTION_DECISION_CONDITIONS, DECISION_CONDITION_RECORD_SIZE),
2431 (SECTION_LIMITATIONS, LIMITATION_RECORD_SIZE),
2432 (SECTION_COVERAGE_MODEL, COVERAGE_MODEL_RECORD_SIZE),
2433 ] {
2434 if index.descriptor(kind)?.record_size as usize != size {
2435 return Err(CoverageIndexError::InvalidRecord("record size"));
2436 }
2437 }
2438 index.descriptor(SECTION_STRING_BYTES)?;
2439 if index.descriptor(SECTION_STRING_RELATIONS)?.record_size != 4 {
2440 return Err(CoverageIndexError::InvalidRecord(
2441 "string-relation record size",
2442 ));
2443 }
2444 Ok(Self { index })
2445 }
2446
2447 pub fn model(&self) -> Result<IndexedCoverageModel, CoverageIndexError> {
2448 let descriptor = self.index.descriptor(SECTION_COVERAGE_MODEL)?;
2449 if descriptor.count != 1 {
2450 return Err(CoverageIndexError::InvalidRecord(
2451 "coverage model record count",
2452 ));
2453 }
2454 let record = self.index.record(SECTION_COVERAGE_MODEL, 0)?;
2455 if record[12..16].iter().any(|byte| *byte != 0) {
2456 return Err(CoverageIndexError::InvalidRecord(
2457 "coverage model reserved bytes",
2458 ));
2459 }
2460 Ok(IndexedCoverageModel {
2461 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
2462 variant: self.string(get_u32(record, 0)?)?,
2463 name: self.string(get_u32(record, 4)?)?,
2464 completeness_meaning: self.string(get_u32(record, 8)?)?,
2465 measured: self.relation_strings(get_u64(record, 16)?, get_u64(record, 24)?)?,
2466 not_measured: self.relation_strings(get_u64(record, 32)?, get_u64(record, 40)?)?,
2467 })
2468 }
2469
2470 fn string(&self, id: u32) -> Result<String, CoverageIndexError> {
2471 let record = self.index.record(SECTION_STRINGS, u64::from(id))?;
2472 if record[12..].iter().any(|byte| *byte != 0) {
2473 return Err(CoverageIndexError::InvalidRecord("string reserved bytes"));
2474 }
2475 let offset = get_u64(record, 0)?;
2476 let length = u64::from(get_u32(record, 8)?);
2477 let value = self.index.bytes(SECTION_STRING_BYTES, offset, length)?;
2478 std::str::from_utf8(value)
2479 .map(str::to_owned)
2480 .map_err(|_| CoverageIndexError::InvalidUtf8)
2481 }
2482
2483 pub fn summary(&self, view: CoverageViewId) -> Result<CoverageSummary, CoverageIndexError> {
2484 let descriptor = self.index.descriptor(SECTION_VIEW_SUMMARIES)?;
2485 if descriptor.count != 3 {
2486 return Err(CoverageIndexError::InvalidRecord("summary view count"));
2487 }
2488 let mut found = None;
2489 for index in 0..descriptor.count {
2490 let record = self.index.record(SECTION_VIEW_SUMMARIES, index)?;
2491 if CoverageViewId::try_from(record[0])? != view {
2492 continue;
2493 }
2494 if record[3] != 0 || record[12..16].iter().any(|byte| *byte != 0) {
2495 return Err(CoverageIndexError::InvalidRecord("summary reserved bytes"));
2496 }
2497 self.string(get_u32(record, 4)?)?;
2498 self.string(get_u32(record, 8)?)?;
2499 let count = |offset: usize| -> Result<CoverageCount, CoverageIndexError> {
2500 let covered = usize::try_from(get_u64(record, offset)?)
2501 .map_err(|_| CoverageIndexError::SizeOverflow)?;
2502 let total = usize::try_from(get_u64(record, offset + 8)?)
2503 .map_err(|_| CoverageIndexError::SizeOverflow)?;
2504 if covered > total {
2505 return Err(CoverageIndexError::InvalidRecord("covered exceeds total"));
2506 }
2507 Ok(CoverageCount {
2508 covered,
2509 total,
2510 percentage: percentage(covered, total),
2511 })
2512 };
2513 let value = |offset: usize| -> Result<usize, CoverageIndexError> {
2514 usize::try_from(get_u64(record, offset)?)
2515 .map_err(|_| CoverageIndexError::SizeOverflow)
2516 };
2517 let conditions = value(40)?;
2518 let covered_conditions = value(48)?;
2519 if covered_conditions > conditions {
2520 return Err(CoverageIndexError::InvalidRecord(
2521 "covered conditions exceed total",
2522 ));
2523 }
2524 let decisions = value(16)?;
2525 let executed_decisions = value(24)?;
2526 let covered_decisions = value(32)?;
2527 if covered_decisions > executed_decisions || executed_decisions > decisions {
2528 return Err(CoverageIndexError::InvalidRecord("decision count ordering"));
2529 }
2530 let summary = CoverageSummary {
2531 unmeasured_obligations: None,
2532 exact_fraction_pct: None,
2533 decisions,
2534 executed_decisions,
2535 covered_decisions,
2536 conditions,
2537 covered_conditions,
2538 condition_coverage_pct: percentage(covered_conditions, conditions),
2539 lines: count(56)?,
2540 statements: count(72)?,
2541 functions: count(88)?,
2542 branches: count(104)?,
2543 decision_outcomes: count(120)?,
2544 condition_outcomes: count(136)?,
2545 value_selections: count(152)?,
2546 coverage_complete: bool_field(record[1])?,
2547 completeness_blocked: match record[2] {
2548 0 => None,
2549 1 => Some(false),
2550 2 => Some(true),
2551 _ => return Err(CoverageIndexError::InvalidRecord("optional boolean")),
2552 },
2553 };
2554 if found.replace(summary).is_some() {
2555 return Err(CoverageIndexError::InvalidRecord("duplicate coverage view"));
2556 }
2557 }
2558 found.ok_or(CoverageIndexError::InvalidRecord("missing coverage view"))
2559 }
2560
2561 pub fn file_gaps(
2562 &self,
2563 view: CoverageViewId,
2564 kind: Option<&str>,
2565 runner: Option<&str>,
2566 ) -> Result<Vec<IndexedFileGap>, CoverageIndexError> {
2567 let descriptor = self.index.descriptor(SECTION_FILE_GAPS)?;
2568 let mut gaps = Vec::new();
2569 for index in 0..descriptor.count {
2570 let record = self.index.record(SECTION_FILE_GAPS, index)?;
2571 if CoverageViewId::try_from(record[0])? != view {
2572 continue;
2573 }
2574 if record[1..4].iter().any(|byte| *byte != 0)
2575 || record[60..64].iter().any(|byte| *byte != 0)
2576 || record[160..].iter().any(|byte| *byte != 0)
2577 {
2578 return Err(CoverageIndexError::InvalidRecord("file-gap reserved bytes"));
2579 }
2580 let number = |offset: usize| -> Result<usize, CoverageIndexError> {
2581 usize::try_from(get_u64(record, offset)?)
2582 .map_err(|_| CoverageIndexError::SizeOverflow)
2583 };
2584 let mask = get_u32(record, 56)?;
2585 if mask & !15 != 0 {
2586 return Err(CoverageIndexError::InvalidRecord("limitation mask"));
2587 }
2588 let measurement_limitations = number(48)?;
2589 if (measurement_limitations == 0) != (mask == 0) {
2590 return Err(CoverageIndexError::InvalidRecord(
2591 "limitation count and kinds disagree",
2592 ));
2593 }
2594 let uncovered_lines = number(8)?;
2595 let uncovered_statements = number(16)?;
2596 let uncovered_functions = number(24)?;
2597 let missing_branches = number(32)?;
2598 let missing_mcdc_conditions = number(40)?;
2599 let score = number(64)?;
2600 let expected_score = uncovered_lines
2601 + uncovered_functions * 2
2602 + missing_branches * 2
2603 + missing_mcdc_conditions * 3
2604 + measurement_limitations * 3;
2605 if score != expected_score {
2606 return Err(CoverageIndexError::InvalidRecord("file-gap score"));
2607 }
2608 let record_kind = self.optional_string(get_u32(record, 72)?)?;
2609 let record_runner = self.optional_string(get_u32(record, 76)?)?;
2610 if record_kind.as_deref() != kind || record_runner.as_deref() != runner {
2611 continue;
2612 }
2613 let mut limitation_kinds = Vec::new();
2614 for (bit, kind) in [
2615 (1, "dynamic-code"),
2616 (2, "semantic-safety"),
2617 (4, "source-scope"),
2618 (8, "unknown"),
2619 ] {
2620 if mask & bit != 0 {
2621 limitation_kinds.push(kind.into());
2622 }
2623 }
2624 gaps.push(IndexedFileGap {
2625 view,
2626 file: self.string(get_u32(record, 4)?)?,
2627 uncovered_lines,
2628 uncovered_statements,
2629 uncovered_functions,
2630 missing_branches,
2631 missing_mcdc_conditions,
2632 measurement_limitations,
2633 limitation_kinds,
2634 covered_by_other_tests: IndexedGapDimensions {
2635 lines: number(80)?,
2636 statements: number(88)?,
2637 functions: number(96)?,
2638 branches: number(104)?,
2639 mcdc_conditions: number(112)?,
2640 },
2641 uncovered_everywhere: IndexedGapDimensions {
2642 lines: number(120)?,
2643 statements: number(128)?,
2644 functions: number(136)?,
2645 branches: number(144)?,
2646 mcdc_conditions: number(152)?,
2647 },
2648 score,
2649 });
2650 }
2651 gaps.sort_by(|left, right| {
2652 right
2653 .score
2654 .cmp(&left.score)
2655 .then_with(|| left.file.cmp(&right.file))
2656 });
2657 Ok(gaps)
2658 }
2659
2660 fn optional_string(&self, id: u32) -> Result<Option<String>, CoverageIndexError> {
2661 if id == NO_STRING {
2662 Ok(None)
2663 } else {
2664 self.string(id).map(Some)
2665 }
2666 }
2667
2668 fn relation_strings(&self, offset: u64, count: u64) -> Result<Vec<String>, CoverageIndexError> {
2669 let end = offset
2670 .checked_add(count)
2671 .ok_or(CoverageIndexError::SizeOverflow)?;
2672 let descriptor = self.index.descriptor(SECTION_STRING_RELATIONS)?;
2673 if end > descriptor.count {
2674 return Err(CoverageIndexError::InvalidRecord("string relation range"));
2675 }
2676 (offset..end)
2677 .map(|index| {
2678 let record = self.index.record(SECTION_STRING_RELATIONS, index)?;
2679 self.string(get_u32(record, 0)?)
2680 })
2681 .collect()
2682 }
2683
2684 pub fn projection(
2685 &self,
2686 view: CoverageViewId,
2687 kind: Option<&str>,
2688 runner: Option<&str>,
2689 ) -> Result<IndexedProjection, CoverageIndexError> {
2690 let descriptor = self.index.descriptor(SECTION_PROJECTIONS)?;
2691 let mut found = None;
2692 for index in 0..descriptor.count {
2693 let record = self.index.record(SECTION_PROJECTIONS, index)?;
2694 if CoverageViewId::try_from(record[0])? != view {
2695 continue;
2696 }
2697 if record[38..40].iter().any(|byte| *byte != 0)
2698 || record[518..520].iter().any(|byte| *byte != 0)
2699 {
2700 return Err(CoverageIndexError::InvalidRecord(
2701 "projection reserved bytes",
2702 ));
2703 }
2704 let record_kind = self.optional_string(get_u32(record, 4)?)?;
2705 let record_runner = self.optional_string(get_u32(record, 8)?)?;
2706 if record_kind.as_deref() != kind || record_runner.as_deref() != runner {
2707 continue;
2708 }
2709 if found.is_some() {
2710 return Err(CoverageIndexError::InvalidRecord(
2711 "duplicate coverage projection",
2712 ));
2713 }
2714 let number = |offset: usize| -> Result<usize, CoverageIndexError> {
2715 usize::try_from(get_u64(record, offset)?)
2716 .map_err(|_| CoverageIndexError::SizeOverflow)
2717 };
2718 let limitations = number(192)?;
2719 let evidence_corruptions = number(200)?;
2720 let blocking = number(208)?;
2721 let declared = number(528)?;
2722 if blocking + declared != limitations + evidence_corruptions {
2723 return Err(CoverageIndexError::InvalidRecord(
2724 "measurement blocking count",
2725 ));
2726 }
2727 let transport_values = (0..8)
2728 .map(|index| number(408 + index * 8))
2729 .collect::<Result<Vec<_>, _>>()?;
2730 let transport = if bool_field(record[1])? {
2731 Some(TransportStats {
2732 processes: transport_values[0],
2733 child_launches: transport_values[1],
2734 remote_launches: transport_values[2],
2735 workspace_capabilities: transport_values[3],
2736 scoped_server_records: transport_values[4],
2737 background_server_records: transport_values[5],
2738 corrupt_records: transport_values[6],
2739 corrupt_files: transport_values[7],
2740 })
2741 } else {
2742 if transport_values.iter().any(|value| *value != 0) {
2743 return Err(CoverageIndexError::InvalidRecord("transport presence flag"));
2744 }
2745 None
2746 };
2747 let has_scope = bool_field(record[2])?;
2748 let scope_kind = match record[3] {
2749 0 => None,
2750 1 => Some((ScopeKind::SourceDiscovery, "source-discovery")),
2751 2 => Some((ScopeKind::Compiler, "compiler")),
2752 _ => return Err(CoverageIndexError::InvalidRecord("coverage scope kind")),
2753 };
2754 let scope_mode = self.optional_string(get_u32(record, 16)?)?;
2755 let scope_language = self.optional_string(get_u32(record, 504)?)?;
2756 let scope_model = self.optional_string(get_u32(record, 508)?)?;
2757 let scope_unit = self.optional_string(get_u32(record, 512)?)?;
2758 let has_measurement_complete = bool_field(record[516])?;
2759 let measurement_complete = bool_field(record[517])?;
2760 if !has_measurement_complete && measurement_complete {
2761 return Err(CoverageIndexError::InvalidRecord(
2762 "scope measurement completeness flag",
2763 ));
2764 }
2765 if has_scope
2766 != (scope_kind.is_some() && scope_language.is_some() && scope_model.is_some())
2767 {
2768 return Err(CoverageIndexError::InvalidRecord("scope presence flag"));
2769 }
2770 let source_scope = if let Some((kind, kind_name)) = scope_kind {
2771 match kind {
2772 ScopeKind::SourceDiscovery => {
2773 if scope_mode.is_none() || scope_unit.is_some() || has_measurement_complete
2774 {
2775 return Err(CoverageIndexError::InvalidRecord(
2776 "source-discovery scope shape",
2777 ));
2778 }
2779 }
2780 ScopeKind::Compiler => {
2781 if scope_mode.is_some()
2782 || scope_unit.is_none()
2783 || !has_measurement_complete
2784 || get_u32(record, 32)? != 0
2785 || number(480)? != 0
2786 || number(488)? != 0
2787 || number(496)? != 0
2788 {
2789 return Err(CoverageIndexError::InvalidRecord("compiler scope shape"));
2790 }
2791 }
2792 }
2793 Some(IndexedSourceScope {
2794 kind: kind_name.into(),
2795 language: scope_language.expect("validated scope language"),
2796 model: scope_model.expect("validated scope model"),
2797 mode: scope_mode,
2798 roots: self
2799 .relation_strings(get_u64(record, 24)?, u64::from(get_u32(record, 32)?))?,
2800 unit: scope_unit,
2801 measurement_complete: has_measurement_complete.then_some(measurement_complete),
2802 included: number(480)?,
2803 excluded: number(488)?,
2804 ambiguous: number(496)?,
2805 })
2806 } else {
2807 if get_u32(record, 32)? != 0
2808 || scope_mode.is_some()
2809 || scope_language.is_some()
2810 || scope_model.is_some()
2811 || scope_unit.is_some()
2812 || has_measurement_complete
2813 || number(480)? != 0
2814 || number(488)? != 0
2815 || number(496)? != 0
2816 {
2817 return Err(CoverageIndexError::InvalidRecord("absent scope data"));
2818 }
2819 None
2820 };
2821 let empty_evidence_tests = number(472)?;
2822 let first_empty_evidence_test = self.optional_string(get_u32(record, 20)?)?;
2823 if (empty_evidence_tests == 0) != first_empty_evidence_test.is_none() {
2824 return Err(CoverageIndexError::InvalidRecord(
2825 "empty-evidence diagnostic identity",
2826 ));
2827 }
2828 found = Some(IndexedProjection {
2829 view,
2830 kind: record_kind,
2831 runner: record_runner,
2832 generated_at: self.string(get_u32(record, 12)?)?,
2833 summary: decode_summary(record, 36, 40)?,
2834 measurement: IndexedMeasurement {
2835 complete: blocking == 0,
2836 limitations,
2837 evidence_corruptions,
2838 blocking,
2839 declared,
2840 files: number(216)?,
2841 by_kind: IndexedMeasurementKinds {
2842 dynamic_code: number(224)?,
2843 semantic_safety: number(232)?,
2844 source_scope: number(240)?,
2845 },
2846 },
2847 attribution: IndexedAttribution {
2848 browser_explicit: number(248)?,
2849 browser_fallback: number(256)?,
2850 server_explicit: number(264)?,
2851 server_fallback: number(272)?,
2852 },
2853 transport,
2854 empty_evidence_tests,
2855 first_empty_evidence_test,
2856 confidence: IndexedSummaryConfidence {
2857 lines: IndexedConfidenceLines {
2858 unexecuted: number(280)?,
2859 executed: number(288)?,
2860 action: number(296)?,
2861 asserted: number(304)?,
2862 },
2863 assertion_covered_mcdc_conditions: number(312)?,
2864 },
2865 files_with_gaps: number(320)?,
2866 files_with_coverage_gaps: number(328)?,
2867 tests: number(336)?,
2868 setups: number(344)?,
2869 test_outcomes: IndexedOutcomeCounts {
2870 passed: number(352)?,
2871 failed: number(360)?,
2872 flaky: number(368)?,
2873 skipped: number(376)?,
2874 timed_out: number(384)?,
2875 interrupted: number(392)?,
2876 unknown: number(400)?,
2877 unstarted: number(520)?,
2878 },
2879 source_scope,
2880 });
2881 }
2882 found.ok_or(CoverageIndexError::InvalidRecord(
2883 "missing coverage projection",
2884 ))
2885 }
2886
2887 pub fn decision_gaps(
2888 &self,
2889 view: CoverageViewId,
2890 kind: Option<&str>,
2891 runner: Option<&str>,
2892 file: &str,
2893 ) -> Result<Vec<IndexedDecisionGap>, CoverageIndexError> {
2894 let descriptor = self.index.descriptor(SECTION_DECISION_GAPS)?;
2895 let mut decisions = Vec::new();
2896 for index in 0..descriptor.count {
2897 let record = self.index.record(SECTION_DECISION_GAPS, index)?;
2898 if CoverageViewId::try_from(record[0])? != view {
2899 continue;
2900 }
2901 if record[1..4].iter().any(|byte| *byte != 0)
2902 || record[28..32].iter().any(|byte| *byte != 0)
2903 || record[64..].iter().any(|byte| *byte != 0)
2904 {
2905 return Err(CoverageIndexError::InvalidRecord(
2906 "decision-gap reserved bytes",
2907 ));
2908 }
2909 let record_kind = self.optional_string(get_u32(record, 4)?)?;
2910 let record_runner = self.optional_string(get_u32(record, 8)?)?;
2911 if record_kind.as_deref() != kind || record_runner.as_deref() != runner {
2912 continue;
2913 }
2914 let record_file = self.string(get_u32(record, 16)?)?;
2915 if record_file != file {
2916 continue;
2917 }
2918 let number = |offset: usize| -> Result<usize, CoverageIndexError> {
2919 usize::try_from(get_u64(record, offset)?)
2920 .map_err(|_| CoverageIndexError::SizeOverflow)
2921 };
2922 let conditions = number(48)?;
2923 let missing_conditions = number(56)?;
2924 if conditions == 0 || missing_conditions > conditions {
2925 return Err(CoverageIndexError::InvalidRecord(
2926 "decision condition counts",
2927 ));
2928 }
2929 decisions.push(IndexedDecisionGap {
2930 view,
2931 file: record_file,
2932 id: self.string(get_u32(record, 12)?)?,
2933 line: number(32)?,
2934 column: number(40)?,
2935 kind: self.string(get_u32(record, 20)?)?,
2936 conditions,
2937 missing_conditions,
2938 source: self.string(get_u32(record, 24)?)?,
2939 });
2940 }
2941 Ok(decisions)
2942 }
2943
2944 pub fn dimensions(
2945 &self,
2946 view: CoverageViewId,
2947 dimension: CoverageDimension,
2948 ) -> Result<Vec<IndexedDimensionCoverage>, CoverageIndexError> {
2949 let descriptor = self.index.descriptor(SECTION_DIMENSIONS)?;
2950 let mut values = Vec::new();
2951 for index in 0..descriptor.count {
2952 let record = self.index.record(SECTION_DIMENSIONS, index)?;
2953 if CoverageViewId::try_from(record[0])? != view {
2954 continue;
2955 }
2956 let record_dimension = match record[1] {
2957 0 => CoverageDimension::Kind,
2958 1 => CoverageDimension::Runner,
2959 _ => return Err(CoverageIndexError::InvalidRecord("dimension type")),
2960 };
2961 if record_dimension != dimension {
2962 continue;
2963 }
2964 if record[26..32].iter().any(|byte| *byte != 0) {
2965 return Err(CoverageIndexError::InvalidRecord(
2966 "dimension reserved bytes",
2967 ));
2968 }
2969 let name = self.string(get_u32(record, 4)?)?;
2970 values.push(IndexedDimensionCoverage {
2971 kind: (dimension == CoverageDimension::Kind).then(|| name.clone()),
2972 runner: (dimension == CoverageDimension::Runner).then_some(name),
2973 tests: usize::try_from(get_u64(record, 8)?)
2974 .map_err(|_| CoverageIndexError::SizeOverflow)?,
2975 setups: usize::try_from(get_u64(record, 16)?)
2976 .map_err(|_| CoverageIndexError::SizeOverflow)?,
2977 attributed: usize::try_from(get_u64(record, 184)?)
2978 .map_err(|_| CoverageIndexError::SizeOverflow)?,
2979 summary: decode_summary(record, 24, 32)?,
2980 });
2981 }
2982 Ok(values)
2983 }
2984
2985 pub fn scope_entries(
2986 &self,
2987 view: CoverageViewId,
2988 ) -> Result<Vec<IndexedScopeEntry>, CoverageIndexError> {
2989 let descriptor = self.index.descriptor(SECTION_SCOPE_ENTRIES)?;
2990 let mut entries = Vec::new();
2991 for index in 0..descriptor.count {
2992 let record = self.index.record(SECTION_SCOPE_ENTRIES, index)?;
2993 if CoverageViewId::try_from(record[0])? != view {
2994 continue;
2995 }
2996 if record[2..4].iter().any(|byte| *byte != 0)
2997 || record[28..].iter().any(|byte| *byte != 0)
2998 {
2999 return Err(CoverageIndexError::InvalidRecord(
3000 "source-scope reserved bytes",
3001 ));
3002 }
3003 let status = match record[1] {
3004 0 => "included",
3005 1 => "excluded",
3006 2 => "ambiguous",
3007 _ => return Err(CoverageIndexError::InvalidRecord("source-scope status")),
3008 };
3009 let measurement_limitations = usize::try_from(get_u64(record, 16)?)
3010 .map_err(|_| CoverageIndexError::SizeOverflow)?;
3011 let mask = get_u32(record, 24)?;
3012 if mask & !7 != 0 || (measurement_limitations == 0) != (mask == 0) {
3013 return Err(CoverageIndexError::InvalidRecord(
3014 "source-scope limitation annotation",
3015 ));
3016 }
3017 let mut limitation_kinds = Vec::new();
3018 for (bit, kind) in [
3019 (1, "dynamic-code"),
3020 (2, "semantic-safety"),
3021 (4, "source-scope"),
3022 ] {
3023 if mask & bit != 0 {
3024 limitation_kinds.push(kind.into());
3025 }
3026 }
3027 entries.push(IndexedScopeEntry {
3028 file: self.string(get_u32(record, 4)?)?,
3029 status: status.into(),
3030 reason: self.string(get_u32(record, 8)?)?,
3031 package_root: self.optional_string(get_u32(record, 12)?)?,
3032 measurement_limitations,
3033 limitation_kinds,
3034 });
3035 }
3036 Ok(entries)
3037 }
3038
3039 fn confidence(
3040 &self,
3041 index: u64,
3042 ) -> Result<crate::coverage_report::CoverageConfidence, CoverageIndexError> {
3043 let record = self.index.record(SECTION_CONFIDENCE, index)?;
3044 if record[2..8].iter().any(|byte| *byte != 0)
3045 || record[72..].iter().any(|byte| *byte != 0)
3046 || record[1] & !15 != 0
3047 {
3048 return Err(CoverageIndexError::InvalidRecord("confidence record"));
3049 }
3050 let values = (0..4)
3051 .map(|index| {
3052 self.relation_strings(
3053 get_u64(record, 8 + index * 16)?,
3054 get_u64(record, 16 + index * 16)?,
3055 )
3056 })
3057 .collect::<Result<Vec<_>, _>>()?;
3058 Ok(crate::coverage_report::CoverageConfidence {
3059 level: match record[0] {
3060 0 => "unexecuted",
3061 1 => "executed",
3062 2 => "action",
3063 3 => "asserted",
3064 _ => return Err(CoverageIndexError::InvalidRecord("confidence level")),
3065 }
3066 .into(),
3067 setup_only: record[1] & 1 != 0,
3068 background_only: record[1] & 2 != 0,
3069 asserted: record[1] & 4 != 0,
3070 e2e: record[1] & 8 != 0,
3071 tests: values[0].clone(),
3072 asserted_tests: values[1].clone(),
3073 runners: values[2].clone(),
3074 kinds: values[3].clone(),
3075 })
3076 }
3077
3078 pub fn line(
3079 &self,
3080 view: CoverageViewId,
3081 file: &str,
3082 line: usize,
3083 ) -> Result<Option<IndexedLine>, CoverageIndexError> {
3084 let descriptor = self.index.descriptor(SECTION_LINES)?;
3085 let mut found = None;
3086 for index in 0..descriptor.count {
3087 let record = self.index.record(SECTION_LINES, index)?;
3088 if CoverageViewId::try_from(record[0])? != view {
3089 continue;
3090 }
3091 if record[3] != 0 || record[56..].iter().any(|byte| *byte != 0) {
3092 return Err(CoverageIndexError::InvalidRecord("line record"));
3093 }
3094 let record_file = self.string(get_u32(record, 4)?)?;
3095 let record_line = usize::try_from(get_u64(record, 8)?)
3096 .map_err(|_| CoverageIndexError::SizeOverflow)?;
3097 if record_file != file || record_line != line {
3098 continue;
3099 }
3100 if found.is_some() {
3101 return Err(CoverageIndexError::InvalidRecord("duplicate line"));
3102 }
3103 found = Some(IndexedLine {
3104 file: record_file,
3105 line: record_line,
3106 covered: bool_field(record[1])?,
3107 measured: !bool_field(record[2])?,
3108 tests: self.relation_strings(get_u64(record, 16)?, get_u64(record, 24)?)?,
3109 phases: self.relation_strings(get_u64(record, 32)?, get_u64(record, 40)?)?,
3110 confidence: self.confidence(get_u64(record, 48)?)?,
3111 });
3112 }
3113 Ok(found)
3114 }
3115
3116 pub fn lines(&self, view: CoverageViewId) -> Result<Vec<IndexedLine>, CoverageIndexError> {
3117 let descriptor = self.index.descriptor(SECTION_LINES)?;
3118 let mut lines = Vec::new();
3119 for index in 0..descriptor.count {
3120 let record = self.index.record(SECTION_LINES, index)?;
3121 if CoverageViewId::try_from(record[0])? != view {
3122 continue;
3123 }
3124 if record[3] != 0 || record[56..].iter().any(|byte| *byte != 0) {
3125 return Err(CoverageIndexError::InvalidRecord("line record"));
3126 }
3127 lines.push(IndexedLine {
3128 file: self.string(get_u32(record, 4)?)?,
3129 line: usize::try_from(get_u64(record, 8)?)
3130 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3131 covered: bool_field(record[1])?,
3132 measured: !bool_field(record[2])?,
3133 tests: self.relation_strings(get_u64(record, 16)?, get_u64(record, 24)?)?,
3134 phases: self.relation_strings(get_u64(record, 32)?, get_u64(record, 40)?)?,
3135 confidence: self.confidence(get_u64(record, 48)?)?,
3136 });
3137 }
3138 Ok(lines)
3139 }
3140
3141 pub fn test_summaries(
3142 &self,
3143 view: CoverageViewId,
3144 ) -> Result<Vec<IndexedTestSummary>, CoverageIndexError> {
3145 let descriptor = self.index.descriptor(SECTION_TEST_SUMMARIES)?;
3146 let mut tests = Vec::new();
3147 for index in 0..descriptor.count {
3148 let record = self.index.record(SECTION_TEST_SUMMARIES, index)?;
3149 if CoverageViewId::try_from(record[0])? != view {
3150 continue;
3151 }
3152 if record[36..].iter().any(|byte| *byte != 0) {
3153 return Err(CoverageIndexError::InvalidRecord("test summary record"));
3154 }
3155 tests.push(IndexedTestSummary {
3156 id: self.string(get_u32(record, 4)?)?,
3157 name: self.string(get_u32(record, 8)?)?,
3158 file: self.optional_string(get_u32(record, 12)?)?,
3159 title: self.optional_string(get_u32(record, 16)?)?,
3160 role: match record[1] {
3161 0 => "test",
3162 1 => "setup",
3163 2 => "background",
3164 _ => return Err(CoverageIndexError::InvalidRecord("test role")),
3165 }
3166 .into(),
3167 attribution: match record[3] {
3168 0 => crate::coverage_report::ATTRIBUTION_EXACT,
3169 1 => crate::coverage_report::ATTRIBUTION_RUN_WIDE,
3170 2 => crate::coverage_report::ATTRIBUTION_PARTIAL,
3171 _ => return Err(CoverageIndexError::InvalidRecord("test attribution")),
3172 }
3173 .into(),
3174 outcome: match record[2] {
3175 0 => "passed",
3176 1 => "failed",
3177 2 => "flaky",
3178 3 => "skipped",
3179 4 => "timedOut",
3180 5 => "interrupted",
3181 6 => "unknown",
3182 7 => "unstarted",
3183 _ => return Err(CoverageIndexError::InvalidRecord("test outcome")),
3184 }
3185 .into(),
3186 provenance: crate::coverage_report::TestProvenance {
3187 runner: self.string(get_u32(record, 20)?)?,
3188 kind: self.string(get_u32(record, 24)?)?,
3189 project: self.optional_string(get_u32(record, 28)?)?,
3190 source: self.string(get_u32(record, 32)?)?,
3191 },
3192 });
3193 }
3194 Ok(tests)
3195 }
3196
3197 fn test_vector(&self, index: u64) -> Result<McdcVector, CoverageIndexError> {
3198 let record = self.index.record(SECTION_TEST_VECTORS, index)?;
3199 if record[1..8].iter().any(|byte| *byte != 0) {
3200 return Err(CoverageIndexError::InvalidRecord("test vector record"));
3201 }
3202 let offset = get_u64(record, 8)?;
3203 let count = get_u64(record, 16)?;
3204 let descriptor = self.index.descriptor(SECTION_VECTOR_VALUES)?;
3205 let end = offset
3206 .checked_add(count)
3207 .ok_or(CoverageIndexError::InvalidRecord("vector value range"))?;
3208 if end > descriptor.count {
3209 return Err(CoverageIndexError::InvalidRecord("vector value range"));
3210 }
3211 let mut values = Vec::with_capacity(
3212 usize::try_from(count).map_err(|_| CoverageIndexError::SizeOverflow)?,
3213 );
3214 for index in offset..end {
3215 values.push(match self.index.record(SECTION_VECTOR_VALUES, index)?[0] {
3216 0 => None,
3217 1 => Some(false),
3218 2 => Some(true),
3219 _ => return Err(CoverageIndexError::InvalidRecord("vector value")),
3220 });
3221 }
3222 Ok(McdcVector {
3223 values,
3224 outcome: bool_field(record[0])?,
3225 })
3226 }
3227
3228 pub fn test_details(
3229 &self,
3230 view: CoverageViewId,
3231 ) -> Result<Vec<IndexedTestDetail>, CoverageIndexError> {
3232 let summaries = self.test_summaries(view)?;
3233 let positions = summaries
3234 .iter()
3235 .enumerate()
3236 .map(|(index, test)| (test.id.clone(), index))
3237 .collect::<HashMap<_, _>>();
3238 if positions.len() != summaries.len() {
3239 return Err(CoverageIndexError::InvalidRecord("duplicate test summary"));
3240 }
3241 let mut details = summaries
3242 .into_iter()
3243 .map(|summary| IndexedTestDetail {
3244 summary,
3245 retries: Vec::new(),
3246 attempts: Vec::new(),
3247 hits: Vec::new(),
3248 decisions: Vec::new(),
3249 lines: Vec::new(),
3250 })
3251 .collect::<Vec<_>>();
3252 let position = |record: &[u8]| -> Result<Option<usize>, CoverageIndexError> {
3253 if CoverageViewId::try_from(record[0])? != view {
3254 return Ok(None);
3255 }
3256 let id = self.string(get_u32(record, 4)?)?;
3257 positions
3258 .get(&id)
3259 .copied()
3260 .map(Some)
3261 .ok_or(CoverageIndexError::InvalidRecord("unknown test relation"))
3262 };
3263 let descriptor = self.index.descriptor(SECTION_TEST_RETRIES)?;
3264 for index in 0..descriptor.count {
3265 let record = self.index.record(SECTION_TEST_RETRIES, index)?;
3266 if record[1..4].iter().any(|byte| *byte != 0) {
3267 return Err(CoverageIndexError::InvalidRecord("test retry record"));
3268 }
3269 if let Some(position) = position(record)? {
3270 details[position].retries.push(
3271 usize::try_from(get_u64(record, 8)?)
3272 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3273 );
3274 }
3275 }
3276 let descriptor = self.index.descriptor(SECTION_TEST_ATTEMPTS)?;
3277 for index in 0..descriptor.count {
3278 let record = self.index.record(SECTION_TEST_ATTEMPTS, index)?;
3279 if record[1..4].iter().any(|byte| *byte != 0) {
3280 return Err(CoverageIndexError::InvalidRecord("test attempt record"));
3281 }
3282 if let Some(position) = position(record)? {
3283 details[position]
3284 .attempts
3285 .push(crate::coverage_report::TestAttempt {
3286 retry: usize::try_from(get_u64(record, 8)?)
3287 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3288 status: self.string(get_u32(record, 16)?)?,
3289 expected_status: self.optional_string(get_u32(record, 20)?)?,
3290 });
3291 }
3292 }
3293 let descriptor = self.index.descriptor(SECTION_TEST_LINES)?;
3294 for index in 0..descriptor.count {
3295 let record = self.index.record(SECTION_TEST_LINES, index)?;
3296 if record[1..4].iter().any(|byte| *byte != 0)
3297 || record[12..16].iter().any(|byte| *byte != 0)
3298 {
3299 return Err(CoverageIndexError::InvalidRecord("test line record"));
3300 }
3301 if let Some(position) = position(record)? {
3302 details[position]
3303 .lines
3304 .push(crate::coverage_report::SourceLine {
3305 file: self.string(get_u32(record, 8)?)?,
3306 line: usize::try_from(get_u64(record, 16)?)
3307 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3308 });
3309 }
3310 }
3311 let descriptor = self.index.descriptor(SECTION_TEST_HITS)?;
3312 for index in 0..descriptor.count {
3313 let record = self.index.record(SECTION_TEST_HITS, index)?;
3314 if record[1..4].iter().any(|byte| *byte != 0)
3315 || record[12..].iter().any(|byte| *byte != 0)
3316 {
3317 return Err(CoverageIndexError::InvalidRecord("test hit record"));
3318 }
3319 if let Some(position) = position(record)? {
3320 details[position]
3321 .hits
3322 .push(self.string(get_u32(record, 8)?)?);
3323 }
3324 }
3325 let descriptor = self.index.descriptor(SECTION_TEST_DECISIONS)?;
3326 let vectors = self.index.descriptor(SECTION_TEST_VECTORS)?.count;
3327 for index in 0..descriptor.count {
3328 let record = self.index.record(SECTION_TEST_DECISIONS, index)?;
3329 if record[1..4].iter().any(|byte| *byte != 0)
3330 || record[12..16].iter().any(|byte| *byte != 0)
3331 {
3332 return Err(CoverageIndexError::InvalidRecord("test decision record"));
3333 }
3334 if let Some(position) = position(record)? {
3335 let offset = get_u64(record, 16)?;
3336 let count = get_u64(record, 24)?;
3337 let end = offset
3338 .checked_add(count)
3339 .ok_or(CoverageIndexError::InvalidRecord("test vector range"))?;
3340 if end > vectors {
3341 return Err(CoverageIndexError::InvalidRecord("test vector range"));
3342 }
3343 let mut observed = Vec::with_capacity(
3344 usize::try_from(count).map_err(|_| CoverageIndexError::SizeOverflow)?,
3345 );
3346 for vector in offset..end {
3347 observed.push(self.test_vector(vector)?);
3348 }
3349 details[position]
3350 .decisions
3351 .push(crate::coverage_report::TestDecisionResult {
3352 id: self.string(get_u32(record, 8)?)?,
3353 vectors: observed,
3354 });
3355 }
3356 }
3357 Ok(details)
3358 }
3359
3360 pub fn hit_metadata(
3361 &self,
3362 view: CoverageViewId,
3363 ) -> Result<Vec<IndexedHitMetadata>, CoverageIndexError> {
3364 let descriptor = self.index.descriptor(SECTION_HIT_METADATA)?;
3365 let mut metadata = Vec::new();
3366 for index in 0..descriptor.count {
3367 let record = self.index.record(SECTION_HIT_METADATA, index)?;
3368 if CoverageViewId::try_from(record[0])? != view {
3369 continue;
3370 }
3371 if record[2..4].iter().any(|byte| *byte != 0)
3372 || record[12..16].iter().any(|byte| *byte != 0)
3373 {
3374 return Err(CoverageIndexError::InvalidRecord("hit metadata record"));
3375 }
3376 let obligation = match record[1] {
3377 0 => "statement",
3378 1 => "function",
3379 2 => "branch",
3380 _ => return Err(CoverageIndexError::InvalidRecord("hit obligation")),
3381 };
3382 let metadata_label = self.optional_string(get_u32(record, 36)?)?;
3383 metadata.push(IndexedHitMetadata {
3384 id: self.string(get_u32(record, 4)?)?,
3385 obligation: obligation.into(),
3386 file: self.string(get_u32(record, 8)?)?,
3387 line: usize::try_from(get_u64(record, 16)?)
3388 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3389 column: usize::try_from(get_u64(record, 24)?)
3390 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3391 branch_kind: self.optional_string(get_u32(record, 32)?)?,
3392 label: (obligation != "branch")
3393 .then_some(metadata_label.clone())
3394 .flatten(),
3395 alternative: self.optional_string(get_u32(record, 40)?)?,
3396 parent_id: (obligation == "branch").then_some(metadata_label).flatten(),
3397 source: self.string(get_u32(record, 44)?)?,
3398 tests: self.relation_strings(get_u64(record, 48)?, get_u64(record, 56)?)?,
3399 });
3400 }
3401 Ok(metadata)
3402 }
3403
3404 pub fn limitations(
3405 &self,
3406 view: CoverageViewId,
3407 ) -> Result<Vec<IndexedLimitation>, CoverageIndexError> {
3408 let descriptor = self.index.descriptor(SECTION_LIMITATIONS)?;
3409 let mut limitations = Vec::new();
3410 for index in 0..descriptor.count {
3411 let record = self.index.record(SECTION_LIMITATIONS, index)?;
3412 if CoverageViewId::try_from(record[0])? != view {
3413 continue;
3414 }
3415 if record[2..4].iter().any(|byte| *byte != 0)
3416 || record[40..].iter().any(|byte| *byte != 0)
3417 {
3418 return Err(CoverageIndexError::InvalidRecord("limitation record"));
3419 }
3420 limitations.push(IndexedLimitation {
3421 id: self.string(get_u32(record, 4)?)?,
3422 kind: self.string(get_u32(record, 8)?)?,
3423 file: self.string(get_u32(record, 12)?)?,
3424 source: self.string(get_u32(record, 16)?)?,
3425 reason: self.string(get_u32(record, 20)?)?,
3426 line: usize::try_from(get_u64(record, 24)?)
3427 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3428 column: usize::try_from(get_u64(record, 32)?)
3429 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3430 blocking: bool_field(record[1])?,
3431 });
3432 }
3433 Ok(limitations)
3434 }
3435
3436 pub fn decision_metadata(
3437 &self,
3438 view: CoverageViewId,
3439 ) -> Result<Vec<crate::coverage_report::DecisionMeta>, CoverageIndexError> {
3440 let descriptor = self.index.descriptor(SECTION_DECISION_METADATA)?;
3441 let mut metadata = Vec::new();
3442 for index in 0..descriptor.count {
3443 let record = self.index.record(SECTION_DECISION_METADATA, index)?;
3444 if CoverageViewId::try_from(record[0])? != view {
3445 continue;
3446 }
3447 if record[1..4].iter().any(|byte| *byte != 0)
3448 || record[20..24].iter().any(|byte| *byte != 0)
3449 || record[56..].iter().any(|byte| *byte != 0)
3450 {
3451 return Err(CoverageIndexError::InvalidRecord(
3452 "decision metadata record",
3453 ));
3454 }
3455 metadata.push(crate::coverage_report::DecisionMeta {
3456 id: self.string(get_u32(record, 4)?)?,
3457 file: self.string(get_u32(record, 8)?)?,
3458 source: self.string(get_u32(record, 12)?)?,
3459 kind: self.string(get_u32(record, 16)?)?,
3460 line: usize::try_from(get_u64(record, 24)?)
3461 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3462 column: usize::try_from(get_u64(record, 32)?)
3463 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3464 conditions: self.relation_strings(get_u64(record, 40)?, get_u64(record, 48)?)?,
3465 });
3466 }
3467 Ok(metadata)
3468 }
3469
3470 fn decision_vector_observation(
3471 &self,
3472 index: u64,
3473 ) -> Result<crate::coverage_report::VectorObservation, CoverageIndexError> {
3474 let record = self
3475 .index
3476 .record(SECTION_DECISION_VECTOR_OBSERVATIONS, index)?;
3477 Ok(crate::coverage_report::VectorObservation {
3478 confidence: self.confidence(get_u64(record, 0)?)?,
3479 vector: self.test_vector(get_u64(record, 8)?)?,
3480 tests: self.relation_strings(get_u64(record, 16)?, get_u64(record, 24)?)?,
3481 phases: self.relation_strings(get_u64(record, 32)?, get_u64(record, 40)?)?,
3482 explicit_phases: self.relation_strings(get_u64(record, 48)?, get_u64(record, 56)?)?,
3483 })
3484 }
3485
3486 fn decision_condition(
3487 &self,
3488 index: u64,
3489 ) -> Result<crate::coverage_report::ConditionResult, CoverageIndexError> {
3490 let record = self.index.record(SECTION_DECISION_CONDITIONS, index)?;
3491 if record[0] & !7 != 0 || record[1..4].iter().any(|byte| *byte != 0) {
3492 return Err(CoverageIndexError::InvalidRecord(
3493 "decision condition record",
3494 ));
3495 }
3496 let has_witness = record[0] & 4 != 0;
3497 let witness = if has_witness {
3498 Some([
3499 self.test_vector(get_u64(record, 16)?)?,
3500 self.test_vector(get_u64(record, 24)?)?,
3501 ])
3502 } else {
3503 None
3504 };
3505 let first_tests = self.relation_strings(get_u64(record, 32)?, get_u64(record, 40)?)?;
3506 let second_tests = self.relation_strings(get_u64(record, 48)?, get_u64(record, 56)?)?;
3507 if !has_witness && (!first_tests.is_empty() || !second_tests.is_empty()) {
3508 return Err(CoverageIndexError::InvalidRecord(
3509 "condition witness tests without witness",
3510 ));
3511 }
3512 Ok(crate::coverage_report::ConditionResult {
3513 index: usize::try_from(get_u64(record, 8)?)
3514 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3515 source: self.string(get_u32(record, 4)?)?,
3516 covered: record[0] & 1 != 0,
3517 assertion_covered: record[0] & 2 != 0,
3518 witness,
3519 witness_tests: has_witness.then_some([first_tests, second_tests]),
3520 })
3521 }
3522
3523 pub fn decision_details(
3524 &self,
3525 view: CoverageViewId,
3526 ) -> Result<Vec<crate::coverage_report::DecisionResult>, CoverageIndexError> {
3527 let metadata = self
3528 .decision_metadata(view)?
3529 .into_iter()
3530 .map(|meta| (meta.id.clone(), meta))
3531 .collect::<HashMap<_, _>>();
3532 let descriptor = self.index.descriptor(SECTION_DECISION_DETAILS)?;
3533 let observation_count = self
3534 .index
3535 .descriptor(SECTION_DECISION_VECTOR_OBSERVATIONS)?
3536 .count;
3537 let condition_count = self.index.descriptor(SECTION_DECISION_CONDITIONS)?.count;
3538 let mut decisions = Vec::new();
3539 for index in 0..descriptor.count {
3540 let record = self.index.record(SECTION_DECISION_DETAILS, index)?;
3541 if CoverageViewId::try_from(record[0])? != view {
3542 continue;
3543 }
3544 if record[1] & !3 != 0 || record[2..4].iter().any(|byte| *byte != 0) {
3545 return Err(CoverageIndexError::InvalidRecord("decision detail record"));
3546 }
3547 let executed = record[1] & 1 != 0;
3548 let covered = record[1] & 2 != 0;
3549 if covered && !executed {
3550 return Err(CoverageIndexError::InvalidRecord(
3551 "covered unexecuted decision",
3552 ));
3553 }
3554 let range = |offset: usize,
3555 available: u64,
3556 label: &'static str|
3557 -> Result<std::ops::Range<u64>, CoverageIndexError> {
3558 let start = get_u64(record, offset)?;
3559 let count = get_u64(record, offset + 8)?;
3560 let end = start
3561 .checked_add(count)
3562 .ok_or(CoverageIndexError::InvalidRecord(label))?;
3563 if end > available {
3564 return Err(CoverageIndexError::InvalidRecord(label));
3565 }
3566 Ok(start..end)
3567 };
3568 let observations = range(32, observation_count, "decision observation range")?
3569 .map(|index| self.decision_vector_observation(index))
3570 .collect::<Result<Vec<_>, _>>()?;
3571 let conditions = range(48, condition_count, "decision condition range")?
3572 .map(|index| self.decision_condition(index))
3573 .collect::<Result<Vec<_>, _>>()?;
3574 let id = self.string(get_u32(record, 4)?)?;
3575 let meta = metadata
3576 .get(&id)
3577 .cloned()
3578 .ok_or(CoverageIndexError::InvalidRecord(
3579 "missing decision metadata",
3580 ))?;
3581 if conditions.len() != meta.conditions.len()
3582 || conditions.iter().enumerate().any(|(index, condition)| {
3583 condition.index != index || condition.source != meta.conditions[index]
3584 })
3585 {
3586 return Err(CoverageIndexError::InvalidRecord(
3587 "decision condition denominator",
3588 ));
3589 }
3590 decisions.push(crate::coverage_report::DecisionResult {
3591 meta,
3592 executed,
3593 covered,
3594 vectors: observations
3595 .iter()
3596 .map(|observation| observation.vector.clone())
3597 .collect(),
3598 vector_observations: observations,
3599 conditions,
3600 tests: self.relation_strings(get_u64(record, 16)?, get_u64(record, 24)?)?,
3601 confidence: self.confidence(get_u64(record, 8)?)?,
3602 });
3603 }
3604 Ok(decisions)
3605 }
3606
3607 pub fn phase_summaries(
3608 &self,
3609 view: CoverageViewId,
3610 ) -> Result<Vec<IndexedPhaseSummary>, CoverageIndexError> {
3611 let descriptor = self.index.descriptor(SECTION_PHASE_SUMMARIES)?;
3612 let mut phases = Vec::new();
3613 for index in 0..descriptor.count {
3614 let record = self.index.record(SECTION_PHASE_SUMMARIES, index)?;
3615 if CoverageViewId::try_from(record[0])? != view {
3616 continue;
3617 }
3618 if record[1..4].iter().any(|byte| *byte != 0)
3619 || record[48..].iter().any(|byte| *byte != 0)
3620 {
3621 return Err(CoverageIndexError::InvalidRecord("phase summary record"));
3622 }
3623 phases.push(IndexedPhaseSummary {
3624 id: self.string(get_u32(record, 4)?)?,
3625 kind: self.string(get_u32(record, 8)?)?,
3626 operation: self.string(get_u32(record, 12)?)?,
3627 source: self.optional_string(get_u32(record, 16)?)?,
3628 test: self.string(get_u32(record, 20)?)?,
3629 status: self.optional_string(get_u32(record, 24)?)?,
3630 caused_by_phase_id: self.optional_string(get_u32(record, 28)?)?,
3631 lines: usize::try_from(get_u64(record, 32)?)
3632 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3633 decisions: usize::try_from(get_u64(record, 40)?)
3634 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3635 });
3636 }
3637 Ok(phases)
3638 }
3639
3640 pub fn anchors(
3641 &self,
3642 view: CoverageViewId,
3643 file: &str,
3644 line: usize,
3645 ) -> Result<Vec<IndexedAnchor>, CoverageIndexError> {
3646 let descriptor = self.index.descriptor(SECTION_ANCHORS)?;
3647 let mut anchors = Vec::new();
3648 for index in 0..descriptor.count {
3649 let record = self.index.record(SECTION_ANCHORS, index)?;
3650 if CoverageViewId::try_from(record[0])? != view {
3651 continue;
3652 }
3653 if record[3] != 0 || record[12..16].iter().any(|byte| *byte != 0) {
3654 return Err(CoverageIndexError::InvalidRecord("anchor record"));
3655 }
3656 let record_file = self.string(get_u32(record, 8)?)?;
3657 let record_line = usize::try_from(get_u64(record, 16)?)
3658 .map_err(|_| CoverageIndexError::SizeOverflow)?;
3659 if record_file != file || record_line != line {
3660 continue;
3661 }
3662 let total = usize::try_from(get_u64(record, 32)?)
3663 .map_err(|_| CoverageIndexError::SizeOverflow)?;
3664 let covered_conditions = usize::try_from(get_u64(record, 40)?)
3665 .map_err(|_| CoverageIndexError::SizeOverflow)?;
3666 let (kind, conditions, covered_conditions) = match record[1] {
3667 0 => {
3668 if total == 0 || covered_conditions > total {
3669 return Err(CoverageIndexError::InvalidRecord(
3670 "decision anchor conditions",
3671 ));
3672 }
3673 ("decision", Some(total), Some(covered_conditions))
3674 }
3675 1 => ("branch", None, None),
3676 2 => ("statement", None, None),
3677 3 => ("function", None, None),
3678 _ => return Err(CoverageIndexError::InvalidRecord("anchor kind")),
3679 };
3680 if kind != "decision" && (total != 0 || covered_conditions.is_some()) {
3681 return Err(CoverageIndexError::InvalidRecord("anchor conditions"));
3682 }
3683 anchors.push(IndexedAnchor {
3684 kind: kind.into(),
3685 id: self.string(get_u32(record, 4)?)?,
3686 file: record_file,
3687 line: record_line,
3688 column: usize::try_from(get_u64(record, 24)?)
3689 .map_err(|_| CoverageIndexError::SizeOverflow)?,
3690 covered: bool_field(record[2])?,
3691 conditions,
3692 covered_conditions,
3693 tests: self.relation_strings(get_u64(record, 48)?, get_u64(record, 56)?)?,
3694 });
3695 }
3696 anchors.sort_by_key(|anchor| anchor.column);
3697 Ok(anchors)
3698 }
3699
3700 pub fn snapshot(&self) -> Result<IndexedCoverageSnapshot, CoverageIndexError> {
3701 Ok(IndexedCoverageSnapshot {
3702 all_summary: self.summary(CoverageViewId::All)?,
3703 passed_summary: self.summary(CoverageViewId::Passed)?,
3704 failed_summary: self.summary(CoverageViewId::Failed)?,
3705 all_files: self.file_gaps(CoverageViewId::All, None, None)?,
3706 passed_files: self.file_gaps(CoverageViewId::Passed, None, None)?,
3707 failed_files: self.file_gaps(CoverageViewId::Failed, None, None)?,
3708 })
3709 }
3710}
3711
3712fn bool_field(value: u8) -> Result<bool, CoverageIndexError> {
3713 match value {
3714 0 => Ok(false),
3715 1 => Ok(true),
3716 _ => Err(CoverageIndexError::InvalidRecord("boolean")),
3717 }
3718}
3719
3720fn decode_summary(
3721 record: &[u8],
3722 flags_offset: usize,
3723 base: usize,
3724) -> Result<CoverageSummary, CoverageIndexError> {
3725 let number = |offset: usize| -> Result<usize, CoverageIndexError> {
3726 usize::try_from(get_u64(record, offset)?).map_err(|_| CoverageIndexError::SizeOverflow)
3727 };
3728 let count = |offset: usize| -> Result<CoverageCount, CoverageIndexError> {
3729 let covered = number(offset)?;
3730 let total = number(offset + 8)?;
3731 if covered > total {
3732 return Err(CoverageIndexError::InvalidRecord("covered exceeds total"));
3733 }
3734 Ok(CoverageCount {
3735 covered,
3736 total,
3737 percentage: percentage(covered, total),
3738 })
3739 };
3740 let decisions = number(base)?;
3741 let executed_decisions = number(base + 8)?;
3742 let covered_decisions = number(base + 16)?;
3743 let conditions = number(base + 24)?;
3744 let covered_conditions = number(base + 32)?;
3745 if covered_decisions > executed_decisions
3746 || executed_decisions > decisions
3747 || covered_conditions > conditions
3748 {
3749 return Err(CoverageIndexError::InvalidRecord("summary count ordering"));
3750 }
3751 Ok(CoverageSummary {
3752 unmeasured_obligations: None,
3753 exact_fraction_pct: None,
3754 decisions,
3755 executed_decisions,
3756 covered_decisions,
3757 conditions,
3758 covered_conditions,
3759 condition_coverage_pct: percentage(covered_conditions, conditions),
3760 lines: count(base + 40)?,
3761 statements: count(base + 56)?,
3762 functions: count(base + 72)?,
3763 branches: count(base + 88)?,
3764 decision_outcomes: count(base + 104)?,
3765 condition_outcomes: count(base + 120)?,
3766 value_selections: count(base + 136)?,
3767 coverage_complete: bool_field(record[flags_offset])?,
3768 completeness_blocked: match record[flags_offset + 1] {
3769 0 => None,
3770 1 => Some(false),
3771 2 => Some(true),
3772 _ => return Err(CoverageIndexError::InvalidRecord("optional boolean")),
3773 },
3774 })
3775}
3776
3777fn percentage(covered: usize, total: usize) -> f64 {
3778 if total == 0 {
3779 100.0
3780 } else {
3781 ((covered as f64 / total as f64) * 10_000.0).round() / 100.0
3782 }
3783}
3784
3785#[cfg(test)]
3786mod tests {
3787 use std::{
3788 fs,
3789 path::PathBuf,
3790 sync::atomic::{AtomicU64, Ordering},
3791 time::{SystemTime, UNIX_EPOCH},
3792 };
3793
3794 use crate::{
3795 coverage_analysis::{McdcVector, PointKind},
3796 coverage_report::{
3797 CoverageManifest, CoverageReportRequest, DecisionMeta, ExitCodeInput, PointMeta,
3798 RawTestResult, RuntimeSnapshot, TestProvenance, analyze_coverage_results,
3799 },
3800 query_index::{QueryIndexIdentity, write_query_index},
3801 };
3802
3803 use super::*;
3804
3805 static ROOT_SEQUENCE: AtomicU64 = AtomicU64::new(0);
3806
3807 fn root() -> PathBuf {
3808 let nonce = SystemTime::now()
3809 .duration_since(UNIX_EPOCH)
3810 .unwrap()
3811 .as_nanos();
3812 let root = std::env::temp_dir().join(format!(
3813 "supercov-coverage-index-{}-{nonce}-{}",
3814 std::process::id(),
3815 ROOT_SEQUENCE.fetch_add(1, Ordering::Relaxed),
3816 ));
3817 fs::create_dir_all(&root).unwrap();
3818 root
3819 }
3820
3821 fn identity() -> QueryIndexIdentity {
3822 QueryIndexIdentity {
3823 evidence_sha256: [1; 32],
3824 evidence_bytes: 100,
3825 analysis_sha256: [2; 32],
3826 producer_sha256: [3; 32],
3827 archive_schema_version: 2,
3828 }
3829 }
3830
3831 fn report() -> CoverageReport {
3832 let decision = DecisionMeta {
3833 id: "d".into(),
3834 file: "src/a.js".into(),
3835 line: 1,
3836 column: 1,
3837 source: "a && b".into(),
3838 conditions: vec!["a".into(), "b".into()],
3839 kind: "if".into(),
3840 };
3841 analyze_coverage_results(&CoverageReportRequest {
3842 run_id: "run".into(),
3843 manifest: CoverageManifest {
3844 unmeasured: Vec::new(),
3845 decisions: vec![decision.clone()],
3846 points: vec![PointMeta {
3847 id: "point".into(),
3848 kind: PointKind::Statement,
3849 file: "src/a.js".into(),
3850 line: 2,
3851 column: 3,
3852 source: "work();".into(),
3853 label: None,
3854 }],
3855 branches: Vec::new(),
3856 limitations: vec![serde_json::json!({
3857 "id": "dynamic",
3858 "kind": "dynamic-code",
3859 "file": "src/a.js",
3860 "line": 3,
3861 "column": 1,
3862 "source": "eval(code)",
3863 "reason": "dynamic source"
3864 })],
3865 scope: None,
3866 },
3867 raw_results: vec![RawTestResult {
3868 test_id: Some("test".into()),
3869 scope: None,
3870 test: "test".into(),
3871 test_file: Some("tests/a.js".into()),
3872 title: None,
3873 retry: Some(0),
3874 status: Some("passed".into()),
3875 expected_status: None,
3876 flaky: false,
3877 provenance: TestProvenance {
3878 runner: "node:test".into(),
3879 kind: "unit".into(),
3880 project: None,
3881 source: "runner-default".into(),
3882 },
3883 role: "test".into(),
3884 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
3885 phases: Vec::new(),
3886 runtime: vec![RuntimeSnapshot {
3887 decisions: vec![crate::coverage_report::DecisionSnapshot {
3888 meta: decision,
3889 vectors: vec![McdcVector {
3890 values: vec![Some(false), None],
3891 outcome: false,
3892 }],
3893 }],
3894 hits: vec!["point".into()],
3895 events: Vec::new(),
3896 logicals: Vec::new(),
3897 }],
3898 browser: Vec::new(),
3899 server: Vec::new(),
3900 }],
3901 generated_at: "time".into(),
3902 coverage_model: None,
3903 integrity: None,
3904 test_exit_code: ExitCodeInput::Present(Some(0)),
3905 })
3906 .unwrap()
3907 }
3908
3909 fn report_attributed(attribution: &str) -> CoverageReport {
3910 let mut report = report();
3911 for view in [
3912 &mut report.view,
3913 &mut report.filters.passed,
3914 &mut report.filters.failed,
3915 ] {
3916 for test in &mut view.tests {
3917 test.attribution = attribution.to_owned();
3918 }
3919 }
3920 report
3921 }
3922
3923 #[test]
3924 fn a_test_keeps_the_attribution_it_was_written_with() {
3925 let root = root();
3931 for attribution in [
3932 crate::coverage_report::ATTRIBUTION_EXACT,
3933 crate::coverage_report::ATTRIBUTION_RUN_WIDE,
3934 crate::coverage_report::ATTRIBUTION_PARTIAL,
3935 ] {
3936 let report = report_attributed(attribution);
3937 let path = root.join(format!("query-index-{attribution}.v1.bin"));
3938 write_query_index(
3939 &coverage_index_sections(&report).unwrap(),
3940 &identity(),
3941 &path,
3942 )
3943 .unwrap();
3944 let container = QueryIndex::open(&path, &identity()).unwrap();
3945 let index = CoverageIndex::new(&container).unwrap();
3946 let tests = index.test_summaries(CoverageViewId::All).unwrap();
3947 assert_eq!(tests.len(), 1);
3948 assert_eq!(
3949 tests[0].attribution, attribution,
3950 "written as `{attribution}` and read back as `{}`",
3951 tests[0].attribution
3952 );
3953 }
3954 }
3955
3956 #[test]
3957 fn an_attribution_the_byte_has_no_room_for_is_refused_at_the_write() {
3958 let error = coverage_index_sections(&report_attributed("probably-exact")).unwrap_err();
3962 assert!(
3963 matches!(error, CoverageIndexError::InvalidRecord("test attribution")),
3964 "{error:?}"
3965 );
3966 }
3967
3968 #[test]
3973 fn a_relation_named_twice_is_stored_once() {
3974 let mut strings = StringTable::default();
3975 let mut relations = StringRelations::default();
3976 let run = || ["alpha".to_string(), "beta".to_string(), "gamma".to_string()];
3977
3978 let first = relations.push(run(), &mut strings).unwrap();
3979 let second = relations.push(run(), &mut strings).unwrap();
3980 assert_eq!(first, second, "the second naming points at the first run");
3981 assert_eq!(relations.values.len(), 3, "and nothing was appended for it");
3982
3983 let other = relations
3987 .push(["alpha".to_string(), "delta".to_string()], &mut strings)
3988 .unwrap();
3989 assert_eq!(other, (3, 2));
3990 assert_eq!(relations.values.len(), 5);
3991
3992 let prefix = relations.push(["alpha".to_string()], &mut strings).unwrap();
3995 assert_eq!(prefix, (5, 1));
3996
3997 let read = |(offset, count): (u64, u64)| {
3999 relations.values[offset as usize..(offset + count) as usize].to_vec()
4000 };
4001 assert_eq!(read(first), read(second));
4002 assert_eq!(read(first).len(), 3);
4003 assert_ne!(read(first), read(other));
4004 }
4005
4006 #[test]
4007 fn typed_columns_round_trip_all_outcome_views_without_json() {
4008 let report = report();
4009 let root = root();
4010 let path = root.join("query-index.v1.bin");
4011 write_query_index(
4012 &coverage_index_sections(&report).unwrap(),
4013 &identity(),
4014 &path,
4015 )
4016 .unwrap();
4017 let container = QueryIndex::open(&path, &identity()).unwrap();
4018 let index = CoverageIndex::new(&container).unwrap();
4019 assert_eq!(
4020 index.model().unwrap(),
4021 IndexedCoverageModel {
4022 schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
4023 variant: report.view.variant.clone(),
4024 name: report.view.model.name.clone(),
4025 completeness_meaning: report.view.model.completeness_meaning.clone(),
4026 measured: report.view.model.measured.clone(),
4027 not_measured: report.view.model.not_measured.clone(),
4028 }
4029 );
4030 for (id, view) in [
4031 (CoverageViewId::All, &report.view),
4032 (CoverageViewId::Passed, &report.filters.passed),
4033 (CoverageViewId::Failed, &report.filters.failed),
4034 ] {
4035 assert_eq!(index.summary(id).unwrap(), view.summary);
4036 }
4037 let gaps = index.file_gaps(CoverageViewId::All, None, None).unwrap();
4038 assert_eq!(gaps.len(), 1);
4039 assert_eq!(gaps[0].file, "src/a.js");
4040 assert_eq!(gaps[0].missing_mcdc_conditions, 2);
4041 let projection = index.projection(CoverageViewId::All, None, None).unwrap();
4042 assert_eq!(projection.summary, report.view.summary);
4043 assert_eq!(projection.tests, 1);
4044 assert_eq!(projection.setups, 0);
4045 assert_eq!(projection.test_outcomes.passed, 1);
4046 assert!(projection.source_scope.is_none());
4047 let line = index
4048 .line(CoverageViewId::All, "src/a.js", 2)
4049 .unwrap()
4050 .unwrap();
4051 assert!(line.covered);
4052 assert_eq!(line.tests, ["test"]);
4053 assert_eq!(line.confidence.level, "executed");
4054 let tests = index.test_summaries(CoverageViewId::All).unwrap();
4055 assert_eq!(tests.len(), 1);
4056 assert_eq!(tests[0].provenance.runner, "node:test");
4057 let decision = index.anchors(CoverageViewId::All, "src/a.js", 1).unwrap();
4058 assert_eq!(decision.len(), 1);
4059 assert_eq!(decision[0].kind, "decision");
4060 assert_eq!(decision[0].conditions, Some(2));
4061 assert_eq!(decision[0].tests, ["test"]);
4062 let point = index.anchors(CoverageViewId::All, "src/a.js", 2).unwrap();
4063 assert_eq!(point.len(), 1);
4064 assert_eq!(point[0].kind, "statement");
4065 assert_eq!(point[0].tests, ["test"]);
4066 let details = index.test_details(CoverageViewId::All).unwrap();
4067 assert_eq!(details.len(), 1);
4068 assert_eq!(details[0].retries, [0]);
4069 assert_eq!(details[0].attempts.len(), 1);
4070 assert_eq!(details[0].hits, ["point"]);
4071 assert_eq!(details[0].lines.len(), 1);
4072 assert_eq!(details[0].lines[0].line, 2);
4073 assert_eq!(details[0].decisions.len(), 1);
4074 assert_eq!(details[0].decisions[0].vectors.len(), 1);
4075 assert_eq!(
4076 details[0].decisions[0].vectors[0].values,
4077 [Some(false), None]
4078 );
4079 let hits = index.hit_metadata(CoverageViewId::All).unwrap();
4080 assert_eq!(hits.len(), 1);
4081 assert_eq!(hits[0].id, "point");
4082 assert_eq!(hits[0].source, "work();");
4083 assert_eq!(hits[0].tests, ["test"]);
4084 let decisions = index.decision_metadata(CoverageViewId::All).unwrap();
4085 assert_eq!(decisions.len(), 1);
4086 assert_eq!(decisions[0].conditions, ["a", "b"]);
4087 assert_eq!(
4088 index.decision_details(CoverageViewId::All).unwrap(),
4089 report.view.decisions
4090 );
4091 let limitations = index.limitations(CoverageViewId::All).unwrap();
4092 assert_eq!(limitations.len(), 1);
4093 assert_eq!(limitations[0].kind, "dynamic-code");
4094 assert_eq!(limitations[0].line, 3);
4095 fs::remove_dir_all(root).unwrap();
4096 }
4097
4098 #[test]
4099 fn index_preserves_catalogued_unstarted_tests_without_attempts() {
4100 let report = analyze_coverage_results(&CoverageReportRequest {
4101 run_id: "run".into(),
4102 manifest: CoverageManifest {
4103 unmeasured: Vec::new(),
4104 decisions: Vec::new(),
4105 points: Vec::new(),
4106 branches: Vec::new(),
4107 limitations: Vec::new(),
4108 scope: None,
4109 },
4110 raw_results: vec![RawTestResult {
4111 test_id: Some("unstarted".into()),
4112 scope: None,
4113 test: "unstarted".into(),
4114 test_file: Some("tests/a.rs".into()),
4115 title: None,
4116 retry: None,
4117 status: Some("unstarted".into()),
4118 expected_status: Some("passed".into()),
4119 flaky: false,
4120 provenance: TestProvenance {
4121 runner: "rust-nextest".into(),
4122 kind: "unit".into(),
4123 project: None,
4124 source: "selected-but-not-started".into(),
4125 },
4126 role: "test".into(),
4127 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
4128 phases: Vec::new(),
4129 runtime: Vec::new(),
4130 browser: Vec::new(),
4131 server: Vec::new(),
4132 }],
4133 generated_at: "time".into(),
4134 coverage_model: None,
4135 integrity: None,
4136 test_exit_code: ExitCodeInput::Present(Some(100)),
4137 })
4138 .unwrap();
4139 let root = root();
4140 let path = root.join("query-index.v1.bin");
4141 write_query_index(
4142 &coverage_index_sections(&report).unwrap(),
4143 &identity(),
4144 &path,
4145 )
4146 .unwrap();
4147 let container = QueryIndex::open(&path, &identity()).unwrap();
4148 let index = CoverageIndex::new(&container).unwrap();
4149 let summaries = index.test_summaries(CoverageViewId::All).unwrap();
4150 assert_eq!(summaries.len(), 1);
4151 assert_eq!(summaries[0].outcome, "unstarted");
4152 let details = index.test_details(CoverageViewId::All).unwrap();
4153 assert!(details[0].retries.is_empty());
4154 assert!(details[0].attempts.is_empty());
4155 let projection = index.projection(CoverageViewId::All, None, None).unwrap();
4156 assert_eq!(projection.tests, 1);
4157 assert_eq!(projection.test_outcomes.unstarted, 1);
4158 fs::remove_dir_all(root).unwrap();
4159 }
4160
4161 #[test]
4162 fn source_discovery_scope_preserves_python_language() {
4163 let mut report = report();
4164 let scope = serde_json::json!({
4165 "version": 1,
4166 "mode": "automatic",
4167 "roots": ["."],
4168 "entries": [{
4169 "file": "src/a.py",
4170 "status": "included",
4171 "reason": "python source",
4172 "packageRoot": "."
4173 }]
4174 });
4175 for view in [
4176 &mut report.view,
4177 &mut report.filters.passed,
4178 &mut report.filters.failed,
4179 ] {
4180 view.scope = Some(scope.clone());
4181 view.model.language = "python".into();
4182 view.model.name = "python-probes-v1".into();
4183 }
4184 let root = root();
4185 let path = root.join("query-index.python.bin");
4186 write_query_index(
4187 &coverage_index_sections(&report).unwrap(),
4188 &identity(),
4189 &path,
4190 )
4191 .unwrap();
4192 let container = QueryIndex::open(&path, &identity()).unwrap();
4193 let index = CoverageIndex::new(&container).unwrap();
4194 let projection = index.projection(CoverageViewId::All, None, None).unwrap();
4195 let source_scope = projection.source_scope.unwrap();
4196 assert_eq!(source_scope.language, "python");
4197 assert_eq!(source_scope.model, "python-probes-v1");
4198 assert_eq!(source_scope.kind, "source-discovery");
4199 assert_eq!(source_scope.included, 1);
4200 assert_eq!(index.scope_entries(CoverageViewId::All).unwrap().len(), 1);
4201 fs::remove_dir_all(root).unwrap();
4202 }
4203
4204 #[test]
4205 fn compiler_owned_scope_round_trips_without_javascript_scope_fields() {
4206 let mut report = report();
4207 let scope = serde_json::json!({
4208 "language": "rust",
4209 "model": "rust-source-v1",
4210 "crate": "fixture",
4211 "measurementComplete": false
4212 });
4213 report.view.scope = Some(scope.clone());
4214 report.filters.passed.scope = Some(scope.clone());
4215 report.filters.failed.scope = Some(scope);
4216 let root = root();
4217 let path = root.join("query-index.v2.bin");
4218 write_query_index(
4219 &coverage_index_sections(&report).unwrap(),
4220 &identity(),
4221 &path,
4222 )
4223 .unwrap();
4224 let container = QueryIndex::open(&path, &identity()).unwrap();
4225 let index = CoverageIndex::new(&container).unwrap();
4226 let projection = index.projection(CoverageViewId::All, None, None).unwrap();
4227 assert_eq!(
4228 projection.source_scope,
4229 Some(IndexedSourceScope {
4230 kind: "compiler".into(),
4231 language: "rust".into(),
4232 model: "rust-source-v1".into(),
4233 mode: None,
4234 roots: Vec::new(),
4235 unit: Some("fixture".into()),
4236 measurement_complete: Some(false),
4237 included: 0,
4238 excluded: 0,
4239 ambiguous: 0,
4240 })
4241 );
4242 assert!(index.scope_entries(CoverageViewId::All).unwrap().is_empty());
4243 fs::remove_dir_all(root).unwrap();
4244 }
4245}