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