1use std::collections::{BTreeMap, BTreeSet, HashMap};
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 agent_json::pagination,
13 coverage_analysis::{CoverageSummary, is_independence_pair},
14 coverage_index::{
15 CoverageDimension, CoverageIndex, CoverageIndexError, CoverageViewId, IndexedDecisionGap,
16 IndexedDimensionCoverage, IndexedFileGap, IndexedHitMetadata, IndexedMeasurement,
17 IndexedOutcomeCounts, IndexedScopeEntry, IndexedSourceScope, IndexedSummaryConfidence,
18 IndexedTestSummary,
19 },
20 coverage_report::{
21 CoverageConfidence, CoverageReportRequest, CoverageView, DecisionMeta, ReportError,
22 SourceLine, TestAttempt, TestProvenance, TransportStats, analyze_coverage_results,
23 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, Deserialize)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54pub struct MinimumTestSetRequest {
55 pub coverage: CoverageReportRequest,
56 #[serde(default = "default_target")]
57 pub target: f64,
58 #[serde(default = "default_metric")]
59 pub metric: MinimizeMetric,
60 #[serde(default = "default_max_states")]
61 pub max_states: usize,
62}
63
64fn default_target() -> f64 {
65 100.0
66}
67
68fn default_metric() -> MinimizeMetric {
69 MinimizeMetric::All
70}
71
72fn default_max_states() -> usize {
73 5_000
74}
75
76pub fn minimum_test_set_for_request(
77 request: &MinimumTestSetRequest,
78) -> Result<MinimumTestSetResult, QueryError> {
79 let report = analyze_coverage_results(&request.coverage)?;
80 minimum_test_set(
81 &report.view,
82 request.target,
83 request.metric,
84 request.max_states,
85 )
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
89pub struct CoverageMinimizedTest {
90 pub id: String,
91 pub name: String,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub file: Option<String>,
94 pub runner: String,
95 pub kind: String,
96}
97
98#[derive(Debug, Clone, PartialEq, Serialize)]
99#[serde(rename_all = "camelCase")]
100pub struct CoverageMinimizeData {
101 pub run: String,
102 pub filters: CoverageQueryFilters,
103 pub optimal: bool,
104 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
105 pub target: f64,
106 pub metric: MinimizeMetric,
107 pub selected: Vec<String>,
108 pub expanded: Vec<String>,
109 pub summary: CoverageSummary,
110 pub explored_states: usize,
111 pub selected_count: usize,
112 pub total_candidate_tests: usize,
113 pub tests: Vec<CoverageMinimizedTest>,
114}
115
116#[derive(Debug, Clone, Copy)]
117pub struct CoverageMinimizeQueryOptions<'a> {
118 pub run: &'a str,
119 pub view_id: CoverageViewId,
120 pub kind: Option<&'a str>,
121 pub runner: Option<&'a str>,
122 pub target: f64,
123 pub metric: MinimizeMetric,
124 pub max_states: usize,
125 pub offset: usize,
126 pub limit: usize,
127}
128
129pub fn coverage_minimize_query(
130 view: &CoverageView,
131 options: CoverageMinimizeQueryOptions<'_>,
132) -> Result<(CoverageMinimizeData, AgentPagination), QueryError> {
133 if options.limit == 0 {
134 return Err(QueryError::InvalidPagination);
135 }
136 let selected_ids = if options.kind.is_none() && options.runner.is_none() {
137 None
138 } else {
139 let ids = view
140 .tests
141 .iter()
142 .filter(|test| {
143 options.kind.is_none_or(|kind| test.provenance.kind == kind)
144 && options
145 .runner
146 .is_none_or(|runner| test.provenance.runner == runner)
147 })
148 .map(|test| test.id.clone())
149 .collect::<BTreeSet<_>>();
150 if ids.is_empty() {
151 return Err(QueryError::TestFilterEmpty {
152 kind: options.kind.map(str::to_owned),
153 runner: options.runner.map(str::to_owned),
154 });
155 }
156 Some(ids)
157 };
158 let mut solver_view = view.clone();
159 if let Some(selected) = &selected_ids {
160 solver_view.tests.retain(|test| selected.contains(&test.id));
161 }
162 let minimized = minimum_test_set(
163 &solver_view,
164 options.target,
165 options.metric,
166 options.max_states,
167 )?;
168 let selected_details = minimized
169 .selected
170 .iter()
171 .map(|id| {
172 let test = view
173 .tests
174 .iter()
175 .find(|test| test.id == *id)
176 .ok_or(QueryError::InvalidRecordSelection)?;
177 Ok(CoverageMinimizedTest {
178 id: id.clone(),
179 name: test.name.clone(),
180 file: test.file.clone(),
181 runner: test.provenance.runner.clone(),
182 kind: test.provenance.kind.clone(),
183 })
184 })
185 .collect::<Result<Vec<_>, QueryError>>()?;
186 let total = selected_details.len();
187 let tests = selected_details
188 .iter()
189 .skip(options.offset)
190 .take(options.limit)
191 .cloned()
192 .collect::<Vec<_>>();
193 let returned = tests.len();
194 let total_candidate_tests = solver_view
195 .tests
196 .iter()
197 .filter(|test| test.role == "test")
198 .count();
199 Ok((
200 CoverageMinimizeData {
201 run: options.run.into(),
202 filters: query_filters(options.view_id, options.kind, options.runner),
203 optimal: minimized.optimal,
204 target: minimized.target,
205 metric: minimized.metric,
206 selected: minimized.selected,
207 expanded: minimized.expanded,
208 summary: minimized.summary,
209 explored_states: minimized.explored_states,
210 selected_count: total,
211 total_candidate_tests,
212 tests,
213 },
214 pagination(options.offset, options.limit, returned, total),
215 ))
216}
217
218#[derive(Debug)]
219pub enum QueryError {
220 InvalidTarget(f64),
221 UnattributedEvidence,
222 TargetUnreachable {
223 metric: MinimizeMetric,
224 target: f64,
225 reachable: f64,
226 },
227 ComplexityLimit {
228 candidate_tests: usize,
229 obligations: usize,
230 explored_states: usize,
231 max_states: usize,
232 target: f64,
233 metric: MinimizeMetric,
234 },
235 Analysis(ReportError),
236 Index(CoverageIndexError),
237 InvalidPagination,
238 TestFilterEmpty {
239 kind: Option<String>,
240 runner: Option<String>,
241 },
242 TestNotFound(String),
243 DecisionNotFound(String),
244 SourceNotFound(String),
245 AmbiguousSelector {
246 selector: String,
247 matches: Vec<String>,
248 },
249 InvalidRecordSelection,
250 ScopeUnavailable,
251}
252
253impl From<ReportError> for QueryError {
254 fn from(value: ReportError) -> Self {
255 Self::Analysis(value)
256 }
257}
258
259impl From<CoverageIndexError> for QueryError {
260 fn from(value: CoverageIndexError) -> Self {
261 Self::Index(value)
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct CoverageQueryFilters {
268 pub outcome: String,
269 pub kind: Option<String>,
270 pub runner: Option<String>,
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize)]
274#[serde(rename_all = "camelCase")]
275pub struct CoverageFilesData {
276 pub run: String,
277 pub filters: CoverageQueryFilters,
278 pub metric: MinimizeMetric,
279 pub files: Vec<IndexedFileGap>,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct CoverageGapsData {
285 pub run: String,
286 pub filters: CoverageQueryFilters,
287 pub metric: MinimizeMetric,
288 pub gaps: Vec<IndexedFileGap>,
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize)]
292#[serde(rename_all = "camelCase")]
293pub struct CoverageKindsData {
294 pub run: String,
295 pub filters: CoverageQueryFilters,
296 pub kinds: Vec<IndexedDimensionCoverage>,
297}
298
299#[derive(Debug, Clone, PartialEq, Serialize)]
300#[serde(rename_all = "camelCase")]
301pub struct CoverageRunnersData {
302 pub run: String,
303 pub filters: CoverageQueryFilters,
304 pub runners: Vec<IndexedDimensionCoverage>,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
308#[serde(rename_all = "camelCase")]
309pub struct CoverageDiagnostic {
310 pub code: String,
311 pub severity: String,
312 pub message: String,
313}
314
315#[derive(Debug, Clone, PartialEq, Serialize)]
316#[serde(rename_all = "camelCase")]
317pub struct CoverageSummaryData {
318 pub run: String,
319 pub filters: CoverageQueryFilters,
320 pub generated_at: String,
321 pub valid: bool,
322 pub stale: bool,
323 pub stale_reasons: Vec<String>,
324 pub structurally_complete: bool,
325 pub complete: bool,
326 pub coverage: CoverageSummary,
327 pub measurement: IndexedMeasurement,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub waivers: Option<crate::coverage_waivers::CoverageWaiverSummary>,
330 pub coverage_by_kind: Vec<IndexedDimensionCoverage>,
331 pub coverage_by_runner: Vec<IndexedDimensionCoverage>,
332 pub attribution: crate::coverage_index::IndexedAttribution,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub transport: Option<TransportStats>,
335 pub diagnostics: Vec<CoverageDiagnostic>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub confidence: Option<IndexedSummaryConfidence>,
338 pub files_with_gaps: usize,
339 pub files_with_coverage_gaps: usize,
340 pub files_with_measurement_limitations: usize,
341 pub tests: usize,
342 pub setups: usize,
343 pub test_outcomes: IndexedOutcomeCounts,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub source_scope: Option<IndexedSourceScope>,
346}
347
348#[derive(Debug, Clone)]
349pub struct CoverageSummaryQueryOptions<'a> {
350 pub run: &'a str,
351 pub view: CoverageViewId,
352 pub kind: Option<&'a str>,
353 pub runner: Option<&'a str>,
354 pub valid: bool,
355 pub stale: bool,
356 pub stale_reasons: Vec<String>,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
360pub struct ScopeCounts {
361 pub included: usize,
362 pub excluded: usize,
363 pub ambiguous: usize,
364}
365
366#[derive(Debug, Clone, PartialEq, Serialize)]
367#[serde(rename_all = "camelCase")]
368pub struct CoverageScopeData {
369 pub run: String,
370 pub filters: CoverageQueryFilters,
371 pub mode: String,
372 pub roots: Vec<String>,
373 pub counts: ScopeCounts,
374 pub measurement: IndexedMeasurement,
375 pub entries: Vec<IndexedScopeEntry>,
376}
377
378#[derive(Debug, Clone, Copy)]
379pub struct CoverageScopeQueryOptions<'a> {
380 pub run: &'a str,
381 pub view: CoverageViewId,
382 pub kind: Option<&'a str>,
383 pub runner: Option<&'a str>,
384 pub offset: usize,
385 pub limit: usize,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
389pub struct CoverageLocation {
390 pub file: String,
391 pub line: usize,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
395pub struct CoverageCoveringTest {
396 pub id: String,
397 pub name: String,
398 pub provenance: TestProvenance,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
402#[serde(rename_all = "camelCase")]
403pub struct CoverageCoveringPhase {
404 pub id: String,
405 pub kind: String,
406 pub operation: String,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub source: Option<String>,
409 pub test: String,
410 #[serde(skip_serializing_if = "Option::is_none")]
411 pub status: Option<String>,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub caused_by_phase_id: Option<String>,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
417#[serde(rename_all = "camelCase")]
418pub struct CoverageAnchor {
419 pub kind: String,
420 pub id: String,
421 pub column: usize,
422 pub covered: bool,
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub covered_conditions: Option<usize>,
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub conditions: Option<usize>,
427}
428
429#[derive(Debug, Clone, PartialEq, Serialize)]
430#[serde(rename_all = "camelCase")]
431pub struct CoverageCoversLineData {
432 pub run: String,
433 pub filters: CoverageQueryFilters,
434 pub location: CoverageLocation,
435 pub covered: bool,
436 pub confidence: CoverageConfidence,
437 pub total_tests: usize,
438 pub total_phases: usize,
439 pub tests: Vec<CoverageCoveringTest>,
440 pub phases: Vec<CoverageCoveringPhase>,
441}
442
443#[derive(Debug, Clone, PartialEq, Serialize)]
444#[serde(rename_all = "camelCase")]
445pub struct CoverageCoversAnchorsData {
446 pub run: String,
447 pub filters: CoverageQueryFilters,
448 pub location: CoverageLocation,
449 pub line_obligation: bool,
450 pub anchored: Vec<CoverageAnchor>,
451 pub total_anchored: usize,
452}
453
454#[derive(Debug, Clone, PartialEq, Serialize)]
455#[serde(untagged)]
456pub enum CoverageCoversData {
457 Line(CoverageCoversLineData),
458 Anchors(CoverageCoversAnchorsData),
459}
460
461#[derive(Debug, Clone, Copy)]
462pub struct CoverageCoversQueryOptions<'a> {
463 pub run: &'a str,
464 pub view: CoverageViewId,
465 pub kind: Option<&'a str>,
466 pub runner: Option<&'a str>,
467 pub file: &'a str,
468 pub line: usize,
469 pub offset: usize,
470 pub limit: usize,
471}
472
473fn query_filters(
474 view: CoverageViewId,
475 kind: Option<&str>,
476 runner: Option<&str>,
477) -> CoverageQueryFilters {
478 CoverageQueryFilters {
479 outcome: match view {
480 CoverageViewId::All => "all",
481 CoverageViewId::Passed => "passed",
482 CoverageViewId::Failed => "failed",
483 }
484 .into(),
485 kind: kind.map(str::to_owned),
486 runner: runner.map(str::to_owned),
487 }
488}
489
490fn selected_test_ids(
491 tests: &[IndexedTestSummary],
492 kind: Option<&str>,
493 runner: Option<&str>,
494) -> Result<Option<BTreeSet<String>>, QueryError> {
495 if kind.is_none() && runner.is_none() {
496 return Ok(None);
497 }
498 let selected = tests
499 .iter()
500 .filter(|test| {
501 kind.is_none_or(|kind| test.provenance.kind == kind)
502 && runner.is_none_or(|runner| test.provenance.runner == runner)
503 })
504 .map(|test| test.id.clone())
505 .collect::<BTreeSet<_>>();
506 if selected.is_empty() {
507 return Err(QueryError::TestFilterEmpty {
508 kind: kind.map(str::to_owned),
509 runner: runner.map(str::to_owned),
510 });
511 }
512 Ok(Some(selected))
513}
514
515pub fn coverage_covers_query(
516 index: &CoverageIndex<'_>,
517 options: CoverageCoversQueryOptions<'_>,
518) -> Result<(CoverageCoversData, AgentPagination), QueryError> {
519 if options.limit == 0 {
520 return Err(QueryError::InvalidPagination);
521 }
522 let tests = index.test_summaries(options.view)?;
523 let selected = selected_test_ids(&tests, options.kind, options.runner)?;
524 let selected_includes = |id: &str| selected.as_ref().is_none_or(|ids| ids.contains(id));
525 let filters = query_filters(options.view, options.kind, options.runner);
526 let location = CoverageLocation {
527 file: options.file.into(),
528 line: options.line,
529 };
530 let Some(line) = index.line(options.view, options.file, options.line)? else {
531 let anchors = index.anchors(options.view, options.file, options.line)?;
532 let total = anchors.len();
533 let anchored = anchors
534 .into_iter()
535 .skip(options.offset)
536 .take(options.limit)
537 .map(|anchor| CoverageAnchor {
538 kind: anchor.kind.clone(),
539 id: anchor.id,
540 column: anchor.column,
541 covered: if matches!(anchor.kind.as_str(), "statement" | "function") {
542 anchor.tests.iter().any(|test| selected_includes(test))
543 } else {
544 anchor.covered
545 },
546 covered_conditions: anchor.covered_conditions,
547 conditions: anchor.conditions,
548 })
549 .collect::<Vec<_>>();
550 let returned = anchored.len();
551 return Ok((
552 CoverageCoversData::Anchors(CoverageCoversAnchorsData {
553 run: options.run.into(),
554 filters,
555 location,
556 line_obligation: false,
557 anchored,
558 total_anchored: total,
559 }),
560 pagination(options.offset, options.limit, returned, total),
561 ));
562 };
563 let tests_by_id = tests
564 .into_iter()
565 .map(|test| (test.id.clone(), test))
566 .collect::<HashMap<_, _>>();
567 let all_tests = line
568 .tests
569 .iter()
570 .filter(|id| selected_includes(id))
571 .map(|id| {
572 let test = tests_by_id.get(id);
573 CoverageCoveringTest {
574 id: id.clone(),
575 name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
576 provenance: test
577 .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
578 }
579 })
580 .collect::<Vec<_>>();
581 let phases_by_id = index
582 .phase_summaries(options.view)?
583 .into_iter()
584 .map(|phase| (phase.id.clone(), phase))
585 .collect::<HashMap<_, _>>();
586 let all_phases = line
587 .phases
588 .iter()
589 .filter_map(|id| phases_by_id.get(id))
590 .filter(|phase| selected_includes(&phase.test))
591 .map(|phase| CoverageCoveringPhase {
592 id: phase.id.clone(),
593 kind: phase.kind.clone(),
594 operation: phase.operation.clone(),
595 source: phase.source.clone(),
596 test: phase.test.clone(),
597 status: phase.status.clone(),
598 caused_by_phase_id: phase.caused_by_phase_id.clone(),
599 })
600 .collect::<Vec<_>>();
601 let tests_page = all_tests
602 .iter()
603 .skip(options.offset)
604 .take(options.limit)
605 .cloned()
606 .collect::<Vec<_>>();
607 let phases_page = all_phases
608 .iter()
609 .skip(options.offset)
610 .take(options.limit)
611 .cloned()
612 .collect::<Vec<_>>();
613 let total = all_tests.len().max(all_phases.len());
614 let returned = tests_page.len().max(phases_page.len());
615 Ok((
616 CoverageCoversData::Line(CoverageCoversLineData {
617 run: options.run.into(),
618 filters,
619 location,
620 covered: line.tests.iter().any(|test| selected_includes(test)),
621 confidence: line.confidence,
622 total_tests: all_tests.len(),
623 total_phases: all_phases.len(),
624 tests: tests_page,
625 phases: phases_page,
626 }),
627 pagination(options.offset, options.limit, returned, total),
628 ))
629}
630
631#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
632pub struct CoverageTestMatch {
633 pub id: String,
634 pub name: String,
635 pub outcome: String,
636 pub provenance: TestProvenance,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
640#[serde(rename_all = "camelCase")]
641pub struct CoverageHitDetail {
642 pub id: String,
643 pub obligation: String,
644 #[serde(skip_serializing_if = "Option::is_none")]
645 pub branch_kind: Option<String>,
646 #[serde(skip_serializing_if = "Option::is_none")]
647 pub file: Option<String>,
648 #[serde(skip_serializing_if = "Option::is_none")]
649 pub line: Option<usize>,
650 #[serde(skip_serializing_if = "Option::is_none")]
651 pub column: Option<usize>,
652 #[serde(skip_serializing_if = "Option::is_none")]
653 pub label: Option<String>,
654 #[serde(skip_serializing_if = "Option::is_none")]
655 pub alternative: Option<String>,
656}
657
658#[derive(Debug, Clone, PartialEq, Serialize)]
659pub struct CoverageTestDecision {
660 pub id: String,
661 pub vectors: Vec<crate::coverage_analysis::McdcVector>,
662 pub meta: DecisionMeta,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
666#[serde(rename_all = "camelCase")]
667pub struct CoverageTestPhase {
668 pub id: String,
669 pub kind: String,
670 pub operation: String,
671 #[serde(skip_serializing_if = "Option::is_none")]
672 pub source: Option<String>,
673 #[serde(skip_serializing_if = "Option::is_none")]
674 pub status: Option<String>,
675 #[serde(skip_serializing_if = "Option::is_none")]
676 pub caused_by_phase_id: Option<String>,
677 pub lines: usize,
678 pub decisions: usize,
679}
680
681#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
682pub struct CoverageTestTotals {
683 pub lines: usize,
684 pub hits: usize,
685 pub decisions: usize,
686 pub phases: usize,
687}
688
689#[derive(Debug, Clone, PartialEq, Serialize)]
690#[serde(rename_all = "camelCase")]
691pub struct CoverageSelectedTest {
692 pub id: String,
693 pub name: String,
694 #[serde(skip_serializing_if = "Option::is_none")]
695 pub file: Option<String>,
696 #[serde(skip_serializing_if = "Option::is_none")]
697 pub title: Option<String>,
698 pub retries: Vec<usize>,
699 pub attempts: Vec<TestAttempt>,
700 pub outcome: String,
701 pub provenance: TestProvenance,
702 pub role: String,
703 pub hits: Vec<String>,
704 pub decisions: Vec<CoverageTestDecision>,
705 pub lines: Vec<SourceLine>,
706 pub hit_details: Vec<CoverageHitDetail>,
707 pub phases: Vec<CoverageTestPhase>,
708 pub totals: CoverageTestTotals,
709}
710
711#[derive(Debug, Clone, PartialEq, Serialize)]
712pub struct CoverageTestMatchesData {
713 pub run: String,
714 pub filters: CoverageQueryFilters,
715 pub tests: Vec<CoverageTestMatch>,
716}
717
718#[derive(Debug, Clone, PartialEq, Serialize)]
719#[serde(rename_all = "camelCase")]
720pub struct CoverageTestDetailData {
721 pub run: String,
722 pub filters: CoverageQueryFilters,
723 pub pagination_applies_to: String,
724 pub tests: Vec<CoverageSelectedTest>,
725}
726
727#[derive(Debug, Clone, PartialEq, Serialize)]
728#[serde(untagged)]
729pub enum CoverageTestData {
730 Matches(CoverageTestMatchesData),
731 Detail(CoverageTestDetailData),
732}
733
734#[derive(Debug, Clone, Copy)]
735pub struct CoverageTestQueryOptions<'a> {
736 pub run: &'a str,
737 pub view: CoverageViewId,
738 pub kind: Option<&'a str>,
739 pub runner: Option<&'a str>,
740 pub selector: &'a str,
741 pub offset: usize,
742 pub limit: usize,
743}
744
745fn hit_detail(id: &str, metadata: Option<&IndexedHitMetadata>) -> CoverageHitDetail {
746 match metadata {
747 Some(metadata) => CoverageHitDetail {
748 id: id.into(),
749 obligation: metadata.obligation.clone(),
750 branch_kind: metadata.branch_kind.clone(),
751 file: Some(metadata.file.clone()),
752 line: Some(metadata.line),
753 column: Some(metadata.column),
754 label: metadata.label.clone(),
755 alternative: metadata.alternative.clone(),
756 },
757 None => CoverageHitDetail {
758 id: id.into(),
759 obligation: "unknown".into(),
760 branch_kind: None,
761 file: None,
762 line: None,
763 column: None,
764 label: None,
765 alternative: None,
766 },
767 }
768}
769
770pub fn coverage_test_query(
771 index: &CoverageIndex<'_>,
772 options: CoverageTestQueryOptions<'_>,
773) -> Result<(CoverageTestData, AgentPagination), QueryError> {
774 if options.limit == 0 {
775 return Err(QueryError::InvalidPagination);
776 }
777 let tests = index.test_details(options.view)?;
778 let summaries = tests
779 .iter()
780 .map(|test| test.summary.clone())
781 .collect::<Vec<_>>();
782 let selected = selected_test_ids(&summaries, options.kind, options.runner)?;
783 let selector = options.selector.to_lowercase();
784 let matches = tests
785 .into_iter()
786 .filter(|test| {
787 selected
788 .as_ref()
789 .is_none_or(|ids| ids.contains(&test.summary.id))
790 })
791 .filter(|test| {
792 test.summary.id == selector || test.summary.name.to_lowercase().contains(&selector)
793 })
794 .collect::<Vec<_>>();
795 if matches.is_empty() {
796 return Err(QueryError::TestNotFound(options.selector.into()));
797 }
798 let filters = query_filters(options.view, options.kind, options.runner);
799 if matches.len() > 1 {
800 let total = matches.len();
801 let page = matches
802 .into_iter()
803 .skip(options.offset)
804 .take(options.limit)
805 .map(|test| CoverageTestMatch {
806 id: test.summary.id,
807 name: test.summary.name,
808 outcome: test.summary.outcome,
809 provenance: test.summary.provenance,
810 })
811 .collect::<Vec<_>>();
812 let returned = page.len();
813 return Ok((
814 CoverageTestData::Matches(CoverageTestMatchesData {
815 run: options.run.into(),
816 filters,
817 tests: page,
818 }),
819 pagination(options.offset, options.limit, returned, total),
820 ));
821 }
822 let test = matches.into_iter().next().expect("one test match");
823 let metadata = index
824 .hit_metadata(options.view)?
825 .into_iter()
826 .map(|metadata| (metadata.id.clone(), metadata))
827 .collect::<HashMap<_, _>>();
828 let decisions = index
829 .decision_metadata(options.view)?
830 .into_iter()
831 .map(|decision| (decision.id.clone(), decision))
832 .collect::<HashMap<_, _>>();
833 let all_phases = index
834 .phase_summaries(options.view)?
835 .into_iter()
836 .filter(|phase| phase.test == test.summary.id)
837 .map(|phase| CoverageTestPhase {
838 id: phase.id,
839 kind: phase.kind,
840 operation: phase.operation,
841 source: phase.source,
842 status: phase.status,
843 caused_by_phase_id: phase.caused_by_phase_id,
844 lines: phase.lines,
845 decisions: phase.decisions,
846 })
847 .collect::<Vec<_>>();
848 let totals = CoverageTestTotals {
849 lines: test.lines.len(),
850 hits: test.hits.len(),
851 decisions: test.decisions.len(),
852 phases: all_phases.len(),
853 };
854 let total = totals
855 .lines
856 .max(totals.hits)
857 .max(totals.decisions)
858 .max(totals.phases);
859 let lines = test
860 .lines
861 .iter()
862 .skip(options.offset)
863 .take(options.limit)
864 .cloned()
865 .collect::<Vec<_>>();
866 let hits = test
867 .hits
868 .iter()
869 .skip(options.offset)
870 .take(options.limit)
871 .cloned()
872 .collect::<Vec<_>>();
873 let hit_details = test
874 .hits
875 .iter()
876 .skip(options.offset)
877 .take(options.limit)
878 .map(|id| hit_detail(id, metadata.get(id)))
879 .collect::<Vec<_>>();
880 let test_decisions = test
881 .decisions
882 .iter()
883 .skip(options.offset)
884 .take(options.limit)
885 .map(|decision| {
886 Ok(CoverageTestDecision {
887 id: decision.id.clone(),
888 vectors: decision.vectors.clone(),
889 meta: decisions
890 .get(&decision.id)
891 .cloned()
892 .ok_or(QueryError::InvalidRecordSelection)?,
893 })
894 })
895 .collect::<Result<Vec<_>, QueryError>>()?;
896 let phases = all_phases
897 .into_iter()
898 .skip(options.offset)
899 .take(options.limit)
900 .collect::<Vec<_>>();
901 let returned = lines
902 .len()
903 .max(hits.len())
904 .max(test_decisions.len())
905 .max(phases.len());
906 Ok((
907 CoverageTestData::Detail(CoverageTestDetailData {
908 run: options.run.into(),
909 filters,
910 pagination_applies_to:
911 "lines, hits/hitDetails, decisions, and phases independently within the test".into(),
912 tests: vec![CoverageSelectedTest {
913 id: test.summary.id,
914 name: test.summary.name,
915 file: test.summary.file,
916 title: test.summary.title,
917 retries: test.retries,
918 attempts: test.attempts,
919 outcome: test.summary.outcome,
920 provenance: test.summary.provenance,
921 role: test.summary.role,
922 hits,
923 decisions: test_decisions,
924 lines,
925 hit_details,
926 phases,
927 totals,
928 }],
929 }),
930 pagination(options.offset, options.limit, returned, total),
931 ))
932}
933
934#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
935pub struct CoverageDecisionMatch {
936 pub id: String,
937 pub file: String,
938 pub line: usize,
939 pub column: usize,
940 pub source: String,
941}
942
943#[derive(Debug, Clone, PartialEq, Serialize)]
944#[serde(rename_all = "camelCase")]
945pub struct CoverageDecisionCondition {
946 pub index: usize,
947 pub source: String,
948 pub covered: bool,
949 #[serde(skip_serializing_if = "Option::is_none")]
950 pub assertion_covered: Option<bool>,
951 #[serde(skip_serializing_if = "Option::is_none")]
952 pub witness: Option<[crate::coverage_analysis::McdcVector; 2]>,
953 #[serde(skip_serializing_if = "Option::is_none")]
954 pub witness_tests: Option<[Vec<String>; 2]>,
955 #[serde(skip_serializing_if = "Option::is_none")]
956 pub waived: Option<bool>,
957 #[serde(skip_serializing_if = "Option::is_none")]
958 pub waiver_reason: Option<String>,
959}
960
961#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
962#[serde(rename_all = "camelCase")]
963pub struct CoverageDecisionTotals {
964 pub conditions: usize,
965 pub vector_observations: usize,
966 pub tests: usize,
967}
968
969#[derive(Debug, Clone, PartialEq, Serialize)]
970#[serde(rename_all = "camelCase")]
971pub struct CoverageSelectedDecision {
972 pub meta: DecisionMeta,
973 pub executed: bool,
974 pub covered: bool,
975 pub vectors: Vec<crate::coverage_analysis::McdcVector>,
976 pub vector_observations: Vec<crate::coverage_report::VectorObservation>,
977 pub conditions: Vec<CoverageDecisionCondition>,
978 pub tests: Vec<String>,
979 pub confidence: CoverageConfidence,
980 pub totals: CoverageDecisionTotals,
981}
982
983#[derive(Debug, Clone, PartialEq, Serialize)]
984pub struct CoverageDecisionMatchesData {
985 pub run: String,
986 pub filters: CoverageQueryFilters,
987 pub decisions: Vec<CoverageDecisionMatch>,
988}
989
990#[derive(Debug, Clone, PartialEq, Serialize)]
991#[serde(rename_all = "camelCase")]
992pub struct CoverageDecisionDetailData {
993 pub run: String,
994 pub filters: CoverageQueryFilters,
995 pub pagination_applies_to: String,
996 pub decisions: Vec<CoverageSelectedDecision>,
997}
998
999#[derive(Debug, Clone, PartialEq, Serialize)]
1000#[serde(untagged)]
1001pub enum CoverageDecisionData {
1002 Matches(CoverageDecisionMatchesData),
1003 Detail(CoverageDecisionDetailData),
1004}
1005
1006#[derive(Debug, Clone, Copy)]
1007pub struct CoverageDecisionQueryOptions<'a> {
1008 pub run: &'a str,
1009 pub view: CoverageViewId,
1010 pub kind: Option<&'a str>,
1011 pub runner: Option<&'a str>,
1012 pub selector: &'a str,
1013 pub offset: usize,
1014 pub limit: usize,
1015}
1016
1017fn selector_location(selector: &str) -> Option<(&str, usize)> {
1018 let (prefix, last) = selector.rsplit_once(':')?;
1019 let last = last.parse::<usize>().ok()?;
1020 if let Some((file, possible_line)) = prefix.rsplit_once(':')
1021 && let Ok(line) = possible_line.parse::<usize>()
1022 {
1023 return Some((file, line));
1024 }
1025 Some((prefix, last))
1026}
1027
1028fn selected_decision(
1029 decision: crate::coverage_report::DecisionResult,
1030 selected: Option<&BTreeSet<String>>,
1031) -> crate::coverage_report::DecisionResult {
1032 let Some(selected) = selected else {
1033 return decision;
1034 };
1035 let vector_observations = decision
1036 .vector_observations
1037 .into_iter()
1038 .filter_map(|mut observation| {
1039 observation.tests.retain(|test| selected.contains(test));
1040 (!observation.tests.is_empty()).then_some(observation)
1041 })
1042 .collect::<Vec<_>>();
1043 let vectors = vector_observations
1044 .iter()
1045 .map(|observation| observation.vector.clone())
1046 .collect::<Vec<_>>();
1047 let conditions = decision
1048 .meta
1049 .conditions
1050 .iter()
1051 .enumerate()
1052 .map(|(index, source)| {
1053 let mut witness = None;
1054 let mut witness_tests = None;
1055 'pairs: for left in 0..vector_observations.len() {
1056 for right in (left + 1)..vector_observations.len() {
1057 let first = &vector_observations[left];
1058 let second = &vector_observations[right];
1059 if is_independence_pair(&first.vector, &second.vector, index) {
1060 witness = Some([first.vector.clone(), second.vector.clone()]);
1061 witness_tests = Some([first.tests.clone(), second.tests.clone()]);
1062 break 'pairs;
1063 }
1064 }
1065 }
1066 crate::coverage_report::ConditionResult {
1067 index,
1068 source: source.clone(),
1069 covered: witness.is_some(),
1070 assertion_covered: false,
1071 witness,
1072 witness_tests,
1073 }
1074 })
1075 .collect::<Vec<_>>();
1076 crate::coverage_report::DecisionResult {
1077 meta: decision.meta,
1078 executed: !vectors.is_empty(),
1079 covered: conditions.iter().all(|condition| condition.covered),
1080 vectors,
1081 vector_observations,
1082 conditions,
1083 tests: decision
1084 .tests
1085 .into_iter()
1086 .filter(|test| selected.contains(test))
1087 .collect(),
1088 confidence: decision.confidence,
1089 }
1090}
1091
1092pub fn filtered_decisions(
1096 index: &CoverageIndex<'_>,
1097 view: CoverageViewId,
1098 kind: Option<&str>,
1099 runner: Option<&str>,
1100) -> Result<Vec<crate::coverage_report::DecisionResult>, QueryError> {
1101 let tests = index.test_summaries(view)?;
1102 let selected = selected_test_ids(&tests, kind, runner)?;
1103 index
1104 .decision_details(view)?
1105 .into_iter()
1106 .map(|decision| Ok(selected_decision(decision, selected.as_ref())))
1107 .collect()
1108}
1109
1110pub fn coverage_decision_query(
1111 index: &CoverageIndex<'_>,
1112 options: CoverageDecisionQueryOptions<'_>,
1113) -> Result<(CoverageDecisionData, AgentPagination), QueryError> {
1114 if options.limit == 0 {
1115 return Err(QueryError::InvalidPagination);
1116 }
1117 let tests = index.test_summaries(options.view)?;
1118 let selected = selected_test_ids(&tests, options.kind, options.runner)?;
1119 let decisions = index.decision_details(options.view)?;
1120 let mut matches = decisions
1121 .into_iter()
1122 .filter(|decision| decision.meta.id == options.selector)
1123 .collect::<Vec<_>>();
1124 if matches.is_empty()
1125 && let Some((file, line)) = selector_location(options.selector)
1126 {
1127 matches = index
1128 .decision_details(options.view)?
1129 .into_iter()
1130 .filter(|decision| decision.meta.file == file && decision.meta.line == line)
1131 .collect();
1132 }
1133 if matches.is_empty() {
1134 return Err(QueryError::DecisionNotFound(options.selector.into()));
1135 }
1136 let filters = query_filters(options.view, options.kind, options.runner);
1137 if matches.len() > 1 {
1138 let total = matches.len();
1139 let page = matches
1140 .into_iter()
1141 .skip(options.offset)
1142 .take(options.limit)
1143 .map(|decision| CoverageDecisionMatch {
1144 id: decision.meta.id,
1145 file: decision.meta.file,
1146 line: decision.meta.line,
1147 column: decision.meta.column,
1148 source: decision.meta.source,
1149 })
1150 .collect::<Vec<_>>();
1151 let returned = page.len();
1152 return Ok((
1153 CoverageDecisionData::Matches(CoverageDecisionMatchesData {
1154 run: options.run.into(),
1155 filters,
1156 decisions: page,
1157 }),
1158 pagination(options.offset, options.limit, returned, total),
1159 ));
1160 }
1161 let filtered = selected_decision(
1162 matches.into_iter().next().expect("one decision match"),
1163 selected.as_ref(),
1164 );
1165 let totals = CoverageDecisionTotals {
1166 conditions: filtered.conditions.len(),
1167 vector_observations: filtered.vector_observations.len(),
1168 tests: filtered.tests.len(),
1169 };
1170 let total = totals
1171 .conditions
1172 .max(totals.vector_observations)
1173 .max(totals.tests);
1174 let vector_observations = filtered
1175 .vector_observations
1176 .iter()
1177 .skip(options.offset)
1178 .take(options.limit)
1179 .cloned()
1180 .collect::<Vec<_>>();
1181 let vectors = vector_observations
1182 .iter()
1183 .map(|observation| observation.vector.clone())
1184 .collect::<Vec<_>>();
1185 let conditions = filtered
1186 .conditions
1187 .iter()
1188 .skip(options.offset)
1189 .take(options.limit)
1190 .map(|condition| CoverageDecisionCondition {
1191 index: condition.index,
1192 source: condition.source.clone(),
1193 covered: condition.covered,
1194 assertion_covered: selected.is_none().then_some(condition.assertion_covered),
1195 witness: condition.witness.clone(),
1196 witness_tests: condition.witness_tests.clone(),
1197 waived: None,
1198 waiver_reason: None,
1199 })
1200 .collect::<Vec<_>>();
1201 let tests = filtered
1202 .tests
1203 .iter()
1204 .skip(options.offset)
1205 .take(options.limit)
1206 .cloned()
1207 .collect::<Vec<_>>();
1208 let returned = vector_observations
1209 .len()
1210 .max(conditions.len())
1211 .max(tests.len());
1212 Ok((
1213 CoverageDecisionData::Detail(CoverageDecisionDetailData {
1214 run: options.run.into(),
1215 filters,
1216 pagination_applies_to:
1217 "conditions, vectorObservations, and tests independently within each decision"
1218 .into(),
1219 decisions: vec![CoverageSelectedDecision {
1220 meta: filtered.meta,
1221 executed: filtered.executed,
1222 covered: filtered.covered,
1223 vectors,
1224 vector_observations,
1225 conditions,
1226 tests,
1227 confidence: filtered.confidence,
1228 totals,
1229 }],
1230 }),
1231 pagination(options.offset, options.limit, returned, total),
1232 ))
1233}
1234
1235#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1236pub struct CoverageOtherTest {
1237 pub id: String,
1238 pub name: String,
1239}
1240
1241#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1242#[serde(rename_all = "camelCase")]
1243pub struct CoverageOtherCoverage {
1244 pub covered_elsewhere: bool,
1245 pub kinds: Vec<String>,
1246 pub runners: Vec<String>,
1247 pub tests: Vec<CoverageOtherTest>,
1248}
1249
1250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1251#[serde(rename_all = "camelCase")]
1252pub struct CoverageLineObligation {
1253 pub kind: String,
1254 pub line: usize,
1255 pub other_coverage: CoverageOtherCoverage,
1256}
1257
1258#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1259#[serde(rename_all = "camelCase")]
1260pub struct CoveragePointObligation {
1261 pub kind: String,
1262 pub line: usize,
1263 pub column: usize,
1264 pub source: String,
1265 pub other_coverage: CoverageOtherCoverage,
1266}
1267
1268#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1269#[serde(rename_all = "camelCase")]
1270pub struct CoverageBranchObligation {
1271 pub kind: String,
1272 pub line: usize,
1273 pub column: usize,
1274 pub source: String,
1275 pub missing: String,
1276 pub other_coverage: CoverageOtherCoverage,
1277}
1278
1279#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1280#[serde(rename_all = "camelCase")]
1281pub struct CoverageMcdcObligation {
1282 pub kind: String,
1283 pub id: String,
1284 pub line: usize,
1285 pub column: usize,
1286 pub decision: String,
1287 pub missing_condition: String,
1288 #[serde(skip)]
1289 pub condition_index: usize,
1290 #[serde(skip_serializing_if = "Option::is_none")]
1291 pub waived: Option<bool>,
1292 #[serde(skip_serializing_if = "Option::is_none")]
1293 pub waiver_reason: Option<String>,
1294 pub observed_vectors: Vec<String>,
1295 pub other_coverage: CoverageOtherCoverage,
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1299#[serde(untagged)]
1300pub enum CoverageFileObligation {
1301 Line(CoverageLineObligation),
1302 Point(CoveragePointObligation),
1303 Branch(CoverageBranchObligation),
1304 Mcdc(CoverageMcdcObligation),
1305}
1306
1307impl CoverageFileObligation {
1308 fn line(&self) -> usize {
1309 match self {
1310 Self::Line(value) => value.line,
1311 Self::Point(value) => value.line,
1312 Self::Branch(value) => value.line,
1313 Self::Mcdc(value) => value.line,
1314 }
1315 }
1316
1317 fn kind(&self) -> &str {
1318 match self {
1319 Self::Line(value) => &value.kind,
1320 Self::Point(value) => &value.kind,
1321 Self::Branch(value) => &value.kind,
1322 Self::Mcdc(value) => &value.kind,
1323 }
1324 }
1325}
1326
1327#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1328pub struct CoverageFileTest {
1329 pub id: String,
1330 pub name: String,
1331 pub provenance: TestProvenance,
1332}
1333
1334#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1335pub struct CoverageFileLimitation {
1336 pub id: String,
1337 pub kind: String,
1338 pub file: String,
1339 pub line: usize,
1340 pub column: usize,
1341 pub source: String,
1342 pub reason: String,
1343 pub blocking: bool,
1344 pub effect: String,
1345}
1346
1347#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1348#[serde(rename_all = "camelCase")]
1349pub struct CoverageFileCounts {
1350 pub uncovered_lines: usize,
1351 pub uncovered_statements: usize,
1352 pub uncovered_functions: usize,
1353 pub missing_branches: usize,
1354 pub missing_mcdc_conditions: usize,
1355 pub waived_mcdc_conditions: usize,
1356 pub measurement_limitations: usize,
1357}
1358
1359#[derive(Debug, Clone, PartialEq, Serialize)]
1360#[serde(rename_all = "camelCase")]
1361pub struct CoverageFileDetailData {
1362 pub run: String,
1363 pub filters: CoverageQueryFilters,
1364 pub file: String,
1365 pub metric: MinimizeMetric,
1366 pub counts: CoverageFileCounts,
1367 pub tests: Vec<CoverageFileTest>,
1368 pub total_tests: usize,
1369 pub total_obligations: usize,
1370 pub obligations: Vec<CoverageFileObligation>,
1371 pub total_limitations: usize,
1372 pub limitations: Vec<CoverageFileLimitation>,
1373}
1374
1375#[derive(Debug, Clone, Copy)]
1376pub struct CoverageFileDetailOptions<'a> {
1377 pub run: &'a str,
1378 pub view: CoverageViewId,
1379 pub kind: Option<&'a str>,
1380 pub runner: Option<&'a str>,
1381 pub selector: &'a str,
1382 pub metric: MinimizeMetric,
1383 pub offset: usize,
1384 pub limit: usize,
1385}
1386
1387fn other_coverage(
1388 test_ids: &[String],
1389 selected: Option<&BTreeSet<String>>,
1390 tests: &HashMap<String, IndexedTestSummary>,
1391) -> CoverageOtherCoverage {
1392 let covered = selected.map_or_else(Vec::new, |selected| {
1393 test_ids
1394 .iter()
1395 .filter(|id| !selected.contains(*id))
1396 .filter_map(|id| tests.get(id))
1397 .collect::<Vec<_>>()
1398 });
1399 CoverageOtherCoverage {
1400 covered_elsewhere: !covered.is_empty(),
1401 kinds: covered
1402 .iter()
1403 .map(|test| test.provenance.kind.clone())
1404 .collect::<BTreeSet<_>>()
1405 .into_iter()
1406 .collect(),
1407 runners: covered
1408 .iter()
1409 .map(|test| test.provenance.runner.clone())
1410 .collect::<BTreeSet<_>>()
1411 .into_iter()
1412 .collect(),
1413 tests: covered
1414 .into_iter()
1415 .map(|test| CoverageOtherTest {
1416 id: test.id.clone(),
1417 name: test.name.clone(),
1418 })
1419 .collect(),
1420 }
1421}
1422
1423fn vector_text(vector: &crate::coverage_analysis::McdcVector) -> String {
1424 let values = vector
1425 .values
1426 .iter()
1427 .map(|value| match value {
1428 None => '-',
1429 Some(false) => 'F',
1430 Some(true) => 'T',
1431 })
1432 .collect::<String>();
1433 format!("{values} -> {}", if vector.outcome { 'T' } else { 'F' })
1434}
1435
1436fn obligation_matches_metric(obligation: &CoverageFileObligation, metric: MinimizeMetric) -> bool {
1437 metric == MinimizeMetric::All
1438 || matches!(
1439 (obligation.kind(), metric),
1440 ("line", MinimizeMetric::Lines)
1441 | ("statement", MinimizeMetric::Statements)
1442 | ("function", MinimizeMetric::Functions)
1443 | ("branch", MinimizeMetric::Branches)
1444 | ("mcdc", MinimizeMetric::Mcdc)
1445 )
1446}
1447
1448pub fn coverage_file_detail_query(
1449 index: &CoverageIndex<'_>,
1450 options: CoverageFileDetailOptions<'_>,
1451) -> Result<(CoverageFileDetailData, AgentPagination), QueryError> {
1452 if options.limit == 0 {
1453 return Err(QueryError::InvalidPagination);
1454 }
1455 let test_details = index.test_details(options.view)?;
1456 let test_summaries = test_details
1457 .iter()
1458 .map(|test| test.summary.clone())
1459 .collect::<Vec<_>>();
1460 let selected = selected_test_ids(&test_summaries, options.kind, options.runner)?;
1461 let tests_by_id = test_summaries
1462 .into_iter()
1463 .map(|test| (test.id.clone(), test))
1464 .collect::<HashMap<_, _>>();
1465 let lines = index.lines(options.view)?;
1466 let limitation_records = index.limitations(options.view)?;
1467 let files = lines
1468 .iter()
1469 .map(|line| line.file.as_str())
1470 .chain(
1471 limitation_records
1472 .iter()
1473 .map(|limitation| limitation.file.as_str()),
1474 )
1475 .collect::<BTreeSet<_>>();
1476 let file = if files.contains(options.selector) {
1477 options.selector.to_owned()
1478 } else {
1479 let matches = files
1480 .into_iter()
1481 .filter(|file| file.contains(options.selector))
1482 .collect::<Vec<_>>();
1483 if matches.is_empty() {
1484 return Err(QueryError::SourceNotFound(options.selector.into()));
1485 }
1486 if matches.len() != 1 {
1487 return Err(QueryError::AmbiguousSelector {
1488 selector: options.selector.into(),
1489 matches: matches.into_iter().map(str::to_owned).collect(),
1490 });
1491 }
1492 matches[0].to_owned()
1493 };
1494 let selected_includes = |tests: &[String]| {
1495 selected.as_ref().map_or(!tests.is_empty(), |selected| {
1496 tests.iter().any(|test| selected.contains(test))
1497 })
1498 };
1499 let uncovered_lines = lines
1500 .iter()
1501 .filter(|line| line.file == file && !selected_includes(&line.tests))
1502 .map(|line| {
1503 CoverageFileObligation::Line(CoverageLineObligation {
1504 kind: "line".into(),
1505 line: line.line,
1506 other_coverage: other_coverage(&line.tests, selected.as_ref(), &tests_by_id),
1507 })
1508 })
1509 .collect::<Vec<_>>();
1510 let metadata = index.hit_metadata(options.view)?;
1511 let statements = metadata
1512 .iter()
1513 .filter(|point| {
1514 point.file == file
1515 && point.obligation == "statement"
1516 && !selected_includes(&point.tests)
1517 })
1518 .map(|point| {
1519 CoverageFileObligation::Point(CoveragePointObligation {
1520 kind: "statement".into(),
1521 line: point.line,
1522 column: point.column,
1523 source: point.label.clone().unwrap_or_else(|| point.source.clone()),
1524 other_coverage: other_coverage(&point.tests, selected.as_ref(), &tests_by_id),
1525 })
1526 })
1527 .collect::<Vec<_>>();
1528 let functions = metadata
1529 .iter()
1530 .filter(|point| {
1531 point.file == file && point.obligation == "function" && !selected_includes(&point.tests)
1532 })
1533 .map(|point| {
1534 CoverageFileObligation::Point(CoveragePointObligation {
1535 kind: "function".into(),
1536 line: point.line,
1537 column: point.column,
1538 source: point.label.clone().unwrap_or_else(|| point.source.clone()),
1539 other_coverage: other_coverage(&point.tests, selected.as_ref(), &tests_by_id),
1540 })
1541 })
1542 .collect::<Vec<_>>();
1543 let branches = metadata
1544 .iter()
1545 .filter(|branch| {
1546 branch.file == file
1547 && branch.obligation == "branch"
1548 && !selected_includes(&branch.tests)
1549 })
1550 .map(|branch| {
1551 CoverageFileObligation::Branch(CoverageBranchObligation {
1552 kind: "branch".into(),
1553 line: branch.line,
1554 column: branch.column,
1555 source: branch.source.clone(),
1556 missing: branch.alternative.clone().unwrap_or_default(),
1557 other_coverage: other_coverage(&branch.tests, selected.as_ref(), &tests_by_id),
1558 })
1559 })
1560 .collect::<Vec<_>>();
1561 let original_decisions = index.decision_details(options.view)?;
1562 let mut mcdc = Vec::new();
1563 for original in original_decisions
1564 .iter()
1565 .filter(|decision| decision.meta.file == file)
1566 {
1567 let filtered = selected_decision(original.clone(), selected.as_ref());
1568 for condition in filtered
1569 .conditions
1570 .iter()
1571 .filter(|condition| !condition.covered)
1572 {
1573 let original_tests = original.conditions[condition.index]
1574 .witness_tests
1575 .clone()
1576 .unwrap_or_default()
1577 .into_iter()
1578 .flatten()
1579 .collect::<Vec<_>>();
1580 mcdc.push(CoverageFileObligation::Mcdc(CoverageMcdcObligation {
1581 kind: "mcdc".into(),
1582 id: original.meta.id.clone(),
1583 line: original.meta.line,
1584 column: original.meta.column,
1585 decision: original.meta.source.clone(),
1586 missing_condition: condition.source.clone(),
1587 condition_index: condition.index,
1588 waived: None,
1589 waiver_reason: None,
1590 observed_vectors: filtered
1591 .vector_observations
1592 .iter()
1593 .map(|observation| vector_text(&observation.vector))
1594 .collect(),
1595 other_coverage: other_coverage(&original_tests, selected.as_ref(), &tests_by_id),
1596 }));
1597 }
1598 }
1599 let mut obligations = uncovered_lines
1600 .iter()
1601 .chain(statements.iter())
1602 .chain(functions.iter())
1603 .chain(branches.iter())
1604 .chain(mcdc.iter())
1605 .filter(|obligation| obligation_matches_metric(obligation, options.metric))
1606 .cloned()
1607 .collect::<Vec<_>>();
1608 obligations.sort_by(|left, right| {
1609 left.line()
1610 .cmp(&right.line())
1611 .then_with(|| left.kind().cmp(right.kind()))
1612 });
1613 let mut limitations = limitation_records
1614 .into_iter()
1615 .filter(|limitation| limitation.file == file)
1616 .map(|limitation| CoverageFileLimitation {
1617 id: limitation.id,
1618 kind: limitation.kind,
1619 file: limitation.file,
1620 line: limitation.line,
1621 column: limitation.column,
1622 source: limitation.source,
1623 reason: limitation.reason,
1624 blocking: true,
1625 effect: "outside-measured-denominator".into(),
1626 })
1627 .collect::<Vec<_>>();
1628 limitations.sort_by(|left, right| {
1629 left.line
1630 .cmp(&right.line)
1631 .then_with(|| left.column.cmp(&right.column))
1632 .then_with(|| left.id.cmp(&right.id))
1633 });
1634 let all_file_tests = test_details
1635 .iter()
1636 .filter(|test| {
1637 selected
1638 .as_ref()
1639 .is_none_or(|selected| selected.contains(&test.summary.id))
1640 && test.lines.iter().any(|line| line.file == file)
1641 })
1642 .map(|test| CoverageFileTest {
1643 id: test.summary.id.clone(),
1644 name: test.summary.name.clone(),
1645 provenance: test.summary.provenance.clone(),
1646 })
1647 .collect::<Vec<_>>();
1648 let selected_obligations = obligations
1649 .iter()
1650 .skip(options.offset)
1651 .take(options.limit)
1652 .cloned()
1653 .collect::<Vec<_>>();
1654 let selected_tests = all_file_tests
1655 .iter()
1656 .skip(options.offset)
1657 .take(options.limit)
1658 .cloned()
1659 .collect::<Vec<_>>();
1660 let selected_limitations = limitations
1661 .iter()
1662 .skip(options.offset)
1663 .take(options.limit)
1664 .cloned()
1665 .collect::<Vec<_>>();
1666 let total = obligations
1667 .len()
1668 .max(all_file_tests.len())
1669 .max(limitations.len());
1670 let returned = selected_obligations
1671 .len()
1672 .max(selected_tests.len())
1673 .max(selected_limitations.len());
1674 Ok((
1675 CoverageFileDetailData {
1676 run: options.run.into(),
1677 filters: query_filters(options.view, options.kind, options.runner),
1678 file,
1679 metric: options.metric,
1680 counts: CoverageFileCounts {
1681 uncovered_lines: uncovered_lines.len(),
1682 uncovered_statements: statements.len(),
1683 uncovered_functions: functions.len(),
1684 missing_branches: branches.len(),
1685 missing_mcdc_conditions: mcdc.len(),
1686 waived_mcdc_conditions: 0,
1687 measurement_limitations: limitations.len(),
1688 },
1689 tests: selected_tests,
1690 total_tests: all_file_tests.len(),
1691 total_obligations: obligations.len(),
1692 obligations: selected_obligations,
1693 total_limitations: limitations.len(),
1694 limitations: selected_limitations,
1695 },
1696 pagination(options.offset, options.limit, returned, total),
1697 ))
1698}
1699
1700#[derive(Debug, Clone, PartialEq, Serialize)]
1701pub struct CoverageDiffDelta {
1702 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
1703 pub lines: f64,
1704 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
1705 pub branches: f64,
1706 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
1707 pub mcdc: f64,
1708}
1709
1710#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1711#[serde(rename_all = "camelCase")]
1712pub struct CoverageDiffSide {
1713 pub line_count: usize,
1714 pub branch_count: usize,
1715 pub mcdc_count: usize,
1716 pub lines: Vec<String>,
1717 pub branches: Vec<String>,
1718 pub mcdc: Vec<String>,
1719}
1720
1721#[derive(Debug, Clone, PartialEq, Serialize)]
1722pub struct CoverageDiffData {
1723 pub filters: CoverageQueryFilters,
1724 pub older: String,
1725 pub newer: String,
1726 pub delta: CoverageDiffDelta,
1727 pub gained: CoverageDiffSide,
1728 pub lost: CoverageDiffSide,
1729}
1730
1731#[derive(Debug, Clone, Copy)]
1732pub struct CoverageDiffQueryOptions<'a> {
1733 pub older_run: &'a str,
1734 pub newer_run: &'a str,
1735 pub view: CoverageViewId,
1736 pub kind: Option<&'a str>,
1737 pub runner: Option<&'a str>,
1738 pub offset: usize,
1739 pub limit: usize,
1740}
1741
1742struct DiffSnapshot {
1743 summary: CoverageSummary,
1744 lines: BTreeSet<String>,
1745 branches: HashMap<String, String>,
1746 mcdc: HashMap<String, String>,
1747}
1748
1749fn diff_snapshot(
1750 index: &CoverageIndex<'_>,
1751 view: CoverageViewId,
1752) -> Result<DiffSnapshot, QueryError> {
1753 let summary = index.summary(view)?;
1754 let lines = index
1755 .lines(view)?
1756 .into_iter()
1757 .filter(|line| line.covered)
1758 .map(|line| format!("{}:{}", line.file, line.line))
1759 .collect();
1760 let branches = index
1761 .hit_metadata(view)?
1762 .into_iter()
1763 .filter(|metadata| metadata.obligation == "branch" && !metadata.tests.is_empty())
1764 .map(|metadata| {
1765 let parent = metadata
1766 .parent_id
1767 .ok_or(QueryError::InvalidRecordSelection)?;
1768 Ok((
1769 format!("{parent}:{}", metadata.id),
1770 format!(
1771 "{}:{} {}",
1772 metadata.file,
1773 metadata.line,
1774 metadata.alternative.unwrap_or_default()
1775 ),
1776 ))
1777 })
1778 .collect::<Result<HashMap<_, _>, QueryError>>()?;
1779 let mcdc = index
1780 .decision_details(view)?
1781 .into_iter()
1782 .flat_map(|decision| {
1783 decision
1784 .conditions
1785 .into_iter()
1786 .filter(|condition| condition.covered)
1787 .map(move |condition| {
1788 (
1789 format!("{}:c{}", decision.meta.id, condition.index),
1790 format!(
1791 "{}:{} C{} {}",
1792 decision.meta.file,
1793 decision.meta.line,
1794 condition.index + 1,
1795 condition.source
1796 ),
1797 )
1798 })
1799 })
1800 .collect();
1801 Ok(DiffSnapshot {
1802 summary,
1803 lines,
1804 branches,
1805 mcdc,
1806 })
1807}
1808
1809fn js_string_cmp(left: &str, right: &str) -> std::cmp::Ordering {
1810 left.encode_utf16().cmp(right.encode_utf16())
1811}
1812
1813fn rounded_delta(newer: f64, older: f64) -> f64 {
1814 ((newer - older) * 100.0).round() / 100.0
1815}
1816
1817pub fn coverage_diff_query(
1818 older: &CoverageIndex<'_>,
1819 newer: &CoverageIndex<'_>,
1820 options: CoverageDiffQueryOptions<'_>,
1821) -> Result<(CoverageDiffData, AgentPagination), QueryError> {
1822 if options.limit == 0 {
1823 return Err(QueryError::InvalidPagination);
1824 }
1825 let older = diff_snapshot(older, options.view)?;
1826 let newer = diff_snapshot(newer, options.view)?;
1827 let mut gained_lines = newer
1828 .lines
1829 .difference(&older.lines)
1830 .cloned()
1831 .collect::<Vec<_>>();
1832 let mut lost_lines = older
1833 .lines
1834 .difference(&newer.lines)
1835 .cloned()
1836 .collect::<Vec<_>>();
1837 let mut gained_branches = newer
1838 .branches
1839 .iter()
1840 .filter(|(id, _)| !older.branches.contains_key(*id))
1841 .map(|(_, label)| label.clone())
1842 .collect::<Vec<_>>();
1843 let mut lost_branches = older
1844 .branches
1845 .iter()
1846 .filter(|(id, _)| !newer.branches.contains_key(*id))
1847 .map(|(_, label)| label.clone())
1848 .collect::<Vec<_>>();
1849 let mut gained_mcdc = newer
1850 .mcdc
1851 .iter()
1852 .filter(|(id, _)| !older.mcdc.contains_key(*id))
1853 .map(|(_, label)| label.clone())
1854 .collect::<Vec<_>>();
1855 let mut lost_mcdc = older
1856 .mcdc
1857 .iter()
1858 .filter(|(id, _)| !newer.mcdc.contains_key(*id))
1859 .map(|(_, label)| label.clone())
1860 .collect::<Vec<_>>();
1861 for values in [
1862 &mut gained_lines,
1863 &mut lost_lines,
1864 &mut gained_branches,
1865 &mut lost_branches,
1866 &mut gained_mcdc,
1867 &mut lost_mcdc,
1868 ] {
1869 values.sort_by(|left, right| js_string_cmp(left, right));
1870 }
1871 let total = [
1872 gained_lines.len(),
1873 gained_branches.len(),
1874 gained_mcdc.len(),
1875 lost_lines.len(),
1876 lost_branches.len(),
1877 lost_mcdc.len(),
1878 ]
1879 .into_iter()
1880 .max()
1881 .unwrap_or(0);
1882 let page = |values: &[String]| {
1883 values
1884 .iter()
1885 .skip(options.offset)
1886 .take(options.limit)
1887 .cloned()
1888 .collect::<Vec<_>>()
1889 };
1890 let gained = CoverageDiffSide {
1891 line_count: gained_lines.len(),
1892 branch_count: gained_branches.len(),
1893 mcdc_count: gained_mcdc.len(),
1894 lines: page(&gained_lines),
1895 branches: page(&gained_branches),
1896 mcdc: page(&gained_mcdc),
1897 };
1898 let lost = CoverageDiffSide {
1899 line_count: lost_lines.len(),
1900 branch_count: lost_branches.len(),
1901 mcdc_count: lost_mcdc.len(),
1902 lines: page(&lost_lines),
1903 branches: page(&lost_branches),
1904 mcdc: page(&lost_mcdc),
1905 };
1906 let returned = [
1907 gained.lines.len(),
1908 gained.branches.len(),
1909 gained.mcdc.len(),
1910 lost.lines.len(),
1911 lost.branches.len(),
1912 lost.mcdc.len(),
1913 ]
1914 .into_iter()
1915 .max()
1916 .unwrap_or(0);
1917 Ok((
1918 CoverageDiffData {
1919 filters: query_filters(options.view, options.kind, options.runner),
1920 older: options.older_run.into(),
1921 newer: options.newer_run.into(),
1922 delta: CoverageDiffDelta {
1923 lines: rounded_delta(
1924 newer.summary.lines.percentage,
1925 older.summary.lines.percentage,
1926 ),
1927 branches: rounded_delta(
1928 newer.summary.branches.percentage,
1929 older.summary.branches.percentage,
1930 ),
1931 mcdc: rounded_delta(
1932 newer.summary.condition_coverage_pct,
1933 older.summary.condition_coverage_pct,
1934 ),
1935 },
1936 gained,
1937 lost,
1938 },
1939 pagination(options.offset, options.limit, returned, total),
1940 ))
1941}
1942
1943pub fn coverage_scope_query(
1944 index: &CoverageIndex<'_>,
1945 options: CoverageScopeQueryOptions<'_>,
1946) -> Result<(CoverageScopeData, AgentPagination), QueryError> {
1947 if options.limit == 0 {
1948 return Err(QueryError::InvalidPagination);
1949 }
1950 let projection = index.projection(options.view, options.kind, options.runner)?;
1951 let scope = projection
1952 .source_scope
1953 .ok_or(QueryError::ScopeUnavailable)?;
1954 let mut entries = index.scope_entries(options.view)?;
1955 entries.sort_by(|left, right| {
1956 let rank = |status: &str| match status {
1957 "ambiguous" => 0,
1958 "included" => 1,
1959 "excluded" => 2,
1960 _ => 3,
1961 };
1962 rank(&left.status)
1963 .cmp(&rank(&right.status))
1964 .then_with(|| left.file.cmp(&right.file))
1965 });
1966 let total = entries.len();
1967 let selected = entries
1968 .into_iter()
1969 .skip(options.offset)
1970 .take(options.limit)
1971 .collect::<Vec<_>>();
1972 let returned = selected.len();
1973 Ok((
1974 CoverageScopeData {
1975 run: options.run.into(),
1976 filters: CoverageQueryFilters {
1977 outcome: match options.view {
1978 CoverageViewId::All => "all",
1979 CoverageViewId::Passed => "passed",
1980 CoverageViewId::Failed => "failed",
1981 }
1982 .into(),
1983 kind: options.kind.map(str::to_owned),
1984 runner: options.runner.map(str::to_owned),
1985 },
1986 mode: scope.mode,
1987 roots: scope.roots,
1988 counts: ScopeCounts {
1989 included: scope.included,
1990 excluded: scope.excluded,
1991 ambiguous: scope.ambiguous,
1992 },
1993 measurement: projection.measurement,
1994 entries: selected,
1995 },
1996 pagination(options.offset, options.limit, returned, total),
1997 ))
1998}
1999
2000pub fn coverage_summary_query(
2001 index: &CoverageIndex<'_>,
2002 options: CoverageSummaryQueryOptions<'_>,
2003) -> Result<CoverageSummaryData, QueryError> {
2004 let projection = index.projection(options.view, options.kind, options.runner)?;
2005 let mut diagnostics = Vec::new();
2006 if projection.empty_evidence_tests > 0 {
2007 diagnostics.push(CoverageDiagnostic {
2008 code: "TEST_EVIDENCE_MISSING".into(),
2009 severity: "warning".into(),
2010 message: format!(
2011 "{} test(s) recorded assertion phases but attributed zero coverage evidence; this is valid for assertions over static or uninstrumented data, but may otherwise indicate missing probe transport. First: {}",
2012 projection.empty_evidence_tests,
2013 projection.first_empty_evidence_test.as_deref().unwrap_or("unknown")
2014 ),
2015 });
2016 }
2017 if let Some(transport) = &projection.transport {
2018 if transport.corrupt_records > 0 {
2019 diagnostics.push(CoverageDiagnostic {
2020 code: "CORRUPT_EVIDENCE_RECORDS".into(),
2021 severity: "error".into(),
2022 message: format!(
2023 "{} malformed evidence record(s) in {} file(s) were excluded; coverage is incomplete.",
2024 transport.corrupt_records, transport.corrupt_files
2025 ),
2026 });
2027 }
2028 if transport.remote_launches > 0
2029 && transport.scoped_server_records == 0
2030 && projection.attribution.server_explicit == 0
2031 && projection.attribution.server_fallback == 0
2032 {
2033 diagnostics.push(CoverageDiagnostic {
2034 code: "REMOTE_SERVER_EVIDENCE_MISSING".into(),
2035 severity: "warning".into(),
2036 message: "Remote launches were supervised, but no server evidence returned. Coverage may describe only browser/test processes; inspect how the application server is launched.".into(),
2037 });
2038 }
2039 }
2040 let coverage_by_kind = index.dimensions(options.view, CoverageDimension::Kind)?;
2041 let coverage_by_runner = index.dimensions(options.view, CoverageDimension::Runner)?;
2042 let filters = CoverageQueryFilters {
2043 outcome: match options.view {
2044 CoverageViewId::All => "all",
2045 CoverageViewId::Passed => "passed",
2046 CoverageViewId::Failed => "failed",
2047 }
2048 .into(),
2049 kind: options.kind.map(str::to_owned),
2050 runner: options.runner.map(str::to_owned),
2051 };
2052 let structurally_complete =
2053 projection.summary.coverage_complete && projection.measurement.complete;
2054 let complete = options.view == CoverageViewId::Passed
2055 && options.valid
2056 && !options.stale
2057 && structurally_complete;
2058 Ok(CoverageSummaryData {
2059 run: options.run.into(),
2060 filters,
2061 generated_at: projection.generated_at,
2062 valid: options.valid,
2063 stale: options.stale,
2064 stale_reasons: options.stale_reasons,
2065 structurally_complete,
2066 complete,
2067 coverage: projection.summary,
2068 measurement: projection.measurement.clone(),
2069 waivers: None,
2070 coverage_by_kind,
2071 coverage_by_runner,
2072 attribution: projection.attribution,
2073 transport: projection.transport,
2074 diagnostics,
2075 confidence: (options.kind.is_none() && options.runner.is_none())
2076 .then_some(projection.confidence),
2077 files_with_gaps: projection.files_with_gaps,
2078 files_with_coverage_gaps: projection.files_with_coverage_gaps,
2079 files_with_measurement_limitations: projection.measurement.files,
2080 tests: projection.tests,
2081 setups: projection.setups,
2082 test_outcomes: projection.test_outcomes,
2083 source_scope: projection.source_scope,
2084 })
2085}
2086
2087#[derive(Debug, Clone, PartialEq)]
2088pub enum CoverageDimensionQueryData {
2089 Kinds(CoverageKindsData),
2090 Runners(CoverageRunnersData),
2091}
2092
2093#[derive(Debug, Clone, PartialEq)]
2094pub enum CoverageFileQueryData {
2095 Files(CoverageFilesData),
2096 Gaps(CoverageGapsData),
2097}
2098
2099#[derive(Debug, Clone, PartialEq)]
2100pub struct CoverageFileQueryResult {
2101 pub data: CoverageFileQueryData,
2102 pub pagination: AgentPagination,
2103}
2104
2105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2106#[serde(rename_all = "lowercase")]
2107pub enum DecisionSort {
2108 Location,
2109 Missing,
2110}
2111
2112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2113#[serde(rename_all = "camelCase")]
2114pub struct DecisionGapTotals {
2115 pub decisions: usize,
2116 pub decisions_with_missing_conditions: usize,
2117 pub conditions: usize,
2118 pub missing_conditions: usize,
2119 pub waived_conditions: usize,
2120}
2121
2122#[derive(Debug, Clone, PartialEq, Serialize)]
2123#[serde(rename_all = "camelCase")]
2124pub struct CoverageFileDecisionsData {
2125 pub run: String,
2126 pub filters: CoverageQueryFilters,
2127 pub file: String,
2128 pub group: String,
2129 pub sort: DecisionSort,
2130 pub totals: DecisionGapTotals,
2131 pub decisions: Vec<IndexedDecisionGap>,
2132}
2133
2134#[derive(Debug, Clone, Copy)]
2135pub struct CoverageFileDecisionsOptions<'a> {
2136 pub run: &'a str,
2137 pub view: CoverageViewId,
2138 pub kind: Option<&'a str>,
2139 pub runner: Option<&'a str>,
2140 pub file: &'a str,
2141 pub waived_by_decision:
2142 Option<&'a BTreeMap<String, BTreeMap<usize, crate::coverage_waivers::CoverageWaiver>>>,
2143 pub sort: DecisionSort,
2144 pub offset: usize,
2145 pub limit: usize,
2146}
2147
2148#[derive(Debug, Clone)]
2149pub struct CoverageDimensionQueryOptions<'a> {
2150 pub run: &'a str,
2151 pub view: CoverageViewId,
2152 pub dimension: CoverageDimension,
2153 pub filters: CoverageQueryFilters,
2154 pub offset: usize,
2155 pub limit: usize,
2156}
2157
2158pub fn coverage_dimension_query(
2159 index: &CoverageIndex<'_>,
2160 options: CoverageDimensionQueryOptions<'_>,
2161) -> Result<(CoverageDimensionQueryData, AgentPagination), QueryError> {
2162 let CoverageDimensionQueryOptions {
2163 run,
2164 view,
2165 dimension,
2166 filters,
2167 offset,
2168 limit,
2169 } = options;
2170 if limit == 0 {
2171 return Err(QueryError::InvalidPagination);
2172 }
2173 let values = index.dimensions(view, dimension)?;
2174 let total = values.len();
2175 let selected = values
2176 .into_iter()
2177 .skip(offset)
2178 .take(limit)
2179 .collect::<Vec<_>>();
2180 let returned = selected.len();
2181 let data = match dimension {
2182 CoverageDimension::Kind => CoverageDimensionQueryData::Kinds(CoverageKindsData {
2183 run: run.into(),
2184 filters,
2185 kinds: selected,
2186 }),
2187 CoverageDimension::Runner => CoverageDimensionQueryData::Runners(CoverageRunnersData {
2188 run: run.into(),
2189 filters,
2190 runners: selected,
2191 }),
2192 };
2193 Ok((data, pagination(offset, limit, returned, total)))
2194}
2195
2196#[derive(Debug, Clone, Copy)]
2197pub struct CoverageFileQueryOptions<'a> {
2198 pub run: &'a str,
2199 pub view: CoverageViewId,
2200 pub metric: MinimizeMetric,
2201 pub gaps_only: bool,
2202 pub kind: Option<&'a str>,
2203 pub runner: Option<&'a str>,
2204 pub offset: usize,
2205 pub limit: usize,
2206}
2207
2208fn gap_metric_value(gap: &IndexedFileGap, metric: MinimizeMetric) -> usize {
2209 match metric {
2210 MinimizeMetric::All => gap.score,
2211 MinimizeMetric::Lines => gap.uncovered_lines,
2212 MinimizeMetric::Statements => gap.uncovered_statements,
2213 MinimizeMetric::Functions => gap.uncovered_functions,
2214 MinimizeMetric::Branches => gap.missing_branches,
2215 MinimizeMetric::Mcdc => gap.missing_mcdc_conditions,
2216 }
2217}
2218
2219pub fn coverage_file_decisions_query(
2220 index: &CoverageIndex<'_>,
2221 options: CoverageFileDecisionsOptions<'_>,
2222) -> Result<(CoverageFileDecisionsData, AgentPagination), QueryError> {
2223 if options.limit == 0 {
2224 return Err(QueryError::InvalidPagination);
2225 }
2226 let mut all = index.decision_gaps(options.view, options.kind, options.runner, options.file)?;
2227 if let Some(waived) = options.waived_by_decision {
2228 for decision in &mut all {
2229 decision.waived_conditions = waived.get(&decision.id).map_or(0, BTreeMap::len);
2230 }
2231 }
2232 if (options.kind.is_some() || options.runner.is_some()) && all.is_empty() {
2233 if index
2236 .file_gaps(options.view, options.kind, options.runner)?
2237 .is_empty()
2238 {
2239 return Err(QueryError::TestFilterEmpty {
2240 kind: options.kind.map(str::to_owned),
2241 runner: options.runner.map(str::to_owned),
2242 });
2243 }
2244 }
2245 let totals = DecisionGapTotals {
2246 decisions: all.len(),
2247 decisions_with_missing_conditions: all
2248 .iter()
2249 .filter(|decision| decision.missing_conditions > 0)
2250 .count(),
2251 conditions: all.iter().map(|decision| decision.conditions).sum(),
2252 missing_conditions: all.iter().map(|decision| decision.missing_conditions).sum(),
2253 waived_conditions: all.iter().map(|decision| decision.waived_conditions).sum(),
2254 };
2255 let mut missing = all
2256 .into_iter()
2257 .filter(|decision| decision.missing_conditions > 0)
2258 .collect::<Vec<_>>();
2259 missing.sort_by(|left, right| match options.sort {
2260 DecisionSort::Missing => right
2261 .missing_conditions
2262 .saturating_sub(right.waived_conditions)
2263 .cmp(
2264 &left
2265 .missing_conditions
2266 .saturating_sub(left.waived_conditions),
2267 )
2268 .then_with(|| right.missing_conditions.cmp(&left.missing_conditions))
2269 .then_with(|| left.line.cmp(&right.line))
2270 .then_with(|| left.column.cmp(&right.column)),
2271 DecisionSort::Location => left
2272 .line
2273 .cmp(&right.line)
2274 .then_with(|| left.column.cmp(&right.column))
2275 .then_with(|| left.id.cmp(&right.id)),
2276 });
2277 let total = missing.len();
2278 let rows = missing
2279 .into_iter()
2280 .skip(options.offset)
2281 .take(options.limit)
2282 .collect::<Vec<_>>();
2283 let returned = rows.len();
2284 let filters = CoverageQueryFilters {
2285 outcome: match options.view {
2286 CoverageViewId::All => "all",
2287 CoverageViewId::Passed => "passed",
2288 CoverageViewId::Failed => "failed",
2289 }
2290 .into(),
2291 kind: options.kind.map(str::to_owned),
2292 runner: options.runner.map(str::to_owned),
2293 };
2294 Ok((
2295 CoverageFileDecisionsData {
2296 run: options.run.into(),
2297 filters,
2298 file: options.file.into(),
2299 group: "decision".into(),
2300 sort: options.sort,
2301 totals,
2302 decisions: rows,
2303 },
2304 pagination(options.offset, options.limit, returned, total),
2305 ))
2306}
2307
2308pub fn coverage_file_query(
2309 index: &CoverageIndex<'_>,
2310 options: CoverageFileQueryOptions<'_>,
2311) -> Result<CoverageFileQueryResult, QueryError> {
2312 let CoverageFileQueryOptions {
2313 run,
2314 view,
2315 metric,
2316 gaps_only,
2317 kind,
2318 runner,
2319 offset,
2320 limit,
2321 } = options;
2322 if limit == 0 {
2323 return Err(QueryError::InvalidPagination);
2324 }
2325 let mut files = index.file_gaps(view, kind, runner)?;
2326 if (kind.is_some() || runner.is_some()) && files.is_empty() {
2327 return Err(QueryError::TestFilterEmpty {
2328 kind: kind.map(str::to_owned),
2329 runner: runner.map(str::to_owned),
2330 });
2331 }
2332 if gaps_only {
2333 files.retain(|gap| gap_metric_value(gap, metric) > 0 || gap.measurement_limitations > 0);
2334 }
2335 files.sort_by(|left, right| {
2336 gap_metric_value(right, metric)
2337 .cmp(&gap_metric_value(left, metric))
2338 .then_with(|| {
2339 right
2340 .measurement_limitations
2341 .cmp(&left.measurement_limitations)
2342 })
2343 .then_with(|| left.file.cmp(&right.file))
2344 });
2345 let total = files.len();
2346 let page = files
2347 .into_iter()
2348 .skip(offset)
2349 .take(limit)
2350 .collect::<Vec<_>>();
2351 let page_info = pagination(offset, limit, page.len(), total);
2352 let filters = CoverageQueryFilters {
2353 outcome: match view {
2354 CoverageViewId::All => "all",
2355 CoverageViewId::Passed => "passed",
2356 CoverageViewId::Failed => "failed",
2357 }
2358 .into(),
2359 kind: kind.map(str::to_owned),
2360 runner: runner.map(str::to_owned),
2361 };
2362 let data = if gaps_only {
2363 CoverageFileQueryData::Gaps(CoverageGapsData {
2364 run: run.into(),
2365 filters,
2366 metric,
2367 gaps: page,
2368 })
2369 } else {
2370 CoverageFileQueryData::Files(CoverageFilesData {
2371 run: run.into(),
2372 filters,
2373 metric,
2374 files: page,
2375 })
2376 };
2377 Ok(CoverageFileQueryResult {
2378 data,
2379 pagination: page_info,
2380 })
2381}
2382
2383#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2384enum ObligationMetric {
2385 Lines,
2386 Statements,
2387 Functions,
2388 Branches,
2389 Mcdc,
2390}
2391
2392impl ObligationMetric {
2393 fn selected(self, metric: MinimizeMetric) -> bool {
2394 metric == MinimizeMetric::All
2395 || matches!(
2396 (self, metric),
2397 (Self::Lines, MinimizeMetric::Lines)
2398 | (Self::Statements, MinimizeMetric::Statements)
2399 | (Self::Functions, MinimizeMetric::Functions)
2400 | (Self::Branches, MinimizeMetric::Branches)
2401 | (Self::Mcdc, MinimizeMetric::Mcdc)
2402 )
2403 }
2404
2405 fn public(self) -> MinimizeMetric {
2406 match self {
2407 Self::Lines => MinimizeMetric::Lines,
2408 Self::Statements => MinimizeMetric::Statements,
2409 Self::Functions => MinimizeMetric::Functions,
2410 Self::Branches => MinimizeMetric::Branches,
2411 Self::Mcdc => MinimizeMetric::Mcdc,
2412 }
2413 }
2414}
2415
2416#[derive(Clone)]
2417struct Obligation {
2418 id: String,
2419 metric: ObligationMetric,
2420 options: Vec<Vec<String>>,
2422}
2423
2424struct ObligationModel {
2425 obligations: Vec<Obligation>,
2426 setup_by_file: BTreeMap<String, Vec<String>>,
2427 tests_by_file: BTreeMap<String, Vec<String>>,
2428 background: Vec<String>,
2429}
2430
2431impl ObligationModel {
2432 fn expand(&self, selected: &BTreeSet<String>) -> BTreeSet<String> {
2433 let mut expanded = selected.clone();
2434 expanded.extend(self.background.iter().cloned());
2435 for (file, setup_ids) in &self.setup_by_file {
2436 if self
2437 .tests_by_file
2438 .get(file)
2439 .is_some_and(|tests| tests.iter().any(|id| selected.contains(id)))
2440 {
2441 expanded.extend(setup_ids.iter().cloned());
2442 }
2443 }
2444 expanded
2445 }
2446}
2447
2448fn deduplicate_options(options: impl IntoIterator<Item = Vec<String>>) -> Vec<Vec<String>> {
2449 let mut unique = BTreeMap::<String, Vec<String>>::new();
2450 for mut option in options {
2451 option.sort();
2452 option.dedup();
2453 let key = option.join("\0");
2454 unique.entry(key).or_insert(option);
2455 }
2456 unique.into_values().collect()
2457}
2458
2459fn evidence_choices(
2460 ids: &[String],
2461 tests: &HashMap<&str, &crate::coverage_report::TestCoverageResult>,
2462 candidates: &BTreeSet<String>,
2463 tests_by_file: &BTreeMap<String, Vec<String>>,
2464) -> Vec<Vec<String>> {
2465 let mut choices = Vec::new();
2466 for id in ids {
2467 let Some(test) = tests.get(id.as_str()) else {
2468 continue;
2469 };
2470 if test.role == "background" {
2471 choices.push(Vec::new());
2472 } else if test.role == "setup" {
2473 if let Some(file) = &test.file {
2474 choices.extend(
2475 tests_by_file
2476 .get(file)
2477 .into_iter()
2478 .flatten()
2479 .map(|candidate| vec![candidate.clone()]),
2480 );
2481 }
2482 } else if candidates.contains(id) {
2483 choices.push(vec![id.clone()]);
2484 }
2485 }
2486 deduplicate_options(choices)
2487}
2488
2489fn build_obligations(view: &CoverageView, candidates: &BTreeSet<String>) -> ObligationModel {
2490 let tests = view
2491 .tests
2492 .iter()
2493 .map(|test| (test.id.as_str(), test))
2494 .collect::<HashMap<_, _>>();
2495 let mut tests_by_file = BTreeMap::<String, Vec<String>>::new();
2496 let mut setup_by_file = BTreeMap::<String, Vec<String>>::new();
2497 let mut background = Vec::new();
2498 for test in &view.tests {
2499 match test.role.as_str() {
2500 "test" if candidates.contains(&test.id) => {
2501 if let Some(file) = &test.file {
2502 tests_by_file
2503 .entry(file.clone())
2504 .or_default()
2505 .push(test.id.clone());
2506 }
2507 }
2508 "setup" => {
2509 if let Some(file) = &test.file {
2510 setup_by_file
2511 .entry(file.clone())
2512 .or_default()
2513 .push(test.id.clone());
2514 }
2515 }
2516 "background" => background.push(test.id.clone()),
2517 _ => {}
2518 }
2519 }
2520 let choices = |ids: &[String]| evidence_choices(ids, &tests, candidates, &tests_by_file);
2521 let mut obligations = Vec::new();
2522 let mut unique_lines = BTreeMap::new();
2523 for line in &view.lines {
2524 unique_lines.insert((line.file.as_str(), line.line), line);
2525 }
2526 for ((file, line), result) in unique_lines {
2527 obligations.push(Obligation {
2528 id: format!("line:{file}:{line}"),
2529 metric: ObligationMetric::Lines,
2530 options: choices(&result.tests),
2531 });
2532 }
2533 for point in &view.points {
2534 let (kind, metric) = match point.meta.kind {
2535 crate::coverage_analysis::PointKind::Statement => {
2536 ("statement", ObligationMetric::Statements)
2537 }
2538 crate::coverage_analysis::PointKind::Function => {
2539 ("function", ObligationMetric::Functions)
2540 }
2541 };
2542 obligations.push(Obligation {
2543 id: format!("{kind}:{}", point.meta.id),
2544 metric,
2545 options: choices(&point.tests),
2546 });
2547 }
2548 for branch in &view.branches {
2549 for alternative in &branch.alternatives {
2550 obligations.push(Obligation {
2551 id: format!("branch:{}:{}", branch.meta.id, alternative.id),
2552 metric: ObligationMetric::Branches,
2553 options: choices(&alternative.tests),
2554 });
2555 }
2556 }
2557 for decision in &view.decisions {
2558 for condition in 0..decision.meta.conditions.len() {
2559 let mut options = Vec::new();
2560 for left in 0..decision.vector_observations.len() {
2561 for right in (left + 1)..decision.vector_observations.len() {
2562 let first = &decision.vector_observations[left];
2563 let second = &decision.vector_observations[right];
2564 if !is_independence_pair(&first.vector, &second.vector, condition) {
2565 continue;
2566 }
2567 for first_choice in choices(&first.tests) {
2568 for second_choice in choices(&second.tests) {
2569 let mut combined = first_choice.clone();
2570 combined.extend(second_choice);
2571 options.push(combined);
2572 }
2573 }
2574 }
2575 }
2576 obligations.push(Obligation {
2577 id: format!("mcdc:{}:{condition}", decision.meta.id),
2578 metric: ObligationMetric::Mcdc,
2579 options: deduplicate_options(options),
2580 });
2581 }
2582 }
2583 ObligationModel {
2584 obligations,
2585 setup_by_file,
2586 tests_by_file,
2587 background,
2588 }
2589}
2590
2591fn percentage(metric: ObligationMetric, summary: &CoverageSummary) -> f64 {
2592 match metric {
2593 ObligationMetric::Lines => summary.lines.percentage,
2594 ObligationMetric::Statements => summary.statements.percentage,
2595 ObligationMetric::Functions => summary.functions.percentage,
2596 ObligationMetric::Branches => summary.branches.percentage,
2597 ObligationMetric::Mcdc => summary.condition_coverage_pct,
2598 }
2599}
2600
2601fn obligation_satisfied(obligation: &Obligation, selected: &BTreeSet<String>) -> bool {
2602 obligation
2603 .options
2604 .iter()
2605 .any(|option| option.iter().all(|test| selected.contains(test)))
2606}
2607
2608struct Search<'a> {
2609 obligations: &'a [Obligation],
2610 skip_limits: BTreeMap<ObligationMetric, usize>,
2611 best: BTreeSet<String>,
2612 explored_states: usize,
2613 max_states: usize,
2614 seen: BTreeSet<String>,
2615 candidate_tests: usize,
2616 target: f64,
2617 metric: MinimizeMetric,
2618}
2619
2620impl Search<'_> {
2621 fn visit(
2622 &mut self,
2623 selected: BTreeSet<String>,
2624 skipped: BTreeSet<String>,
2625 skipped_by_metric: BTreeMap<ObligationMetric, usize>,
2626 ) -> Result<(), QueryError> {
2627 self.explored_states += 1;
2628 if self.explored_states > self.max_states {
2629 return Err(QueryError::ComplexityLimit {
2630 candidate_tests: self.candidate_tests,
2631 obligations: self.obligations.len(),
2632 explored_states: self.explored_states,
2633 max_states: self.max_states,
2634 target: self.target,
2635 metric: self.metric,
2636 });
2637 }
2638 if selected.len() >= self.best.len() {
2639 return Ok(());
2640 }
2641 let state_key = format!(
2642 "{}|{}",
2643 selected.iter().cloned().collect::<Vec<_>>().join(","),
2644 skipped.iter().cloned().collect::<Vec<_>>().join(",")
2645 );
2646 if !self.seen.insert(state_key) {
2647 return Ok(());
2648 }
2649 let mut unmet = self
2650 .obligations
2651 .iter()
2652 .filter(|obligation| {
2653 !skipped.contains(&obligation.id) && !obligation_satisfied(obligation, &selected)
2654 })
2655 .collect::<Vec<_>>();
2656 if unmet.is_empty() {
2657 self.best = selected;
2658 return Ok(());
2659 }
2660 unmet.sort_by(|left, right| {
2661 let feasible = |obligation: &Obligation| {
2662 obligation
2663 .options
2664 .iter()
2665 .filter(|option| option.iter().any(|test| !selected.contains(test)))
2666 .count()
2667 };
2668 feasible(left)
2669 .cmp(&feasible(right))
2670 .then_with(|| left.id.cmp(&right.id))
2671 });
2672 let obligation = unmet[0];
2673 let mut additions = deduplicate_options(obligation.options.iter().filter_map(|option| {
2674 let addition = option
2675 .iter()
2676 .filter(|test| !selected.contains(*test))
2677 .cloned()
2678 .collect::<Vec<_>>();
2679 (!addition.is_empty()).then_some(addition)
2680 }));
2681 additions.sort_by(|left, right| {
2682 left.len()
2683 .cmp(&right.len())
2684 .then_with(|| left.join("\0").cmp(&right.join("\0")))
2685 });
2686 for addition in additions {
2687 if selected.len() + addition.len() >= self.best.len() {
2688 continue;
2689 }
2690 let mut next = selected.clone();
2691 next.extend(addition);
2692 self.visit(next, skipped.clone(), skipped_by_metric.clone())?;
2693 }
2694 let skipped_count = skipped_by_metric
2695 .get(&obligation.metric)
2696 .copied()
2697 .unwrap_or(0);
2698 if skipped_count
2699 < self
2700 .skip_limits
2701 .get(&obligation.metric)
2702 .copied()
2703 .unwrap_or(0)
2704 {
2705 let mut next_skipped = skipped;
2706 next_skipped.insert(obligation.id.clone());
2707 let mut next_counts = skipped_by_metric;
2708 next_counts.insert(obligation.metric, skipped_count + 1);
2709 self.visit(selected, next_skipped, next_counts)?;
2710 }
2711 Ok(())
2712 }
2713}
2714
2715pub fn minimum_test_set(
2716 view: &CoverageView,
2717 target: f64,
2718 metric: MinimizeMetric,
2719 max_states: usize,
2720) -> Result<MinimumTestSetResult, QueryError> {
2721 if !target.is_finite() || !(0.0..=100.0).contains(&target) {
2722 return Err(QueryError::InvalidTarget(target));
2723 }
2724 if view.tests.iter().any(|test| {
2725 test.role == "background" && (!test.hits.is_empty() || !test.decisions.is_empty())
2726 }) {
2727 return Err(QueryError::UnattributedEvidence);
2728 }
2729 let candidate_tests = view
2730 .tests
2731 .iter()
2732 .filter(|test| test.role == "test")
2733 .map(|test| test.id.clone())
2734 .collect::<BTreeSet<_>>();
2735 let model = build_obligations(view, &candidate_tests);
2736 let obligations = model
2737 .obligations
2738 .iter()
2739 .filter(|obligation| obligation.metric.selected(metric))
2740 .cloned()
2741 .collect::<Vec<_>>();
2742 let mut totals = BTreeMap::<ObligationMetric, usize>::new();
2743 for obligation in &obligations {
2744 *totals.entry(obligation.metric).or_default() += 1;
2745 }
2746 let metrics = [
2747 ObligationMetric::Lines,
2748 ObligationMetric::Statements,
2749 ObligationMetric::Functions,
2750 ObligationMetric::Branches,
2751 ObligationMetric::Mcdc,
2752 ]
2753 .into_iter()
2754 .filter(|candidate| candidate.selected(metric))
2755 .collect::<Vec<_>>();
2756 let skip_limits = metrics
2757 .iter()
2758 .map(|selected_metric| {
2759 let total = totals.get(selected_metric).copied().unwrap_or(0);
2760 let required = ((total as f64 * target) / 100.0).ceil() as usize;
2761 (*selected_metric, total.saturating_sub(required))
2762 })
2763 .collect::<BTreeMap<_, _>>();
2764 let full_expanded = model.expand(&candidate_tests);
2765 let full_summary = coverage_summary_for_tests(view, &full_expanded)?;
2766 for selected_metric in &metrics {
2767 let reachable = percentage(*selected_metric, &full_summary);
2768 if reachable + 1e-9 < target {
2769 return Err(QueryError::TargetUnreachable {
2770 metric: selected_metric.public(),
2771 target,
2772 reachable,
2773 });
2774 }
2775 }
2776 let mut search = Search {
2777 obligations: &obligations,
2778 skip_limits,
2779 best: candidate_tests.clone(),
2780 explored_states: 0,
2781 max_states,
2782 seen: BTreeSet::new(),
2783 candidate_tests: candidate_tests.len(),
2784 target,
2785 metric,
2786 };
2787 search.visit(BTreeSet::new(), BTreeSet::new(), BTreeMap::new())?;
2788 let expanded = model.expand(&search.best);
2789 let summary = coverage_summary_for_tests(view, &expanded)?;
2790 Ok(MinimumTestSetResult {
2791 optimal: true,
2792 target,
2793 metric,
2794 selected: search.best.into_iter().collect(),
2795 expanded: expanded.into_iter().collect(),
2796 summary,
2797 explored_states: search.explored_states,
2798 })
2799}
2800
2801#[cfg(test)]
2802mod tests {
2803 use crate::{
2804 coverage_analysis::McdcVector,
2805 coverage_report::{
2806 CoverageManifest, CoverageReport, CoverageReportRequest, DecisionMeta, ExitCodeInput,
2807 RawTestResult, RuntimeSnapshot, TestProvenance, analyze_coverage_results,
2808 },
2809 };
2810
2811 use super::*;
2812
2813 fn result(id: &str, vector: McdcVector) -> RawTestResult {
2814 RawTestResult {
2815 test_id: Some(id.into()),
2816 scope: None,
2817 test: id.into(),
2818 test_file: Some("tests/permission.test.js".into()),
2819 title: None,
2820 retry: Some(0),
2821 status: Some("passed".into()),
2822 expected_status: None,
2823 flaky: false,
2824 provenance: TestProvenance {
2825 runner: "node:test".into(),
2826 kind: "unit".into(),
2827 project: None,
2828 source: "runner-default".into(),
2829 },
2830 role: "test".into(),
2831 phases: Vec::new(),
2832 runtime: vec![RuntimeSnapshot {
2833 decisions: vec![crate::coverage_report::DecisionSnapshot {
2834 meta: decision(),
2835 vectors: vec![vector],
2836 }],
2837 hits: Vec::new(),
2838 events: Vec::new(),
2839 }],
2840 browser: Vec::new(),
2841 server: Vec::new(),
2842 }
2843 }
2844
2845 fn decision() -> DecisionMeta {
2846 DecisionMeta {
2847 id: "decision".into(),
2848 file: "src/permission.js".into(),
2849 line: 1,
2850 column: 1,
2851 source: "admin || owner".into(),
2852 conditions: vec!["admin".into(), "owner".into()],
2853 kind: "if".into(),
2854 }
2855 }
2856
2857 fn report(mut results: Vec<RawTestResult>) -> CoverageReport {
2858 analyze_coverage_results(&CoverageReportRequest {
2859 run_id: "run".into(),
2860 manifest: CoverageManifest {
2861 decisions: vec![decision()],
2862 points: Vec::new(),
2863 branches: Vec::new(),
2864 limitations: Vec::new(),
2865 scope: None,
2866 },
2867 raw_results: std::mem::take(&mut results),
2868 generated_at: "time".into(),
2869 coverage_model: None,
2870 integrity: None,
2871 test_exit_code: ExitCodeInput::Present(Some(0)),
2872 })
2873 .unwrap()
2874 }
2875
2876 #[test]
2877 fn recomputes_mcdc_witnesses_and_removes_a_redundant_vector() {
2878 let report = report(vec![
2879 result(
2880 "admin",
2881 McdcVector {
2882 values: vec![Some(true), None],
2883 outcome: true,
2884 },
2885 ),
2886 result(
2887 "owner",
2888 McdcVector {
2889 values: vec![Some(false), Some(true)],
2890 outcome: true,
2891 },
2892 ),
2893 result(
2894 "both",
2895 McdcVector {
2896 values: vec![Some(true), None],
2897 outcome: true,
2898 },
2899 ),
2900 result(
2901 "neither",
2902 McdcVector {
2903 values: vec![Some(false), Some(false)],
2904 outcome: false,
2905 },
2906 ),
2907 ]);
2908 let minimized = minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 5_000).unwrap();
2909 assert_eq!(minimized.selected.len(), 3);
2910 assert!(minimized.selected.contains(&"owner".into()));
2911 assert!(minimized.selected.contains(&"neither".into()));
2912 assert_eq!(minimized.summary.condition_coverage_pct, 100.0);
2913 }
2914
2915 #[test]
2916 fn refuses_background_evidence() {
2917 let mut aggregate = result(
2918 "aggregate",
2919 McdcVector {
2920 values: vec![Some(false), Some(false)],
2921 outcome: false,
2922 },
2923 );
2924 aggregate.role = "background".into();
2925 assert!(matches!(
2926 minimum_test_set(
2927 &report(vec![aggregate]).view,
2928 100.0,
2929 MinimizeMetric::Mcdc,
2930 5_000,
2931 ),
2932 Err(QueryError::UnattributedEvidence)
2933 ));
2934 }
2935
2936 #[test]
2937 fn bounds_the_exact_search() {
2938 let report = report(vec![
2939 result(
2940 "admin",
2941 McdcVector {
2942 values: vec![Some(true), None],
2943 outcome: true,
2944 },
2945 ),
2946 result(
2947 "owner",
2948 McdcVector {
2949 values: vec![Some(false), Some(true)],
2950 outcome: true,
2951 },
2952 ),
2953 result(
2954 "neither",
2955 McdcVector {
2956 values: vec![Some(false), Some(false)],
2957 outcome: false,
2958 },
2959 ),
2960 ]);
2961 assert!(matches!(
2962 minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 1),
2963 Err(QueryError::ComplexityLimit { .. })
2964 ));
2965 }
2966}