1use 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
100pub 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
109pub 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 #[serde(skip_serializing_if = "Option::is_none")]
249 pub unmeasured_obligations: Option<usize>,
250 #[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
272pub(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 let branches = count(
415 decision_outcome_covered + generic_alternative_covered,
416 input.decisions.len() * 2 + generic_alternative_total,
417 );
418 let decision_outcomes = count(decision_outcome_covered, input.decisions.len() * 2);
419 let condition_outcomes = count(condition_outcome_covered, conditions * 2);
420 let value_selections = count(value_alternative_covered, value_alternative_total);
421 let condition_coverage_pct = percentage(covered_conditions, conditions);
422 let coverage_complete = lines.percentage == 100.0
423 && statements.percentage == 100.0
424 && functions.percentage == 100.0
425 && branches.percentage == 100.0
426 && condition_outcomes.percentage == 100.0
427 && condition_coverage_pct == 100.0;
428 Ok(CoverageCoreOutput {
429 witnesses,
430 summary: CoverageSummary {
431 unmeasured_obligations: None,
432 exact_fraction_pct: None,
433 decisions: input.decisions.len(),
434 executed_decisions,
435 covered_decisions,
436 conditions,
437 covered_conditions,
438 condition_coverage_pct,
439 lines,
440 statements,
441 functions,
442 branches,
443 decision_outcomes,
444 condition_outcomes,
445 value_selections,
446 coverage_complete,
447 completeness_blocked: None,
448 },
449 })
450}
451
452#[cfg(test)]
453mod tests {
454 use serde::{Deserialize, Serialize};
455
456 use super::*;
457
458 #[derive(Deserialize)]
459 struct Oracle {
460 conditions: usize,
461 #[serde(rename = "observedVectors")]
462 observed_vectors: Vec<Vec<Option<bool>>>,
463 outcomes: Vec<bool>,
464 cases: Vec<OracleCase>,
465 }
466
467 #[derive(Deserialize)]
468 #[serde(rename_all = "camelCase")]
469 struct OracleCase {
470 input_indexes: Vec<usize>,
471 covered_conditions: usize,
472 }
473
474 #[derive(Serialize)]
475 struct JavascriptNumber {
476 #[serde(serialize_with = "serialize_javascript_number")]
477 value: f64,
478 }
479
480 #[test]
481 fn serializes_positive_and_negative_integers_like_json_stringify() {
482 assert_eq!(
483 serde_json::to_string(&JavascriptNumber { value: 50.0 }).unwrap(),
484 r#"{"value":50}"#
485 );
486 assert_eq!(
487 serde_json::to_string(&JavascriptNumber { value: -50.0 }).unwrap(),
488 r#"{"value":-50}"#
489 );
490 assert_eq!(
491 serde_json::to_string(&JavascriptNumber { value: -0.0 }).unwrap(),
492 r#"{"value":0}"#
493 );
494 }
495
496 #[test]
497 fn masking_witnesses_match_the_independent_clang_oracle() {
498 let oracle: Oracle = serde_json::from_str(include_str!(
499 "../../../tests/fixtures/clang-mcdc/oracle.json"
500 ))
501 .expect("Clang oracle fixture must be valid JSON");
502 for case in oracle.cases {
503 let vectors = case
504 .input_indexes
505 .iter()
506 .map(|index| McdcVector {
507 values: oracle.observed_vectors[*index].clone(),
508 outcome: oracle.outcomes[*index],
509 })
510 .collect::<Vec<_>>();
511 let witnesses = find_witnesses(&vectors).expect("uniform oracle vectors");
512 assert_eq!(witnesses.len(), oracle.conditions);
513 assert_eq!(
514 witnesses.iter().filter(|witness| witness.is_some()).count(),
515 case.covered_conditions
516 );
517 }
518 }
519
520 #[test]
521 fn bitset_search_preserves_the_frozen_first_pair_order() {
522 let vectors = vec![
523 McdcVector {
524 values: vec![Some(false), None],
525 outcome: false,
526 },
527 McdcVector {
528 values: vec![Some(true), Some(false)],
529 outcome: false,
530 },
531 McdcVector {
532 values: vec![Some(true), Some(true)],
533 outcome: true,
534 },
535 McdcVector {
536 values: vec![Some(false), None],
537 outcome: false,
538 },
539 ];
540 assert_eq!(
541 find_witnesses(&vectors).unwrap(),
542 vec![
543 Some(WitnessIndexes {
544 first: 0,
545 second: 2
546 }),
547 Some(WitnessIndexes {
548 first: 1,
549 second: 2
550 }),
551 ]
552 );
553 }
554
555 #[test]
556 fn rejects_mixed_vector_widths() {
557 assert_eq!(
558 find_witnesses(&[
559 McdcVector {
560 values: vec![Some(true)],
561 outcome: true
562 },
563 McdcVector {
564 values: vec![],
565 outcome: false
566 },
567 ]),
568 Err(AnalysisError::InconsistentVectorWidth {
569 vector: 1,
570 expected: 1,
571 actual: 0,
572 })
573 );
574 }
575
576 #[test]
577 fn retains_unexecuted_manifest_conditions_in_the_denominator() {
578 let output = analyze_core(&CoverageCoreInput {
579 decisions: vec![DecisionCoverage {
580 condition_count: 3,
581 vectors: vec![],
582 }],
583 points: vec![],
584 branches: vec![],
585 lines: vec![],
586 })
587 .unwrap();
588 assert_eq!(output.summary.conditions, 3);
589 assert_eq!(output.summary.covered_conditions, 0);
590 assert_eq!(output.summary.condition_coverage_pct, 0.0);
591 assert_eq!(output.witnesses, vec![vec![None, None, None]]);
592 }
593}