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