ripr 0.5.0

Find Rust test-oracle gaps before mutation testing with static RIPR evidence
Documentation
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use super::{
    ExposureClass, OracleKind, OracleStrength, ProbeId, RiprEvidence, SourceLocation, SymbolId,
};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProbeFamily {
    Predicate,
    ReturnValue,
    ErrorPath,
    CallDeletion,
    FieldConstruction,
    SideEffect,
    MatchArm,
    StaticUnknown,
}

impl ProbeFamily {
    pub fn as_str(&self) -> &'static str {
        match self {
            ProbeFamily::Predicate => "predicate",
            ProbeFamily::ReturnValue => "return_value",
            ProbeFamily::ErrorPath => "error_path",
            ProbeFamily::CallDeletion => "call_deletion",
            ProbeFamily::FieldConstruction => "field_construction",
            ProbeFamily::SideEffect => "side_effect",
            ProbeFamily::MatchArm => "match_arm",
            ProbeFamily::StaticUnknown => "static_unknown",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeltaKind {
    Value,
    Control,
    Effect,
    Unknown,
}

impl DeltaKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            DeltaKind::Value => "value",
            DeltaKind::Control => "control",
            DeltaKind::Effect => "effect",
            DeltaKind::Unknown => "unknown",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StopReason {
    MaxDepthReached,
    ExternalCrateBoundary,
    DynamicDispatchUnresolved,
    ProcMacroOpaque,
    FixtureOpaque,
    FeatureUnknown,
    AsyncBoundaryOpaque,
    NoChangedRustLine,
    InfectionEvidenceUnknown,
    PropagationEvidenceUnknown,
    StaticProbeUnknown,
}

impl StopReason {
    pub fn as_str(&self) -> &'static str {
        match self {
            StopReason::MaxDepthReached => "max_depth_reached",
            StopReason::ExternalCrateBoundary => "external_crate_boundary",
            StopReason::DynamicDispatchUnresolved => "dynamic_dispatch_unresolved",
            StopReason::ProcMacroOpaque => "proc_macro_opaque",
            StopReason::FixtureOpaque => "fixture_opaque",
            StopReason::FeatureUnknown => "feature_unknown",
            StopReason::AsyncBoundaryOpaque => "async_boundary_opaque",
            StopReason::NoChangedRustLine => "no_changed_rust_line",
            StopReason::InfectionEvidenceUnknown => "infection_evidence_unknown",
            StopReason::PropagationEvidenceUnknown => "propagation_evidence_unknown",
            StopReason::StaticProbeUnknown => "static_probe_unknown",
        }
    }

    pub fn for_unknown_class(class: &ExposureClass) -> Option<Self> {
        match class {
            ExposureClass::InfectionUnknown => Some(StopReason::InfectionEvidenceUnknown),
            ExposureClass::PropagationUnknown => Some(StopReason::PropagationEvidenceUnknown),
            ExposureClass::StaticUnknown => Some(StopReason::StaticProbeUnknown),
            _ => None,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Probe {
    pub id: ProbeId,
    pub location: SourceLocation,
    pub owner: Option<SymbolId>,
    pub family: ProbeFamily,
    pub delta: DeltaKind,
    pub before: Option<String>,
    pub after: Option<String>,
    pub expression: String,
    pub expected_sinks: Vec<String>,
    pub required_oracles: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FlowSinkKind {
    ReturnValue,
    ErrorVariant,
    StructField,
    EventCall,
    StateWrite,
    Persistence,
    LogMessage,
    ConfigChange,
    CallEffect,
    MatchArm,
    Unknown,
}

impl FlowSinkKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            FlowSinkKind::ReturnValue => "return_value",
            FlowSinkKind::ErrorVariant => "error_variant",
            FlowSinkKind::StructField => "struct_field",
            FlowSinkKind::EventCall => "event_call",
            FlowSinkKind::StateWrite => "state_write",
            FlowSinkKind::Persistence => "persistence",
            FlowSinkKind::LogMessage => "log_message",
            FlowSinkKind::ConfigChange => "config_change",
            FlowSinkKind::CallEffect => "call_effect",
            FlowSinkKind::MatchArm => "match_arm",
            FlowSinkKind::Unknown => "unknown",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            FlowSinkKind::ReturnValue => "returned value",
            FlowSinkKind::ErrorVariant => "error variant",
            FlowSinkKind::StructField => "constructed field",
            FlowSinkKind::EventCall => "event or outbound call",
            FlowSinkKind::StateWrite => "state write",
            FlowSinkKind::Persistence => "persistence write",
            FlowSinkKind::LogMessage => "log message",
            FlowSinkKind::ConfigChange => "configuration change",
            FlowSinkKind::CallEffect => "call effect",
            FlowSinkKind::MatchArm => "match arm result",
            FlowSinkKind::Unknown => "unknown sink",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FlowSinkFact {
    pub kind: FlowSinkKind,
    pub text: String,
    pub line: usize,
    pub owner: Option<SymbolId>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValueContext {
    FunctionArgument,
    AssertionArgument,
    BuilderMethod,
    TableRow,
    EnumVariant,
    ReturnValue,
    Unknown,
}

impl ValueContext {
    pub fn as_str(&self) -> &'static str {
        match self {
            ValueContext::FunctionArgument => "function_argument",
            ValueContext::AssertionArgument => "assertion_argument",
            ValueContext::BuilderMethod => "builder_method",
            ValueContext::TableRow => "table_row",
            ValueContext::EnumVariant => "enum_variant",
            ValueContext::ReturnValue => "return_value",
            ValueContext::Unknown => "unknown",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValueFact {
    pub line: usize,
    pub text: String,
    pub value: String,
    pub context: ValueContext,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MissingDiscriminatorFact {
    pub value: String,
    pub reason: String,
    pub flow_sink: Option<FlowSinkFact>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ActivationEvidence {
    pub observed_values: Vec<ValueFact>,
    pub missing_discriminators: Vec<MissingDiscriminatorFact>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RelatedTest {
    pub name: String,
    pub file: PathBuf,
    pub line: usize,
    pub oracle: Option<String>,
    pub oracle_kind: OracleKind,
    pub oracle_strength: OracleStrength,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Finding {
    pub id: String,
    pub probe: Probe,
    pub class: ExposureClass,
    pub ripr: RiprEvidence,
    pub confidence: f32,
    pub evidence: Vec<String>,
    pub missing: Vec<String>,
    pub flow_sinks: Vec<FlowSinkFact>,
    pub activation: ActivationEvidence,
    pub stop_reasons: Vec<StopReason>,
    pub related_tests: Vec<RelatedTest>,
    pub recommended_next_step: Option<String>,
}

impl Finding {
    pub fn unknown_has_stop_reason(&self) -> bool {
        !self.class.requires_stop_reason() || !self.stop_reasons.is_empty()
    }

    pub fn effective_stop_reasons(&self) -> Vec<StopReason> {
        if self.unknown_has_stop_reason() {
            return self.stop_reasons.clone();
        }
        StopReason::for_unknown_class(&self.class)
            .into_iter()
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::{FlowSinkKind, StopReason, ValueContext};
    use crate::domain::ExposureClass;

    #[test]
    fn flow_sink_kind_labels_are_stable_contract_terms() {
        let cases = [
            (FlowSinkKind::ReturnValue, "return_value", "returned value"),
            (FlowSinkKind::ErrorVariant, "error_variant", "error variant"),
            (
                FlowSinkKind::StructField,
                "struct_field",
                "constructed field",
            ),
            (
                FlowSinkKind::EventCall,
                "event_call",
                "event or outbound call",
            ),
            (FlowSinkKind::StateWrite, "state_write", "state write"),
            (
                FlowSinkKind::Persistence,
                "persistence",
                "persistence write",
            ),
            (FlowSinkKind::LogMessage, "log_message", "log message"),
            (
                FlowSinkKind::ConfigChange,
                "config_change",
                "configuration change",
            ),
            (FlowSinkKind::CallEffect, "call_effect", "call effect"),
            (FlowSinkKind::MatchArm, "match_arm", "match arm result"),
            (FlowSinkKind::Unknown, "unknown", "unknown sink"),
        ];

        for (kind, value, label) in cases {
            assert_eq!(kind.as_str(), value);
            assert_eq!(kind.label(), label);
        }
    }

    #[test]
    fn stop_reason_for_unknown_class_matches_contract() {
        assert_eq!(
            StopReason::for_unknown_class(&ExposureClass::PropagationUnknown)
                .map(|reason| reason.as_str()),
            Some("propagation_evidence_unknown")
        );
        assert_eq!(StopReason::for_unknown_class(&ExposureClass::Exposed), None);
    }

    #[test]
    fn value_context_labels_are_stable_contract_terms() {
        let cases = [
            (ValueContext::FunctionArgument, "function_argument"),
            (ValueContext::AssertionArgument, "assertion_argument"),
            (ValueContext::BuilderMethod, "builder_method"),
            (ValueContext::TableRow, "table_row"),
            (ValueContext::EnumVariant, "enum_variant"),
            (ValueContext::ReturnValue, "return_value"),
            (ValueContext::Unknown, "unknown"),
        ];

        for (context, value) in cases {
            assert_eq!(context.as_str(), value);
        }
    }
}