sim-lib-topology 0.1.4

Data-driven topology engine.
Documentation
use sim_kernel::{Export, Symbol};
use sim_lib_stream_core::{BridgeLatency, ClockDomain, DomainBridgeKind, LatencyClass};

use super::{Graph, Node, PortRef, sym, test_cx};
use crate::{
    Edge, PlacementNodeProfile, PlacementRefusalReason, Port, SiteMap, SiteProfile, place,
    place_graph,
};

#[test]
fn all_local_map_places_without_bridges() {
    let mut cx = test_cx();
    let graph = fx_graph();
    let report = place(&mut cx, &graph, &all_audio_site_map()).expect("place graph");

    assert!(report.is_accepted());
    assert_eq!(report.placed.len(), 3);
    assert!(
        report
            .placed
            .iter()
            .all(|node| node.site.as_symbol() == &Symbol::new("audio"))
    );
    assert!(report.bridges.is_empty());
    assert_eq!(report.latency.len(), 1);
    assert_eq!(report.latency[0].node.as_symbol(), &Symbol::new("out"));
    assert_eq!(report.latency[0].latency, BridgeLatency::frames(16));
}

#[test]
fn offloaded_fx_map_inserts_latency_comp_bridges() {
    let mut cx = test_cx();
    let compiled = crate::compile_graph(&mut cx, &fx_graph()).expect("compiled graph");
    let report = place_graph(&compiled, &offloaded_fx_site_map());

    assert!(report.is_accepted());
    assert_eq!(report.bridges.len(), 2);
    assert!(
        report
            .bridges
            .iter()
            .all(|bridge| bridge.kind() == DomainBridgeKind::LatencyCompDelay)
    );
    assert_eq!(report.bridges[0].from.as_symbol(), &Symbol::new("in"));
    assert_eq!(report.bridges[0].to.as_symbol(), &Symbol::new("fx"));
    assert_eq!(report.bridges[1].from.as_symbol(), &Symbol::new("fx"));
    assert_eq!(report.bridges[1].to.as_symbol(), &Symbol::new("out"));
    assert_eq!(report.latency[0].latency, BridgeLatency::frames(64));
}

#[test]
fn illegal_realtime_remote_map_is_refused_as_data() {
    let mut cx = test_cx();
    let graph = fx_graph();
    let report = place(&mut cx, &graph, &illegal_realtime_site_map()).expect("place graph");

    assert!(!report.is_accepted());
    assert!(report.refusals.iter().any(|refusal| {
        refusal.node.as_symbol() == &Symbol::new("fx")
            && refusal.site.as_symbol() == &Symbol::new("worker")
            && refusal.reason == PlacementRefusalReason::RealtimePinViolation
    }));
}

#[test]
fn site_latency_class_mismatch_is_refused_as_data() {
    let mut cx = test_cx();
    let graph = fx_graph();
    let report = place(&mut cx, &graph, &unsupported_latency_site_map()).expect("place graph");

    assert!(!report.is_accepted());
    assert!(report.refusals.iter().any(|refusal| {
        refusal.node.as_symbol() == &Symbol::new("fx")
            && refusal.reason == PlacementRefusalReason::UnsupportedLatencyClass
    }));
}

#[test]
fn incomparable_clock_domain_edge_is_refused_as_data() {
    let mut cx = test_cx();
    let graph = fx_graph();
    let report = place(&mut cx, &graph, &incomparable_clock_site_map()).expect("place graph");

    assert!(!report.is_accepted());
    assert!(report.bridges.is_empty());
    assert!(report.refusals.iter().any(|refusal| {
        refusal.node.as_symbol() == &Symbol::new("fx")
            && refusal.reason
                == PlacementRefusalReason::IncomparableClockDomain {
                    from: ClockDomain::Block,
                    to: ClockDomain::Job,
                }
    }));
}

#[test]
fn site_profile_bridges_kernel_site_export_records() {
    let export = Export::Site {
        symbol: Symbol::qualified("topology/site", "audio"),
        runtime_id: None,
    };
    let profile =
        SiteProfile::from_site_export(export.clone(), vec![LatencyClass::BlockLocal], false)
            .expect("site export profile");

    assert_eq!(profile.site_export(), &export);
    assert_eq!(
        profile.id().as_symbol(),
        &Symbol::qualified("topology/site", "audio")
    );
}

#[test]
fn non_declaration_order_dag_latency_uses_topological_order() {
    let mut cx = test_cx();
    let mut graph = Graph::minimal("latency-order");
    graph.nodes = vec![
        Node::named("in", "in"),
        Node::named("out", "out"),
        Node::named("fx", "wire"),
    ];
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("fx")),
        Edge::new(1, PortRef::output("fx"), PortRef::input("out")),
    ];
    let sites = SiteMap::new(SiteProfile::audio_clock("audio"))
        .with_node_profile("in", block_profile())
        .with_node_profile(
            "fx",
            block_profile().with_latency(BridgeLatency::frames(16)),
        )
        .with_node_profile("out", block_profile());

    let report = place(&mut cx, &graph, &sites).expect("place graph");

    assert!(report.is_accepted());
    assert_eq!(report.latency[0].latency, BridgeLatency::frames(16));
}

