Skip to main content

supercov_engine/
coverage_analysis.rs

1//! Language-neutral coverage arithmetic and masking MC/DC witness search.
2//!
3//! Frontends provide obligations and observed vectors; this module owns the
4//! structural verdicts for every language. Witness selection is deterministic
5//! in observation order while bitsets avoid a scalar pair scan for each
6//! condition.
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12pub struct McdcVector {
13    pub values: Vec<Option<bool>>,
14    pub outcome: bool,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct WitnessIndexes {
20    pub first: usize,
21    pub second: usize,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum AnalysisError {
26    EmptyDecision {
27        decision: usize,
28    },
29    InconsistentVectorWidth {
30        vector: usize,
31        expected: usize,
32        actual: usize,
33    },
34}
35
36#[derive(Clone)]
37struct Bits(Vec<u64>);
38
39impl Bits {
40    fn empty(items: usize) -> Self {
41        Self(vec![0; items.div_ceil(64)])
42    }
43
44    fn insert(&mut self, index: usize) {
45        self.0[index / 64] |= 1_u64 << (index % 64);
46    }
47
48    fn and_assign(&mut self, other: &Self) {
49        for (word, mask) in self.0.iter_mut().zip(&other.0) {
50            *word &= mask;
51        }
52    }
53
54    fn and_not_assign(&mut self, other: &Self) {
55        for (word, mask) in self.0.iter_mut().zip(&other.0) {
56            *word &= !mask;
57        }
58    }
59
60    fn remove_through(&mut self, index: usize) {
61        let word = index / 64;
62        for entry in &mut self.0[..word] {
63            *entry = 0;
64        }
65        if let Some(entry) = self.0.get_mut(word) {
66            let bit = index % 64;
67            *entry &= if bit == 63 { 0 } else { !0_u64 << (bit + 1) };
68        }
69    }
70
71    fn first(&self) -> Option<usize> {
72        self.0.iter().enumerate().find_map(|(word, value)| {
73            (*value != 0).then(|| word * 64 + value.trailing_zeros() as usize)
74        })
75    }
76}
77
78pub fn is_independence_pair(first: &McdcVector, second: &McdcVector, condition: usize) -> bool {
79    if first.values.len() != second.values.len() || condition >= first.values.len() {
80        return false;
81    }
82    let (Some(first_target), Some(second_target)) =
83        (first.values[condition], second.values[condition])
84    else {
85        return false;
86    };
87    if first_target == second_target || first.outcome == second.outcome {
88        return false;
89    }
90    first
91        .values
92        .iter()
93        .zip(&second.values)
94        .enumerate()
95        .all(|(index, (left, right))| {
96            index == condition || left.is_none() || right.is_none() || left == right
97        })
98}
99
100/// Return the same first witness pair as the reference nested-order scan for
101/// every condition, using dense bitsets to reject incompatible candidates.
102pub fn find_witnesses(
103    vectors: &[McdcVector],
104) -> Result<Vec<Option<WitnessIndexes>>, AnalysisError> {
105    let width = vectors.first().map_or(0, |vector| vector.values.len());
106    find_witnesses_for_conditions(vectors, width)
107}
108
109/// Find witnesses against the manifest denominator. Unlike [`find_witnesses`],
110/// this retains every condition when a decision has no observations.
111pub fn find_witnesses_for_conditions(
112    vectors: &[McdcVector],
113    width: usize,
114) -> Result<Vec<Option<WitnessIndexes>>, AnalysisError> {
115    for (index, vector) in vectors.iter().enumerate() {
116        if vector.values.len() != width {
117            return Err(AnalysisError::InconsistentVectorWidth {
118                vector: index,
119                expected: width,
120                actual: vector.values.len(),
121            });
122        }
123    }
124    let mut outcomes = [Bits::empty(vectors.len()), Bits::empty(vectors.len())];
125    let mut false_values = (0..width)
126        .map(|_| Bits::empty(vectors.len()))
127        .collect::<Vec<_>>();
128    let mut true_values = false_values.clone();
129    for (index, vector) in vectors.iter().enumerate() {
130        outcomes[usize::from(vector.outcome)].insert(index);
131        for (condition, value) in vector.values.iter().enumerate() {
132            match value {
133                Some(false) => false_values[condition].insert(index),
134                Some(true) => true_values[condition].insert(index),
135                None => {}
136            }
137        }
138    }
139
140    let witnesses = (0..width)
141        .map(|target| {
142            for (left_index, left) in vectors.iter().enumerate() {
143                let Some(left_target) = left.values[target] else {
144                    continue;
145                };
146                let mut candidates = outcomes[usize::from(!left.outcome)].clone();
147                candidates.and_assign(if left_target {
148                    &false_values[target]
149                } else {
150                    &true_values[target]
151                });
152                candidates.remove_through(left_index);
153                for (condition, value) in left.values.iter().enumerate() {
154                    if condition == target {
155                        continue;
156                    }
157                    match value {
158                        Some(false) => candidates.and_not_assign(&true_values[condition]),
159                        Some(true) => candidates.and_not_assign(&false_values[condition]),
160                        None => {}
161                    }
162                }
163                if let Some(second) = candidates.first() {
164                    debug_assert!(is_independence_pair(left, &vectors[second], target));
165                    return Some(WitnessIndexes {
166                        first: left_index,
167                        second,
168                    });
169                }
170            }
171            None
172        })
173        .collect();
174    Ok(witnesses)
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "kebab-case")]
179pub enum PointKind {
180    Statement,
181    Function,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase", deny_unknown_fields)]
186pub struct PointCoverage {
187    pub kind: PointKind,
188    pub covered: bool,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase", deny_unknown_fields)]
193pub struct BranchCoverage {
194    pub kind: String,
195    pub alternatives: Vec<bool>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase", deny_unknown_fields)]
200pub struct DecisionCoverage {
201    pub condition_count: usize,
202    pub vectors: Vec<McdcVector>,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase", deny_unknown_fields)]
207pub struct CoverageCoreInput {
208    pub decisions: Vec<DecisionCoverage>,
209    pub points: Vec<PointCoverage>,
210    pub branches: Vec<BranchCoverage>,
211    pub lines: Vec<bool>,
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize)]
215#[serde(rename_all = "camelCase")]
216pub struct CoverageCount {
217    pub covered: usize,
218    pub total: usize,
219    #[serde(serialize_with = "serialize_javascript_number")]
220    pub percentage: f64,
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize)]
224#[serde(rename_all = "camelCase")]
225pub struct CoverageSummary {
226    pub decisions: usize,
227    pub executed_decisions: usize,
228    pub covered_decisions: usize,
229    pub conditions: usize,
230    pub covered_conditions: usize,
231    #[serde(serialize_with = "serialize_javascript_number")]
232    pub condition_coverage_pct: f64,
233    pub lines: CoverageCount,
234    pub statements: CoverageCount,
235    pub functions: CoverageCount,
236    pub branches: CoverageCount,
237    pub decision_outcomes: CoverageCount,
238    pub condition_outcomes: CoverageCount,
239    pub value_selections: CoverageCount,
240    pub coverage_complete: bool,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub completeness_blocked: Option<bool>,
243    /// Obligations Supercov declined to measure exactly. They are excluded
244    /// from every covered/uncovered count above, because a measurement gap is
245    /// not a coverage gap and reporting one as the other is a wrong number.
246    /// Absent when everything was measured, so fully exact runs keep their
247    /// existing output byte for byte.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub unmeasured_obligations: Option<usize>,
250    /// Share of obligations Supercov measured exactly, as a percentage. This
251    /// is the number that must ratchet toward 100 and never regress.
252    #[serde(
253        skip_serializing_if = "Option::is_none",
254        serialize_with = "serialize_optional_javascript_number"
255    )]
256    pub exact_fraction_pct: Option<f64>,
257}
258
259pub(crate) fn serialize_optional_javascript_number<S>(
260    value: &Option<f64>,
261    serializer: S,
262) -> Result<S::Ok, S::Error>
263where
264    S: serde::Serializer,
265{
266    match value {
267        Some(value) => serialize_javascript_number(value, serializer),
268        None => serializer.serialize_none(),
269    }
270}
271
272/// `JSON.stringify` emits integer-valued Numbers without a trailing `.0`.
273/// Agent output is a frozen byte contract, so match that representation while
274/// retaining floating-point arithmetic internally.
275pub(crate) fn serialize_javascript_number<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
276where
277    S: serde::Serializer,
278{
279    const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
280    if value.is_finite() && value.fract() == 0.0 && value.abs() <= MAX_SAFE_INTEGER {
281        serializer.serialize_i64(*value as i64)
282    } else {
283        serializer.serialize_f64(*value)
284    }
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct CoverageCoreOutput {
290    pub witnesses: Vec<Vec<Option<WitnessIndexes>>>,
291    pub summary: CoverageSummary,
292}
293
294fn percentage(covered: usize, total: usize) -> f64 {
295    if total == 0 {
296        100.0
297    } else {
298        ((covered as f64 / total as f64) * 10_000.0).round() / 100.0
299    }
300}
301
302fn count(covered: usize, total: usize) -> CoverageCount {
303    CoverageCount {
304        covered,
305        total,
306        percentage: percentage(covered, total),
307    }
308}
309
310pub fn analyze_core(input: &CoverageCoreInput) -> Result<CoverageCoreOutput, AnalysisError> {
311    let witnesses = input
312        .decisions
313        .iter()
314        .enumerate()
315        .map(|(decision, coverage)| {
316            if coverage.condition_count == 0 {
317                return Err(AnalysisError::EmptyDecision { decision });
318            }
319            find_witnesses_for_conditions(&coverage.vectors, coverage.condition_count)
320        })
321        .collect::<Result<Vec<_>, _>>()?;
322    let conditions = witnesses.iter().map(Vec::len).sum::<usize>();
323    let covered_conditions = witnesses
324        .iter()
325        .flatten()
326        .filter(|witness| witness.is_some())
327        .count();
328    let executed_decisions = input
329        .decisions
330        .iter()
331        .filter(|coverage| !coverage.vectors.is_empty())
332        .count();
333    let covered_decisions = witnesses
334        .iter()
335        .filter(|conditions| conditions.iter().all(Option::is_some))
336        .count();
337    let decision_outcome_covered = input
338        .decisions
339        .iter()
340        .map(|coverage| {
341            let vectors = &coverage.vectors;
342            usize::from(vectors.iter().any(|vector| !vector.outcome))
343                + usize::from(vectors.iter().any(|vector| vector.outcome))
344        })
345        .sum::<usize>();
346    let condition_outcome_covered = input
347        .decisions
348        .iter()
349        .map(|coverage| {
350            (0..coverage.condition_count)
351                .map(|condition| {
352                    usize::from(
353                        coverage
354                            .vectors
355                            .iter()
356                            .any(|vector| vector.values[condition] == Some(false)),
357                    ) + usize::from(
358                        coverage
359                            .vectors
360                            .iter()
361                            .any(|vector| vector.values[condition] == Some(true)),
362                    )
363                })
364                .sum::<usize>()
365        })
366        .sum::<usize>();
367    let generic_alternative_total = input
368        .branches
369        .iter()
370        .map(|branch| branch.alternatives.len())
371        .sum::<usize>();
372    let generic_alternative_covered = input
373        .branches
374        .iter()
375        .flat_map(|branch| &branch.alternatives)
376        .filter(|covered| **covered)
377        .count();
378    let value_branches = input
379        .branches
380        .iter()
381        .filter(|branch| branch.kind == "logical-value")
382        .collect::<Vec<_>>();
383    let value_alternative_total = value_branches
384        .iter()
385        .map(|branch| branch.alternatives.len())
386        .sum::<usize>();
387    let value_alternative_covered = value_branches
388        .iter()
389        .flat_map(|branch| &branch.alternatives)
390        .filter(|covered| **covered)
391        .count();
392    let statements = input
393        .points
394        .iter()
395        .filter(|point| point.kind == PointKind::Statement)
396        .collect::<Vec<_>>();
397    let functions = input
398        .points
399        .iter()
400        .filter(|point| point.kind == PointKind::Function)
401        .collect::<Vec<_>>();
402    let lines = count(
403        input.lines.iter().filter(|covered| **covered).count(),
404        input.lines.len(),
405    );
406    let statements = count(
407        statements.iter().filter(|point| point.covered).count(),
408        statements.len(),
409    );
410    let functions = count(
411        functions.iter().filter(|point| point.covered).count(),
412        functions.len(),
413    );
414    // A decision's outcomes reach here as a branch of their own, beside the
415    // decision, so branch coverage is the branches alone.
416    let branches = count(generic_alternative_covered, generic_alternative_total);
417    let decision_outcomes = count(decision_outcome_covered, input.decisions.len() * 2);
418    let condition_outcomes = count(condition_outcome_covered, conditions * 2);
419    let value_selections = count(value_alternative_covered, value_alternative_total);
420    let condition_coverage_pct = percentage(covered_conditions, conditions);
421    let coverage_complete = lines.percentage == 100.0
422        && statements.percentage == 100.0
423        && functions.percentage == 100.0
424        && branches.percentage == 100.0
425        && condition_outcomes.percentage == 100.0
426        && condition_coverage_pct == 100.0;
427    Ok(CoverageCoreOutput {
428        witnesses,
429        summary: CoverageSummary {
430            unmeasured_obligations: None,
431            exact_fraction_pct: None,
432            decisions: input.decisions.len(),
433            executed_decisions,
434            covered_decisions,
435            conditions,
436            covered_conditions,
437            condition_coverage_pct,
438            lines,
439            statements,
440            functions,
441            branches,
442            decision_outcomes,
443            condition_outcomes,
444            value_selections,
445            coverage_complete,
446            completeness_blocked: None,
447        },
448    })
449}
450
451#[cfg(test)]
452mod tests {
453    use serde::{Deserialize, Serialize};
454
455    use super::*;
456
457    #[derive(Deserialize)]
458    struct Oracle {
459        conditions: usize,
460        #[serde(rename = "observedVectors")]
461        observed_vectors: Vec<Vec<Option<bool>>>,
462        outcomes: Vec<bool>,
463        cases: Vec<OracleCase>,
464    }
465
466    #[derive(Deserialize)]
467    #[serde(rename_all = "camelCase")]
468    struct OracleCase {
469        input_indexes: Vec<usize>,
470        covered_conditions: usize,
471    }
472
473    #[derive(Serialize)]
474    struct JavascriptNumber {
475        #[serde(serialize_with = "serialize_javascript_number")]
476        value: f64,
477    }
478
479    #[test]
480    fn serializes_positive_and_negative_integers_like_json_stringify() {
481        assert_eq!(
482            serde_json::to_string(&JavascriptNumber { value: 50.0 }).unwrap(),
483            r#"{"value":50}"#
484        );
485        assert_eq!(
486            serde_json::to_string(&JavascriptNumber { value: -50.0 }).unwrap(),
487            r#"{"value":-50}"#
488        );
489        assert_eq!(
490            serde_json::to_string(&JavascriptNumber { value: -0.0 }).unwrap(),
491            r#"{"value":0}"#
492        );
493    }
494
495    #[test]
496    fn masking_witnesses_match_the_independent_clang_oracle() {
497        let oracle: Oracle = serde_json::from_str(include_str!(
498            "../../../tests/fixtures/clang-mcdc/oracle.json"
499        ))
500        .expect("Clang oracle fixture must be valid JSON");
501        for case in oracle.cases {
502            let vectors = case
503                .input_indexes
504                .iter()
505                .map(|index| McdcVector {
506                    values: oracle.observed_vectors[*index].clone(),
507                    outcome: oracle.outcomes[*index],
508                })
509                .collect::<Vec<_>>();
510            let witnesses = find_witnesses(&vectors).expect("uniform oracle vectors");
511            assert_eq!(witnesses.len(), oracle.conditions);
512            assert_eq!(
513                witnesses.iter().filter(|witness| witness.is_some()).count(),
514                case.covered_conditions
515            );
516        }
517    }
518
519    #[test]
520    fn bitset_search_preserves_the_frozen_first_pair_order() {
521        let vectors = vec![
522            McdcVector {
523                values: vec![Some(false), None],
524                outcome: false,
525            },
526            McdcVector {
527                values: vec![Some(true), Some(false)],
528                outcome: false,
529            },
530            McdcVector {
531                values: vec![Some(true), Some(true)],
532                outcome: true,
533            },
534            McdcVector {
535                values: vec![Some(false), None],
536                outcome: false,
537            },
538        ];
539        assert_eq!(
540            find_witnesses(&vectors).unwrap(),
541            vec![
542                Some(WitnessIndexes {
543                    first: 0,
544                    second: 2
545                }),
546                Some(WitnessIndexes {
547                    first: 1,
548                    second: 2
549                }),
550            ]
551        );
552    }
553
554    #[test]
555    fn rejects_mixed_vector_widths() {
556        assert_eq!(
557            find_witnesses(&[
558                McdcVector {
559                    values: vec![Some(true)],
560                    outcome: true
561                },
562                McdcVector {
563                    values: vec![],
564                    outcome: false
565                },
566            ]),
567            Err(AnalysisError::InconsistentVectorWidth {
568                vector: 1,
569                expected: 1,
570                actual: 0,
571            })
572        );
573    }
574
575    #[test]
576    fn retains_unexecuted_manifest_conditions_in_the_denominator() {
577        let output = analyze_core(&CoverageCoreInput {
578            decisions: vec![DecisionCoverage {
579                condition_count: 3,
580                vectors: vec![],
581            }],
582            points: vec![],
583            branches: vec![],
584            lines: vec![],
585        })
586        .unwrap();
587        assert_eq!(output.summary.conditions, 3);
588        assert_eq!(output.summary.covered_conditions, 0);
589        assert_eq!(output.summary.condition_coverage_pct, 0.0);
590        assert_eq!(output.witnesses, vec![vec![None, None, None]]);
591    }
592}