Skip to main content

sim_lib_stream_host/
cookbook.rs

1//! Deterministic cookbook builders for stream-host recipes.
2
3use std::sync::Arc;
4
5use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Expr, NumberLiteral, Symbol};
6
7use crate::{FakeBackend, HostBackend, HostBackendRegistry, stream_host_capability};
8
9/// Build the modeled fake backend descriptor used by the cookbook recipe.
10pub fn fake_backend_demo() -> Expr {
11    let backend = FakeBackend::new();
12    let inventory = backend.enumerate().expect("fake backend enumerates");
13    let mut registry = HostBackendRegistry::new();
14    registry.register(backend).expect("fake backend registers");
15    let mut cx = authorized_demo_cx();
16    let opened = registry
17        .open_checked(
18            &mut cx,
19            FakeBackend::data_request(8).expect("valid fake data request"),
20        )
21        .expect("fake backend opens data stream");
22    let config = opened.config();
23
24    Expr::Map(vec![
25        (field("kind"), sym("stream-host", "fake-backend")),
26        (field("backend"), Expr::Symbol(config.backend().clone())),
27        (field("device"), Expr::Symbol(config.device().clone())),
28        (field("media"), Expr::Symbol(config.media().symbol())),
29        (field("clock"), Expr::Symbol(config.clock().clock().clone())),
30        (field("devices"), number(inventory.devices().len())),
31        (field("ports"), number(inventory.ports().len())),
32        (field("callback-queue"), sym("stream-host", "bounded")),
33        (field("hardware-required"), Expr::Bool(false)),
34    ])
35}
36
37fn field(name: &str) -> Expr {
38    Expr::Symbol(Symbol::qualified("stream-host", name))
39}
40
41fn sym(namespace: &str, name: &str) -> Expr {
42    Expr::Symbol(Symbol::qualified(namespace, name))
43}
44
45fn number(value: impl ToString) -> Expr {
46    Expr::Number(NumberLiteral {
47        domain: Symbol::qualified("numbers", "i64"),
48        canonical: value.to_string(),
49    })
50}
51
52#[allow(
53    clippy::unit_arg,
54    reason = "published sim-kernel grants return unit; workspace grants return Result"
55)]
56fn authorized_demo_cx() -> Cx {
57    let (mut cx, seat) = Cx::new_seated(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
58    seat.grant(&mut cx, stream_host_capability())
59        .assert_demo_granted();
60    cx
61}
62
63trait DemoGrantResult {
64    fn assert_demo_granted(self);
65}
66
67impl DemoGrantResult for () {
68    fn assert_demo_granted(self) {}
69}
70
71impl<E: std::fmt::Display> DemoGrantResult for std::result::Result<(), E> {
72    fn assert_demo_granted(self) {
73        if let Err(err) = self {
74            panic!("demo host grant is valid: {err}");
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use sim_codec::encode_with_codec;
83    use sim_codec_lisp::LispCodecLib;
84    use sim_lib_stream_core::StreamMedia;
85
86    #[test]
87    fn fake_backend_demo_opens_modeled_data_stream() {
88        let Expr::Map(entries) = fake_backend_demo() else {
89            panic!("fake backend demo is a map")
90        };
91        assert!(entries.iter().any(|(_, value)| {
92            matches!(value, Expr::Symbol(symbol) if symbol.as_qualified_str() == "stream/media/data")
93        }));
94        assert!(entries.iter().any(|(_, value)| *value == Expr::Bool(false)));
95        let mut registry = HostBackendRegistry::new();
96        registry.register(FakeBackend::new()).expect("register");
97        let mut cx = authorized_demo_cx();
98        let opened = registry
99            .open_checked(&mut cx, FakeBackend::data_request(4).expect("request"))
100            .expect("open");
101        assert_eq!(opened.config().media(), StreamMedia::Data);
102    }
103
104    #[test]
105    fn fake_backend_recipe_expectation_matches_demo_output() {
106        let mut cx = authorized_demo_cx();
107        cx.load_lib(&sim_lib_numbers_i64::I64NumbersLib::new())
108            .expect("load i64 numbers");
109        let lisp = Symbol::qualified("codec", "lisp");
110        let codec = LispCodecLib::new(cx.registry_mut().fresh_codec_id()).expect("lisp codec");
111        cx.load_lib(&codec).expect("load lisp codec");
112
113        let rendered = encode_with_codec(&mut cx, &lisp, &fake_backend_demo(), Default::default())
114            .expect("encode fake backend demo")
115            .into_text()
116            .expect("lisp codec emits text");
117        assert_eq!(recipe_expected_result(), rendered);
118    }
119
120    fn recipe_expected_result() -> String {
121        let recipe = include_str!("../recipes/01-basics/fake-backend/recipe.toml");
122        let result_line = recipe
123            .lines()
124            .find(|line| line.starts_with("result = "))
125            .expect("recipe result line");
126        result_line
127            .strip_prefix("result = \"")
128            .and_then(|line| line.strip_suffix('"'))
129            .expect("quoted recipe result")
130            .replace("\\\"", "\"")
131    }
132}