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