lattice_plugin_host/
trampoline.rs1pub fn collect_batches<T, E>(mut next: impl FnMut() -> Result<Vec<T>, E>) -> Result<Vec<T>, E> {
26 let mut all = Vec::new();
27 loop {
28 let batch = next()?;
29 if batch.is_empty() {
30 return Ok(all);
31 }
32 all.extend(batch);
33 }
34}
35
36#[cfg(test)]
37mod tests {
38 #![allow(clippy::unwrap_used, clippy::panic)]
39
40 use super::*;
41
42 #[test]
43 fn collect_batches_aggregates_until_empty() {
44 let batches = [vec![1, 2], vec![3], vec![]];
45 let mut i = 0;
46 let out = collect_batches(|| {
47 let b = batches[i].clone();
48 i += 1;
49 Ok::<_, ()>(b)
50 })
51 .unwrap();
52 assert_eq!(out, vec![1, 2, 3]);
53 }
54
55 #[test]
56 fn collect_batches_propagates_an_error() {
57 let mut calls = 0;
58 let out: Result<Vec<i32>, &str> = collect_batches(|| {
59 calls += 1;
60 if calls == 2 { Err("boom") } else { Ok(vec![1]) }
61 });
62 assert_eq!(out, Err("boom"));
63 }
64}
65
66#[cfg(test)]
73mod fixture {
74 #![allow(clippy::unwrap_used, clippy::panic)]
75
76 use super::collect_batches;
77 use crate::WitBoundary;
78 use lattice_grammar::effect::Effect as NativeEffect;
79 use wasmtime::component::{Component, Linker};
80 use wasmtime::{Engine, Store};
81 use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
82
83 wasmtime::component::bindgen!({
89 world: "trampoline-fixture",
90 path: "../lattice-wit/wit",
91 with: {
92 "lattice:plugin-host/types": crate::lattice::plugin_host::types,
93 },
94 });
95
96 struct FixtureState {
99 wasi: WasiCtx,
100 table: ResourceTable,
101 }
102 impl WasiView for FixtureState {
103 fn ctx(&mut self) -> WasiCtxView<'_> {
104 WasiCtxView {
105 ctx: &mut self.wasi,
106 table: &mut self.table,
107 }
108 }
109 }
110
111 fn instantiate() -> Option<(Store<FixtureState>, TrampolineFixture)> {
113 let path = env!("TRAMPOLINE_GUEST_WASM");
114 if path.is_empty() {
115 eprintln!("SKIP: trampoline fixture guest not built (add the wasm32-wasip2 target)");
116 return None;
117 }
118 let engine = Engine::new(wasmtime::Config::new().wasm_component_model(true)).unwrap();
119 let component = Component::from_file(&engine, path).unwrap();
120 let mut linker: Linker<FixtureState> = Linker::new(&engine);
121 wasmtime_wasi::p2::add_to_linker_sync(&mut linker).unwrap();
122 let mut store = Store::new(
123 &engine,
124 FixtureState {
125 wasi: WasiCtxBuilder::new().build(),
126 table: ResourceTable::new(),
127 },
128 );
129 let bindings = TrampolineFixture::instantiate(&mut store, &component, &linker).unwrap();
130 Some((store, bindings))
131 }
132
133 #[test]
136 fn apply_effect_round_trips_a_payload_arm_through_the_guest() {
137 let Some((mut store, guest)) = instantiate() else {
138 return;
139 };
140 let wit = guest
141 .call_apply_effect(&mut store, &Args::String("hi".to_string()))
142 .unwrap();
143 let native = NativeEffect::from_wit(wit).expect("from_wit");
144 match native {
145 NativeEffect::Echo { level, text } => {
146 assert_eq!(text, "hi");
147 assert_eq!(level, lattice_grammar::effect::EchoLevel::Info);
148 }
149 other => panic!("expected Echo, got {other:?}"),
150 }
151 }
152
153 #[test]
156 fn apply_effect_rebuilds_many_from_a_list() {
157 let Some((mut store, guest)) = instantiate() else {
158 return;
159 };
160 let wit = guest.call_apply_effect(&mut store, &Args::None).unwrap();
161 let native = NativeEffect::from_wit(wit).expect("from_wit");
162 match native {
163 NativeEffect::Many(list) => {
164 assert_eq!(list.len(), 2);
165 assert!(matches!(list[0], NativeEffect::RecordJump));
166 assert!(matches!(list[1], NativeEffect::SetColorscheme(ref s) if s == "nord"));
167 }
168 other => panic!("expected Many, got {other:?}"),
169 }
170 }
171
172 #[test]
175 fn next_batch_carrier_aggregates_host_side() {
176 let Some((mut store, guest)) = instantiate() else {
177 return;
178 };
179 let all = collect_batches(|| guest.call_next_batch(&mut store)).unwrap();
180 assert_eq!(all, vec!["a", "b", "c"]);
181 }
182}