1use std::{future::Future, time::Duration};
7
8use lenso_app_plan::ResolvedAppPlan;
9use lenso_kernel::{Kernel, NativeApp, PluginDependencies, RuntimeFailure, ShutdownOutcome};
10use lenso_native_adapter::{NativePluginFactory, NativePluginRegistry};
11use lenso_plugin_authoring::CapabilityClient;
12
13mod clock;
14mod entropy;
15mod faults;
16mod receipt;
17mod simulator;
18
19pub use clock::TestWallClock;
20pub use entropy::TestEntropy;
21pub use faults::{FaultInjector, FaultPointError, ScenarioBoundary, SimulatorFault};
22pub use receipt::{ScenarioReceipt, ScenarioReceiptEvent, ScenarioTerminal, ScenarioTransition};
23pub use simulator::{SimulatorGate, SimulatorResource, TestSimulator};
24
25#[derive(Debug)]
27pub struct TestAppBuilder {
28 plan: ResolvedAppPlan,
29 registry: NativePluginRegistry,
30 simulator: TestSimulator,
31}
32
33impl TestAppBuilder {
34 pub fn new(plan: ResolvedAppPlan) -> Self {
36 Self {
37 plan,
38 registry: NativePluginRegistry::new(),
39 simulator: TestSimulator::new(),
40 }
41 }
42
43 #[must_use]
45 pub fn with_factory(mut self, factory: impl NativePluginFactory) -> Self {
46 self.registry = self.registry.with_factory(factory);
47 self
48 }
49
50 #[must_use]
56 pub fn with_registry(mut self, registry: NativePluginRegistry) -> Self {
57 self.registry = registry;
58 self
59 }
60
61 #[must_use]
63 pub fn with_linked_factories(mut self) -> Self {
64 self.registry = self.registry.with_linked_factories();
65 self
66 }
67
68 #[must_use]
70 pub fn with_simulator(mut self, simulator: TestSimulator) -> Self {
71 self.simulator = simulator;
72 self
73 }
74
75 pub fn start(self) -> Result<TestApp, RuntimeFailure> {
77 let driver = self.simulator.driver();
78 let app = driver.run(Kernel::start_native(
79 self.plan,
80 driver.clone(),
81 self.registry,
82 ))?;
83 Ok(TestApp {
84 simulator: self.simulator,
85 app,
86 })
87 }
88}
89
90#[derive(Debug)]
92pub struct TestApp {
93 simulator: TestSimulator,
94 app: NativeApp,
95}
96
97impl TestApp {
98 pub fn builder(plan: ResolvedAppPlan) -> TestAppBuilder {
100 TestAppBuilder::new(plan)
101 }
102
103 pub fn app(&self) -> &NativeApp {
105 &self.app
106 }
107
108 pub fn simulator(&self) -> &TestSimulator {
110 &self.simulator
111 }
112
113 pub fn client<C>(&self, consumer_instance: &str) -> Result<C, RuntimeFailure>
115 where
116 C: CapabilityClient<Dependencies = PluginDependencies, Error = RuntimeFailure>,
117 {
118 let dependencies = self.app.dependencies(consumer_instance)?;
119 C::from_dependencies(&dependencies)
120 }
121
122 pub fn run<F: Future>(&self, future: F) -> F::Output {
124 self.simulator.run(future)
125 }
126
127 pub fn advance(&self, duration: Duration) {
129 self.simulator.advance(duration);
130 }
131
132 pub fn shutdown(&self, timeout: Duration) -> ShutdownOutcome {
134 self.simulator.run(self.app.shutdown(timeout))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use std::{cell::Cell, rc::Rc};
141
142 use futures::{FutureExt, future::Either};
143 use lenso_app_plan::PluginInstancePlan;
144 use lenso_kernel::{ActivateContext, PluginFuture, PluginLifecycle};
145 use lenso_native_adapter::{NativePluginFactoryContext, NativePluginInstance};
146
147 use super::*;
148
149 #[derive(Debug)]
150 struct GateLifecycle {
151 gate: SimulatorGate,
152 completed: Rc<Cell<bool>>,
153 cancelled: Rc<Cell<bool>>,
154 }
155
156 impl PluginLifecycle for GateLifecycle {
157 fn activate(&self, context: ActivateContext) -> PluginFuture {
158 let gate = self.gate.clone();
159 let completed = self.completed.clone();
160 let cancelled = self.cancelled.clone();
161 let cancellation = context.cancellation();
162 let spawned = context.tasks().spawn_local(Box::pin(async move {
163 let gate_wait = gate.wait().fuse();
164 let cancellation_wait = cancellation.cancelled().fuse();
165 futures::pin_mut!(gate_wait, cancellation_wait);
166 match futures::future::select(gate_wait, cancellation_wait).await {
167 Either::Left(((), _)) => completed.set(true),
168 Either::Right(((), _)) => cancelled.set(true),
169 }
170 }));
171 Box::pin(async move {
172 spawned
173 .map(|_| ())
174 .map_err(|error| RuntimeFailure::PluginFailure {
175 detail: format!("could not spawn simulator fixture task: {error:?}"),
176 })
177 })
178 }
179 }
180
181 #[derive(Debug)]
182 struct GateFactory {
183 gate: SimulatorGate,
184 completed: Rc<Cell<bool>>,
185 cancelled: Rc<Cell<bool>>,
186 }
187
188 impl NativePluginFactory for GateFactory {
189 fn package_id(&self) -> &'static str {
190 "test.simulator-gate"
191 }
192
193 fn instantiate(
194 &self,
195 _context: NativePluginFactoryContext<'_>,
196 ) -> Result<NativePluginInstance, RuntimeFailure> {
197 Ok(NativePluginInstance::with_lifecycle(
198 Vec::new(),
199 GateLifecycle {
200 gate: self.gate.clone(),
201 completed: self.completed.clone(),
202 cancelled: self.cancelled.clone(),
203 },
204 ))
205 }
206 }
207
208 fn gate_plan() -> ResolvedAppPlan {
209 ResolvedAppPlan::new(
210 vec![PluginInstancePlan::new("gate", "test.simulator-gate")],
211 vec![],
212 )
213 }
214
215 #[test]
216 fn empty_test_app_starts_ready_and_shuts_down_cleanly() {
217 let app = TestApp::builder(ResolvedAppPlan::empty()).start().unwrap();
218
219 assert!(app.app().is_ready());
220 assert_eq!(app.shutdown(Duration::from_secs(1)), ShutdownOutcome::Clean);
221 }
222
223 #[test]
224 fn plan_selected_plugin_without_a_factory_fails_closed() {
225 let plan = ResolvedAppPlan::new(
226 vec![PluginInstancePlan::new("missing", "test.missing")],
227 vec![],
228 );
229
230 let error = TestApp::builder(plan).start().unwrap_err();
231 assert!(
232 matches!(error, RuntimeFailure::MissingPluginFactory { instance, .. } if instance == "missing")
233 );
234 }
235
236 #[test]
237 fn builder_keeps_the_supplied_simulator_with_the_real_native_app() {
238 let simulator = TestSimulator::new();
239 let app = TestApp::builder(ResolvedAppPlan::empty())
240 .with_simulator(simulator.clone())
241 .start()
242 .unwrap();
243
244 simulator.advance(Duration::from_millis(5));
245 assert_eq!(app.simulator().now(), Duration::from_millis(5));
246 assert_eq!(app.shutdown(Duration::from_secs(1)), ShutdownOutcome::Clean);
247 }
248
249 #[test]
250 fn gate_controls_managed_native_plugin_work_after_app_start() {
251 let simulator = TestSimulator::new();
252 let gate = simulator.gate("fixture.after-activate");
253 let completed = Rc::new(Cell::new(false));
254 let cancelled = Rc::new(Cell::new(false));
255 let app = TestApp::builder(gate_plan())
256 .with_simulator(simulator.clone())
257 .with_factory(GateFactory {
258 gate: gate.clone(),
259 completed: completed.clone(),
260 cancelled: cancelled.clone(),
261 })
262 .start()
263 .unwrap();
264
265 simulator.pump();
266 assert_eq!(gate.reached_count(), 1);
267 assert!(!completed.get());
268
269 assert!(gate.release());
270 simulator.pump();
271 assert!(completed.get());
272 assert!(!cancelled.get());
273 assert_eq!(app.shutdown(Duration::from_secs(1)), ShutdownOutcome::Clean);
274 }
275
276 #[test]
277 fn shutdown_cancels_unreleased_managed_native_plugin_work() {
278 let simulator = TestSimulator::new();
279 let gate = simulator.gate("fixture.awaiting-shutdown");
280 let completed = Rc::new(Cell::new(false));
281 let cancelled = Rc::new(Cell::new(false));
282 let app = TestApp::builder(gate_plan())
283 .with_simulator(simulator.clone())
284 .with_factory(GateFactory {
285 gate: gate.clone(),
286 completed: completed.clone(),
287 cancelled: cancelled.clone(),
288 })
289 .start()
290 .unwrap();
291
292 simulator.pump();
293 assert_eq!(gate.reached_count(), 1);
294 assert!(!completed.get());
295 assert_eq!(app.shutdown(Duration::from_secs(1)), ShutdownOutcome::Clean);
296 assert!(!completed.get());
297 assert!(cancelled.get());
298 }
299}