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