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