sim-lib-stream-host 0.1.3

Host-device stream backend substrate for SIM streams.
Documentation
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};

use sim_kernel::{
    AbiVersion, Cx, DefaultFactory, EagerPolicy, Export, Lib, LibLoader, LibManifest, LibSource,
    LibTarget, Linker, LoadCx, LoaderRegistry, Result, Symbol, Version,
};
use sim_lib_stream_core::{BufferPolicy, StreamMedia};

use crate::{
    AudioDeviceCard, AudioPlacementRequest, AudioProviderHost, AudioProviderRegistrar, AudioRouter,
    AudioSiteKey, DeviceCatalog, FakeBackend, HostDirection, HostStreamConfigRequest,
    ModeledAudioSite, Placement, audio_site_export_symbol, fake_backend_symbol,
    native_audio_provider_capability, stream_host_capability,
};

fn modeled_provider_symbol() -> Symbol {
    Symbol::qualified("audio/provider", "modeled-fixture")
}

fn modeled_site_symbol() -> Symbol {
    audio_site_export_symbol("provider-modeled-stereo-0")
}

fn modeled_site_key() -> AudioSiteKey {
    AudioSiteKey(modeled_site_symbol())
}

fn modeled_provider_entry(registrar: &mut dyn AudioProviderRegistrar) -> Result<()> {
    let card = AudioDeviceCard {
        key: modeled_site_key(),
        display_name: "Modeled Provider Stereo 0".to_owned(),
        channels_out: 2,
        channels_in: 2,
        sample_rates: vec![44_100, 48_000],
        hardware_required: true,
    };
    registrar.register_site(Arc::new(ModeledAudioSite::new(
        card,
        Arc::new(FakeBackend::new()),
    )));
    Ok(())
}

#[derive(Clone)]
struct ModeledProviderLoader {
    loads: Arc<AtomicUsize>,
    exports: ProviderExports,
}

impl ModeledProviderLoader {
    fn new(loads: Arc<AtomicUsize>) -> Self {
        Self {
            loads,
            exports: ProviderExports::Valid,
        }
    }

    fn with_exports(mut self, exports: ProviderExports) -> Self {
        self.exports = exports;
        self
    }
}

#[derive(Clone, Copy)]
enum ProviderExports {
    Valid,
    Missing,
    InvalidNamespace,
}

impl LibLoader for ModeledProviderLoader {
    fn can_load(&self, source: &LibSource) -> bool {
        matches!(source, LibSource::Symbol(symbol) if symbol == &modeled_provider_symbol())
    }

    fn load(&self, _cx: &mut Cx, source: LibSource) -> Result<Box<dyn Lib>> {
        assert!(self.can_load(&source));
        self.loads.fetch_add(1, Ordering::SeqCst);
        Ok(Box::new(ModeledProviderLib {
            exports: self.exports,
        }))
    }
}

struct ModeledProviderLib {
    exports: ProviderExports,
}

impl Lib for ModeledProviderLib {
    fn manifest(&self) -> LibManifest {
        LibManifest {
            id: modeled_provider_symbol(),
            version: Version("0.1.0".to_owned()),
            abi: AbiVersion { major: 0, minor: 1 },
            target: LibTarget::HostRegistered,
            requires: Vec::new(),
            capabilities: Vec::new(),
            exports: match self.exports {
                ProviderExports::Valid => vec![Export::Site {
                    symbol: modeled_site_symbol(),
                    runtime_id: None,
                }],
                ProviderExports::Missing => Vec::new(),
                ProviderExports::InvalidNamespace => vec![Export::Site {
                    symbol: Symbol::qualified("audio/device", "provider-modeled-stereo-0"),
                    runtime_id: None,
                }],
            },
        }
    }

    fn load(&self, _cx: &mut LoadCx, _linker: &mut Linker) -> Result<()> {
        Ok(())
    }
}

fn test_cx() -> Cx {
    Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory))
}

#[test]
fn modeled_provider_load_denied_without_capability() {
    let loads = Arc::new(AtomicUsize::new(0));
    let loaders = LoaderRegistry::new().with_loader(ModeledProviderLoader::new(Arc::clone(&loads)));
    let mut cx = test_cx();
    let mut router = AudioRouter::new();
    let mut host = AudioProviderHost::new(&mut cx, &loaders)
        .with_proof_entry(modeled_provider_symbol(), modeled_provider_entry);

    let err = host
        .load_into(LibSource::Symbol(modeled_provider_symbol()), &mut router)
        .unwrap_err();

    assert!(err.to_string().contains("audio.provider.native"));
    assert_eq!(loads.load(Ordering::SeqCst), 0);
    assert!(router.site_keys().next().is_none());
}

#[test]
fn modeled_provider_registers_site_and_catalog_discovers_it() {
    let loads = Arc::new(AtomicUsize::new(0));
    let loaders = LoaderRegistry::new().with_loader(ModeledProviderLoader::new(Arc::clone(&loads)));
    let mut cx = test_cx();
    cx.grant(native_audio_provider_capability());
    cx.grant(stream_host_capability());
    let mut router = AudioRouter::new();
    let mut host = AudioProviderHost::new(&mut cx, &loaders)
        .with_proof_entry(modeled_provider_symbol(), modeled_provider_entry);

    host.load_into(LibSource::Symbol(modeled_provider_symbol()), &mut router)
        .unwrap();

    assert_eq!(loads.load(Ordering::SeqCst), 1);
    let key = modeled_site_key();
    assert!(router.site(&key).is_some());
    assert_eq!(router.site_owner(&key), Some(&modeled_provider_symbol()));

    let mut catalog = DeviceCatalog::default_modeled();
    catalog.register_provider_sites(&router);
    let records = catalog.enumerate_audio().unwrap();
    let record = records
        .iter()
        .find(|record| record.id == key.0)
        .expect("provider site catalog record");
    assert_eq!(
        record.placement,
        Placement::Hardware {
            transport: modeled_provider_symbol(),
        }
    );
}

