ripr 0.7.0

Find static mutation-exposure gaps before expensive mutation testing
Documentation
use super::super::rust_index::{TestSummary, extract_literals};
use super::activation::has_observed_boundary_equality;
use crate::domain::*;

pub(in crate::analysis) fn infection_evidence(
    probe: &Probe,
    related_tests: &[&TestSummary],
    activation: &ActivationEvidence,
) -> StageEvidence {
    match probe.family {
        ProbeFamily::Predicate => {
            let probe_literals = extract_literals(&probe.expression);
            let test_literals = related_tests
                .iter()
                .flat_map(|test| test.literals.iter().map(|literal| literal.value.clone()))
                .collect::<Vec<_>>();
            if related_tests.is_empty() {
                StageEvidence::new(
                    StageState::Unknown,
                    Confidence::Low,
                    "No tests were found, so activation/infection cannot be estimated",
                )
            } else if activation
                .missing_discriminators
                .iter()
                .any(|fact| fact.value.contains("=="))
            {
                StageEvidence::new(
                    StageState::Weak,
                    Confidence::Medium,
                    "Related tests contain input values, but the equality-boundary discriminator is missing",
                )
            } else if has_observed_boundary_equality(activation) {
                StageEvidence::new(
                    StageState::Yes,
                    Confidence::Medium,
                    "Detected related test input at the changed boundary",
                )
            } else if probe_literals.is_empty() {
                StageEvidence::new(
                    StageState::Unknown,
                    Confidence::Low,
                    "Predicate changed, but no literal boundary was visible in the changed expression",
                )
            } else if probe_literals
                .iter()
                .any(|literal| test_literals.iter().any(|t| t == literal))
            {
                StageEvidence::new(
                    StageState::Yes,
                    Confidence::Medium,
                    format!(
                        "Detected test input literal matching changed boundary: {}",
                        probe_literals.join(", ")
                    ),
                )
            } else if !test_literals.is_empty() {
                StageEvidence::new(
                    StageState::Weak,
                    Confidence::Medium,
                    format!(
                        "Tests have literals [{}], but no detected value matches changed boundary [{}]",
                        test_literals.join(", "),
                        probe_literals.join(", ")
                    ),
                )
            } else {
                StageEvidence::new(
                    StageState::Unknown,
                    Confidence::Low,
                    "Related tests use opaque fixtures; activation/infection is unknown",
                )
            }
        }
        ProbeFamily::StaticUnknown => StageEvidence::new(
            StageState::Unknown,
            Confidence::Unknown,
            "Changed syntax is not mapped to a high-confidence probe family",
        ),
        _ => {
            if related_tests.is_empty() {
                StageEvidence::new(
                    StageState::Unknown,
                    Confidence::Low,
                    "No reachable tests were found, so infection cannot be established",
                )
            } else {
                StageEvidence::new(
                    StageState::Yes,
                    Confidence::Medium,
                    "Reachable tests can plausibly activate this changed behavior",
                )
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::super::super::rust_index::LiteralFact;
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn predicate_infection_uses_matching_test_literal() {
        let probe = probe(ProbeFamily::Predicate, "value > 10");
        let test = test_with_literals(&["10"]);
        let evidence = infection_evidence(&probe, &[&test], &ActivationEvidence::default());

        assert_eq!(evidence.state, StageState::Yes);
        assert_eq!(
            evidence.summary,
            "Detected test input literal matching changed boundary: 10"
        );
    }

    #[test]
    fn predicate_infection_matches_decimal_exponent_case() {
        let probe = probe(ProbeFamily::Predicate, "ratio < 4E-2");
        let test = test_with_literals(&["4e-2"]);
        let evidence = infection_evidence(&probe, &[&test], &ActivationEvidence::default());

        assert_eq!(evidence.state, StageState::Yes);
        assert_eq!(
            evidence.summary,
            "Detected test input literal matching changed boundary: 4e-2"
        );
    }

    #[test]
    fn predicate_infection_reports_opaque_fixture_when_literals_are_missing() {
        let probe = probe(ProbeFamily::Predicate, "value > 10");
        let test = test_with_literals(&[]);
        let evidence = infection_evidence(&probe, &[&test], &ActivationEvidence::default());

        assert_eq!(evidence.state, StageState::Unknown);
        assert_eq!(
            evidence.summary,
            "Related tests use opaque fixtures; activation/infection is unknown"
        );
    }

    #[test]
    fn non_predicate_infection_without_related_tests_is_unknown() {
        let probe = probe(ProbeFamily::ReturnValue, "value + 1");
        let evidence = infection_evidence(&probe, &[], &ActivationEvidence::default());

        assert_eq!(evidence.state, StageState::Unknown);
        assert_eq!(
            evidence.summary,
            "No reachable tests were found, so infection cannot be established"
        );
    }

    fn probe(family: ProbeFamily, expression: &str) -> Probe {
        Probe {
            id: ProbeId("probe:test".to_string()),
            location: SourceLocation::new("src/lib.rs", 1, 1),
            owner: None,
            family,
            delta: DeltaKind::Value,
            before: None,
            after: None,
            expression: expression.to_string(),
            expected_sinks: Vec::new(),
            required_oracles: Vec::new(),
        }
    }

    fn test_with_literals(values: &[&str]) -> TestSummary {
        TestSummary {
            name: "value_boundary".to_string(),
            file: PathBuf::from("tests/value.rs"),
            start_line: 1,
            end_line: 3,
            body: "assert_eq!(score(10), 11);".to_string(),
            calls: Vec::new(),
            assertions: Vec::new(),
            literals: values
                .iter()
                .map(|value| LiteralFact {
                    line: 1,
                    value: (*value).to_string(),
                })
                .collect(),
            attrs: Vec::new(),
        }
    }
}