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