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