1use std::collections::{BTreeMap, BTreeSet, HashMap};
8
9use serde::{Deserialize, Serialize};
10
11use crate::interned::Id;
12use crate::{
13 agent_json::pagination,
14 coverage_analysis::{CoverageSummary, is_independence_pair},
15 coverage_index::{
16 CoverageDimension, CoverageIndex, CoverageIndexError, CoverageViewId, IndexedCoverageModel,
17 IndexedDecisionGap, IndexedDimensionCoverage, IndexedFileGap, IndexedGapDimensions,
18 IndexedHitMetadata, IndexedMeasurement, IndexedOutcomeCounts, IndexedScopeEntry,
19 IndexedSourceScope, IndexedSummaryConfidence, IndexedTestSummary,
20 },
21 coverage_report::{
22 CoverageConfidence, CoverageView, DecisionMeta, ReportError, SourceLine, TestAttempt,
23 TestProvenance, TransportStats, coverage_summary_for_tests,
24 },
25};
26use supercov_contracts::AgentPagination;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum MinimizeMetric {
31 All,
32 Lines,
33 Statements,
34 Functions,
35 Branches,
36 Mcdc,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct MinimumTestSetResult {
42 pub optimal: bool,
43 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
44 pub target: f64,
45 pub metric: MinimizeMetric,
46 pub selected: Vec<String>,
47 pub expanded: Vec<String>,
48 pub summary: CoverageSummary,
49 pub explored_states: usize,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53pub struct CoverageMinimizedTest {
54 pub id: String,
55 pub name: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub file: Option<String>,
58 pub runner: String,
59 pub kind: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct CoverageMinimizeData {
65 pub run: String,
66 pub filters: CoverageQueryFilters,
67 pub optimal: bool,
68 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
69 pub target: f64,
70 pub metric: MinimizeMetric,
71 pub selected: Vec<String>,
72 pub expanded: Vec<String>,
73 pub summary: CoverageSummary,
74 pub explored_states: usize,
75 pub selected_count: usize,
76 pub total_candidate_tests: usize,
77 pub tests: Vec<CoverageMinimizedTest>,
78}
79
80#[derive(Debug, Clone, Copy)]
81pub struct CoverageMinimizeQueryOptions<'a> {
82 pub run: &'a str,
83 pub view_id: CoverageViewId,
84 pub kind: Option<&'a str>,
85 pub runner: Option<&'a str>,
86 pub target: f64,
87 pub metric: MinimizeMetric,
88 pub max_states: usize,
89 pub offset: usize,
90 pub limit: usize,
91}
92
93pub fn coverage_minimize_query(
94 view: &CoverageView,
95 options: CoverageMinimizeQueryOptions<'_>,
96) -> Result<(CoverageMinimizeData, AgentPagination), QueryError> {
97 if options.limit == 0 {
98 return Err(QueryError::InvalidPagination);
99 }
100 let selected_ids = if options.kind.is_none() && options.runner.is_none() {
101 None
102 } else {
103 let ids = view
104 .tests
105 .iter()
106 .filter(|test| {
107 options.kind.is_none_or(|kind| test.provenance.kind == kind)
108 && options
109 .runner
110 .is_none_or(|runner| test.provenance.runner == runner)
111 })
112 .map(|test| test.id.clone())
113 .collect::<BTreeSet<_>>();
114 if ids.is_empty() {
115 return Err(QueryError::TestFilterEmpty {
116 kind: options.kind.map(str::to_owned),
117 runner: options.runner.map(str::to_owned),
118 });
119 }
120 Some(ids)
121 };
122 let mut solver_view = view.clone();
123 if let Some(selected) = &selected_ids {
124 solver_view.tests.retain(|test| selected.contains(&test.id));
125 }
126 let minimized = minimum_test_set(
127 &solver_view,
128 options.target,
129 options.metric,
130 options.max_states,
131 )?;
132 let selected_details = minimized
133 .selected
134 .iter()
135 .map(|id| {
136 let test = view
137 .tests
138 .iter()
139 .find(|test| test.id == *id)
140 .ok_or(QueryError::InvalidRecordSelection)?;
141 Ok(CoverageMinimizedTest {
142 id: id.clone(),
143 name: test.name.clone(),
144 file: test.file.clone(),
145 runner: test.provenance.runner.clone(),
146 kind: test.provenance.kind.clone(),
147 })
148 })
149 .collect::<Result<Vec<_>, QueryError>>()?;
150 let total = selected_details.len();
151 let tests = selected_details
152 .iter()
153 .skip(options.offset)
154 .take(options.limit)
155 .cloned()
156 .collect::<Vec<_>>();
157 let returned = tests.len();
158 let total_candidate_tests = solver_view
159 .tests
160 .iter()
161 .filter(|test| test.role == "test")
162 .count();
163 Ok((
164 CoverageMinimizeData {
165 run: options.run.into(),
166 filters: query_filters(options.view_id, options.kind, options.runner),
167 optimal: minimized.optimal,
168 target: minimized.target,
169 metric: minimized.metric,
170 selected: minimized.selected,
171 expanded: minimized.expanded,
172 summary: minimized.summary,
173 explored_states: minimized.explored_states,
174 selected_count: total,
175 total_candidate_tests,
176 tests,
177 },
178 pagination(options.offset, options.limit, returned, total),
179 ))
180}
181
182#[derive(Debug)]
183pub enum QueryError {
184 InvalidTarget(f64),
185 UnattributedEvidence,
186 TargetUnreachable {
187 metric: MinimizeMetric,
188 target: f64,
189 reachable: f64,
190 },
191 ComplexityLimit {
192 candidate_tests: usize,
193 obligations: usize,
194 explored_states: usize,
195 max_states: usize,
196 target: f64,
197 metric: MinimizeMetric,
198 },
199 Analysis(ReportError),
200 Index(CoverageIndexError),
201 InvalidPagination,
202 TestFilterEmpty {
203 kind: Option<String>,
204 runner: Option<String>,
205 },
206 TestNotFound(String),
207 DecisionNotFound(String),
208 SourceNotFound(String),
209 AmbiguousSelector {
210 selector: String,
211 matches: Vec<String>,
212 },
213 InvalidRecordSelection,
214 ScopeUnavailable,
215}
216
217impl From<ReportError> for QueryError {
218 fn from(value: ReportError) -> Self {
219 Self::Analysis(value)
220 }
221}
222
223impl From<CoverageIndexError> for QueryError {
224 fn from(value: CoverageIndexError) -> Self {
225 Self::Index(value)
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
230#[serde(rename_all = "camelCase")]
231pub struct CoverageQueryFilters {
232 pub outcome: String,
233 pub kind: Option<String>,
234 pub runner: Option<String>,
235}
236
237#[derive(Debug, Clone, PartialEq, Serialize)]
238#[serde(rename_all = "camelCase")]
239pub struct CoverageFilesData {
240 pub run: String,
241 pub filters: CoverageQueryFilters,
242 pub metric: MinimizeMetric,
243 pub files: Vec<IndexedFileGap>,
244}
245
246#[derive(Debug, Clone, PartialEq, Serialize)]
247#[serde(rename_all = "camelCase")]
248pub struct CoverageGapsData {
249 pub run: String,
250 pub filters: CoverageQueryFilters,
251 pub metric: MinimizeMetric,
252 pub gaps: Vec<IndexedFileGap>,
253}
254
255#[derive(Debug, Clone, PartialEq, Serialize)]
256#[serde(rename_all = "camelCase")]
257pub struct CoverageKindsData {
258 pub run: String,
259 pub filters: CoverageQueryFilters,
260 pub kinds: Vec<IndexedDimensionCoverage>,
261}
262
263#[derive(Debug, Clone, PartialEq, Serialize)]
264#[serde(rename_all = "camelCase")]
265pub struct CoverageRunnersData {
266 pub run: String,
267 pub filters: CoverageQueryFilters,
268 pub runners: Vec<IndexedDimensionCoverage>,
269}
270
271#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct TestAttributionCounts {
275 pub exact: usize,
277 pub partial: usize,
279 pub run_wide: usize,
281}
282
283impl TestAttributionCounts {
284 pub fn total(&self) -> usize {
285 self.exact + self.partial + self.run_wide
286 }
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
290#[serde(rename_all = "camelCase")]
291pub struct CoverageDiagnostic {
292 pub code: String,
293 pub severity: String,
294 pub message: String,
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize)]
298#[serde(rename_all = "camelCase")]
299pub struct CoverageSummaryData {
300 pub test_kind_sources: BTreeMap<String, usize>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub assertion_coverage: Option<serde_json::Value>,
303 pub run: String,
304 #[serde(default)]
305 pub command: Vec<String>,
306 #[serde(default, skip_serializing_if = "Vec::is_empty")]
307 pub hints: Vec<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub workspace: Option<String>,
310 pub filters: CoverageQueryFilters,
311 pub model: IndexedCoverageModel,
312 pub generated_at: String,
313 pub valid: bool,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub test_exit_code: Option<i32>,
316 pub stale: bool,
317 pub stale_reasons: Vec<String>,
318 pub structurally_complete: bool,
319 pub complete: bool,
320 pub coverage: CoverageSummary,
321 pub measurement: IndexedMeasurement,
322 pub coverage_by_kind: Vec<IndexedDimensionCoverage>,
323 #[serde(skip_serializing_if = "Option::is_none")]
327 pub coverage_by_tests: Option<CoverageSummary>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub e2e_gap_context: Option<CoverageKindGapContext>,
330 pub coverage_by_runner: Vec<IndexedDimensionCoverage>,
331 pub attribution: crate::coverage_index::IndexedAttribution,
332 pub test_attribution: TestAttributionCounts,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub transport: Option<TransportStats>,
340 pub diagnostics: Vec<CoverageDiagnostic>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub confidence: Option<IndexedSummaryConfidence>,
343 pub files_with_gaps: usize,
344 pub files_with_coverage_gaps: usize,
345 pub files_with_measurement_limitations: usize,
346 pub tests: usize,
347 pub setups: usize,
348 pub test_outcomes: IndexedOutcomeCounts,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub source_scope: Option<IndexedSourceScope>,
351 #[serde(skip_serializing_if = "Option::is_none")]
354 pub unclassified_source: Option<UnclassifiedSource>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
360#[serde(rename_all = "camelCase")]
361pub struct ScopeGroup {
362 pub status: String,
363 pub directory: String,
364 pub reason: String,
365 pub files: usize,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
369#[serde(rename_all = "camelCase")]
370pub struct ScopeDirectory {
371 pub directory: String,
372 pub files: usize,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
379#[serde(rename_all = "camelCase")]
380pub struct UnclassifiedSource {
381 pub directories: Vec<ScopeDirectory>,
382 pub roots: Vec<String>,
383}
384
385fn scope_directory(entry: &IndexedScopeEntry) -> String {
389 let package = entry.package_root.as_deref().filter(|root| *root != ".");
390 let local = package
391 .and_then(|root| entry.file.strip_prefix(root))
392 .and_then(|rest| rest.strip_prefix('/'))
393 .unwrap_or(&entry.file);
394 match (package, local.split_once('/')) {
395 (Some(package), Some((first, _))) => format!("{package}/{first}"),
396 (Some(package), None) => package.to_owned(),
397 (None, Some((first, _))) => first.to_owned(),
398 (None, None) => ".".to_owned(),
399 }
400}
401
402fn scope_status_rank(status: &str) -> u8 {
403 match status {
404 "ambiguous" => 0,
405 "included" => 1,
406 "excluded" => 2,
407 _ => 3,
408 }
409}
410
411pub fn scope_groups(entries: &[IndexedScopeEntry]) -> Vec<ScopeGroup> {
414 let mut groups = BTreeMap::<(u8, String, String, String), usize>::new();
415 for entry in entries {
416 *groups
417 .entry((
418 scope_status_rank(&entry.status),
419 entry.status.clone(),
420 scope_directory(entry),
421 entry.reason.clone(),
422 ))
423 .or_default() += 1;
424 }
425 let mut groups = groups
426 .into_iter()
427 .map(|((rank, status, directory, reason), files)| {
428 (
429 rank,
430 ScopeGroup {
431 status,
432 directory,
433 reason,
434 files,
435 },
436 )
437 })
438 .collect::<Vec<_>>();
439 groups.sort_by(|(left_rank, left), (right_rank, right)| {
440 left_rank
441 .cmp(right_rank)
442 .then_with(|| right.files.cmp(&left.files))
443 .then_with(|| left.directory.cmp(&right.directory))
444 .then_with(|| left.reason.cmp(&right.reason))
445 });
446 groups.into_iter().map(|(_, group)| group).collect()
447}
448
449pub fn unclassified_source(
450 roots: &[String],
451 entries: &[IndexedScopeEntry],
452) -> Option<UnclassifiedSource> {
453 let mut places = BTreeMap::<String, usize>::new();
454 let mut named = Vec::<(String, usize)>::new();
455 for entry in entries.iter().filter(|entry| entry.status == "ambiguous") {
456 let directory = scope_directory(entry);
457 *places.entry(directory.clone()).or_default() += 1;
458 let root = if entry.file.contains('/') && directory != "." {
461 directory
462 } else {
463 entry.file.clone()
464 };
465 match named.iter_mut().find(|(name, _)| *name == root) {
466 Some((_, files)) => *files += 1,
467 None => named.push((root, 1)),
468 }
469 }
470 if places.is_empty() {
471 return None;
472 }
473 let mut directories = places
474 .into_iter()
475 .map(|(directory, files)| ScopeDirectory { directory, files })
476 .collect::<Vec<_>>();
477 directories.sort_by(|left, right| {
478 right
479 .files
480 .cmp(&left.files)
481 .then_with(|| left.directory.cmp(&right.directory))
482 });
483 named.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
484 let mut all = roots.to_vec();
485 for (root, _) in named {
486 if !all.contains(&root) {
487 all.push(root);
488 }
489 }
490 Some(UnclassifiedSource {
491 directories,
492 roots: all,
493 })
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
497#[serde(rename_all = "camelCase")]
498pub struct CoverageKindGapContext {
499 pub kind: String,
500 pub other_kinds: Vec<String>,
501 pub covered_elsewhere: IndexedGapDimensions,
502 pub uncovered_everywhere: IndexedGapDimensions,
503}
504
505#[derive(Debug, Clone)]
506pub struct CoverageSummaryQueryOptions<'a> {
507 pub run: &'a str,
508 pub view: CoverageViewId,
509 pub kind: Option<&'a str>,
510 pub runner: Option<&'a str>,
511 pub valid: bool,
512 pub test_exit_code: Option<i32>,
513 pub stale: bool,
514 pub stale_reasons: Vec<String>,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
518pub struct ScopeCounts {
519 pub included: usize,
520 pub excluded: usize,
521 pub ambiguous: usize,
522}
523
524#[derive(Debug, Clone, PartialEq, Serialize)]
525#[serde(rename_all = "camelCase")]
526pub struct CoverageScopeData {
527 pub run: String,
528 pub filters: CoverageQueryFilters,
529 pub kind: String,
530 pub language: String,
531 pub model: String,
532 #[serde(skip_serializing_if = "Option::is_none")]
533 pub mode: Option<String>,
534 pub roots: Vec<String>,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub unit: Option<String>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub measurement_complete: Option<bool>,
539 pub counts: ScopeCounts,
540 pub measurement: IndexedMeasurement,
541 pub groups: Vec<ScopeGroup>,
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub unclassified_source: Option<UnclassifiedSource>,
545 pub entries: Vec<IndexedScopeEntry>,
546}
547
548#[derive(Debug, Clone, Copy)]
549pub struct CoverageScopeQueryOptions<'a> {
550 pub run: &'a str,
551 pub view: CoverageViewId,
552 pub kind: Option<&'a str>,
553 pub runner: Option<&'a str>,
554 pub offset: usize,
555 pub limit: usize,
556}
557
558#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
559pub struct CoverageLocation {
560 pub file: String,
561 pub line: usize,
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
565pub struct CoverageCoveringTest {
566 pub id: String,
567 pub name: String,
568 pub provenance: TestProvenance,
569}
570
571#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
572#[serde(rename_all = "camelCase")]
573pub struct CoverageCoveringPhase {
574 pub id: String,
575 pub kind: String,
576 pub operation: String,
577 #[serde(skip_serializing_if = "Option::is_none")]
578 pub source: Option<String>,
579 pub test: String,
580 #[serde(skip_serializing_if = "Option::is_none")]
581 pub status: Option<String>,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub caused_by_phase_id: Option<String>,
584}
585
586#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
587#[serde(rename_all = "camelCase")]
588pub struct CoverageAnchor {
589 pub kind: String,
590 pub id: String,
591 pub column: usize,
592 pub covered: bool,
593 #[serde(skip_serializing_if = "Option::is_none")]
594 pub source: Option<String>,
595 #[serde(skip_serializing_if = "Option::is_none")]
596 pub missing: Option<String>,
597 pub covering_tests: usize,
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub covered_conditions: Option<usize>,
600 #[serde(skip_serializing_if = "Option::is_none")]
601 pub conditions: Option<usize>,
602}
603
604#[derive(Debug, Clone, PartialEq, Serialize)]
605#[serde(rename_all = "camelCase")]
606pub struct CoverageCoversLineData {
607 pub run: String,
608 pub filters: CoverageQueryFilters,
609 pub location: CoverageLocation,
610 #[serde(skip_serializing_if = "Option::is_none")]
611 pub source: Option<String>,
612 #[serde(skip_serializing_if = "Option::is_none")]
613 pub source_origin: Option<String>,
614 pub covered: bool,
615 pub confidence: CoverageConfidence,
616 pub total_tests: usize,
617 pub total_phases: usize,
618 pub total_anchored: usize,
619 pub covered_anchored: usize,
620 pub total_limitations: usize,
621 pub total_remaining: usize,
622 pub tests: Vec<CoverageCoveringTest>,
623 pub phases: Vec<CoverageCoveringPhase>,
624 pub anchored: Vec<CoverageAnchor>,
625 pub limitations: Vec<CoverageFileLimitation>,
626 pub remaining: Vec<CoverageFileObligation>,
627 #[serde(skip_serializing_if = "Option::is_none")]
630 pub assertions: Option<serde_json::Value>,
631}
632
633#[derive(Debug, Clone, PartialEq, Serialize)]
634#[serde(rename_all = "camelCase")]
635pub struct CoverageCoversAnchorsData {
636 pub run: String,
637 pub filters: CoverageQueryFilters,
638 pub location: CoverageLocation,
639 #[serde(skip_serializing_if = "Option::is_none")]
640 pub source: Option<String>,
641 #[serde(skip_serializing_if = "Option::is_none")]
642 pub source_origin: Option<String>,
643 pub line_obligation: bool,
644 pub anchored: Vec<CoverageAnchor>,
645 pub total_anchored: usize,
646 pub covered_anchored: usize,
647 pub total_limitations: usize,
648 pub limitations: Vec<CoverageFileLimitation>,
649 pub total_remaining: usize,
650 pub remaining: Vec<CoverageFileObligation>,
651 pub total_tests: usize,
652 pub tests: Vec<CoverageCoveringTest>,
653 #[serde(skip_serializing_if = "Option::is_none")]
656 pub assertions: Option<serde_json::Value>,
657}
658
659#[derive(Debug, Clone, PartialEq, Serialize)]
660#[serde(untagged)]
661pub enum CoverageCoversData {
662 Line(CoverageCoversLineData),
663 Anchors(CoverageCoversAnchorsData),
664}
665
666#[derive(Debug, Clone, Copy)]
667pub struct CoverageCoversQueryOptions<'a> {
668 pub run: &'a str,
669 pub view: CoverageViewId,
670 pub kind: Option<&'a str>,
671 pub runner: Option<&'a str>,
672 pub file: &'a str,
673 pub line: usize,
674 pub offset: usize,
675 pub limit: usize,
676}
677
678fn query_filters(
679 view: CoverageViewId,
680 kind: Option<&str>,
681 runner: Option<&str>,
682) -> CoverageQueryFilters {
683 CoverageQueryFilters {
684 outcome: match view {
685 CoverageViewId::All => "all",
686 CoverageViewId::Passed => "passed",
687 CoverageViewId::Failed => "failed",
688 }
689 .into(),
690 kind: kind.map(str::to_owned),
691 runner: runner.map(str::to_owned),
692 }
693}
694
695fn selected_test_ids(
696 tests: &[IndexedTestSummary],
697 kind: Option<&str>,
698 runner: Option<&str>,
699) -> Result<Option<BTreeSet<String>>, QueryError> {
700 if kind.is_none() && runner.is_none() {
701 return Ok(None);
702 }
703 let selected = tests
704 .iter()
705 .filter(|test| {
706 kind.is_none_or(|kind| test.provenance.kind == kind)
707 && runner.is_none_or(|runner| test.provenance.runner == runner)
708 })
709 .map(|test| test.id.clone())
710 .collect::<BTreeSet<_>>();
711 if selected.is_empty() {
712 return Err(QueryError::TestFilterEmpty {
713 kind: kind.map(str::to_owned),
714 runner: runner.map(str::to_owned),
715 });
716 }
717 Ok(Some(selected))
718}
719
720pub fn coverage_covers_query(
721 index: &CoverageIndex<'_>,
722 options: CoverageCoversQueryOptions<'_>,
723) -> Result<(CoverageCoversData, AgentPagination), QueryError> {
724 if options.limit == 0 {
725 return Err(QueryError::InvalidPagination);
726 }
727 let tests = index.test_summaries(options.view)?;
728 let selected = selected_test_ids(&tests, options.kind, options.runner)?;
729 let selected_includes = |id: &str| selected.as_ref().is_none_or(|ids| ids.contains(id));
730 let filters = query_filters(options.view, options.kind, options.runner);
731 let location = CoverageLocation {
732 file: options.file.into(),
733 line: options.line,
734 };
735 let metadata = index
736 .hit_metadata(options.view)?
737 .into_iter()
738 .map(|value| (value.id.clone(), value))
739 .collect::<HashMap<_, _>>();
740 let decisions = index
741 .decision_details(options.view)?
742 .into_iter()
743 .map(|value| (value.meta.id.clone(), value))
744 .collect::<HashMap<_, _>>();
745 let anchors = index.anchors(options.view, options.file, options.line)?;
746 let tests_by_id = tests
747 .iter()
748 .map(|test| (test.id.clone(), test.clone()))
749 .collect::<HashMap<_, _>>();
750 let mut anchor_test_ids = anchors
751 .iter()
752 .flat_map(|anchor| anchor.tests.iter())
753 .filter(|id| selected_includes(id))
754 .cloned()
755 .collect::<BTreeSet<_>>();
756 for anchor in anchors.iter().filter(|anchor| anchor.kind == "branch") {
757 anchor_test_ids.extend(
758 metadata
759 .values()
760 .filter(|detail| detail.parent_id.as_deref() == Some(anchor.id.as_str()))
761 .flat_map(|detail| detail.tests.iter())
762 .filter(|id| selected_includes(id))
763 .cloned(),
764 );
765 }
766 let all_anchor_tests = anchor_test_ids
767 .iter()
768 .map(|id| {
769 let test = tests_by_id.get(id);
770 CoverageCoveringTest {
771 id: id.clone(),
772 name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
773 provenance: test
774 .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
775 }
776 })
777 .collect::<Vec<_>>();
778 let total_anchor_tests = all_anchor_tests.len();
779 let anchor_tests_page = all_anchor_tests
780 .iter()
781 .skip(options.offset)
782 .take(options.limit)
783 .cloned()
784 .collect::<Vec<_>>();
785 let render_anchor = |anchor: crate::coverage_index::IndexedAnchor| {
786 let branch_alternatives = if anchor.kind == "branch" {
787 metadata
788 .values()
789 .filter(|detail| {
790 detail.obligation == "branch"
791 && detail.parent_id.as_deref() == Some(anchor.id.as_str())
792 })
793 .collect::<Vec<_>>()
794 } else {
795 Vec::new()
796 };
797 let branch_tests = branch_alternatives
798 .iter()
799 .flat_map(|detail| detail.tests.iter())
800 .filter(|test| selected_includes(test))
801 .cloned()
802 .collect::<BTreeSet<_>>();
803 let covering_tests = if anchor.kind == "branch" {
804 branch_tests.len()
805 } else {
806 anchor
807 .tests
808 .iter()
809 .filter(|test| selected_includes(test))
810 .count()
811 };
812 let detail = metadata.get(&anchor.id);
813 let decision = decisions
814 .get(&anchor.id)
815 .cloned()
816 .map(|decision| selected_decision(decision, selected.as_ref()));
817 let conditions = decision.as_ref().map_or(anchor.conditions, |decision| {
818 Some(decision.conditions.len())
819 });
820 let covered_conditions = decision
821 .as_ref()
822 .map_or(anchor.covered_conditions, |decision| {
823 Some(
824 decision
825 .conditions
826 .iter()
827 .filter(|condition| condition.covered)
828 .count(),
829 )
830 });
831 let covered = match anchor.kind.as_str() {
832 "decision" => conditions == covered_conditions,
833 "branch" if !branch_alternatives.is_empty() => branch_alternatives
834 .iter()
835 .all(|detail| detail.tests.iter().any(|test| selected_includes(test))),
836 "branch" => anchor.covered,
837 _ => covering_tests > 0,
838 };
839 let branch_source = branch_alternatives
840 .first()
841 .map(|detail| detail.source.clone());
842 let missing_branch_alternatives = branch_alternatives
843 .iter()
844 .filter(|detail| !detail.tests.iter().any(|test| selected_includes(test)))
845 .filter_map(|detail| detail.alternative.clone())
846 .collect::<Vec<_>>();
847 CoverageAnchor {
848 kind: anchor.kind,
849 id: anchor.id,
850 column: anchor.column,
851 covered,
852 source: decision
853 .as_ref()
854 .map(|decision| decision.meta.source.clone())
855 .or(branch_source)
856 .or_else(|| {
857 detail.map(|detail| {
858 detail
859 .label
860 .clone()
861 .unwrap_or_else(|| detail.source.clone())
862 })
863 })
864 .and_then(|source| compact_source(&source)),
865 missing: if missing_branch_alternatives.is_empty() {
866 detail.and_then(|detail| detail.alternative.clone())
867 } else {
868 Some(missing_branch_alternatives.join("; "))
869 },
870 covering_tests,
871 covered_conditions,
872 conditions,
873 }
874 };
875 let rendered_anchors = anchors.into_iter().map(render_anchor).collect::<Vec<_>>();
876 let total_anchored = rendered_anchors.len();
877 let covered_anchored = rendered_anchors
878 .iter()
879 .filter(|anchor| anchor.covered)
880 .count();
881 let all_limitations = index
882 .limitations(options.view)?
883 .into_iter()
884 .filter(|limitation| limitation.file == options.file && limitation.line == options.line)
885 .map(|limitation| CoverageFileLimitation {
886 id: limitation.id,
887 kind: limitation.kind,
888 file: limitation.file,
889 line: limitation.line,
890 column: limitation.column,
891 source: limitation.source,
892 reason: limitation.reason,
893 blocking: limitation.blocking,
894 effect: "outside-measured-denominator".into(),
895 })
896 .collect::<Vec<_>>();
897 let total_limitations = all_limitations.len();
898 let limitations_page = all_limitations
899 .iter()
900 .skip(options.offset)
901 .take(options.limit)
902 .cloned()
903 .collect::<Vec<_>>();
904 let (file_detail, _) = coverage_file_detail_query(
905 index,
906 CoverageFileDetailOptions {
907 run: options.run,
908 view: options.view,
909 kind: options.kind,
910 runner: options.runner,
911 selector: options.file,
912 metric: MinimizeMetric::All,
913 offset: 0,
914 limit: usize::MAX,
915 },
916 )?;
917 let gap_line = file_detail
918 .gap_lines
919 .into_iter()
920 .find(|gap| gap.line == options.line);
921 let source = gap_line
927 .as_ref()
928 .and_then(|gap| gap.source.clone())
929 .or_else(|| {
930 rendered_anchors
931 .iter()
932 .filter_map(|anchor| anchor.source.clone())
933 .min_by_key(|source| source.len())
934 });
935 let all_remaining = gap_line.map_or_else(Vec::new, |gap| gap.obligations);
936 let total_remaining = all_remaining.len();
937 let remaining_page = all_remaining
938 .iter()
939 .skip(options.offset)
940 .take(options.limit)
941 .cloned()
942 .collect::<Vec<_>>();
943 let Some(line) = index.line(options.view, options.file, options.line)? else {
944 let anchored = rendered_anchors
945 .iter()
946 .skip(options.offset)
947 .take(options.limit)
948 .cloned()
949 .collect::<Vec<_>>();
950 let total = total_anchored.max(total_limitations).max(total_remaining);
951 let total = total.max(total_anchor_tests);
952 let returned = anchored
953 .len()
954 .max(limitations_page.len())
955 .max(remaining_page.len())
956 .max(anchor_tests_page.len());
957 return Ok((
958 CoverageCoversData::Anchors(CoverageCoversAnchorsData {
959 run: options.run.into(),
960 filters,
961 location,
962 source,
963 source_origin: None,
964 line_obligation: false,
965 anchored,
966 total_anchored,
967 covered_anchored,
968 total_limitations,
969 limitations: limitations_page,
970 total_remaining,
971 remaining: remaining_page,
972 total_tests: total_anchor_tests,
973 tests: anchor_tests_page,
974 assertions: None,
975 }),
976 pagination(options.offset, options.limit, returned, total),
977 ));
978 };
979 let all_tests = line
980 .tests
981 .iter()
982 .filter(|id| selected_includes(id))
983 .map(|id| {
984 let test = tests_by_id.get(id);
985 CoverageCoveringTest {
986 id: id.clone(),
987 name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
988 provenance: test
989 .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
990 }
991 })
992 .collect::<Vec<_>>();
993 let phases_by_id = index
994 .phase_summaries(options.view)?
995 .into_iter()
996 .map(|phase| (phase.id.clone(), phase))
997 .collect::<HashMap<_, _>>();
998 let all_phases = line
999 .phases
1000 .iter()
1001 .filter_map(|id| phases_by_id.get(id))
1002 .filter(|phase| selected_includes(&phase.test))
1003 .map(|phase| CoverageCoveringPhase {
1004 id: phase.id.clone(),
1005 kind: phase.kind.clone(),
1006 operation: phase.operation.clone(),
1007 source: phase.source.clone(),
1008 test: phase.test.clone(),
1009 status: phase.status.clone(),
1010 caused_by_phase_id: phase.caused_by_phase_id.clone(),
1011 })
1012 .collect::<Vec<_>>();
1013 let tests_page = all_tests
1014 .iter()
1015 .skip(options.offset)
1016 .take(options.limit)
1017 .cloned()
1018 .collect::<Vec<_>>();
1019 let phases_page = all_phases
1020 .iter()
1021 .skip(options.offset)
1022 .take(options.limit)
1023 .cloned()
1024 .collect::<Vec<_>>();
1025 let anchored_page = rendered_anchors
1026 .into_iter()
1027 .skip(options.offset)
1028 .take(options.limit)
1029 .collect::<Vec<_>>();
1030 let total = all_tests
1031 .len()
1032 .max(all_phases.len())
1033 .max(total_anchored)
1034 .max(total_limitations)
1035 .max(total_remaining);
1036 let returned = tests_page
1037 .len()
1038 .max(phases_page.len())
1039 .max(anchored_page.len())
1040 .max(limitations_page.len())
1041 .max(remaining_page.len());
1042 Ok((
1043 CoverageCoversData::Line(CoverageCoversLineData {
1044 run: options.run.into(),
1045 filters,
1046 location,
1047 source,
1048 source_origin: None,
1049 covered: line.tests.iter().any(|test| selected_includes(test)),
1050 confidence: line.confidence,
1051 total_tests: all_tests.len(),
1052 total_phases: all_phases.len(),
1053 total_anchored,
1054 covered_anchored,
1055 total_limitations,
1056 total_remaining,
1057 tests: tests_page,
1058 phases: phases_page,
1059 anchored: anchored_page,
1060 limitations: limitations_page,
1061 remaining: remaining_page,
1062 assertions: None,
1063 }),
1064 pagination(options.offset, options.limit, returned, total),
1065 ))
1066}
1067
1068#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1069pub struct CoverageTestMatch {
1070 pub id: String,
1071 pub name: String,
1072 pub outcome: String,
1073 pub provenance: TestProvenance,
1074}
1075
1076#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct CoverageHitDetail {
1079 pub id: String,
1080 pub obligation: String,
1081 #[serde(skip_serializing_if = "Option::is_none")]
1082 pub branch_kind: Option<String>,
1083 #[serde(skip_serializing_if = "Option::is_none")]
1084 pub file: Option<String>,
1085 #[serde(skip_serializing_if = "Option::is_none")]
1086 pub line: Option<usize>,
1087 #[serde(skip_serializing_if = "Option::is_none")]
1088 pub column: Option<usize>,
1089 #[serde(skip_serializing_if = "Option::is_none")]
1090 pub label: Option<String>,
1091 #[serde(skip_serializing_if = "Option::is_none")]
1092 pub alternative: Option<String>,
1093}
1094
1095#[derive(Debug, Clone, PartialEq, Serialize)]
1096pub struct CoverageTestDecision {
1097 pub id: String,
1098 pub vectors: Vec<crate::coverage_analysis::McdcVector>,
1099 pub meta: DecisionMeta,
1100}
1101
1102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct CoverageTestPhase {
1105 pub id: String,
1106 pub kind: String,
1107 pub operation: String,
1108 #[serde(skip_serializing_if = "Option::is_none")]
1109 pub source: Option<String>,
1110 #[serde(skip_serializing_if = "Option::is_none")]
1111 pub status: Option<String>,
1112 #[serde(skip_serializing_if = "Option::is_none")]
1113 pub caused_by_phase_id: Option<String>,
1114 pub lines: usize,
1115 pub decisions: usize,
1116}
1117
1118#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1119pub struct CoverageTestTotals {
1120 pub lines: usize,
1121 pub hits: usize,
1122 pub decisions: usize,
1123 pub phases: usize,
1124}
1125
1126#[derive(Debug, Clone, PartialEq, Serialize)]
1127#[serde(rename_all = "camelCase")]
1128pub struct CoverageSelectedTest {
1129 pub id: String,
1130 pub name: String,
1131 #[serde(skip_serializing_if = "Option::is_none")]
1132 pub file: Option<String>,
1133 #[serde(skip_serializing_if = "Option::is_none")]
1134 pub title: Option<String>,
1135 pub retries: Vec<usize>,
1136 pub attempts: Vec<TestAttempt>,
1137 pub outcome: String,
1138 pub provenance: TestProvenance,
1139 pub role: String,
1140 pub attribution: String,
1144 pub hits: Vec<String>,
1145 pub decisions: Vec<CoverageTestDecision>,
1146 pub lines: Vec<SourceLine>,
1147 pub hit_details: Vec<CoverageHitDetail>,
1148 pub phases: Vec<CoverageTestPhase>,
1149 pub totals: CoverageTestTotals,
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Serialize)]
1153pub struct CoverageTestMatchesData {
1154 pub run: String,
1155 pub filters: CoverageQueryFilters,
1156 pub tests: Vec<CoverageTestMatch>,
1157}
1158
1159#[derive(Debug, Clone, PartialEq, Serialize)]
1160#[serde(rename_all = "camelCase")]
1161pub struct CoverageTestDetailData {
1162 pub run: String,
1163 pub filters: CoverageQueryFilters,
1164 pub pagination_applies_to: String,
1165 pub tests: Vec<CoverageSelectedTest>,
1166}
1167
1168#[derive(Debug, Clone, PartialEq, Serialize)]
1169#[serde(untagged)]
1170pub enum CoverageTestData {
1171 Matches(CoverageTestMatchesData),
1172 Detail(CoverageTestDetailData),
1173}
1174
1175#[derive(Debug, Clone, Copy)]
1176pub struct CoverageTestQueryOptions<'a> {
1177 pub run: &'a str,
1178 pub view: CoverageViewId,
1179 pub kind: Option<&'a str>,
1180 pub runner: Option<&'a str>,
1181 pub selector: &'a str,
1182 pub offset: usize,
1183 pub limit: usize,
1184}
1185
1186fn hit_detail(id: &str, metadata: Option<&IndexedHitMetadata>) -> CoverageHitDetail {
1187 match metadata {
1188 Some(metadata) => CoverageHitDetail {
1189 id: id.into(),
1190 obligation: metadata.obligation.clone(),
1191 branch_kind: metadata.branch_kind.clone(),
1192 file: Some(metadata.file.clone()),
1193 line: Some(metadata.line),
1194 column: Some(metadata.column),
1195 label: metadata.label.clone(),
1196 alternative: metadata.alternative.clone(),
1197 },
1198 None => CoverageHitDetail {
1199 id: id.into(),
1200 obligation: "unknown".into(),
1201 branch_kind: None,
1202 file: None,
1203 line: None,
1204 column: None,
1205 label: None,
1206 alternative: None,
1207 },
1208 }
1209}
1210
1211pub fn coverage_test_query(
1212 index: &CoverageIndex<'_>,
1213 options: CoverageTestQueryOptions<'_>,
1214) -> Result<(CoverageTestData, AgentPagination), QueryError> {
1215 if options.limit == 0 {
1216 return Err(QueryError::InvalidPagination);
1217 }
1218 let tests = index.test_details(options.view)?;
1219 let summaries = tests
1220 .iter()
1221 .map(|test| test.summary.clone())
1222 .collect::<Vec<_>>();
1223 let selected = selected_test_ids(&summaries, options.kind, options.runner)?;
1224 let selector = options.selector.to_lowercase();
1225 let matches = tests
1226 .into_iter()
1227 .filter(|test| {
1228 selected
1229 .as_ref()
1230 .is_none_or(|ids| ids.contains(&test.summary.id))
1231 })
1232 .filter(|test| {
1233 test.summary.id == selector || test.summary.name.to_lowercase().contains(&selector)
1234 })
1235 .collect::<Vec<_>>();
1236 if matches.is_empty() {
1237 return Err(QueryError::TestNotFound(options.selector.into()));
1238 }
1239 let filters = query_filters(options.view, options.kind, options.runner);
1240 if matches.len() > 1 {
1241 let total = matches.len();
1242 let page = matches
1243 .into_iter()
1244 .skip(options.offset)
1245 .take(options.limit)
1246 .map(|test| CoverageTestMatch {
1247 id: test.summary.id,
1248 name: test.summary.name,
1249 outcome: test.summary.outcome,
1250 provenance: test.summary.provenance,
1251 })
1252 .collect::<Vec<_>>();
1253 let returned = page.len();
1254 return Ok((
1255 CoverageTestData::Matches(CoverageTestMatchesData {
1256 run: options.run.into(),
1257 filters,
1258 tests: page,
1259 }),
1260 pagination(options.offset, options.limit, returned, total),
1261 ));
1262 }
1263 let test = matches.into_iter().next().expect("one test match");
1264 let metadata = index
1265 .hit_metadata(options.view)?
1266 .into_iter()
1267 .map(|metadata| (metadata.id.clone(), metadata))
1268 .collect::<HashMap<_, _>>();
1269 let decisions = index
1270 .decision_metadata(options.view)?
1271 .into_iter()
1272 .map(|decision| (decision.id.clone(), decision))
1273 .collect::<HashMap<_, _>>();
1274 let all_phases = index
1275 .phase_summaries(options.view)?
1276 .into_iter()
1277 .filter(|phase| phase.test == test.summary.id)
1278 .map(|phase| CoverageTestPhase {
1279 id: phase.id,
1280 kind: phase.kind,
1281 operation: phase.operation,
1282 source: phase.source,
1283 status: phase.status,
1284 caused_by_phase_id: phase.caused_by_phase_id,
1285 lines: phase.lines,
1286 decisions: phase.decisions,
1287 })
1288 .collect::<Vec<_>>();
1289 let totals = CoverageTestTotals {
1290 lines: test.lines.len(),
1291 hits: test.hits.len(),
1292 decisions: test.decisions.len(),
1293 phases: all_phases.len(),
1294 };
1295 let total = totals
1296 .lines
1297 .max(totals.hits)
1298 .max(totals.decisions)
1299 .max(totals.phases);
1300 let lines = test
1301 .lines
1302 .iter()
1303 .skip(options.offset)
1304 .take(options.limit)
1305 .cloned()
1306 .collect::<Vec<_>>();
1307 let hits = test
1308 .hits
1309 .iter()
1310 .skip(options.offset)
1311 .take(options.limit)
1312 .cloned()
1313 .collect::<Vec<_>>();
1314 let hit_details = test
1315 .hits
1316 .iter()
1317 .skip(options.offset)
1318 .take(options.limit)
1319 .map(|id| hit_detail(id, metadata.get(id)))
1320 .collect::<Vec<_>>();
1321 let test_decisions = test
1322 .decisions
1323 .iter()
1324 .skip(options.offset)
1325 .take(options.limit)
1326 .map(|decision| {
1327 Ok(CoverageTestDecision {
1328 id: decision.id.clone(),
1329 vectors: decision.vectors.clone(),
1330 meta: decisions
1331 .get(&decision.id)
1332 .cloned()
1333 .ok_or(QueryError::InvalidRecordSelection)?,
1334 })
1335 })
1336 .collect::<Result<Vec<_>, QueryError>>()?;
1337 let phases = all_phases
1338 .into_iter()
1339 .skip(options.offset)
1340 .take(options.limit)
1341 .collect::<Vec<_>>();
1342 let returned = lines
1343 .len()
1344 .max(hits.len())
1345 .max(test_decisions.len())
1346 .max(phases.len());
1347 Ok((
1348 CoverageTestData::Detail(CoverageTestDetailData {
1349 run: options.run.into(),
1350 filters,
1351 pagination_applies_to:
1352 "lines, hits/hitDetails, decisions, and phases independently within the test".into(),
1353 tests: vec![CoverageSelectedTest {
1354 id: test.summary.id,
1355 name: test.summary.name,
1356 file: test.summary.file,
1357 title: test.summary.title,
1358 retries: test.retries,
1359 attempts: test.attempts,
1360 outcome: test.summary.outcome,
1361 provenance: test.summary.provenance,
1362 role: test.summary.role,
1363 attribution: test.summary.attribution,
1364 hits,
1365 decisions: test_decisions,
1366 lines,
1367 hit_details,
1368 phases,
1369 totals,
1370 }],
1371 }),
1372 pagination(options.offset, options.limit, returned, total),
1373 ))
1374}
1375
1376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1377pub struct CoverageDecisionMatch {
1378 pub id: String,
1379 pub file: String,
1380 pub line: usize,
1381 pub column: usize,
1382 pub source: String,
1383}
1384
1385#[derive(Debug, Clone, PartialEq, Serialize)]
1386#[serde(rename_all = "camelCase")]
1387pub struct CoverageDecisionCondition {
1388 pub index: usize,
1389 pub source: String,
1390 pub covered: bool,
1391 #[serde(skip_serializing_if = "Option::is_none")]
1392 pub assertion_covered: Option<bool>,
1393 #[serde(skip_serializing_if = "Option::is_none")]
1394 pub witness: Option<[crate::coverage_analysis::McdcVector; 2]>,
1395 #[serde(skip_serializing_if = "Option::is_none")]
1396 pub witness_tests: Option<[Vec<Id>; 2]>,
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1400#[serde(rename_all = "camelCase")]
1401pub struct CoverageDecisionTotals {
1402 pub conditions: usize,
1403 pub vector_observations: usize,
1404 pub tests: usize,
1405}
1406
1407#[derive(Debug, Clone, PartialEq, Serialize)]
1408#[serde(rename_all = "camelCase")]
1409pub struct CoverageSelectedDecision {
1410 pub meta: DecisionMeta,
1411 pub executed: bool,
1412 pub covered: bool,
1413 pub vectors: Vec<crate::coverage_analysis::McdcVector>,
1414 pub vector_observations: Vec<crate::coverage_report::VectorObservation>,
1415 pub conditions: Vec<CoverageDecisionCondition>,
1416 pub tests: Vec<Id>,
1417 pub confidence: CoverageConfidence,
1418 pub totals: CoverageDecisionTotals,
1419}
1420
1421#[derive(Debug, Clone, PartialEq, Serialize)]
1422pub struct CoverageDecisionMatchesData {
1423 pub run: String,
1424 pub filters: CoverageQueryFilters,
1425 pub decisions: Vec<CoverageDecisionMatch>,
1426}
1427
1428#[derive(Debug, Clone, PartialEq, Serialize)]
1429#[serde(rename_all = "camelCase")]
1430pub struct CoverageDecisionDetailData {
1431 pub run: String,
1432 pub filters: CoverageQueryFilters,
1433 pub pagination_applies_to: String,
1434 pub decisions: Vec<CoverageSelectedDecision>,
1435}
1436
1437#[derive(Debug, Clone, PartialEq, Serialize)]
1438#[serde(untagged)]
1439pub enum CoverageDecisionData {
1440 Matches(CoverageDecisionMatchesData),
1441 Detail(CoverageDecisionDetailData),
1442}
1443
1444#[derive(Debug, Clone, Copy)]
1445pub struct CoverageDecisionQueryOptions<'a> {
1446 pub run: &'a str,
1447 pub view: CoverageViewId,
1448 pub kind: Option<&'a str>,
1449 pub runner: Option<&'a str>,
1450 pub selector: &'a str,
1451 pub offset: usize,
1452 pub limit: usize,
1453}
1454
1455fn selector_location(selector: &str) -> Option<(&str, usize)> {
1456 let (prefix, last) = selector.rsplit_once(':')?;
1457 let last = last.parse::<usize>().ok()?;
1458 if let Some((file, possible_line)) = prefix.rsplit_once(':')
1459 && let Ok(line) = possible_line.parse::<usize>()
1460 {
1461 return Some((file, line));
1462 }
1463 Some((prefix, last))
1464}
1465
1466fn selected_decision(
1467 decision: crate::coverage_report::DecisionResult,
1468 selected: Option<&BTreeSet<String>>,
1469) -> crate::coverage_report::DecisionResult {
1470 let Some(selected) = selected else {
1471 return decision;
1472 };
1473 let vector_observations = decision
1474 .vector_observations
1475 .into_iter()
1476 .filter_map(|mut observation| {
1477 observation
1478 .tests
1479 .retain(|test| selected.contains(test.as_str()));
1480 (!observation.tests.is_empty()).then_some(observation)
1481 })
1482 .collect::<Vec<_>>();
1483 let vectors = vector_observations
1484 .iter()
1485 .map(|observation| observation.vector.clone())
1486 .collect::<Vec<_>>();
1487 let conditions = decision
1488 .meta
1489 .conditions
1490 .iter()
1491 .enumerate()
1492 .map(|(index, source)| {
1493 let mut witness = None;
1494 let mut witness_tests = None;
1495 'pairs: for left in 0..vector_observations.len() {
1496 for right in (left + 1)..vector_observations.len() {
1497 let first = &vector_observations[left];
1498 let second = &vector_observations[right];
1499 if is_independence_pair(&first.vector, &second.vector, index) {
1500 witness = Some([first.vector.clone(), second.vector.clone()]);
1501 witness_tests = Some([first.tests.clone(), second.tests.clone()]);
1502 break 'pairs;
1503 }
1504 }
1505 }
1506 crate::coverage_report::ConditionResult {
1507 index,
1508 source: source.clone(),
1509 covered: witness.is_some(),
1510 assertion_covered: false,
1511 witness,
1512 witness_tests,
1513 }
1514 })
1515 .collect::<Vec<_>>();
1516 crate::coverage_report::DecisionResult {
1517 meta: decision.meta,
1518 executed: !vectors.is_empty(),
1519 covered: conditions.iter().all(|condition| condition.covered),
1520 vectors,
1521 vector_observations,
1522 conditions,
1523 tests: decision
1524 .tests
1525 .into_iter()
1526 .filter(|test| selected.contains(test.as_str()))
1527 .collect(),
1528 confidence: decision.confidence,
1529 }
1530}
1531
1532pub fn coverage_decision_query(
1533 index: &CoverageIndex<'_>,
1534 options: CoverageDecisionQueryOptions<'_>,
1535) -> Result<(CoverageDecisionData, AgentPagination), QueryError> {
1536 if options.limit == 0 {
1537 return Err(QueryError::InvalidPagination);
1538 }
1539 let tests = index.test_summaries(options.view)?;
1540 let selected = selected_test_ids(&tests, options.kind, options.runner)?;
1541 let decisions = index.decision_details(options.view)?;
1542 let mut matches = decisions
1543 .into_iter()
1544 .filter(|decision| decision.meta.id == options.selector)
1545 .collect::<Vec<_>>();
1546 if matches.is_empty()
1547 && let Some((file, line)) = selector_location(options.selector)
1548 {
1549 matches = index
1550 .decision_details(options.view)?
1551 .into_iter()
1552 .filter(|decision| decision.meta.file == file && decision.meta.line == line)
1553 .collect();
1554 }
1555 if matches.is_empty() {
1556 return Err(QueryError::DecisionNotFound(options.selector.into()));
1557 }
1558 let filters = query_filters(options.view, options.kind, options.runner);
1559 if matches.len() > 1 {
1560 let total = matches.len();
1561 let page = matches
1562 .into_iter()
1563 .skip(options.offset)
1564 .take(options.limit)
1565 .map(|decision| CoverageDecisionMatch {
1566 id: decision.meta.id,
1567 file: decision.meta.file,
1568 line: decision.meta.line,
1569 column: decision.meta.column,
1570 source: decision.meta.source,
1571 })
1572 .collect::<Vec<_>>();
1573 let returned = page.len();
1574 return Ok((
1575 CoverageDecisionData::Matches(CoverageDecisionMatchesData {
1576 run: options.run.into(),
1577 filters,
1578 decisions: page,
1579 }),
1580 pagination(options.offset, options.limit, returned, total),
1581 ));
1582 }
1583 let filtered = selected_decision(
1584 matches.into_iter().next().expect("one decision match"),
1585 selected.as_ref(),
1586 );
1587 let totals = CoverageDecisionTotals {
1588 conditions: filtered.conditions.len(),
1589 vector_observations: filtered.vector_observations.len(),
1590 tests: filtered.tests.len(),
1591 };
1592 let total = totals
1593 .conditions
1594 .max(totals.vector_observations)
1595 .max(totals.tests);
1596 let vector_observations = filtered
1597 .vector_observations
1598 .iter()
1599 .skip(options.offset)
1600 .take(options.limit)
1601 .cloned()
1602 .collect::<Vec<_>>();
1603 let vectors = vector_observations
1604 .iter()
1605 .map(|observation| observation.vector.clone())
1606 .collect::<Vec<_>>();
1607 let conditions = filtered
1608 .conditions
1609 .iter()
1610 .skip(options.offset)
1611 .take(options.limit)
1612 .map(|condition| CoverageDecisionCondition {
1613 index: condition.index,
1614 source: condition.source.clone(),
1615 covered: condition.covered,
1616 assertion_covered: selected.is_none().then_some(condition.assertion_covered),
1617 witness: condition.witness.clone(),
1618 witness_tests: condition.witness_tests.clone(),
1619 })
1620 .collect::<Vec<_>>();
1621 let tests = filtered
1622 .tests
1623 .iter()
1624 .skip(options.offset)
1625 .take(options.limit)
1626 .cloned()
1627 .collect::<Vec<_>>();
1628 let returned = vector_observations
1629 .len()
1630 .max(conditions.len())
1631 .max(tests.len());
1632 Ok((
1633 CoverageDecisionData::Detail(CoverageDecisionDetailData {
1634 run: options.run.into(),
1635 filters,
1636 pagination_applies_to:
1637 "conditions, vectorObservations, and tests independently within each decision"
1638 .into(),
1639 decisions: vec![CoverageSelectedDecision {
1640 meta: filtered.meta,
1641 executed: filtered.executed,
1642 covered: filtered.covered,
1643 vectors,
1644 vector_observations,
1645 conditions,
1646 tests,
1647 confidence: filtered.confidence,
1648 totals,
1649 }],
1650 }),
1651 pagination(options.offset, options.limit, returned, total),
1652 ))
1653}
1654
1655#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1656pub struct CoverageOtherTest {
1657 pub id: String,
1658 pub name: String,
1659}
1660
1661#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1662#[serde(rename_all = "camelCase")]
1663pub struct CoverageOtherCoverage {
1664 pub covered_elsewhere: bool,
1665 pub kinds: Vec<String>,
1666 pub runners: Vec<String>,
1667 pub tests: Vec<CoverageOtherTest>,
1668}
1669
1670#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1671#[serde(rename_all = "camelCase")]
1672pub struct CoverageLineObligation {
1673 pub kind: String,
1674 pub id: String,
1675 pub line: usize,
1676 pub other_coverage: CoverageOtherCoverage,
1677}
1678
1679#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1680#[serde(rename_all = "camelCase")]
1681pub struct CoveragePointObligation {
1682 pub kind: String,
1683 pub id: String,
1684 pub line: usize,
1685 pub column: usize,
1686 pub source: String,
1687 pub other_coverage: CoverageOtherCoverage,
1688}
1689
1690#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1691#[serde(rename_all = "camelCase")]
1692pub struct CoverageBranchObligation {
1693 pub kind: String,
1694 pub id: String,
1695 pub line: usize,
1696 pub column: usize,
1697 pub source: String,
1698 pub missing: String,
1699 pub other_coverage: CoverageOtherCoverage,
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct CoverageMcdcObligation {
1705 pub kind: String,
1706 pub id: String,
1707 pub line: usize,
1708 pub column: usize,
1709 pub decision: String,
1710 pub missing_condition: String,
1711 #[serde(skip)]
1712 pub condition_index: usize,
1713 pub observed_vectors: Vec<String>,
1714 pub other_coverage: CoverageOtherCoverage,
1715}
1716
1717#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1718#[serde(untagged)]
1719pub enum CoverageFileObligation {
1720 Line(CoverageLineObligation),
1721 Point(CoveragePointObligation),
1722 Branch(CoverageBranchObligation),
1723 Mcdc(CoverageMcdcObligation),
1724}
1725
1726impl CoverageFileObligation {
1727 fn line(&self) -> usize {
1728 match self {
1729 Self::Line(value) => value.line,
1730 Self::Point(value) => value.line,
1731 Self::Branch(value) => value.line,
1732 Self::Mcdc(value) => value.line,
1733 }
1734 }
1735
1736 fn kind(&self) -> &str {
1737 match self {
1738 Self::Line(value) => &value.kind,
1739 Self::Point(value) => &value.kind,
1740 Self::Branch(value) => &value.kind,
1741 Self::Mcdc(value) => &value.kind,
1742 }
1743 }
1744}
1745
1746#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1747pub struct CoverageFileLimitation {
1748 pub id: String,
1749 pub kind: String,
1750 pub file: String,
1751 pub line: usize,
1752 pub column: usize,
1753 pub source: String,
1754 pub reason: String,
1755 pub blocking: bool,
1756 pub effect: String,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1760#[serde(rename_all = "camelCase")]
1761pub struct CoverageFileCounts {
1762 pub total_lines: usize,
1765 pub covered_lines: usize,
1766 pub uncovered_lines: usize,
1767 pub uncovered_statements: usize,
1768 pub uncovered_functions: usize,
1769 pub missing_branches: usize,
1770 pub missing_mcdc_conditions: usize,
1771 pub measurement_limitations: usize,
1772}
1773
1774#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1775#[serde(rename_all = "camelCase")]
1776pub struct CoverageFileGapLine {
1777 pub line: usize,
1778 pub state: String,
1779 #[serde(skip_serializing_if = "Option::is_none")]
1780 pub source: Option<String>,
1781 pub obligations: Vec<CoverageFileObligation>,
1782 pub limitations: Vec<CoverageFileLimitation>,
1783}
1784
1785#[derive(Debug, Clone, PartialEq, Serialize)]
1786#[serde(rename_all = "camelCase")]
1787pub struct CoverageFileDetailData {
1788 pub run: String,
1789 pub filters: CoverageQueryFilters,
1790 pub file: String,
1791 pub metric: MinimizeMetric,
1792 pub counts: CoverageFileCounts,
1793 pub total_tests: usize,
1794 pub total_obligations: usize,
1795 pub total_gap_lines: usize,
1796 pub gap_lines: Vec<CoverageFileGapLine>,
1797 pub total_limitations: usize,
1798 #[serde(skip_serializing_if = "Option::is_none")]
1801 pub assertions: Option<serde_json::Value>,
1802}
1803
1804#[derive(Debug, Clone, Copy)]
1805pub struct CoverageFileDetailOptions<'a> {
1806 pub run: &'a str,
1807 pub view: CoverageViewId,
1808 pub kind: Option<&'a str>,
1809 pub runner: Option<&'a str>,
1810 pub selector: &'a str,
1811 pub metric: MinimizeMetric,
1812 pub offset: usize,
1813 pub limit: usize,
1814}
1815
1816fn other_coverage<T: AsRef<str>>(
1817 test_ids: &[T],
1818 selected: Option<&BTreeSet<String>>,
1819 tests: &HashMap<String, IndexedTestSummary>,
1820) -> CoverageOtherCoverage {
1821 let covered = selected.map_or_else(Vec::new, |selected| {
1822 test_ids
1823 .iter()
1824 .filter(|id| !selected.contains(id.as_ref()))
1825 .filter_map(|id| tests.get(id.as_ref()))
1826 .collect::<Vec<_>>()
1827 });
1828 CoverageOtherCoverage {
1829 covered_elsewhere: !covered.is_empty(),
1830 kinds: covered
1831 .iter()
1832 .map(|test| test.provenance.kind.clone())
1833 .collect::<BTreeSet<_>>()
1834 .into_iter()
1835 .collect(),
1836 runners: covered
1837 .iter()
1838 .map(|test| test.provenance.runner.clone())
1839 .collect::<BTreeSet<_>>()
1840 .into_iter()
1841 .collect(),
1842 tests: covered
1843 .into_iter()
1844 .map(|test| CoverageOtherTest {
1845 id: test.id.clone(),
1846 name: test.name.clone(),
1847 })
1848 .collect(),
1849 }
1850}
1851
1852fn vector_text(vector: &crate::coverage_analysis::McdcVector) -> String {
1853 let values = vector
1854 .values
1855 .iter()
1856 .map(|value| match value {
1857 None => '-',
1858 Some(false) => 'F',
1859 Some(true) => 'T',
1860 })
1861 .collect::<String>();
1862 format!("{values} -> {}", if vector.outcome { 'T' } else { 'F' })
1863}
1864
1865fn obligation_matches_metric(obligation: &CoverageFileObligation, metric: MinimizeMetric) -> bool {
1866 metric == MinimizeMetric::All
1867 || matches!(
1868 (obligation.kind(), metric),
1869 ("line", MinimizeMetric::Lines)
1870 | ("statement", MinimizeMetric::Statements)
1871 | ("function", MinimizeMetric::Functions)
1872 | ("branch", MinimizeMetric::Branches)
1873 | ("mcdc", MinimizeMetric::Mcdc)
1874 )
1875}
1876
1877fn compact_source(value: &str) -> Option<String> {
1878 let line = value.lines().find(|line| !line.trim().is_empty())?.trim();
1879 let compact = line.split_whitespace().collect::<Vec<_>>().join(" ");
1880 if compact.is_empty() {
1881 None
1882 } else if compact.chars().count() > 120 {
1883 Some(format!(
1884 "{}…",
1885 compact.chars().take(119).collect::<String>()
1886 ))
1887 } else {
1888 Some(compact)
1889 }
1890}
1891
1892fn obligation_source(obligation: &CoverageFileObligation) -> Option<String> {
1893 match obligation {
1894 CoverageFileObligation::Line(_) => None,
1895 CoverageFileObligation::Point(value) => compact_source(&value.source),
1896 CoverageFileObligation::Branch(value) => compact_source(&value.source),
1897 CoverageFileObligation::Mcdc(value) => compact_source(&value.decision),
1898 }
1899}
1900
1901pub struct FileDetailSource {
1909 view: CoverageViewId,
1910 tests: Vec<crate::coverage_index::IndexedTestDetail>,
1911 test_summaries: Vec<IndexedTestSummary>,
1912 tests_by_id: HashMap<String, IndexedTestSummary>,
1913 tests_by_file: HashMap<String, Vec<String>>,
1915 lines: Vec<crate::coverage_index::IndexedLine>,
1916 lines_by_file: BTreeMap<String, Vec<usize>>,
1917 points: Vec<crate::coverage_index::IndexedHitMetadata>,
1918 points_by_file: HashMap<String, Vec<usize>>,
1919 decisions: Vec<crate::coverage_report::DecisionResult>,
1920 decisions_by_file: HashMap<String, Vec<usize>>,
1921 limitations: BTreeMap<String, Vec<crate::coverage_index::IndexedLimitation>>,
1922}
1923
1924impl FileDetailSource {
1925 pub fn read(index: &CoverageIndex<'_>, view: CoverageViewId) -> Result<Self, QueryError> {
1926 let tests = index.test_details(view)?;
1927 let mut tests_by_file = HashMap::<String, Vec<String>>::new();
1928 for test in &tests {
1929 let mut files = test
1930 .lines
1931 .iter()
1932 .map(|line| line.file.as_str())
1933 .collect::<Vec<_>>();
1934 files.sort_unstable();
1935 files.dedup();
1936 for file in files {
1937 tests_by_file
1938 .entry(file.to_owned())
1939 .or_default()
1940 .push(test.summary.id.clone());
1941 }
1942 }
1943 let test_summaries = tests
1944 .iter()
1945 .map(|test| test.summary.clone())
1946 .collect::<Vec<_>>();
1947 let tests_by_id = test_summaries
1948 .iter()
1949 .map(|test| (test.id.clone(), test.clone()))
1950 .collect();
1951 let lines = index.lines(view)?;
1952 let mut lines_by_file = BTreeMap::<String, Vec<usize>>::new();
1953 for (at, line) in lines.iter().enumerate() {
1954 lines_by_file.entry(line.file.clone()).or_default().push(at);
1955 }
1956 let points = index.hit_metadata(view)?;
1957 let mut points_by_file = HashMap::<String, Vec<usize>>::new();
1958 for (at, point) in points.iter().enumerate() {
1959 points_by_file
1960 .entry(point.file.clone())
1961 .or_default()
1962 .push(at);
1963 }
1964 let decisions = index.decision_details(view)?;
1965 let mut decisions_by_file = HashMap::<String, Vec<usize>>::new();
1966 for (at, decision) in decisions.iter().enumerate() {
1967 decisions_by_file
1968 .entry(decision.meta.file.clone())
1969 .or_default()
1970 .push(at);
1971 }
1972 let mut limitations = BTreeMap::<String, Vec<_>>::new();
1973 for limitation in index.limitations(view)? {
1974 limitations
1975 .entry(limitation.file.clone())
1976 .or_default()
1977 .push(limitation);
1978 }
1979 Ok(Self {
1980 view,
1981 tests,
1982 test_summaries,
1983 tests_by_id,
1984 tests_by_file,
1985 lines,
1986 lines_by_file,
1987 points,
1988 points_by_file,
1989 decisions,
1990 decisions_by_file,
1991 limitations,
1992 })
1993 }
1994
1995 pub fn tests(&self) -> &[crate::coverage_index::IndexedTestDetail] {
1998 &self.tests
1999 }
2000
2001 pub fn lines(&self) -> &[crate::coverage_index::IndexedLine] {
2002 &self.lines
2003 }
2004
2005 pub fn points(&self) -> &[crate::coverage_index::IndexedHitMetadata] {
2006 &self.points
2007 }
2008
2009 pub fn decisions(&self) -> &[crate::coverage_report::DecisionResult] {
2010 &self.decisions
2011 }
2012}
2013
2014pub fn coverage_file_detail_query(
2015 index: &CoverageIndex<'_>,
2016 options: CoverageFileDetailOptions<'_>,
2017) -> Result<(CoverageFileDetailData, AgentPagination), QueryError> {
2018 if options.limit == 0 {
2019 return Err(QueryError::InvalidPagination);
2020 }
2021 coverage_file_detail_from(&FileDetailSource::read(index, options.view)?, options)
2022}
2023
2024pub fn coverage_file_detail_from(
2027 read: &FileDetailSource,
2028 options: CoverageFileDetailOptions<'_>,
2029) -> Result<(CoverageFileDetailData, AgentPagination), QueryError> {
2030 if options.limit == 0 {
2031 return Err(QueryError::InvalidPagination);
2032 }
2033 debug_assert!(read.view == options.view);
2034 let selected = selected_test_ids(&read.test_summaries, options.kind, options.runner)?;
2035 let tests_by_id = &read.tests_by_id;
2036 let files = read
2037 .lines_by_file
2038 .keys()
2039 .chain(read.limitations.keys())
2040 .map(String::as_str)
2041 .collect::<BTreeSet<_>>();
2042 let file = if files.contains(options.selector) {
2043 options.selector.to_owned()
2044 } else {
2045 let matches = files
2046 .into_iter()
2047 .filter(|file| file.contains(options.selector))
2048 .collect::<Vec<_>>();
2049 if matches.is_empty() {
2050 return Err(QueryError::SourceNotFound(options.selector.into()));
2051 }
2052 if matches.len() != 1 {
2053 return Err(QueryError::AmbiguousSelector {
2054 selector: options.selector.into(),
2055 matches: matches.into_iter().map(str::to_owned).collect(),
2056 });
2057 }
2058 matches[0].to_owned()
2059 };
2060 let lines = read
2061 .lines_by_file
2062 .get(&file)
2063 .into_iter()
2064 .flatten()
2065 .map(|at| &read.lines[*at])
2066 .collect::<Vec<_>>();
2067 let metadata = read
2068 .points_by_file
2069 .get(&file)
2070 .into_iter()
2071 .flatten()
2072 .map(|at| &read.points[*at])
2073 .collect::<Vec<_>>();
2074 let selected_includes = |tests: &[String]| {
2075 selected.as_ref().map_or(!tests.is_empty(), |selected| {
2076 tests.iter().any(|test| selected.contains(test))
2077 })
2078 };
2079 let total_lines = lines.iter().filter(|line| line.measured).count();
2080 let uncovered_lines = lines
2081 .iter()
2082 .filter(|line| line.measured && !selected_includes(&line.tests))
2083 .map(|line| {
2084 CoverageFileObligation::Line(CoverageLineObligation {
2085 kind: "line".into(),
2086 id: format!("line:{}:{}", line.file, line.line),
2087 line: line.line,
2088 other_coverage: other_coverage(&line.tests, selected.as_ref(), tests_by_id),
2089 })
2090 })
2091 .collect::<Vec<_>>();
2092 let statements = metadata
2093 .iter()
2094 .filter(|point| point.obligation == "statement" && !selected_includes(&point.tests))
2095 .map(|point| {
2096 CoverageFileObligation::Point(CoveragePointObligation {
2097 kind: "statement".into(),
2098 id: point.id.clone(),
2099 line: point.line,
2100 column: point.column,
2101 source: point.label.clone().unwrap_or_else(|| point.source.clone()),
2102 other_coverage: other_coverage(&point.tests, selected.as_ref(), tests_by_id),
2103 })
2104 })
2105 .collect::<Vec<_>>();
2106 let functions = metadata
2107 .iter()
2108 .filter(|point| point.obligation == "function" && !selected_includes(&point.tests))
2109 .map(|point| {
2110 CoverageFileObligation::Point(CoveragePointObligation {
2111 kind: "function".into(),
2112 id: point.id.clone(),
2113 line: point.line,
2114 column: point.column,
2115 source: point.label.clone().unwrap_or_else(|| point.source.clone()),
2116 other_coverage: other_coverage(&point.tests, selected.as_ref(), tests_by_id),
2117 })
2118 })
2119 .collect::<Vec<_>>();
2120 let branches = metadata
2121 .iter()
2122 .filter(|branch| branch.obligation == "branch" && !selected_includes(&branch.tests))
2123 .map(|branch| {
2124 CoverageFileObligation::Branch(CoverageBranchObligation {
2125 kind: "branch".into(),
2126 id: branch.id.clone(),
2127 line: branch.line,
2128 column: branch.column,
2129 source: branch.source.clone(),
2130 missing: branch.alternative.clone().unwrap_or_default(),
2131 other_coverage: other_coverage(&branch.tests, selected.as_ref(), tests_by_id),
2132 })
2133 })
2134 .collect::<Vec<_>>();
2135 let observed_lines = metadata
2139 .iter()
2140 .filter(|point| selected_includes(&point.tests))
2141 .map(|point| point.line)
2142 .collect::<BTreeSet<_>>();
2143 let mut mcdc = Vec::new();
2144 for original in read
2145 .decisions_by_file
2146 .get(&file)
2147 .into_iter()
2148 .flatten()
2149 .map(|at| &read.decisions[*at])
2150 {
2151 let filtered = selected_decision(original.clone(), selected.as_ref());
2152 for condition in filtered
2153 .conditions
2154 .iter()
2155 .filter(|condition| !condition.covered)
2156 {
2157 let original_tests = original.conditions[condition.index]
2158 .witness_tests
2159 .clone()
2160 .unwrap_or_default()
2161 .into_iter()
2162 .flatten()
2163 .collect::<Vec<_>>();
2164 mcdc.push(CoverageFileObligation::Mcdc(CoverageMcdcObligation {
2165 kind: "mcdc".into(),
2166 id: original.meta.id.clone(),
2167 line: original.meta.line,
2168 column: original.meta.column,
2169 decision: original.meta.source.clone(),
2170 missing_condition: condition.source.clone(),
2171 condition_index: condition.index,
2172 observed_vectors: filtered
2173 .vector_observations
2174 .iter()
2175 .map(|observation| vector_text(&observation.vector))
2176 .collect(),
2177 other_coverage: other_coverage(&original_tests, selected.as_ref(), tests_by_id),
2178 }));
2179 }
2180 }
2181 let mut obligations = uncovered_lines
2182 .iter()
2183 .chain(statements.iter())
2184 .chain(functions.iter())
2185 .chain(branches.iter())
2186 .chain(mcdc.iter())
2187 .filter(|obligation| obligation_matches_metric(obligation, options.metric))
2188 .cloned()
2189 .collect::<Vec<_>>();
2190 obligations.sort_by(|left, right| {
2191 left.line()
2192 .cmp(&right.line())
2193 .then_with(|| left.kind().cmp(right.kind()))
2194 });
2195 let mut limitations = read
2196 .limitations
2197 .get(&file)
2198 .into_iter()
2199 .flatten()
2200 .cloned()
2201 .map(|limitation| CoverageFileLimitation {
2202 id: limitation.id,
2203 kind: limitation.kind,
2204 file: limitation.file,
2205 line: limitation.line,
2206 column: limitation.column,
2207 source: limitation.source,
2208 reason: limitation.reason,
2209 blocking: limitation.blocking,
2210 effect: "outside-measured-denominator".into(),
2211 })
2212 .collect::<Vec<_>>();
2213 limitations.sort_by(|left, right| {
2214 left.line
2215 .cmp(&right.line)
2216 .then_with(|| left.column.cmp(&right.column))
2217 .then_with(|| left.id.cmp(&right.id))
2218 });
2219 let total_tests = read
2220 .tests_by_file
2221 .get(&file)
2222 .into_iter()
2223 .flatten()
2224 .filter(|test| {
2225 selected
2226 .as_ref()
2227 .is_none_or(|selected| selected.contains(*test))
2228 })
2229 .count();
2230 let total_obligations = obligations.len();
2231 let total_limitations = limitations.len();
2232 let mut grouped =
2233 BTreeMap::<usize, (Vec<CoverageFileObligation>, Vec<CoverageFileLimitation>)>::new();
2234 for obligation in obligations {
2235 grouped
2236 .entry(obligation.line())
2237 .or_default()
2238 .0
2239 .push(obligation);
2240 }
2241 for limitation in limitations {
2242 grouped
2243 .entry(limitation.line)
2244 .or_default()
2245 .1
2246 .push(limitation);
2247 }
2248 let gap_lines = grouped
2249 .into_iter()
2250 .map(|(line, (obligations, limitations))| {
2251 let source = obligations.iter().find_map(obligation_source).or_else(|| {
2252 limitations
2253 .iter()
2254 .find_map(|value| compact_source(&value.source))
2255 });
2256 let never_ran = !observed_lines.contains(&line)
2260 && !obligations.is_empty()
2261 && obligations
2262 .iter()
2263 .all(|value| !matches!(value, CoverageFileObligation::Mcdc(_)));
2264 let state = if never_ran
2265 || obligations
2266 .iter()
2267 .any(|value| matches!(value, CoverageFileObligation::Line(_)))
2268 {
2269 "missing"
2270 } else if !obligations.is_empty() || observed_lines.contains(&line) {
2271 "part"
2274 } else {
2275 "limited"
2276 };
2277 CoverageFileGapLine {
2278 line,
2279 state: state.into(),
2280 source,
2281 obligations,
2282 limitations,
2283 }
2284 })
2285 .collect::<Vec<_>>();
2286 let total_gap_lines = gap_lines.len();
2287 let selected_gap_lines = gap_lines
2288 .into_iter()
2289 .skip(options.offset)
2290 .take(options.limit)
2291 .collect::<Vec<_>>();
2292 let returned = selected_gap_lines.len();
2293 Ok((
2294 CoverageFileDetailData {
2295 run: options.run.into(),
2296 filters: query_filters(options.view, options.kind, options.runner),
2297 file,
2298 metric: options.metric,
2299 counts: CoverageFileCounts {
2300 total_lines,
2301 covered_lines: total_lines - uncovered_lines.len(),
2302 uncovered_lines: uncovered_lines.len(),
2303 uncovered_statements: statements.len(),
2304 uncovered_functions: functions.len(),
2305 missing_branches: branches.len(),
2306 missing_mcdc_conditions: mcdc.len(),
2307 measurement_limitations: total_limitations,
2308 },
2309 total_tests,
2310 total_obligations,
2311 total_gap_lines,
2312 gap_lines: selected_gap_lines,
2313 total_limitations,
2314 assertions: None,
2315 },
2316 pagination(options.offset, options.limit, returned, total_gap_lines),
2317 ))
2318}
2319
2320#[derive(Debug, Clone, PartialEq, Serialize)]
2321pub struct CoverageDiffDelta {
2322 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2323 pub lines: f64,
2324 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2325 pub branches: f64,
2326 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2327 pub mcdc: f64,
2328}
2329
2330#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2331#[serde(rename_all = "camelCase")]
2332pub struct CoverageDiffSide {
2333 pub line_count: usize,
2334 pub branch_count: usize,
2335 pub mcdc_count: usize,
2336 pub lines: Vec<String>,
2337 pub branches: Vec<String>,
2338 pub mcdc: Vec<String>,
2339}
2340
2341#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2343#[serde(rename_all = "camelCase")]
2344pub struct CoverageDiffFile {
2345 pub file: String,
2346 pub lost: CoverageDiffCounts,
2347 pub gained: CoverageDiffCounts,
2348}
2349
2350#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
2351pub struct CoverageDiffCounts {
2352 pub lines: usize,
2353 pub branches: usize,
2354 pub mcdc: usize,
2355}
2356
2357impl CoverageDiffCounts {
2358 fn total(&self) -> usize {
2359 self.lines + self.branches + self.mcdc
2360 }
2361}
2362
2363const DIFF_FILES: usize = 20;
2365
2366#[derive(Debug, Clone, PartialEq, Serialize)]
2367#[serde(rename_all = "camelCase")]
2368pub struct CoverageDiffData {
2369 pub filters: CoverageQueryFilters,
2370 pub older: String,
2371 pub newer: String,
2372 pub delta: CoverageDiffDelta,
2373 pub gained: CoverageDiffSide,
2374 pub lost: CoverageDiffSide,
2375 pub files: usize,
2379 pub by_file: Vec<CoverageDiffFile>,
2380 #[serde(skip_serializing_if = "Option::is_none")]
2383 pub inputs: Option<CoverageDiffInputs>,
2384}
2385
2386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2390#[serde(rename_all = "camelCase")]
2391pub struct CoverageDiffInputs {
2392 pub source_changed: bool,
2394 pub tests_changed: bool,
2395}
2396
2397#[derive(Debug, Clone, Copy)]
2398pub struct CoverageDiffQueryOptions<'a> {
2399 pub older_run: &'a str,
2400 pub newer_run: &'a str,
2401 pub view: CoverageViewId,
2402 pub kind: Option<&'a str>,
2403 pub runner: Option<&'a str>,
2404 pub offset: usize,
2405 pub limit: usize,
2406}
2407
2408struct DiffSnapshot {
2409 summary: CoverageSummary,
2410 lines: BTreeSet<String>,
2411 branches: HashMap<String, String>,
2412 mcdc: HashMap<String, String>,
2413}
2414
2415fn diff_snapshot(
2416 index: &CoverageIndex<'_>,
2417 view: CoverageViewId,
2418) -> Result<DiffSnapshot, QueryError> {
2419 let summary = index.summary(view)?;
2420 let lines = index
2421 .lines(view)?
2422 .into_iter()
2423 .filter(|line| line.covered)
2424 .map(|line| format!("{}:{}", line.file, line.line))
2425 .collect();
2426 let branches = index
2427 .hit_metadata(view)?
2428 .into_iter()
2429 .filter(|metadata| metadata.obligation == "branch" && !metadata.tests.is_empty())
2430 .map(|metadata| {
2431 let parent = metadata
2432 .parent_id
2433 .ok_or(QueryError::InvalidRecordSelection)?;
2434 Ok((
2435 format!("{parent}:{}", metadata.id),
2436 format!(
2437 "{}:{} {}",
2438 metadata.file,
2439 metadata.line,
2440 metadata.alternative.unwrap_or_default()
2441 ),
2442 ))
2443 })
2444 .collect::<Result<HashMap<_, _>, QueryError>>()?;
2445 let mcdc = index
2446 .decision_details(view)?
2447 .into_iter()
2448 .flat_map(|decision| {
2449 decision
2450 .conditions
2451 .into_iter()
2452 .filter(|condition| condition.covered)
2453 .map(move |condition| {
2454 (
2455 format!("{}:c{}", decision.meta.id, condition.index),
2456 format!(
2457 "{}:{} C{} {}",
2458 decision.meta.file,
2459 decision.meta.line,
2460 condition.index + 1,
2461 condition.source
2462 ),
2463 )
2464 })
2465 })
2466 .collect();
2467 Ok(DiffSnapshot {
2468 summary,
2469 lines,
2470 branches,
2471 mcdc,
2472 })
2473}
2474
2475fn js_string_cmp(left: &str, right: &str) -> std::cmp::Ordering {
2476 left.encode_utf16().cmp(right.encode_utf16())
2477}
2478
2479fn rounded_delta(newer: f64, older: f64) -> f64 {
2480 ((newer - older) * 100.0).round() / 100.0
2481}
2482
2483pub fn coverage_diff_query(
2484 older: &CoverageIndex<'_>,
2485 newer: &CoverageIndex<'_>,
2486 options: CoverageDiffQueryOptions<'_>,
2487) -> Result<(CoverageDiffData, AgentPagination), QueryError> {
2488 if options.limit == 0 {
2489 return Err(QueryError::InvalidPagination);
2490 }
2491 let older = diff_snapshot(older, options.view)?;
2492 let newer = diff_snapshot(newer, options.view)?;
2493 let mut gained_lines = newer
2494 .lines
2495 .difference(&older.lines)
2496 .cloned()
2497 .collect::<Vec<_>>();
2498 let mut lost_lines = older
2499 .lines
2500 .difference(&newer.lines)
2501 .cloned()
2502 .collect::<Vec<_>>();
2503 let mut gained_branches = newer
2504 .branches
2505 .iter()
2506 .filter(|(id, _)| !older.branches.contains_key(*id))
2507 .map(|(_, label)| label.clone())
2508 .collect::<Vec<_>>();
2509 let mut lost_branches = older
2510 .branches
2511 .iter()
2512 .filter(|(id, _)| !newer.branches.contains_key(*id))
2513 .map(|(_, label)| label.clone())
2514 .collect::<Vec<_>>();
2515 let mut gained_mcdc = newer
2516 .mcdc
2517 .iter()
2518 .filter(|(id, _)| !older.mcdc.contains_key(*id))
2519 .map(|(_, label)| label.clone())
2520 .collect::<Vec<_>>();
2521 let mut lost_mcdc = older
2522 .mcdc
2523 .iter()
2524 .filter(|(id, _)| !newer.mcdc.contains_key(*id))
2525 .map(|(_, label)| label.clone())
2526 .collect::<Vec<_>>();
2527 for values in [
2528 &mut gained_lines,
2529 &mut lost_lines,
2530 &mut gained_branches,
2531 &mut lost_branches,
2532 &mut gained_mcdc,
2533 &mut lost_mcdc,
2534 ] {
2535 values.sort_by(|left, right| js_string_cmp(left, right));
2536 }
2537 let mut by_file = BTreeMap::<&str, (CoverageDiffCounts, CoverageDiffCounts)>::new();
2539 for (values, count) in [
2540 (&lost_lines, 0),
2541 (&lost_branches, 1),
2542 (&lost_mcdc, 2),
2543 (&gained_lines, 3),
2544 (&gained_branches, 4),
2545 (&gained_mcdc, 5),
2546 ] {
2547 for label in values {
2548 let place = label.split(' ').next().unwrap_or(label);
2549 let file = place.rsplit_once(':').map_or(place, |(file, _)| file);
2550 let (lost, gained) = by_file.entry(file).or_default();
2551 let side = if count < 3 { lost } else { gained };
2552 match count % 3 {
2553 0 => side.lines += 1,
2554 1 => side.branches += 1,
2555 _ => side.mcdc += 1,
2556 }
2557 }
2558 }
2559 let files = by_file.len();
2560 let mut by_file = by_file
2561 .into_iter()
2562 .map(|(file, (lost, gained))| CoverageDiffFile {
2563 file: file.to_owned(),
2564 lost,
2565 gained,
2566 })
2567 .collect::<Vec<_>>();
2568 by_file.sort_by(|left, right| {
2569 (right.lost.total(), right.gained.total())
2570 .cmp(&(left.lost.total(), left.gained.total()))
2571 .then_with(|| js_string_cmp(&left.file, &right.file))
2572 });
2573 by_file.truncate(DIFF_FILES);
2574 let total = [
2575 gained_lines.len(),
2576 gained_branches.len(),
2577 gained_mcdc.len(),
2578 lost_lines.len(),
2579 lost_branches.len(),
2580 lost_mcdc.len(),
2581 ]
2582 .into_iter()
2583 .max()
2584 .unwrap_or(0);
2585 let page = |values: &[String]| {
2586 values
2587 .iter()
2588 .skip(options.offset)
2589 .take(options.limit)
2590 .cloned()
2591 .collect::<Vec<_>>()
2592 };
2593 let gained = CoverageDiffSide {
2594 line_count: gained_lines.len(),
2595 branch_count: gained_branches.len(),
2596 mcdc_count: gained_mcdc.len(),
2597 lines: page(&gained_lines),
2598 branches: page(&gained_branches),
2599 mcdc: page(&gained_mcdc),
2600 };
2601 let lost = CoverageDiffSide {
2602 line_count: lost_lines.len(),
2603 branch_count: lost_branches.len(),
2604 mcdc_count: lost_mcdc.len(),
2605 lines: page(&lost_lines),
2606 branches: page(&lost_branches),
2607 mcdc: page(&lost_mcdc),
2608 };
2609 let returned = [
2610 gained.lines.len(),
2611 gained.branches.len(),
2612 gained.mcdc.len(),
2613 lost.lines.len(),
2614 lost.branches.len(),
2615 lost.mcdc.len(),
2616 ]
2617 .into_iter()
2618 .max()
2619 .unwrap_or(0);
2620 Ok((
2621 CoverageDiffData {
2622 filters: query_filters(options.view, options.kind, options.runner),
2623 older: options.older_run.into(),
2624 newer: options.newer_run.into(),
2625 delta: CoverageDiffDelta {
2626 lines: rounded_delta(
2627 newer.summary.lines.percentage,
2628 older.summary.lines.percentage,
2629 ),
2630 branches: rounded_delta(
2631 newer.summary.branches.percentage,
2632 older.summary.branches.percentage,
2633 ),
2634 mcdc: rounded_delta(
2635 newer.summary.condition_coverage_pct,
2636 older.summary.condition_coverage_pct,
2637 ),
2638 },
2639 gained,
2640 lost,
2641 files,
2642 by_file,
2643 inputs: None,
2644 },
2645 pagination(options.offset, options.limit, returned, total),
2646 ))
2647}
2648
2649pub fn coverage_scope_query(
2650 index: &CoverageIndex<'_>,
2651 options: CoverageScopeQueryOptions<'_>,
2652) -> Result<(CoverageScopeData, AgentPagination), QueryError> {
2653 if options.limit == 0 {
2654 return Err(QueryError::InvalidPagination);
2655 }
2656 let projection = index.projection(options.view, options.kind, options.runner)?;
2657 let scope = projection
2658 .source_scope
2659 .ok_or(QueryError::ScopeUnavailable)?;
2660 let mut entries = index.scope_entries(options.view)?;
2661 entries.sort_by(|left, right| {
2662 scope_status_rank(&left.status)
2663 .cmp(&scope_status_rank(&right.status))
2664 .then_with(|| left.file.cmp(&right.file))
2665 });
2666 let groups = scope_groups(&entries);
2667 let unclassified = unclassified_source(&scope.roots, &entries);
2668 let total = entries.len();
2669 let selected = entries
2670 .into_iter()
2671 .skip(options.offset)
2672 .take(options.limit)
2673 .collect::<Vec<_>>();
2674 let returned = selected.len();
2675 Ok((
2676 CoverageScopeData {
2677 run: options.run.into(),
2678 filters: CoverageQueryFilters {
2679 outcome: match options.view {
2680 CoverageViewId::All => "all",
2681 CoverageViewId::Passed => "passed",
2682 CoverageViewId::Failed => "failed",
2683 }
2684 .into(),
2685 kind: options.kind.map(str::to_owned),
2686 runner: options.runner.map(str::to_owned),
2687 },
2688 kind: scope.kind,
2689 language: scope.language,
2690 model: scope.model,
2691 mode: scope.mode,
2692 roots: scope.roots,
2693 unit: scope.unit,
2694 measurement_complete: scope.measurement_complete,
2695 counts: ScopeCounts {
2696 included: scope.included,
2697 excluded: scope.excluded,
2698 ambiguous: scope.ambiguous,
2699 },
2700 measurement: projection.measurement,
2701 groups,
2702 unclassified_source: unclassified,
2703 entries: selected,
2704 },
2705 pagination(options.offset, options.limit, returned, total),
2706 ))
2707}
2708
2709pub fn coverage_summary_query(
2710 index: &CoverageIndex<'_>,
2711 options: CoverageSummaryQueryOptions<'_>,
2712) -> Result<CoverageSummaryData, QueryError> {
2713 let projection = index.projection(options.view, options.kind, options.runner)?;
2714 let mut test_kind_sources = BTreeMap::new();
2715 for test in index.test_summaries(options.view)?.iter().filter(|t| {
2716 t.role == "test"
2717 && options.kind.is_none_or(|k| t.provenance.kind == k)
2718 && options.runner.is_none_or(|r| t.provenance.runner == r)
2719 }) {
2720 *test_kind_sources
2721 .entry(test.provenance.source.clone())
2722 .or_insert(0) += 1;
2723 }
2724 let mut diagnostics = Vec::new();
2725 let mut transport_blockers = 0usize;
2726 if projection.empty_evidence_tests > 0 {
2727 diagnostics.push(CoverageDiagnostic {
2728 code: "TEST_EVIDENCE_MISSING".into(),
2729 severity: "warning".into(),
2730 message: format!(
2731 "{} test(s) recorded assertion phases but attributed zero coverage evidence; possible causes include checks of uninstrumented data, shared setup, lost async context or missing probe transport. First: {}",
2732 projection.empty_evidence_tests,
2733 projection.first_empty_evidence_test.as_deref().unwrap_or("unknown")
2734 ),
2735 });
2736 }
2737 if let Some(transport) = &projection.transport {
2738 if transport.corrupt_records > 0 {
2739 transport_blockers += 1;
2740 diagnostics.push(CoverageDiagnostic {
2741 code: "CORRUPT_EVIDENCE_RECORDS".into(),
2742 severity: "error".into(),
2743 message: format!(
2744 "{} malformed evidence record(s) in {} file(s) were excluded; coverage is incomplete.",
2745 transport.corrupt_records, transport.corrupt_files
2746 ),
2747 });
2748 }
2749 if transport.remote_launches > 0
2750 && transport.scoped_server_records == 0
2751 && transport.background_server_records == 0
2752 && projection.attribution.server_explicit == 0
2753 && projection.attribution.server_fallback == 0
2754 {
2755 transport_blockers += 1;
2756 diagnostics.push(CoverageDiagnostic {
2757 code: "REMOTE_SERVER_EVIDENCE_MISSING".into(),
2758 severity: "error".into(),
2759 message: "Remote launches were supervised, but neither scoped nor background server evidence returned. Supercov refuses to describe this measurement as complete.".into(),
2760 });
2761 }
2762 }
2763 let coverage_by_kind = index.dimensions(options.view, CoverageDimension::Kind)?;
2764 let coverage_by_runner = index.dimensions(options.view, CoverageDimension::Runner)?;
2765 let coverage_by_tests = if options.kind.is_none() && options.runner.is_none() {
2766 index
2767 .dimensions(options.view, CoverageDimension::Tests)?
2768 .into_iter()
2769 .next()
2770 .filter(|tests| tests.tests + tests.setups > 0)
2771 .map(|tests| tests.summary)
2772 } else {
2773 None
2774 };
2775 let other_e2e_kinds = coverage_by_kind
2776 .iter()
2777 .filter(|dimension| dimension.tests > 0)
2778 .filter_map(|dimension| dimension.kind.clone())
2779 .filter(|kind| kind != "e2e")
2780 .collect::<Vec<_>>();
2781 let e2e_observed = coverage_by_kind
2782 .iter()
2783 .any(|dimension| dimension.tests > 0 && dimension.kind.as_deref() == Some("e2e"));
2784 let e2e_gap_context = if options.kind.is_none()
2785 && options.runner.is_none()
2786 && e2e_observed
2787 && !other_e2e_kinds.is_empty()
2788 {
2789 let mut covered_elsewhere = IndexedGapDimensions {
2790 lines: 0,
2791 statements: 0,
2792 functions: 0,
2793 branches: 0,
2794 mcdc_conditions: 0,
2795 };
2796 let mut uncovered_everywhere = covered_elsewhere.clone();
2797 for gap in index.file_gaps(options.view, Some("e2e"), None)? {
2798 covered_elsewhere.lines += gap.covered_by_other_tests.lines;
2799 covered_elsewhere.statements += gap.covered_by_other_tests.statements;
2800 covered_elsewhere.functions += gap.covered_by_other_tests.functions;
2801 covered_elsewhere.branches += gap.covered_by_other_tests.branches;
2802 covered_elsewhere.mcdc_conditions += gap.covered_by_other_tests.mcdc_conditions;
2803 uncovered_everywhere.lines += gap.uncovered_everywhere.lines;
2804 uncovered_everywhere.statements += gap.uncovered_everywhere.statements;
2805 uncovered_everywhere.functions += gap.uncovered_everywhere.functions;
2806 uncovered_everywhere.branches += gap.uncovered_everywhere.branches;
2807 uncovered_everywhere.mcdc_conditions += gap.uncovered_everywhere.mcdc_conditions;
2808 }
2809 Some(CoverageKindGapContext {
2810 kind: "e2e".into(),
2811 other_kinds: other_e2e_kinds,
2812 covered_elsewhere,
2813 uncovered_everywhere,
2814 })
2815 } else {
2816 None
2817 };
2818 let filters = CoverageQueryFilters {
2819 outcome: match options.view {
2820 CoverageViewId::All => "all",
2821 CoverageViewId::Passed => "passed",
2822 CoverageViewId::Failed => "failed",
2823 }
2824 .into(),
2825 kind: options.kind.map(str::to_owned),
2826 runner: options.runner.map(str::to_owned),
2827 };
2828 let mut measurement = projection.measurement.clone();
2829 if transport_blockers > 0 {
2830 measurement.complete = false;
2831 measurement.limitations = measurement.limitations.saturating_add(transport_blockers);
2832 measurement.blocking = measurement.blocking.saturating_add(transport_blockers);
2833 }
2834 let structurally_complete = projection.summary.coverage_complete && measurement.complete;
2835 let complete = options.view == CoverageViewId::Passed
2836 && options.valid
2837 && !options.stale
2838 && structurally_complete;
2839 Ok(CoverageSummaryData {
2840 test_kind_sources,
2841 assertion_coverage: None,
2842 run: options.run.into(),
2843 command: Vec::new(),
2844 hints: Vec::new(),
2845 workspace: None,
2846 filters,
2847 model: index.model()?,
2848 generated_at: projection.generated_at,
2849 valid: options.valid,
2850 test_exit_code: options.test_exit_code,
2851 stale: options.stale,
2852 stale_reasons: options.stale_reasons,
2853 structurally_complete,
2854 complete,
2855 coverage: projection.summary,
2856 measurement,
2857 coverage_by_kind,
2858 coverage_by_tests,
2859 e2e_gap_context,
2860 coverage_by_runner,
2861 attribution: projection.attribution,
2862 test_attribution: {
2863 let mut counts = TestAttributionCounts::default();
2864 for test in index
2865 .test_summaries(options.view)?
2866 .iter()
2867 .filter(|test| test.role == "test")
2868 {
2869 match test.attribution.as_str() {
2870 crate::coverage_report::ATTRIBUTION_RUN_WIDE => counts.run_wide += 1,
2871 crate::coverage_report::ATTRIBUTION_PARTIAL => counts.partial += 1,
2872 _ => counts.exact += 1,
2873 }
2874 }
2875 counts
2876 },
2877 transport: projection.transport,
2878 diagnostics,
2879 confidence: (options.kind.is_none() && options.runner.is_none())
2880 .then_some(projection.confidence),
2881 files_with_gaps: projection.files_with_gaps,
2882 files_with_coverage_gaps: projection.files_with_coverage_gaps,
2883 files_with_measurement_limitations: projection.measurement.files,
2884 tests: projection.tests,
2885 setups: projection.setups,
2886 test_outcomes: projection.test_outcomes,
2887 unclassified_source: match &projection.source_scope {
2888 Some(scope) if scope.ambiguous > 0 => {
2889 unclassified_source(&scope.roots, &index.scope_entries(options.view)?)
2890 }
2891 _ => None,
2892 },
2893 source_scope: projection.source_scope,
2894 })
2895}
2896
2897#[derive(Debug, Clone, PartialEq)]
2898pub enum CoverageDimensionQueryData {
2899 Kinds(CoverageKindsData),
2900 Runners(CoverageRunnersData),
2901}
2902
2903#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2906#[serde(rename_all = "camelCase")]
2907pub struct CoverageArea {
2908 pub directory: String,
2909 pub files: usize,
2910 pub totals: IndexedGapDimensions,
2911 pub covered: IndexedGapDimensions,
2912 pub by_kind: Vec<CoverageAreaKind>,
2913 #[serde(skip_serializing_if = "Option::is_none")]
2917 pub covered_by_tests: Option<IndexedGapDimensions>,
2918}
2919
2920#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2921#[serde(rename_all = "camelCase")]
2922pub struct CoverageAreaKind {
2923 pub kind: String,
2924 pub covered: IndexedGapDimensions,
2925}
2926
2927#[derive(Debug, Clone, PartialEq, Serialize)]
2928#[serde(rename_all = "camelCase")]
2929pub struct CoverageAreasData {
2930 pub run: String,
2931 pub filters: CoverageQueryFilters,
2932 pub metric: MinimizeMetric,
2933 pub depth: usize,
2934 pub kinds: Vec<String>,
2937 pub areas: Vec<CoverageArea>,
2938}
2939
2940#[derive(Debug, Clone, Copy)]
2941pub struct CoverageAreasQueryOptions<'a> {
2942 pub run: &'a str,
2943 pub view: CoverageViewId,
2944 pub metric: MinimizeMetric,
2945 pub kind: Option<&'a str>,
2946 pub runner: Option<&'a str>,
2947 pub depth: usize,
2948 pub offset: usize,
2949 pub limit: usize,
2950}
2951
2952fn area_directory(file: &str, depth: usize) -> String {
2955 let mut folders = file.split('/').collect::<Vec<_>>();
2956 folders.pop();
2957 if folders.is_empty() {
2958 ".".into()
2959 } else {
2960 folders[..folders.len().min(depth)].join("/")
2961 }
2962}
2963
2964fn add_dimensions(sum: &mut IndexedGapDimensions, value: &IndexedGapDimensions) {
2965 sum.lines += value.lines;
2966 sum.statements += value.statements;
2967 sum.functions += value.functions;
2968 sum.branches += value.branches;
2969 sum.mcdc_conditions += value.mcdc_conditions;
2970}
2971
2972const NO_DIMENSIONS: IndexedGapDimensions = IndexedGapDimensions {
2973 lines: 0,
2974 statements: 0,
2975 functions: 0,
2976 branches: 0,
2977 mcdc_conditions: 0,
2978};
2979
2980pub fn dimension_for_metric(dimensions: &IndexedGapDimensions, metric: MinimizeMetric) -> usize {
2982 match metric {
2983 MinimizeMetric::All | MinimizeMetric::Lines => dimensions.lines,
2984 MinimizeMetric::Statements => dimensions.statements,
2985 MinimizeMetric::Functions => dimensions.functions,
2986 MinimizeMetric::Branches => dimensions.branches,
2987 MinimizeMetric::Mcdc => dimensions.mcdc_conditions,
2988 }
2989}
2990
2991pub fn coverage_areas_query(
2995 index: &CoverageIndex<'_>,
2996 options: CoverageAreasQueryOptions<'_>,
2997) -> Result<(CoverageAreasData, AgentPagination), QueryError> {
2998 if options.limit == 0 || options.depth == 0 {
2999 return Err(QueryError::InvalidPagination);
3000 }
3001 let files = index.file_gaps(options.view, options.kind, options.runner)?;
3002 if (options.kind.is_some() || options.runner.is_some()) && files.is_empty() {
3003 return Err(QueryError::TestFilterEmpty {
3004 kind: options.kind.map(str::to_owned),
3005 runner: options.runner.map(str::to_owned),
3006 });
3007 }
3008 let kinds = if options.kind.is_none() && options.runner.is_none() {
3009 index
3010 .dimensions(options.view, CoverageDimension::Kind)?
3011 .into_iter()
3012 .filter(|kind| kind.tests > 0 && kind.attributed > 0)
3013 .filter_map(|kind| kind.kind)
3014 .collect::<Vec<_>>()
3015 } else {
3016 Vec::new()
3017 };
3018 let mut areas = BTreeMap::<String, CoverageArea>::new();
3019 for file in &files {
3020 let directory = area_directory(&file.file, options.depth);
3021 let area = areas
3022 .entry(directory.clone())
3023 .or_insert_with(|| CoverageArea {
3024 directory,
3025 files: 0,
3026 totals: NO_DIMENSIONS,
3027 covered: NO_DIMENSIONS,
3028 by_kind: kinds
3029 .iter()
3030 .map(|kind| CoverageAreaKind {
3031 kind: kind.clone(),
3032 covered: NO_DIMENSIONS,
3033 })
3034 .collect(),
3035 covered_by_tests: None,
3036 });
3037 area.files += 1;
3038 add_dimensions(&mut area.totals, &file.totals);
3039 add_dimensions(&mut area.covered, &file.covered);
3040 }
3041 for (position, kind) in kinds.iter().enumerate() {
3042 for file in index.file_gaps(options.view, Some(kind), None)? {
3043 if let Some(area) = areas.get_mut(&area_directory(&file.file, options.depth)) {
3044 add_dimensions(&mut area.by_kind[position].covered, &file.covered);
3045 }
3046 }
3047 }
3048 if !kinds.is_empty() {
3049 let tested = index.file_gaps(options.view, Some(crate::coverage_index::ALL_TESTS), None)?;
3050 if !tested.is_empty() {
3051 for area in areas.values_mut() {
3052 area.covered_by_tests = Some(NO_DIMENSIONS);
3053 }
3054 for file in tested {
3055 if let Some(sum) = areas
3056 .get_mut(&area_directory(&file.file, options.depth))
3057 .and_then(|area| area.covered_by_tests.as_mut())
3058 {
3059 add_dimensions(sum, &file.covered);
3060 }
3061 }
3062 }
3063 }
3064 let mut areas = areas.into_values().collect::<Vec<_>>();
3065 areas.sort_by(|left, right| {
3066 dimension_for_metric(&right.totals, options.metric)
3067 .cmp(&dimension_for_metric(&left.totals, options.metric))
3068 .then_with(|| left.directory.cmp(&right.directory))
3069 });
3070 let total = areas.len();
3071 let page = areas
3072 .into_iter()
3073 .skip(options.offset)
3074 .take(options.limit)
3075 .collect::<Vec<_>>();
3076 let returned = page.len();
3077 Ok((
3078 CoverageAreasData {
3079 run: options.run.into(),
3080 filters: CoverageQueryFilters {
3081 outcome: match options.view {
3082 CoverageViewId::All => "all",
3083 CoverageViewId::Passed => "passed",
3084 CoverageViewId::Failed => "failed",
3085 }
3086 .into(),
3087 kind: options.kind.map(str::to_owned),
3088 runner: options.runner.map(str::to_owned),
3089 },
3090 metric: options.metric,
3091 depth: options.depth,
3092 kinds,
3093 areas: page,
3094 },
3095 pagination(options.offset, options.limit, returned, total),
3096 ))
3097}
3098
3099#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3101#[serde(rename_all = "camelCase")]
3102pub struct TestWithoutEvidence {
3103 pub id: String,
3104 pub name: String,
3105 #[serde(skip_serializing_if = "Option::is_none")]
3106 pub file: Option<String>,
3107 pub kind: String,
3108 pub runner: String,
3109 pub outcome: String,
3110 pub assertions: usize,
3111}
3112
3113#[derive(Debug, Clone, PartialEq, Serialize)]
3114#[serde(rename_all = "camelCase")]
3115pub struct TestsWithoutEvidenceData {
3116 pub run: String,
3117 pub filters: CoverageQueryFilters,
3118 pub tests: Vec<TestWithoutEvidence>,
3119}
3120
3121#[derive(Debug, Clone, Copy)]
3122pub struct TestsWithoutEvidenceOptions<'a> {
3123 pub run: &'a str,
3124 pub view: CoverageViewId,
3125 pub kind: Option<&'a str>,
3126 pub runner: Option<&'a str>,
3127 pub offset: usize,
3128 pub limit: usize,
3129}
3130
3131pub fn tests_without_evidence_query(
3135 index: &CoverageIndex<'_>,
3136 options: TestsWithoutEvidenceOptions<'_>,
3137) -> Result<(TestsWithoutEvidenceData, AgentPagination), QueryError> {
3138 if options.limit == 0 {
3139 return Err(QueryError::InvalidPagination);
3140 }
3141 let mut assertions = BTreeMap::<String, usize>::new();
3142 for phase in index.phase_summaries(options.view)? {
3143 *assertions.entry(phase.test).or_insert(0) += 1;
3144 }
3145 let mut tests = index
3146 .test_details(options.view)?
3147 .into_iter()
3148 .filter(|test| {
3149 test.summary.role == "test"
3150 && test.lines.is_empty()
3151 && test.hits.is_empty()
3152 && test.decisions.is_empty()
3153 && options
3154 .kind
3155 .is_none_or(|kind| test.summary.provenance.kind == kind)
3156 && options
3157 .runner
3158 .is_none_or(|runner| test.summary.provenance.runner == runner)
3159 })
3160 .filter_map(|test| {
3161 let assertions = *assertions.get(&test.summary.id)?;
3162 Some(TestWithoutEvidence {
3163 id: test.summary.id,
3164 name: test.summary.name,
3165 file: test.summary.file,
3166 kind: test.summary.provenance.kind,
3167 runner: test.summary.provenance.runner,
3168 outcome: test.summary.outcome,
3169 assertions,
3170 })
3171 })
3172 .collect::<Vec<_>>();
3173 tests.sort_by(|left, right| {
3174 (&left.file, &left.name, &left.id).cmp(&(&right.file, &right.name, &right.id))
3175 });
3176 let total = tests.len();
3177 let page = tests
3178 .into_iter()
3179 .skip(options.offset)
3180 .take(options.limit)
3181 .collect::<Vec<_>>();
3182 let returned = page.len();
3183 Ok((
3184 TestsWithoutEvidenceData {
3185 run: options.run.into(),
3186 filters: CoverageQueryFilters {
3187 outcome: match options.view {
3188 CoverageViewId::All => "all",
3189 CoverageViewId::Passed => "passed",
3190 CoverageViewId::Failed => "failed",
3191 }
3192 .into(),
3193 kind: options.kind.map(str::to_owned),
3194 runner: options.runner.map(str::to_owned),
3195 },
3196 tests: page,
3197 },
3198 pagination(options.offset, options.limit, returned, total),
3199 ))
3200}
3201
3202#[derive(Debug, Clone, PartialEq)]
3203pub enum CoverageFileQueryData {
3204 Files(CoverageFilesData),
3205 Gaps(CoverageGapsData),
3206}
3207
3208#[derive(Debug, Clone, PartialEq)]
3209pub struct CoverageFileQueryResult {
3210 pub data: CoverageFileQueryData,
3211 pub pagination: AgentPagination,
3212}
3213
3214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3215#[serde(rename_all = "lowercase")]
3216pub enum DecisionSort {
3217 Location,
3218 Missing,
3219}
3220
3221#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3222#[serde(rename_all = "camelCase")]
3223pub struct DecisionGapTotals {
3224 pub decisions: usize,
3225 pub decisions_with_missing_conditions: usize,
3226 pub conditions: usize,
3227 pub missing_conditions: usize,
3228}
3229
3230#[derive(Debug, Clone, PartialEq, Serialize)]
3231#[serde(rename_all = "camelCase")]
3232pub struct CoverageFileDecisionsData {
3233 pub run: String,
3234 pub filters: CoverageQueryFilters,
3235 pub file: String,
3236 pub group: String,
3237 pub sort: DecisionSort,
3238 pub totals: DecisionGapTotals,
3239 pub decisions: Vec<IndexedDecisionGap>,
3240}
3241
3242#[derive(Debug, Clone, Copy)]
3243pub struct CoverageFileDecisionsOptions<'a> {
3244 pub run: &'a str,
3245 pub view: CoverageViewId,
3246 pub kind: Option<&'a str>,
3247 pub runner: Option<&'a str>,
3248 pub file: &'a str,
3249 pub sort: DecisionSort,
3250 pub offset: usize,
3251 pub limit: usize,
3252}
3253
3254#[derive(Debug, Clone)]
3255pub struct CoverageDimensionQueryOptions<'a> {
3256 pub run: &'a str,
3257 pub view: CoverageViewId,
3258 pub dimension: CoverageDimension,
3259 pub filters: CoverageQueryFilters,
3260 pub offset: usize,
3261 pub limit: usize,
3262}
3263
3264pub fn coverage_dimension_query(
3265 index: &CoverageIndex<'_>,
3266 options: CoverageDimensionQueryOptions<'_>,
3267) -> Result<(CoverageDimensionQueryData, AgentPagination), QueryError> {
3268 let CoverageDimensionQueryOptions {
3269 run,
3270 view,
3271 dimension,
3272 filters,
3273 offset,
3274 limit,
3275 } = options;
3276 if limit == 0 {
3277 return Err(QueryError::InvalidPagination);
3278 }
3279 let values = index.dimensions(view, dimension)?;
3280 let total = values.len();
3281 let selected = values
3282 .into_iter()
3283 .skip(offset)
3284 .take(limit)
3285 .collect::<Vec<_>>();
3286 let returned = selected.len();
3287 let data = match dimension {
3288 CoverageDimension::Kind | CoverageDimension::Tests => {
3290 CoverageDimensionQueryData::Kinds(CoverageKindsData {
3291 run: run.into(),
3292 filters,
3293 kinds: selected,
3294 })
3295 }
3296 CoverageDimension::Runner => CoverageDimensionQueryData::Runners(CoverageRunnersData {
3297 run: run.into(),
3298 filters,
3299 runners: selected,
3300 }),
3301 };
3302 Ok((data, pagination(offset, limit, returned, total)))
3303}
3304
3305#[derive(Debug, Clone, Copy)]
3306pub struct CoverageFileQueryOptions<'a> {
3307 pub run: &'a str,
3308 pub view: CoverageViewId,
3309 pub metric: MinimizeMetric,
3310 pub gaps_only: bool,
3311 pub kind: Option<&'a str>,
3312 pub runner: Option<&'a str>,
3313 pub offset: usize,
3314 pub limit: usize,
3315}
3316
3317fn gap_metric_value(gap: &IndexedFileGap, metric: MinimizeMetric) -> usize {
3318 match metric {
3319 MinimizeMetric::All => gap.score,
3320 MinimizeMetric::Lines => gap.uncovered_lines,
3321 MinimizeMetric::Statements => gap.uncovered_statements,
3322 MinimizeMetric::Functions => gap.uncovered_functions,
3323 MinimizeMetric::Branches => gap.missing_branches,
3324 MinimizeMetric::Mcdc => gap.missing_mcdc_conditions,
3325 }
3326}
3327
3328fn has_gap_for_metric(gap: &IndexedFileGap, metric: MinimizeMetric) -> bool {
3329 match metric {
3330 MinimizeMetric::All => {
3331 gap.uncovered_lines > 0
3332 || gap.uncovered_statements > 0
3333 || gap.uncovered_functions > 0
3334 || gap.missing_branches > 0
3335 || gap.missing_mcdc_conditions > 0
3336 }
3337 _ => gap_metric_value(gap, metric) > 0,
3338 }
3339}
3340
3341pub fn coverage_file_decisions_query(
3342 index: &CoverageIndex<'_>,
3343 options: CoverageFileDecisionsOptions<'_>,
3344) -> Result<(CoverageFileDecisionsData, AgentPagination), QueryError> {
3345 if options.limit == 0 {
3346 return Err(QueryError::InvalidPagination);
3347 }
3348 let all = index.decision_gaps(options.view, options.kind, options.runner, options.file)?;
3349 if (options.kind.is_some() || options.runner.is_some()) && all.is_empty() {
3350 if index
3353 .file_gaps(options.view, options.kind, options.runner)?
3354 .is_empty()
3355 {
3356 return Err(QueryError::TestFilterEmpty {
3357 kind: options.kind.map(str::to_owned),
3358 runner: options.runner.map(str::to_owned),
3359 });
3360 }
3361 }
3362 let totals = DecisionGapTotals {
3363 decisions: all.len(),
3364 decisions_with_missing_conditions: all
3365 .iter()
3366 .filter(|decision| decision.missing_conditions > 0)
3367 .count(),
3368 conditions: all.iter().map(|decision| decision.conditions).sum(),
3369 missing_conditions: all.iter().map(|decision| decision.missing_conditions).sum(),
3370 };
3371 let mut missing = all
3372 .into_iter()
3373 .filter(|decision| decision.missing_conditions > 0)
3374 .collect::<Vec<_>>();
3375 missing.sort_by(|left, right| match options.sort {
3376 DecisionSort::Missing => right
3377 .missing_conditions
3378 .cmp(&left.missing_conditions)
3379 .then_with(|| left.line.cmp(&right.line))
3380 .then_with(|| left.column.cmp(&right.column)),
3381 DecisionSort::Location => left
3382 .line
3383 .cmp(&right.line)
3384 .then_with(|| left.column.cmp(&right.column))
3385 .then_with(|| left.id.cmp(&right.id)),
3386 });
3387 let total = missing.len();
3388 let rows = missing
3389 .into_iter()
3390 .skip(options.offset)
3391 .take(options.limit)
3392 .collect::<Vec<_>>();
3393 let returned = rows.len();
3394 let filters = CoverageQueryFilters {
3395 outcome: match options.view {
3396 CoverageViewId::All => "all",
3397 CoverageViewId::Passed => "passed",
3398 CoverageViewId::Failed => "failed",
3399 }
3400 .into(),
3401 kind: options.kind.map(str::to_owned),
3402 runner: options.runner.map(str::to_owned),
3403 };
3404 Ok((
3405 CoverageFileDecisionsData {
3406 run: options.run.into(),
3407 filters,
3408 file: options.file.into(),
3409 group: "decision".into(),
3410 sort: options.sort,
3411 totals,
3412 decisions: rows,
3413 },
3414 pagination(options.offset, options.limit, returned, total),
3415 ))
3416}
3417
3418pub fn coverage_file_query(
3419 index: &CoverageIndex<'_>,
3420 options: CoverageFileQueryOptions<'_>,
3421) -> Result<CoverageFileQueryResult, QueryError> {
3422 let CoverageFileQueryOptions {
3423 run,
3424 view,
3425 metric,
3426 gaps_only,
3427 kind,
3428 runner,
3429 offset,
3430 limit,
3431 } = options;
3432 if limit == 0 {
3433 return Err(QueryError::InvalidPagination);
3434 }
3435 let mut files = index.file_gaps(view, kind, runner)?;
3436 if (kind.is_some() || runner.is_some()) && files.is_empty() {
3437 return Err(QueryError::TestFilterEmpty {
3438 kind: kind.map(str::to_owned),
3439 runner: runner.map(str::to_owned),
3440 });
3441 }
3442 if gaps_only {
3443 files.retain(|gap| has_gap_for_metric(gap, metric) || gap.measurement_limitations > 0);
3444 }
3445 files.sort_by(|left, right| {
3446 gap_metric_value(right, metric)
3447 .cmp(&gap_metric_value(left, metric))
3448 .then_with(|| {
3449 right
3450 .measurement_limitations
3451 .cmp(&left.measurement_limitations)
3452 })
3453 .then_with(|| left.file.cmp(&right.file))
3454 });
3455 let total = files.len();
3456 let page = files
3457 .into_iter()
3458 .skip(offset)
3459 .take(limit)
3460 .collect::<Vec<_>>();
3461 let page_info = pagination(offset, limit, page.len(), total);
3462 let filters = CoverageQueryFilters {
3463 outcome: match view {
3464 CoverageViewId::All => "all",
3465 CoverageViewId::Passed => "passed",
3466 CoverageViewId::Failed => "failed",
3467 }
3468 .into(),
3469 kind: kind.map(str::to_owned),
3470 runner: runner.map(str::to_owned),
3471 };
3472 let data = if gaps_only {
3473 CoverageFileQueryData::Gaps(CoverageGapsData {
3474 run: run.into(),
3475 filters,
3476 metric,
3477 gaps: page,
3478 })
3479 } else {
3480 CoverageFileQueryData::Files(CoverageFilesData {
3481 run: run.into(),
3482 filters,
3483 metric,
3484 files: page,
3485 })
3486 };
3487 Ok(CoverageFileQueryResult {
3488 data,
3489 pagination: page_info,
3490 })
3491}
3492
3493#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3494enum ObligationMetric {
3495 Lines,
3496 Statements,
3497 Functions,
3498 Branches,
3499 Mcdc,
3500}
3501
3502impl ObligationMetric {
3503 fn selected(self, metric: MinimizeMetric) -> bool {
3504 metric == MinimizeMetric::All
3505 || matches!(
3506 (self, metric),
3507 (Self::Lines, MinimizeMetric::Lines)
3508 | (Self::Statements, MinimizeMetric::Statements)
3509 | (Self::Functions, MinimizeMetric::Functions)
3510 | (Self::Branches, MinimizeMetric::Branches)
3511 | (Self::Mcdc, MinimizeMetric::Mcdc)
3512 )
3513 }
3514
3515 fn public(self) -> MinimizeMetric {
3516 match self {
3517 Self::Lines => MinimizeMetric::Lines,
3518 Self::Statements => MinimizeMetric::Statements,
3519 Self::Functions => MinimizeMetric::Functions,
3520 Self::Branches => MinimizeMetric::Branches,
3521 Self::Mcdc => MinimizeMetric::Mcdc,
3522 }
3523 }
3524}
3525
3526#[derive(Clone)]
3527struct Obligation {
3528 id: String,
3529 metric: ObligationMetric,
3530 options: Vec<Vec<String>>,
3532}
3533
3534struct ObligationModel {
3535 obligations: Vec<Obligation>,
3536 setup_by_file: BTreeMap<String, Vec<String>>,
3537 tests_by_file: BTreeMap<String, Vec<String>>,
3538 background: Vec<String>,
3539}
3540
3541impl ObligationModel {
3542 fn expand(&self, selected: &BTreeSet<String>) -> BTreeSet<String> {
3543 let mut expanded = selected.clone();
3544 expanded.extend(self.background.iter().cloned());
3545 for (file, setup_ids) in &self.setup_by_file {
3546 if self
3547 .tests_by_file
3548 .get(file)
3549 .is_some_and(|tests| tests.iter().any(|id| selected.contains(id)))
3550 {
3551 expanded.extend(setup_ids.iter().cloned());
3552 }
3553 }
3554 expanded
3555 }
3556}
3557
3558fn deduplicate_options(options: impl IntoIterator<Item = Vec<String>>) -> Vec<Vec<String>> {
3559 let mut unique = BTreeMap::<String, Vec<String>>::new();
3560 for mut option in options {
3561 option.sort();
3562 option.dedup();
3563 let key = option.join("\0");
3564 unique.entry(key).or_insert(option);
3565 }
3566 unique.into_values().collect()
3567}
3568
3569fn evidence_choices(
3570 ids: &[Id],
3571 tests: &HashMap<&str, &crate::coverage_report::TestCoverageResult>,
3572 candidates: &BTreeSet<String>,
3573 tests_by_file: &BTreeMap<String, Vec<String>>,
3574) -> Vec<Vec<String>> {
3575 let mut choices = Vec::new();
3576 for id in ids {
3577 let Some(test) = tests.get(id.as_str()) else {
3578 continue;
3579 };
3580 if test.role == "background" {
3581 choices.push(Vec::new());
3582 } else if test.role == "setup" {
3583 if let Some(file) = &test.file {
3584 choices.extend(
3585 tests_by_file
3586 .get(file)
3587 .into_iter()
3588 .flatten()
3589 .map(|candidate| vec![candidate.clone()]),
3590 );
3591 }
3592 } else if candidates.contains(id.as_str()) {
3593 choices.push(vec![id.to_string()]);
3594 }
3595 }
3596 deduplicate_options(choices)
3597}
3598
3599fn build_obligations(view: &CoverageView, candidates: &BTreeSet<String>) -> ObligationModel {
3600 let tests = view
3601 .tests
3602 .iter()
3603 .map(|test| (test.id.as_str(), test))
3604 .collect::<HashMap<_, _>>();
3605 let mut tests_by_file = BTreeMap::<String, Vec<String>>::new();
3606 let mut setup_by_file = BTreeMap::<String, Vec<String>>::new();
3607 let mut background = Vec::new();
3608 for test in &view.tests {
3609 match test.role.as_str() {
3610 "test" if candidates.contains(&test.id) => {
3611 if let Some(file) = &test.file {
3612 tests_by_file
3613 .entry(file.clone())
3614 .or_default()
3615 .push(test.id.clone());
3616 }
3617 }
3618 "setup" => {
3619 if let Some(file) = &test.file {
3620 setup_by_file
3621 .entry(file.clone())
3622 .or_default()
3623 .push(test.id.clone());
3624 }
3625 }
3626 "background" => background.push(test.id.clone()),
3627 _ => {}
3628 }
3629 }
3630 let choices = |ids: &[Id]| evidence_choices(ids, &tests, candidates, &tests_by_file);
3631 let mut obligations = Vec::new();
3632 let mut unique_lines = BTreeMap::new();
3633 for line in &view.lines {
3634 unique_lines.insert((line.file.as_str(), line.line), line);
3635 }
3636 for ((file, line), result) in unique_lines {
3637 obligations.push(Obligation {
3638 id: format!("line:{file}:{line}"),
3639 metric: ObligationMetric::Lines,
3640 options: choices(&result.tests),
3641 });
3642 }
3643 for point in &view.points {
3644 let (kind, metric) = match point.meta.kind {
3645 crate::coverage_analysis::PointKind::Statement => {
3646 ("statement", ObligationMetric::Statements)
3647 }
3648 crate::coverage_analysis::PointKind::Function => {
3649 ("function", ObligationMetric::Functions)
3650 }
3651 };
3652 obligations.push(Obligation {
3653 id: format!("{kind}:{}", point.meta.id),
3654 metric,
3655 options: choices(&point.tests),
3656 });
3657 }
3658 for branch in &view.branches {
3659 for alternative in &branch.alternatives {
3660 obligations.push(Obligation {
3661 id: format!("branch:{}:{}", branch.meta.id, alternative.id),
3662 metric: ObligationMetric::Branches,
3663 options: choices(&alternative.tests),
3664 });
3665 }
3666 }
3667 for decision in &view.decisions {
3668 for condition in 0..decision.meta.conditions.len() {
3669 let mut options = Vec::new();
3670 for left in 0..decision.vector_observations.len() {
3671 for right in (left + 1)..decision.vector_observations.len() {
3672 let first = &decision.vector_observations[left];
3673 let second = &decision.vector_observations[right];
3674 if !is_independence_pair(&first.vector, &second.vector, condition) {
3675 continue;
3676 }
3677 for first_choice in choices(&first.tests) {
3678 for second_choice in choices(&second.tests) {
3679 let mut combined = first_choice.clone();
3680 combined.extend(second_choice);
3681 options.push(combined);
3682 }
3683 }
3684 }
3685 }
3686 obligations.push(Obligation {
3687 id: format!("mcdc:{}:{condition}", decision.meta.id),
3688 metric: ObligationMetric::Mcdc,
3689 options: deduplicate_options(options),
3690 });
3691 }
3692 }
3693 ObligationModel {
3694 obligations,
3695 setup_by_file,
3696 tests_by_file,
3697 background,
3698 }
3699}
3700
3701fn percentage(metric: ObligationMetric, summary: &CoverageSummary) -> f64 {
3702 match metric {
3703 ObligationMetric::Lines => summary.lines.percentage,
3704 ObligationMetric::Statements => summary.statements.percentage,
3705 ObligationMetric::Functions => summary.functions.percentage,
3706 ObligationMetric::Branches => summary.branches.percentage,
3707 ObligationMetric::Mcdc => summary.condition_coverage_pct,
3708 }
3709}
3710
3711fn obligation_satisfied(obligation: &Obligation, selected: &BTreeSet<String>) -> bool {
3712 obligation
3713 .options
3714 .iter()
3715 .any(|option| option.iter().all(|test| selected.contains(test)))
3716}
3717
3718struct Search<'a> {
3719 obligations: &'a [Obligation],
3720 skip_limits: BTreeMap<ObligationMetric, usize>,
3721 best: BTreeSet<String>,
3722 explored_states: usize,
3723 max_states: usize,
3724 seen: BTreeSet<String>,
3725 candidate_tests: usize,
3726 target: f64,
3727 metric: MinimizeMetric,
3728}
3729
3730impl Search<'_> {
3731 fn visit(
3732 &mut self,
3733 selected: BTreeSet<String>,
3734 skipped: BTreeSet<String>,
3735 skipped_by_metric: BTreeMap<ObligationMetric, usize>,
3736 ) -> Result<(), QueryError> {
3737 self.explored_states += 1;
3738 if self.explored_states > self.max_states {
3739 return Err(QueryError::ComplexityLimit {
3740 candidate_tests: self.candidate_tests,
3741 obligations: self.obligations.len(),
3742 explored_states: self.explored_states,
3743 max_states: self.max_states,
3744 target: self.target,
3745 metric: self.metric,
3746 });
3747 }
3748 if selected.len() >= self.best.len() {
3749 return Ok(());
3750 }
3751 let state_key = format!(
3752 "{}|{}",
3753 selected.iter().cloned().collect::<Vec<_>>().join(","),
3754 skipped.iter().cloned().collect::<Vec<_>>().join(",")
3755 );
3756 if !self.seen.insert(state_key) {
3757 return Ok(());
3758 }
3759 let mut unmet = self
3760 .obligations
3761 .iter()
3762 .filter(|obligation| {
3763 !skipped.contains(&obligation.id) && !obligation_satisfied(obligation, &selected)
3764 })
3765 .collect::<Vec<_>>();
3766 if unmet.is_empty() {
3767 self.best = selected;
3768 return Ok(());
3769 }
3770 unmet.sort_by(|left, right| {
3771 let feasible = |obligation: &Obligation| {
3772 obligation
3773 .options
3774 .iter()
3775 .filter(|option| option.iter().any(|test| !selected.contains(test)))
3776 .count()
3777 };
3778 feasible(left)
3779 .cmp(&feasible(right))
3780 .then_with(|| left.id.cmp(&right.id))
3781 });
3782 let obligation = unmet[0];
3783 let mut additions = deduplicate_options(obligation.options.iter().filter_map(|option| {
3784 let addition = option
3785 .iter()
3786 .filter(|test| !selected.contains(*test))
3787 .cloned()
3788 .collect::<Vec<_>>();
3789 (!addition.is_empty()).then_some(addition)
3790 }));
3791 additions.sort_by(|left, right| {
3792 left.len()
3793 .cmp(&right.len())
3794 .then_with(|| left.join("\0").cmp(&right.join("\0")))
3795 });
3796 for addition in additions {
3797 if selected.len() + addition.len() >= self.best.len() {
3798 continue;
3799 }
3800 let mut next = selected.clone();
3801 next.extend(addition);
3802 self.visit(next, skipped.clone(), skipped_by_metric.clone())?;
3803 }
3804 let skipped_count = skipped_by_metric
3805 .get(&obligation.metric)
3806 .copied()
3807 .unwrap_or(0);
3808 if skipped_count
3809 < self
3810 .skip_limits
3811 .get(&obligation.metric)
3812 .copied()
3813 .unwrap_or(0)
3814 {
3815 let mut next_skipped = skipped;
3816 next_skipped.insert(obligation.id.clone());
3817 let mut next_counts = skipped_by_metric;
3818 next_counts.insert(obligation.metric, skipped_count + 1);
3819 self.visit(selected, next_skipped, next_counts)?;
3820 }
3821 Ok(())
3822 }
3823}
3824
3825pub fn minimum_test_set(
3826 view: &CoverageView,
3827 target: f64,
3828 metric: MinimizeMetric,
3829 max_states: usize,
3830) -> Result<MinimumTestSetResult, QueryError> {
3831 use crate::coverage_report::coverage_is_complete;
3832 if !target.is_finite() || !(0.0..=100.0).contains(&target) {
3833 return Err(QueryError::InvalidTarget(target));
3834 }
3835 if view.tests.iter().any(|test| {
3836 test.role == "background" && (!test.hits.is_empty() || !test.decisions.is_empty())
3837 }) {
3838 return Err(QueryError::UnattributedEvidence);
3839 }
3840 if view
3847 .tests
3848 .iter()
3849 .any(|test| test.role == "test" && !coverage_is_complete(&test.attribution))
3850 {
3851 return Err(QueryError::UnattributedEvidence);
3852 }
3853 let candidate_tests = view
3854 .tests
3855 .iter()
3856 .filter(|test| test.role == "test")
3857 .map(|test| test.id.clone())
3858 .collect::<BTreeSet<_>>();
3859 let model = build_obligations(view, &candidate_tests);
3860 let obligations = model
3861 .obligations
3862 .iter()
3863 .filter(|obligation| obligation.metric.selected(metric))
3864 .cloned()
3865 .collect::<Vec<_>>();
3866 let mut totals = BTreeMap::<ObligationMetric, usize>::new();
3867 for obligation in &obligations {
3868 *totals.entry(obligation.metric).or_default() += 1;
3869 }
3870 let metrics = [
3871 ObligationMetric::Lines,
3872 ObligationMetric::Statements,
3873 ObligationMetric::Functions,
3874 ObligationMetric::Branches,
3875 ObligationMetric::Mcdc,
3876 ]
3877 .into_iter()
3878 .filter(|candidate| candidate.selected(metric))
3879 .collect::<Vec<_>>();
3880 let skip_limits = metrics
3881 .iter()
3882 .map(|selected_metric| {
3883 let total = totals.get(selected_metric).copied().unwrap_or(0);
3884 let required = ((total as f64 * target) / 100.0).ceil() as usize;
3885 (*selected_metric, total.saturating_sub(required))
3886 })
3887 .collect::<BTreeMap<_, _>>();
3888 let full_expanded = model.expand(&candidate_tests);
3889 let full_summary = coverage_summary_for_tests(view, &full_expanded)?;
3890 for selected_metric in &metrics {
3891 let reachable = percentage(*selected_metric, &full_summary);
3892 if reachable + 1e-9 < target {
3893 return Err(QueryError::TargetUnreachable {
3894 metric: selected_metric.public(),
3895 target,
3896 reachable,
3897 });
3898 }
3899 }
3900 let mut search = Search {
3901 obligations: &obligations,
3902 skip_limits,
3903 best: candidate_tests.clone(),
3904 explored_states: 0,
3905 max_states,
3906 seen: BTreeSet::new(),
3907 candidate_tests: candidate_tests.len(),
3908 target,
3909 metric,
3910 };
3911 search.visit(BTreeSet::new(), BTreeSet::new(), BTreeMap::new())?;
3912 let expanded = model.expand(&search.best);
3913 let summary = coverage_summary_for_tests(view, &expanded)?;
3914 Ok(MinimumTestSetResult {
3915 optimal: true,
3916 target,
3917 metric,
3918 selected: search.best.into_iter().collect(),
3919 expanded: expanded.into_iter().collect(),
3920 summary,
3921 explored_states: search.explored_states,
3922 })
3923}
3924
3925#[cfg(test)]
3926mod tests {
3927 use crate::{
3928 coverage_analysis::McdcVector,
3929 coverage_report::{
3930 CoverageManifest, CoverageReport, CoverageReportRequest, DecisionMeta, ExitCodeInput,
3931 RawTestResult, RuntimeSnapshot, TestProvenance, analyze_coverage_results,
3932 },
3933 };
3934
3935 use super::*;
3936
3937 fn scope_entry(
3938 file: &str,
3939 status: &str,
3940 reason: &str,
3941 package: Option<&str>,
3942 ) -> IndexedScopeEntry {
3943 IndexedScopeEntry {
3944 file: file.into(),
3945 status: status.into(),
3946 reason: reason.into(),
3947 package_root: package.map(str::to_owned),
3948 measurement_limitations: 0,
3949 limitation_kinds: Vec::new(),
3950 }
3951 }
3952
3953 #[test]
3954 fn a_scope_is_grouped_by_place_and_names_the_roots_that_settle_it() {
3955 let unclassified = "unclassified first-party source";
3956 let entries = [
3957 scope_entry("components/a.tsx", "ambiguous", unclassified, None),
3958 scope_entry("components/ui/b.tsx", "ambiguous", unclassified, None),
3959 scope_entry("proxy.ts", "ambiguous", unclassified, None),
3960 scope_entry(
3961 "app/page.tsx",
3962 "included",
3963 "discovered package source root",
3964 None,
3965 ),
3966 scope_entry(
3967 "tests/a.test.ts",
3968 "excluded",
3969 "test or fixture source",
3970 None,
3971 ),
3972 scope_entry(
3973 "next.config.ts",
3974 "excluded",
3975 "build/test/tool configuration",
3976 None,
3977 ),
3978 scope_entry(
3979 "packages/ui/extra/c.ts",
3980 "ambiguous",
3981 unclassified,
3982 Some("packages/ui"),
3983 ),
3984 scope_entry(
3985 "packages/ui/src/d.ts",
3986 "included",
3987 "discovered package source root",
3988 Some("packages/ui"),
3989 ),
3990 ];
3991 let groups = scope_groups(&entries)
3992 .into_iter()
3993 .map(|group| (group.files, group.status, group.directory))
3994 .collect::<Vec<_>>();
3995 assert_eq!(
3996 groups,
3997 [
3998 (2, "ambiguous".to_owned(), "components".to_owned()),
3999 (1, "ambiguous".into(), ".".into()),
4000 (1, "ambiguous".into(), "packages/ui/extra".into()),
4001 (1, "included".into(), "app".into()),
4002 (1, "included".into(), "packages/ui/src".into()),
4003 (1, "excluded".into(), ".".into()),
4004 (1, "excluded".into(), "tests".into()),
4005 ]
4006 );
4007 let hint =
4010 unclassified_source(&["app".into(), "packages/ui/src".into()], &entries).unwrap();
4011 assert_eq!(
4012 hint.roots,
4013 [
4014 "app",
4015 "packages/ui/src",
4016 "components",
4017 "packages/ui/extra",
4018 "proxy.ts"
4019 ]
4020 );
4021 assert_eq!(hint.directories[0].directory, "components");
4022 assert_eq!(hint.directories[0].files, 2);
4023 assert_eq!(unclassified_source(&[], &entries[3..6]), None);
4024 }
4025
4026 #[test]
4027 fn a_file_is_counted_under_its_first_folders() {
4028 assert_eq!(area_directory("app/api/cron/route.ts", 1), "app");
4029 assert_eq!(area_directory("app/api/cron/route.ts", 2), "app/api");
4030 assert_eq!(area_directory("app/page.tsx", 2), "app");
4031 assert_eq!(area_directory("proxy.ts", 2), ".");
4032 }
4033
4034 #[test]
4035 fn all_metric_keeps_statement_only_files_in_the_gap_set() {
4036 let gap = IndexedFileGap {
4037 view: CoverageViewId::All,
4038 file: "src/statement.js".into(),
4039 uncovered_lines: 0,
4040 uncovered_statements: 1,
4041 uncovered_functions: 0,
4042 missing_branches: 0,
4043 missing_mcdc_conditions: 0,
4044 measurement_limitations: 0,
4045 limitation_kinds: Vec::new(),
4046 covered_by_other_tests: crate::coverage_index::IndexedGapDimensions {
4047 lines: 0,
4048 statements: 0,
4049 functions: 0,
4050 branches: 0,
4051 mcdc_conditions: 0,
4052 },
4053 uncovered_everywhere: crate::coverage_index::IndexedGapDimensions {
4054 lines: 0,
4055 statements: 1,
4056 functions: 0,
4057 branches: 0,
4058 mcdc_conditions: 0,
4059 },
4060 totals: crate::coverage_index::IndexedGapDimensions {
4061 lines: 1,
4062 statements: 2,
4063 functions: 0,
4064 branches: 0,
4065 mcdc_conditions: 0,
4066 },
4067 covered: crate::coverage_index::IndexedGapDimensions {
4068 lines: 1,
4069 statements: 1,
4070 functions: 0,
4071 branches: 0,
4072 mcdc_conditions: 0,
4073 },
4074 score: 0,
4075 };
4076 assert!(has_gap_for_metric(&gap, MinimizeMetric::All));
4077 assert!(has_gap_for_metric(&gap, MinimizeMetric::Statements));
4078 assert!(!has_gap_for_metric(&gap, MinimizeMetric::Lines));
4079 }
4080
4081 fn result(id: &str, vector: McdcVector) -> RawTestResult {
4082 RawTestResult {
4083 test_id: Some(id.into()),
4084 scope: None,
4085 test: id.into(),
4086 test_file: Some("tests/permission.test.js".into()),
4087 title: None,
4088 retry: Some(0),
4089 status: Some("passed".into()),
4090 expected_status: None,
4091 flaky: false,
4092 provenance: TestProvenance {
4093 runner: "node:test".into(),
4094 kind: "unit".into(),
4095 project: None,
4096 source: "runner-default".into(),
4097 },
4098 role: "test".into(),
4099 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
4100 phases: Vec::new(),
4101 runtime: vec![RuntimeSnapshot {
4102 decisions: vec![crate::coverage_report::DecisionSnapshot {
4103 meta: decision(),
4104 vectors: vec![vector],
4105 }],
4106 hits: Vec::new(),
4107 events: Vec::new(),
4108 logicals: Vec::new(),
4109 phase_id: None,
4110 }],
4111 browser: Vec::new(),
4112 server: Vec::new(),
4113 }
4114 }
4115
4116 fn decision() -> DecisionMeta {
4117 DecisionMeta {
4118 id: "decision".into(),
4119 file: "src/permission.js".into(),
4120 line: 1,
4121 column: 1,
4122 source: "admin || owner".into(),
4123 conditions: vec!["admin".into(), "owner".into()],
4124 kind: "if".into(),
4125 }
4126 }
4127
4128 fn report(mut results: Vec<RawTestResult>) -> CoverageReport {
4129 analyze_coverage_results(&CoverageReportRequest {
4130 run_id: "run".into(),
4131 manifest: CoverageManifest {
4132 unmeasured: Vec::new(),
4133 decisions: vec![decision()],
4134 points: Vec::new(),
4135 branches: Vec::new(),
4136 limitations: Vec::new(),
4137 scope: None,
4138 assertion_sites: Vec::new(),
4139 },
4140 raw_results: std::mem::take(&mut results),
4141 generated_at: "time".into(),
4142 coverage_model: None,
4143 integrity: None,
4144 test_exit_code: ExitCodeInput::Present(Some(0)),
4145 })
4146 .unwrap()
4147 }
4148
4149 #[test]
4150 fn recomputes_mcdc_witnesses_and_removes_a_redundant_vector() {
4151 let report = report(vec![
4152 result(
4153 "admin",
4154 McdcVector {
4155 values: vec![Some(true), None],
4156 outcome: true,
4157 },
4158 ),
4159 result(
4160 "owner",
4161 McdcVector {
4162 values: vec![Some(false), Some(true)],
4163 outcome: true,
4164 },
4165 ),
4166 result(
4167 "both",
4168 McdcVector {
4169 values: vec![Some(true), None],
4170 outcome: true,
4171 },
4172 ),
4173 result(
4174 "neither",
4175 McdcVector {
4176 values: vec![Some(false), Some(false)],
4177 outcome: false,
4178 },
4179 ),
4180 ]);
4181 let minimized = minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 5_000).unwrap();
4182 assert_eq!(minimized.selected.len(), 3);
4183 assert!(minimized.selected.contains(&"owner".into()));
4184 assert!(minimized.selected.contains(&"neither".into()));
4185 assert_eq!(minimized.summary.condition_coverage_pct, 100.0);
4186 }
4187
4188 #[test]
4189 fn a_test_that_belongs_to_no_test_is_not_minimized_away() {
4190 let mut parallel = result(
4197 "parallel",
4198 McdcVector {
4199 values: vec![Some(false), Some(false)],
4200 outcome: false,
4201 },
4202 );
4203 parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
4204 assert!(matches!(
4205 minimum_test_set(
4206 &report(vec![parallel]).view,
4207 100.0,
4208 MinimizeMetric::Mcdc,
4209 5_000,
4210 ),
4211 Err(QueryError::UnattributedEvidence)
4212 ));
4213 }
4214
4215 #[test]
4216 fn the_summary_counts_each_test_under_the_attribution_it_carries() {
4217 let vector = || McdcVector {
4221 values: vec![Some(false), Some(false)],
4222 outcome: false,
4223 };
4224 let mut serial = result("serial", vector());
4225 serial.attribution = crate::coverage_report::ATTRIBUTION_EXACT.into();
4226 let mut parallel = result("parallel", vector());
4227 parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
4228 let mut ruby = result("ruby", vector());
4229 ruby.attribution = crate::coverage_report::ATTRIBUTION_PARTIAL.into();
4230 let mut also_parallel = result("also-parallel", vector());
4231 also_parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
4232 let report = report(vec![serial, parallel, ruby, also_parallel]);
4233
4234 let nonce = std::time::SystemTime::now()
4235 .duration_since(std::time::UNIX_EPOCH)
4236 .unwrap()
4237 .as_nanos();
4238 let root = std::env::temp_dir().join(format!(
4239 "supercov-attribution-counts-{}-{nonce}",
4240 std::process::id()
4241 ));
4242 std::fs::create_dir(&root).unwrap();
4243 let path = root.join("query-index.v1.bin");
4244 let identity = crate::query_index::QueryIndexIdentity {
4245 evidence_sha256: [1; 32],
4246 evidence_bytes: 100,
4247 analysis_sha256: [2; 32],
4248 producer_sha256: [3; 32],
4249 archive_schema_version: 2,
4250 };
4251 crate::query_index::write_query_index(
4252 &crate::coverage_index::coverage_index_sections(&report).unwrap(),
4253 &identity,
4254 &path,
4255 )
4256 .unwrap();
4257 let container = crate::query_index::QueryIndex::open(&path, &identity).unwrap();
4258 let index = crate::coverage_index::CoverageIndex::new(&container).unwrap();
4259
4260 let summary = coverage_summary_query(
4261 &index,
4262 CoverageSummaryQueryOptions {
4263 run: "run",
4264 view: CoverageViewId::All,
4265 kind: None,
4266 runner: None,
4267 valid: true,
4268 test_exit_code: Some(0),
4269 stale: false,
4270 stale_reasons: Vec::new(),
4271 },
4272 )
4273 .unwrap();
4274 assert_eq!(summary.test_attribution.exact, 1);
4275 assert_eq!(summary.test_attribution.run_wide, 2);
4276 assert_eq!(summary.test_attribution.partial, 1);
4277 assert_eq!(summary.test_attribution.total(), 4);
4278 std::fs::remove_dir_all(&root).ok();
4279 }
4280
4281 #[test]
4282 fn refuses_background_evidence() {
4283 let mut aggregate = result(
4284 "aggregate",
4285 McdcVector {
4286 values: vec![Some(false), Some(false)],
4287 outcome: false,
4288 },
4289 );
4290 aggregate.role = "background".into();
4291 assert!(matches!(
4292 minimum_test_set(
4293 &report(vec![aggregate]).view,
4294 100.0,
4295 MinimizeMetric::Mcdc,
4296 5_000,
4297 ),
4298 Err(QueryError::UnattributedEvidence)
4299 ));
4300 }
4301
4302 #[test]
4303 fn bounds_the_exact_search() {
4304 let report = report(vec![
4305 result(
4306 "admin",
4307 McdcVector {
4308 values: vec![Some(true), None],
4309 outcome: true,
4310 },
4311 ),
4312 result(
4313 "owner",
4314 McdcVector {
4315 values: vec![Some(false), Some(true)],
4316 outcome: true,
4317 },
4318 ),
4319 result(
4320 "neither",
4321 McdcVector {
4322 values: vec![Some(false), Some(false)],
4323 outcome: false,
4324 },
4325 ),
4326 ]);
4327 assert!(matches!(
4328 minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 1),
4329 Err(QueryError::ComplexityLimit { .. })
4330 ));
4331 }
4332}