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