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