ripr 0.9.0

Find static mutation-exposure gaps before expensive mutation testing
Documentation
mod arguments;
mod classify;
mod patterns;
mod scan;

pub(crate) use classify::classify_assertion;
#[cfg(test)]
pub(crate) use patterns::contains_macro_invocation;
pub(crate) use scan::{extract_assertions, extract_line_scanned_oracles};

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::{OracleKind, OracleStrength};

    fn assert_classification(line: &str, kind: OracleKind, strength: OracleStrength) {
        let classification = classify_assertion(line);
        assert_eq!(classification.kind, kind, "unexpected kind for {line}");
        assert_eq!(
            classification.strength, strength,
            "unexpected strength for {line}"
        );
    }

    #[test]
    fn classify_assertion_distinguishes_error_specificity() {
        assert_classification(
            "assert_matches!(result, Err(ParseError::InvalidDigit));",
            OracleKind::ExactErrorVariant,
            OracleStrength::Strong,
        );
        assert_classification(
            "assert!(result.is_err());",
            OracleKind::BroadError,
            OracleStrength::Weak,
        );
        assert_classification(
            "assert_matches!(result, Err(_));",
            OracleKind::BroadError,
            OracleStrength::Weak,
        );
    }

    #[test]
    fn classify_assertion_downgrades_duplicative_equality_even_with_nested_commas() {
        assert_classification(
            r#"assert_eq!(render(vec!["a,b", nested(1, 2)]), render(vec!["a,b", nested(1, 2)]));"#,
            OracleKind::RelationalCheck,
            OracleStrength::Weak,
        );
        assert_classification(
            "assert_eq!(actual.total, expected.total);",
            OracleKind::ExactValue,
            OracleStrength::Strong,
        );
    }

    #[test]
    fn classify_assertion_recognizes_snapshot_and_custom_exact_helpers() {
        assert_classification(
            "insta::assert_json_snapshot!(payload);",
            OracleKind::Snapshot,
            OracleStrength::Medium,
        );
        assert_classification(
            "assert_payload_eq(actual, expected_with_commas(\"a,b\"));",
            OracleKind::ExactValue,
            OracleStrength::Strong,
        );
        assert_classification(
            "helpers::assert_equal(actual, expected);",
            OracleKind::ExactValue,
            OracleStrength::Strong,
        );
        assert_classification(
            "checker.assert_matches(expected_pattern);",
            OracleKind::ExactValue,
            OracleStrength::Strong,
        );
    }

    #[test]
    fn extract_assertions_preserves_line_numbers_and_observed_tokens() {
        let facts = extract_assertions(
            r#"
let value = compute();
assert_eq!(value, expected_count);
assert!(events.contains(&Event::Saved));
"#,
            40,
        );

        assert_eq!(facts.len(), 2);
        assert_eq!(facts[0].line, 42);
        assert_eq!(facts[0].kind, OracleKind::ExactValue);
        assert!(facts[0].observed_tokens.contains(&"value".to_string()));
        assert!(
            facts[0]
                .observed_tokens
                .contains(&"expected_count".to_string())
        );
        assert_eq!(facts[1].line, 43);
        assert_eq!(facts[1].kind, OracleKind::MockExpectation);
        assert_eq!(facts[1].strength, OracleStrength::Medium);
    }

    #[test]
    fn extract_line_scanned_oracles_captures_helpers_without_general_asserts() {
        let facts = extract_line_scanned_oracles(
            r#"
assert_eq!(actual, expected);
mock_writer.verify();
assert_payload_eq(actual, expected);
expect_metric_recorded(counter);
"#,
            7,
        );

        let lines: Vec<usize> = facts.iter().map(|fact| fact.line).collect();
        assert_eq!(lines, vec![9, 10, 11]);
        assert_eq!(facts[0].kind, OracleKind::MockExpectation);
        assert_eq!(facts[1].kind, OracleKind::ExactValue);
        assert_eq!(facts[2].kind, OracleKind::MockExpectation);
    }

    #[test]
    fn contains_macro_invocation_requires_token_boundaries_and_invocation_suffix() {
        assert!(contains_macro_invocation(
            "assert_snapshot!{payload}",
            "assert_snapshot!"
        ));
        assert!(contains_macro_invocation(
            "insta::assert_snapshot![payload]",
            "assert_snapshot!"
        ));
        assert!(!contains_macro_invocation(
            "my_assert_snapshot!(payload)",
            "assert_snapshot!"
        ));
        assert!(!contains_macro_invocation(
            "assert_snapshot_helper(payload)",
            "assert_snapshot!"
        ));
        assert!(!contains_macro_invocation(
            "assert_snapshot!",
            "assert_snapshot!"
        ));
    }

    #[test]
    fn extract_assertions_keeps_line_numbers_and_observed_tokens() {
        let body = "let value = compute();\nassert_eq!(value, expected_total);\nvalue.unwrap();";

        let assertions = extract_assertions(body, 20);

        assert_eq!(assertions.len(), 2);
        assert_eq!(assertions[0].line, 21);
        assert_eq!(assertions[0].kind, OracleKind::ExactValue);
        assert_eq!(assertions[0].strength, OracleStrength::Strong);
        assert_eq!(
            assertions[0].observed_tokens,
            vec!["expected_total", "value"]
        );
        assert_eq!(assertions[1].line, 22);
        assert_eq!(assertions[1].kind, OracleKind::SmokeOnly);
        assert_eq!(assertions[1].strength, OracleStrength::Smoke);
    }

    #[test]
    fn classify_assertion_distinguishes_exact_and_broad_error_oracles() {
        let exact = classify_assertion("assert_matches!(result, Err(ConfigError::MissingPort))");
        let broad = classify_assertion("assert!(result.is_err())");

        assert_eq!(exact.kind, OracleKind::ExactErrorVariant);
        assert_eq!(exact.strength, OracleStrength::Strong);
        assert_eq!(broad.kind, OracleKind::BroadError);
        assert_eq!(broad.strength, OracleStrength::Weak);
    }

    #[test]
    fn classify_assertion_marks_duplicative_equality_as_weak() {
        let classification = classify_assertion("assert_eq!(&actual, actual)");

        assert_eq!(classification.kind, OracleKind::RelationalCheck);
        assert_eq!(classification.strength, OracleStrength::Weak);
    }

    #[test]
    fn classify_assertion_handles_nested_commas_in_custom_exact_helpers() {
        let classification = classify_assertion(
            "crate::helpers::assert_config_matches(actual, Config { ports: [80, 443] })",
        );

        assert_eq!(classification.kind, OracleKind::ExactValue);
        assert_eq!(classification.strength, OracleStrength::Strong);
    }

    #[test]
    fn contains_macro_invocation_requires_identifier_boundary_and_delimiter() {
        assert!(contains_macro_invocation(
            "insta::assert_json_snapshot!({\"ok\": true})",
            "assert_json_snapshot!"
        ));
        assert!(!contains_macro_invocation(
            "my_assert_json_snapshot!({\"ok\": true})",
            "assert_json_snapshot!"
        ));
        assert!(!contains_macro_invocation(
            "assert_json_snapshot_helper({\"ok\": true})",
            "assert_json_snapshot!"
        ));
    }

    #[test]
    fn line_scanned_oracles_include_mock_expectations_without_macro_asserts() {
        let body = "mock_service.expect_publish().times(1);\nlet value = 1;";

        let oracles = extract_line_scanned_oracles(body, 7);

        assert_eq!(oracles.len(), 1);
        assert_eq!(oracles[0].line, 7);
        assert_eq!(oracles[0].kind, OracleKind::MockExpectation);
        assert_eq!(oracles[0].strength, OracleStrength::Medium);
    }
}