#[test]
fn provider_key_collision_is_rejected() {
    let loads = Arc::new(AtomicUsize::new(0));
    let loaders = LoaderRegistry::new().with_loader(ModeledProviderLoader::new(Arc::clone(&loads)));
    let mut cx = test_cx();
    cx.grant(native_audio_provider_capability());
    let mut router = AudioRouter::new();
    router.register(Arc::new(ModeledAudioSite::new(
        AudioDeviceCard::modeled(modeled_site_key(), "Existing Site"),
        Arc::new(FakeBackend::new()),
    )));
    let mut host = AudioProviderHost::new(&mut cx, &loaders)
        .with_proof_entry(modeled_provider_symbol(), modeled_provider_entry);

    let err = host
        .load_into(LibSource::Symbol(modeled_provider_symbol()), &mut router)
        .unwrap_err();

    assert_eq!(loads.load(Ordering::SeqCst), 1);
    assert!(err.to_string().contains("already registered"));
    assert_ne!(
        router.site_owner(&modeled_site_key()),
        Some(&modeled_provider_symbol())
    );
}

#[test]
fn provider_unload_removes_owned_sites() {
    let loads = Arc::new(AtomicUsize::new(0));
    let loaders = LoaderRegistry::new().with_loader(ModeledProviderLoader::new(Arc::clone(&loads)));
    let mut cx = test_cx();
    cx.grant(native_audio_provider_capability());
    let mut router = AudioRouter::new();
    let mut host = AudioProviderHost::new(&mut cx, &loaders)
        .with_proof_entry(modeled_provider_symbol(), modeled_provider_entry);

    host.load_into(LibSource::Symbol(modeled_provider_symbol()), &mut router)
        .unwrap();

    assert!(router.site(&modeled_site_key()).is_some());
    assert_eq!(router.unregister_owner(&modeled_provider_symbol()), 1);
    assert!(router.site(&modeled_site_key()).is_none());
}

#[test]
fn missing_site_export_is_rejected() {
    let loads = Arc::new(AtomicUsize::new(0));
    let loaders = LoaderRegistry::new().with_loader(
        ModeledProviderLoader::new(Arc::clone(&loads)).with_exports(ProviderExports::Missing),
    );
    let mut cx = test_cx();
    cx.grant(native_audio_provider_capability());
    let mut router = AudioRouter::new();
    let mut host = AudioProviderHost::new(&mut cx, &loaders)
        .with_proof_entry(modeled_provider_symbol(), modeled_provider_entry);

    let err = host
        .load_into(LibSource::Symbol(modeled_provider_symbol()), &mut router)
        .unwrap_err();

    assert_eq!(loads.load(Ordering::SeqCst), 1);
    assert!(err.to_string().contains("no audio/site exports"));
    assert!(router.site_keys().next().is_none());
}

#[test]
fn invalid_site_export_namespace_is_rejected() {
    let loads = Arc::new(AtomicUsize::new(0));
    let loaders = LoaderRegistry::new().with_loader(
        ModeledProviderLoader::new(Arc::clone(&loads))
            .with_exports(ProviderExports::InvalidNamespace),
    );
    let mut cx = test_cx();
    cx.grant(native_audio_provider_capability());
    let mut router = AudioRouter::new();
    let mut host = AudioProviderHost::new(&mut cx, &loaders)
        .with_proof_entry(modeled_provider_symbol(), modeled_provider_entry);

    let err = host
        .load_into(LibSource::Symbol(modeled_provider_symbol()), &mut router)
        .unwrap_err();

    assert_eq!(loads.load(Ordering::SeqCst), 1);
    assert!(err.to_string().contains("invalid site export"));
    assert!(router.site_keys().next().is_none());
}

#[test]
fn missing_provider_proof_entry_keeps_modeled_fallback() {
    let loads = Arc::new(AtomicUsize::new(0));
    let loaders = LoaderRegistry::new().with_loader(ModeledProviderLoader::new(Arc::clone(&loads)));
    let mut cx = test_cx();
    cx.grant(native_audio_provider_capability());
    cx.grant(stream_host_capability());
    let modeled = AudioSiteKey::new("audio/modeled/stereo-0");
    let mut router = AudioRouter::new();
    router.register(Arc::new(ModeledAudioSite::new(
        AudioDeviceCard::modeled(modeled.clone(), "Modeled Stereo 0"),
        Arc::new(FakeBackend::new()),
    )));
    let mut host = AudioProviderHost::new(&mut cx, &loaders);

    let err = host
        .load_into(LibSource::Symbol(modeled_provider_symbol()), &mut router)
        .unwrap_err();

    assert_eq!(loads.load(Ordering::SeqCst), 1);
    assert!(err.to_string().contains("sim_audio_provider_v1"));
    let requested = modeled_site_key();
    let resolved = router.resolve_or_modeled(&requested, &modeled).unwrap();
    assert_eq!(resolved, modeled);
    let opened = router
        .open_placement_checked(
            &mut cx,
            AudioPlacementRequest {
                site_key: resolved,
                stream_request: HostStreamConfigRequest::new(
                    fake_backend_symbol(),
                    Symbol::new("fake/pcm"),
                    StreamMedia::Pcm,
                    HostDirection::Output,
                    BufferPolicy::bounded(8).unwrap(),
                ),
            },
        )
        .unwrap();
    assert_eq!(opened.config().media(), StreamMedia::Pcm);
    opened.close().unwrap();
}