sim-lib-stream-host 0.1.2

Host-device stream backend substrate for SIM streams.
Documentation
use sim_kernel::{Error, Expr, Symbol};
use sim_lib_stream_core::{BufferPolicy, PushResult, StreamMedia, StreamPacket};

use crate::{
    FakeBackend, HostBackendCapability, HostBackendRegistry, HostDeviceSpec, HostDirection,
    RtpMidiBackend, fake_backend_symbol, rtp_midi_backend_symbol, stream_host_capability,
    stream_host_device_read_effect_kind, stream_host_device_write_effect_kind,
};

use super::support::{authorized_cx, field, recorded_effect_kinds, test_cx};

#[test]
fn rtp_midi_backend_card_is_host_independent_and_bounded() {
    let backend = RtpMidiBackend::new();
    assert_eq!(backend.info().id(), &rtp_midi_backend_symbol());
    assert_eq!(backend.info().media(), StreamMedia::Midi);
    assert!(!backend.info().hardware_required());
    assert!(backend.info().callbacks_bounded());

    let card = backend.info().card_expr();
    assert_eq!(
        field(&card, "kind"),
        Some(&Expr::Symbol(Symbol::qualified("stream", "host-backend")))
    );
    assert_eq!(
        field(&card, "transport"),
        Some(&Expr::Symbol(Symbol::qualified(
            "stream/transport",
            "rtp-midi"
        )))
    );
}

#[test]
fn host_backend_registry_opens_fake_stream_without_hardware() {
    let mut registry = HostBackendRegistry::new();
    registry.register(FakeBackend::new()).unwrap();

    let inventories = registry.enumerate().unwrap();
    assert_eq!(inventories.len(), 1);
    assert!(
        inventories[0]
            .devices()
            .iter()
            .any(|device| device.id() == &Symbol::new("fake/data"))
    );

    let denied = match registry.open_checked(&mut test_cx(), FakeBackend::data_request(2).unwrap())
    {
        Ok(_) => panic!("host stream open should require stream.host"),
        Err(err) => err,
    };
    assert!(matches!(
        denied,
        Error::CapabilityDenied { capability } if capability == stream_host_capability()
    ));

    let mut cx = authorized_cx();
    let opened = registry
        .open_checked(&mut cx, FakeBackend::data_request(2).unwrap())
        .unwrap();
    assert_eq!(opened.config().backend(), &fake_backend_symbol());
    assert_eq!(
        opened
            .queue()
            .callback_packet(StreamPacket::data(
                Symbol::qualified("stream/data", "expr"),
                Expr::String("from fake".to_owned()),
            ))
            .unwrap(),
        PushResult::Accepted
    );
    assert_eq!(opened.queue().drain(4).unwrap().len(), 1);
}

#[test]
fn host_backend_registry_records_duplex_read_and_write_effects() {
    let mut registry = HostBackendRegistry::new();
    registry.register(FakeBackend::new()).unwrap();
    let request = FakeBackend::duplex_data_request(2).unwrap();
    let plan = registry.plan_open(&request).unwrap();
    assert_eq!(
        plan.effect_kinds(),
        &[
            stream_host_device_read_effect_kind(),
            stream_host_device_write_effect_kind()
        ]
    );

    let mut cx = authorized_cx();
    let opened = registry.open_checked(&mut cx, request).unwrap();

    assert_eq!(opened.config().direction(), HostDirection::Duplex);
    assert_eq!(
        recorded_effect_kinds(&cx),
        vec![
            stream_host_device_read_effect_kind(),
            stream_host_device_write_effect_kind(),
        ]
    );
}

#[test]
fn host_backend_registry_rejects_duplicate_backend_ids() {
    let mut registry = HostBackendRegistry::new();
    registry.register(FakeBackend::new()).unwrap();

    assert!(registry.register(FakeBackend::new()).is_err());
    assert!(registry.backend(&fake_backend_symbol()).is_some());
}

#[test]
fn host_browse_cards_cover_backend_devices_ports_and_missing_capabilities() {
    let mut registry = HostBackendRegistry::new();
    registry.register(FakeBackend::new()).unwrap();

    let cards = registry.card_exprs().unwrap();
    assert!(cards.iter().any(|card| {
        field(card, "kind") == Some(&Expr::Symbol(Symbol::qualified("stream", "host-backend")))
    }));
    assert!(cards.iter().any(|card| {
        field(card, "kind") == Some(&Expr::Symbol(Symbol::qualified("stream", "host-device")))
    }));
    assert!(cards.iter().any(|card| {
        field(card, "kind") == Some(&Expr::Symbol(Symbol::qualified("stream", "host-port")))
    }));

    let missing =
        registry.missing_capability_card(&fake_backend_symbol(), HostBackendCapability::Hotplug);
    assert_eq!(
        field(&missing, "kind"),
        Some(&Expr::Symbol(Symbol::qualified(
            "stream",
            "host-missing-capability"
        )))
    );
}

#[test]
fn host_open_plan_uses_device_effect_and_stream_capability() {
    let spec = RtpMidiBackend::source_spec("rtp-midi/effect", 4).unwrap();
    let plan = spec.open_plan();
    assert_eq!(plan.backend(), &rtp_midi_backend_symbol());
    assert_eq!(plan.device(), spec.id());
    assert_eq!(
        plan.effect_kinds(),
        &[stream_host_device_read_effect_kind()]
    );
    assert_eq!(plan.requires(), &[stream_host_capability()]);
}

#[test]
fn rtp_midi_rejects_wrong_backend_or_media() {
    let backend = RtpMidiBackend::new();
    let mut cx = authorized_cx();
    let wrong_backend = HostDeviceSpec::new(
        Symbol::new("bad"),
        Symbol::qualified("stream/host", "alsa"),
        StreamMedia::Midi,
        HostDirection::Input,
        Symbol::qualified("clock", "midi"),
        BufferPolicy::bounded(2).unwrap(),
    );
    assert!(backend.open_source(&mut cx, wrong_backend).is_err());

    let wrong_media = HostDeviceSpec::new(
        Symbol::new("audio"),
        rtp_midi_backend_symbol(),
        StreamMedia::Pcm,
        HostDirection::Input,
        Symbol::qualified("clock", "audio"),
        BufferPolicy::bounded(2).unwrap(),
    );
    assert!(backend.open_source(&mut cx, wrong_media).is_err());
}