sim-lib-view-device 0.1.0

Device profiles, frame timing, and tier derivation for SIM Web surfaces.
Documentation
use sim_kernel::Expr;
use sim_lib_view::SurfaceCaps;
use sim_value::access;

use crate::{
    DegradationResolver, DeviceProfile, DeviceSurfaceCapsExt, DeviceTier, ObservedRoute, RateClass,
    derive_tier, tier_preset,
};

#[test]
fn presets_at_each_tier_round_trip_through_expr() {
    for tier in DeviceTier::ALL {
        let profile = tier_preset(tier);
        assert_eq!(profile.tier, tier);
        let back = DeviceProfile::from_expr(&profile.to_expr()).expect("profile round-trips");
        assert_eq!(back, profile);
    }
}

#[test]
fn derive_tier_is_authoritative_and_rate_defaults_safe() {
    for tier in DeviceTier::ALL {
        let profile = tier_preset(tier);
        assert_eq!(derive_tier(&profile), tier);
        for lower in DeviceTier::ALL {
            assert_eq!(tier.supports(lower), tier >= lower);
        }
    }
}

#[test]
fn surface_caps_extension_derives_rate_and_tier() {
    let caps = SurfaceCaps::from_preset("watch", "watch.local.1").expect("watch caps");
    let profile = caps.device_profile();
    assert_eq!(profile.kind.name.as_ref(), "watch");
    assert_eq!(profile.rate, RateClass::watch());
    assert_eq!(profile.tier, DeviceTier::Actuator);
}

#[test]
fn missing_sensor_field_degrades_with_reason() {
    let requested = tier_preset(DeviceTier::Rich);
    let mut observed = ObservedRoute::from_profile(&requested);
    observed
        .streams
        .retain(|symbol| symbol.name.as_ref() != "pose");

    let degradation = DegradationResolver::resolve(&requested, &observed);

    assert_eq!(degradation.tier, DeviceTier::Actuator);
    assert!(
        degradation
            .reasons
            .iter()
            .any(|reason| reason == "missing stream: pose")
    );
}

#[test]
fn missing_rate_map_defaults_to_safe_envelope() {
    let mut entries = match SurfaceCaps::from_preset("watch", "watch.local.2")
        .expect("watch caps")
        .to_expr()
    {
        Expr::Map(entries) => entries,
        _ => unreachable!(),
    };
    entries
        .retain(|(key, _)| !matches!(key, Expr::Symbol(symbol) if symbol.name.as_ref() == "rate"));

    let caps = SurfaceCaps::from_expr(&Expr::Map(entries)).expect("older caps parse");

    assert_eq!(caps.device_rate(), RateClass::safe_default());
    assert_eq!(
        access::field(&caps.rate, "content-hz"),
        access::field(&RateClass::safe_default().to_expr(), "content-hz")
    );
}