#[test]
fn bounded_cycle_latency_is_explicitly_accumulated() {
    let mut cx = test_cx();
    let mut graph = Graph::minimal("latency-cycle");
    graph.nodes = vec![
        Node::named("in", "in"),
        Node::named("a", "wire"),
        Node::named("b", "wire"),
        Node::named("out", "out"),
    ];
    let mut back_edge = Edge::new(2, PortRef::output("b"), PortRef::input("a"));
    back_edge.max_visits = Some(2);
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("a")),
        Edge::new(1, PortRef::output("a"), PortRef::input("b")),
        back_edge,
        Edge::new(3, PortRef::output("b"), PortRef::input("out")),
    ];
    let sites = SiteMap::new(SiteProfile::audio_clock("audio"))
        .with_node_profile("in", block_profile())
        .with_node_profile("a", block_profile().with_latency(BridgeLatency::frames(10)))
        .with_node_profile("b", block_profile().with_latency(BridgeLatency::frames(1)))
        .with_node_profile("out", block_profile());

    let report = place(&mut cx, &graph, &sites).expect("place graph");

    assert!(report.is_accepted());
    assert_eq!(report.latency[0].latency, BridgeLatency::frames(33));
}

#[test]
fn unsupported_site_clock_claim_is_refused() {
    let mut cx = test_cx();
    let graph = fx_graph();
    let sites = SiteMap::new(
        SiteProfile::audio_clock("audio").with_clock_domains(vec![ClockDomain::Control]),
    )
    .with_node_profile("in", block_profile())
    .with_node_profile("fx", block_profile())
    .with_node_profile("out", block_profile());

    let report = place(&mut cx, &graph, &sites).expect("place graph");

    assert!(!report.is_accepted());
    assert!(report.refusals.iter().any(|refusal| {
        refusal.node.as_symbol() == &Symbol::new("fx")
            && refusal.reason
                == PlacementRefusalReason::UnsupportedClockDomain {
                    domain: ClockDomain::Block,
                }
    }));
}

#[test]
fn unsupported_stream_port_claim_is_refused() {
    let mut cx = test_cx();
    let mut graph = Graph::minimal("stream-claim");
    graph.nodes = vec![
        Node::named("in", "in"),
        Node::with_ports(
            "stream",
            Symbol::new("wire"),
            vec![Port::stream("in", true)],
            vec![Port::stream("out", true)],
        ),
        Node::named("out", "out"),
    ];
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("stream")),
        Edge::new(1, PortRef::output("stream"), PortRef::input("out")),
    ];
    let sites = SiteMap::new(SiteProfile::audio_clock("audio").with_stream_ports(false))
        .with_node_profile("in", block_profile())
        .with_node_profile("stream", block_profile())
        .with_node_profile("out", block_profile());

    let report = place(&mut cx, &graph, &sites).expect("place graph");

    assert!(!report.is_accepted());
    assert!(report.refusals.iter().any(|refusal| {
        refusal.node.as_symbol() == &Symbol::new("stream")
            && refusal.reason == PlacementRefusalReason::UnsupportedStreamPorts
    }));
}

fn fx_graph() -> Graph {
    let mut graph = Graph::minimal("placement-fx");
    let mut fx = Node::named("fx", "call");
    fx.target = Some(sym("gain"));
    graph.nodes = vec![Node::named("in", "in"), fx, Node::named("out", "out")];
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("fx")),
        Edge::new(1, PortRef::output("fx"), PortRef::input("out")),
    ];
    graph
}

fn all_audio_site_map() -> SiteMap {
    SiteMap::new(SiteProfile::audio_clock("audio"))
        .with_node_profile("in", block_profile())
        .with_node_profile(
            "fx",
            block_profile().with_latency(BridgeLatency::frames(16)),
        )
        .with_node_profile("out", block_profile())
}

fn offloaded_fx_site_map() -> SiteMap {
    SiteMap::new(SiteProfile::audio_clock("audio"))
        .with_site(SiteProfile::local_worker("worker"))
        .assign_node("fx", "worker")
        .with_node_profile("in", block_profile())
        .with_node_profile(
            "fx",
            block_profile().with_latency(BridgeLatency::frames(64)),
        )
        .with_node_profile("out", block_profile())
}

fn illegal_realtime_site_map() -> SiteMap {
    SiteMap::new(SiteProfile::audio_clock("audio"))
        .with_site(SiteProfile::local_worker("worker"))
        .assign_node("fx", "worker")
        .with_node_profile("in", block_profile())
        .with_node_profile("fx", PlacementNodeProfile::sample_exact(Some(48_000), true))
        .with_node_profile("out", block_profile())
}

fn unsupported_latency_site_map() -> SiteMap {
    SiteMap::new(SiteProfile::audio_clock("audio"))
        .with_site(SiteProfile::local_worker("worker"))
        .assign_node("fx", "worker")
        .with_node_profile("in", block_profile())
        .with_node_profile(
            "fx",
            PlacementNodeProfile::new(
                sim_lib_stream_core::RateContract::new(
                    sim_lib_stream_core::ClockDomain::Job,
                    LatencyClass::RemoteCollaboration,
                    None,
                ),
                false,
            ),
        )
        .with_node_profile("out", block_profile())
}

fn incomparable_clock_site_map() -> SiteMap {
    SiteMap::new(SiteProfile::audio_clock("audio"))
        .with_node_profile("in", block_profile())
        .with_node_profile(
            "fx",
            PlacementNodeProfile::new(
                sim_lib_stream_core::RateContract::new(
                    ClockDomain::Job,
                    LatencyClass::BlockLocal,
                    None,
                ),
                false,
            ),
        )
        .with_node_profile("out", block_profile())
}

fn block_profile() -> PlacementNodeProfile {
    PlacementNodeProfile::block_local()
}