cu-safetymon 1.2.0

Safety monitor for Copper: watchdog, panic capture, fault latching with exit codes.
use crate::CuSafetyMon;
use cu29::prelude::*;

const MONITORED_COMPONENTS: &[MonitorComponentMetadata] = &[
    MonitorComponentMetadata::new("planner", ComponentType::Task, None),
    MonitorComponentMetadata::new("driver", ComponentType::Task, None),
];
const CULIST_COMPONENT_MAPPING: &[ComponentId] = &[ComponentId::new(0)];

fn config_with_single_monitor(extra_config: &str) -> CuConfig {
    let ron = format!(
        r#"
(
    tasks: [],
    cnx: [],
    monitors: [(
        type: "cu_safetymon::CuSafetyMon",
        config: {{
            {extra_config}
        }},
    )],
)
"#
    );
    CuConfig::deserialize_ron(&ron).expect("failed to parse monitor config")
}

fn monitor_metadata(config: &CuConfig) -> (CuMonitoringMetadata, CuMonitoringRuntime) {
    let metadata = CuMonitoringMetadata::new(
        DEFAULT_MISSION_ID.into(),
        MONITORED_COMPONENTS,
        CULIST_COMPONENT_MAPPING,
        CopperListInfo::new(0, 0),
        MonitorTopology::default(),
        config
            .get_monitor_configs()
            .first()
            .and_then(|entry| entry.get_config().cloned()),
    )
    .expect("valid monitor metadata");
    let runtime = CuMonitoringRuntime::default();
    (metadata, runtime)
}

fn new_monitor(extra_config: &str) -> CuResult<CuSafetyMon> {
    let config = config_with_single_monitor(extra_config);
    let (metadata, runtime) = monitor_metadata(&config);
    CuSafetyMon::new(metadata, runtime)
}

#[cfg_attr(test, test)]
#[safety_case("SMON-TEST-001")]
fn safety_monitor_rejects_non_positive_watchdog_timing() {
    let deadline_error = match new_monitor(r#""copperlist_deadline_ms": 0,"#) {
        Ok(_) => panic!("expected invalid copperlist_deadline_ms to fail"),
        Err(error) => error,
    };
    safety_check!(
        "SMON-TEST-001-C1",
        "SMON-REQ-001",
        deadline_error
            .to_string()
            .contains("copperlist_deadline_ms must be > 0"),
    );

    let period_error = match new_monitor(
        r#"
        "copperlist_deadline_ms": 100,
        "watchdog_period_ms": 0,
    "#,
    ) {
        Ok(_) => panic!("expected invalid watchdog_period_ms to fail"),
        Err(error) => error,
    };
    safety_check!(
        "SMON-TEST-001-C2",
        "SMON-REQ-001",
        period_error
            .to_string()
            .contains("watchdog_period_ms must be > 0"),
    );
}

#[cfg_attr(test, test)]
#[safety_case("SMON-TEST-002")]
fn safety_monitor_accepts_default_and_configured_fault_codes() {
    safety_check!("SMON-TEST-002-C1", "SMON-REQ-002", new_monitor("").is_ok(),);

    safety_check!(
        "SMON-TEST-002-C2",
        "SMON-REQ-002",
        new_monitor(
            r#"
            "copperlist_deadline_ms": 10,
            "watchdog_period_ms": 5,
            "exit_code_shutdown": 65,
            "exit_code_lock": 66,
            "exit_code_panic": 67,
        "#
        )
        .is_ok(),
    );
}

#[cfg_attr(test, test)]
#[safety_case("SMON-TEST-003")]
fn safety_monitor_turns_runtime_errors_into_shutdown_decisions() {
    let monitor = new_monitor(
        r#"
        "copperlist_deadline_ms": 1000,
        "watchdog_period_ms": 100,
        "exit_code_shutdown": 65,
    "#,
    )
    .expect("safety monitor");

    let decision = monitor.process_error(
        ComponentId::new(1),
        CuComponentState::Process,
        &CuError::from("fault"),
    );
    safety_check!(
        "SMON-TEST-003-C1",
        "SMON-REQ-003",
        matches!(decision, Decision::Shutdown),
    );
}

#[cfg(feature = "safety-ids")]
pub fn link_safety_ids() {
    let _ = safety_monitor_rejects_non_positive_watchdog_timing as fn();
    let _ = safety_monitor_accepts_default_and_configured_fault_codes as fn();
    let _ = safety_monitor_turns_runtime_errors_into_shutdown_decisions as fn();
}