Skip to main content

lenso_runtime_conformance/
lib.rs

1//! Kernel-owned fixtures for testing Runtime Drivers and Execution Adapters.
2//!
3//! The types in this crate deliberately carry no product semantics. They make
4//! the Kernel Interface executable without depending on a concrete Adapter,
5//! an example Capability package, or an example App.
6
7use std::{collections::BTreeMap, fmt, rc::Rc};
8
9use futures::future::LocalBoxFuture;
10use lenso_app_plan::{ExecutionClassId, ModuleInstancePlan, ResolvedAppPlan};
11use lenso_kernel::{
12    ActivateContext, InvocationContext, ModuleLifecycle, NativeRequestEndpoint,
13    NativeRequestHandle, NoopModuleLifecycle, PreparedBinding, PreparedNativeApp,
14    PreparedNativeModule, RequestCapability, RuntimeFailure,
15};
16
17/// Stable identity used only by the runtime conformance suite.
18pub const PROBE_CAPABILITY_ID: &str = "lenso.runtime.conformance.probe@1";
19/// Exact conformance Descriptor version.
20pub const PROBE_DESCRIPTOR_VERSION: &str = "1.0.0";
21/// The request Operation exercised by the conformance suite.
22pub const PROBE_OPERATION: &str = "probe";
23
24/// Default provider package used by the conformance suite.
25pub const PROBE_PROVIDER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-provider";
26/// Replaceable provider package used to prove binding stability.
27pub const ALTERNATE_PROBE_PROVIDER_PACKAGE_ID: &str =
28    "lenso.runtime.conformance.alternate-probe-provider";
29/// Consumer package used by the conformance suite.
30pub const PROBE_CONSUMER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-consumer";
31
32/// Request value transferred through the runtime seam.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct ProbeRequest {
35    pub value: String,
36}
37
38/// Success value transferred through the runtime seam.
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct ProbeResponse {
41    pub value: String,
42}
43
44/// Domain outcome used to prove that runtime and domain failures stay distinct.
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub enum ProbeError {
47    EmptyValue,
48}
49
50/// Typed conformance Capability.
51#[derive(Debug)]
52pub struct Probe;
53
54impl RequestCapability for Probe {
55    type Request = ProbeRequest;
56    type Response = ProbeResponse;
57    type DomainError = ProbeError;
58
59    const ID: &'static str = PROBE_CAPABILITY_ID;
60    const DESCRIPTOR_VERSION: &'static str = PROBE_DESCRIPTOR_VERSION;
61}
62
63/// Provider-side Interface for the conformance Capability.
64pub trait ProbeProvider: fmt::Debug + 'static {
65    fn probe(
66        &self,
67        context: InvocationContext,
68        request: ProbeRequest,
69    ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>>;
70}
71
72/// Typed endpoint backed by one conformance provider.
73#[derive(Debug)]
74pub struct ProbeEndpoint<P> {
75    provider: Rc<P>,
76}
77
78impl<P: ProbeProvider> ProbeEndpoint<P> {
79    pub fn new(provider: P) -> Self {
80        Self {
81            provider: Rc::new(provider),
82        }
83    }
84}
85
86impl<P: ProbeProvider> NativeRequestEndpoint for ProbeEndpoint<P> {
87    fn capability_id(&self) -> &'static str {
88        PROBE_CAPABILITY_ID
89    }
90
91    fn descriptor_version(&self) -> &'static str {
92        PROBE_DESCRIPTOR_VERSION
93    }
94
95    fn operations(&self) -> &'static [&'static str] {
96        &[PROBE_OPERATION]
97    }
98
99    fn invoke(
100        &self,
101        operation: &str,
102        request: Box<dyn std::any::Any>,
103        context: InvocationContext,
104    ) -> LocalBoxFuture<
105        'static,
106        Result<Result<Box<dyn std::any::Any>, Box<dyn std::any::Any>>, RuntimeFailure>,
107    > {
108        if operation != PROBE_OPERATION {
109            return Box::pin(futures::future::ready(Err(
110                RuntimeFailure::UnknownOperation {
111                    capability: PROBE_CAPABILITY_ID,
112                    operation: operation.to_owned(),
113                },
114            )));
115        }
116        let Ok(request) = request.downcast::<ProbeRequest>() else {
117            return Box::pin(futures::future::ready(Err(
118                RuntimeFailure::ProtocolViolation {
119                    capability: PROBE_CAPABILITY_ID,
120                },
121            )));
122        };
123        let provider = Rc::clone(&self.provider);
124        Box::pin(async move {
125            match provider.probe(context, *request).await {
126                Ok(value) => Ok(Ok(Box::new(value) as Box<dyn std::any::Any>)),
127                Err(ProbeInvocationError::Domain(error)) => {
128                    Ok(Err(Box::new(error) as Box<dyn std::any::Any>))
129                }
130                Err(ProbeInvocationError::Runtime(error)) => Err(error),
131            }
132        })
133    }
134}
135
136/// Consumer wrapper used by Driver and Adapter conformance tests.
137#[derive(Debug)]
138pub struct ProbeClient {
139    handle: NativeRequestHandle<Probe>,
140}
141
142impl ProbeClient {
143    pub fn new(handle: NativeRequestHandle<Probe>) -> Self {
144        Self { handle }
145    }
146
147    pub fn from_dependencies(
148        dependencies: &lenso_kernel::ModuleDependencies,
149    ) -> Result<Self, RuntimeFailure> {
150        Ok(Self::new(dependencies.one::<Probe>()?))
151    }
152
153    pub async fn probe(
154        &self,
155        request: ProbeRequest,
156    ) -> Result<ProbeResponse, ProbeInvocationError> {
157        self.handle
158            .invoke(PROBE_OPERATION, request)
159            .await
160            .map_err(ProbeInvocationError::Runtime)?
161            .map_err(ProbeInvocationError::Domain)
162    }
163}
164
165/// Keeps typed domain outcomes separate from runtime failures.
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum ProbeInvocationError {
168    Domain(ProbeError),
169    Runtime(RuntimeFailure),
170}
171
172/// One generation returned by the conformance Adapter.
173#[derive(Debug)]
174pub struct ConformanceModule {
175    endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
176    lifecycle: Rc<dyn ModuleLifecycle>,
177}
178
179impl ConformanceModule {
180    pub fn new(endpoints: Vec<Rc<dyn NativeRequestEndpoint>>) -> Self {
181        Self::with_lifecycle(endpoints, NoopModuleLifecycle)
182    }
183
184    pub fn with_lifecycle(
185        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
186        lifecycle: impl ModuleLifecycle,
187    ) -> Self {
188        Self {
189            endpoints,
190            lifecycle: Rc::new(lifecycle),
191        }
192    }
193
194    fn prepared(&self) -> PreparedNativeModule {
195        PreparedNativeModule::with_lifecycle(self.endpoints.clone(), self.lifecycle.clone())
196    }
197}
198
199impl Default for ConformanceModule {
200    fn default() -> Self {
201        Self::new(Vec::new())
202    }
203}
204
205/// Adapter-specific factory used only by the runtime conformance suite.
206pub trait ConformanceModuleFactory: fmt::Debug + 'static {
207    fn package_id(&self) -> &'static str;
208
209    fn package_version(&self) -> &'static str {
210        ""
211    }
212
213    fn instantiate(
214        &self,
215        instance: &ModuleInstancePlan,
216    ) -> Result<ConformanceModule, RuntimeFailure>;
217}
218
219/// Request-only Execution Adapter used to test the Kernel Interface directly.
220#[derive(Debug, Default)]
221pub struct ConformanceExecutionAdapter {
222    factories: Vec<Rc<dyn ConformanceModuleFactory>>,
223}
224
225impl ConformanceExecutionAdapter {
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    #[must_use]
231    pub fn with_factory(mut self, factory: impl ConformanceModuleFactory) -> Self {
232        self.factories.push(Rc::new(factory));
233        self
234    }
235
236    fn instantiate(
237        &self,
238        instance: &ModuleInstancePlan,
239    ) -> Result<ConformanceModule, RuntimeFailure> {
240        let matches = self
241            .factories
242            .iter()
243            .filter(|factory| {
244                factory.package_id() == instance.package_id()
245                    && (instance.package_revision().is_empty()
246                        || factory.package_version() == instance.package_revision())
247            })
248            .collect::<Vec<_>>();
249        match matches.as_slice() {
250            [] => Err(RuntimeFailure::MissingModuleFactory {
251                instance: instance.instance_key().to_owned(),
252                package_id: instance.package_id().to_owned(),
253            }),
254            [factory] => factory.instantiate(instance),
255            _ => Err(RuntimeFailure::InvalidResolvedPlan {
256                detail: format!(
257                    "multiple conformance factories declare package `{}`",
258                    instance.package_id()
259                ),
260            }),
261        }
262    }
263}
264
265impl lenso_kernel::NativeExecutionAdapter for ConformanceExecutionAdapter {
266    fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
267        plan.validate()
268            .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
269                detail: error.to_string(),
270            })?;
271
272        let mut modules = BTreeMap::new();
273        let mut generations = BTreeMap::new();
274        for instance in plan
275            .module_instances()
276            .iter()
277            .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
278        {
279            let module = self.instantiate(instance)?;
280            generations.insert(instance.instance_key().to_owned(), module.prepared());
281            modules.insert(instance.instance_key().to_owned(), module);
282        }
283
284        let mut bindings = Vec::new();
285        for binding in plan.capability_bindings() {
286            let Some(module) = modules.get(binding.provider_instance()) else {
287                continue;
288            };
289            let endpoint = module
290                .endpoints
291                .iter()
292                .find(|endpoint| {
293                    endpoint.capability_id() == binding.capability_id()
294                        && endpoint.descriptor_version() == binding.descriptor_version()
295                })
296                .cloned()
297                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
298                    detail: format!(
299                        "Capability `{}` version `{}` has no request endpoint on provider `{}`",
300                        binding.capability_id(),
301                        binding.descriptor_version(),
302                        binding.provider_instance()
303                    ),
304                })?;
305            bindings.push(PreparedBinding::new(
306                binding.consumer_instance(),
307                binding.provider_instance(),
308                endpoint,
309            ));
310        }
311
312        Ok(PreparedNativeApp::new(bindings, generations))
313    }
314
315    fn recreate(
316        &self,
317        plan: &ResolvedAppPlan,
318        instance_key: &str,
319    ) -> Result<PreparedNativeModule, RuntimeFailure> {
320        let instance = plan.module_instance(instance_key).ok_or_else(|| {
321            RuntimeFailure::InvalidResolvedPlan {
322                detail: format!("unknown Module Instance `{instance_key}`"),
323            }
324        })?;
325        Ok(self.instantiate(instance)?.prepared())
326    }
327}
328
329/// Default consumer factory. It verifies its singular dependency during activation.
330#[derive(Debug)]
331pub struct ProbeConsumerFactory;
332
333impl ConformanceModuleFactory for ProbeConsumerFactory {
334    fn package_id(&self) -> &'static str {
335        PROBE_CONSUMER_PACKAGE_ID
336    }
337
338    fn package_version(&self) -> &'static str {
339        env!("CARGO_PKG_VERSION")
340    }
341
342    fn instantiate(
343        &self,
344        _instance: &ModuleInstancePlan,
345    ) -> Result<ConformanceModule, RuntimeFailure> {
346        Ok(ConformanceModule::with_lifecycle(
347            Vec::new(),
348            ProbeConsumerLifecycle,
349        ))
350    }
351}
352
353#[derive(Debug)]
354struct ProbeConsumerLifecycle;
355
356impl ModuleLifecycle for ProbeConsumerLifecycle {
357    fn activate(&self, context: ActivateContext) -> lenso_kernel::ModuleFuture {
358        let client = (context.dependencies().len() == 1)
359            .then(|| ProbeClient::from_dependencies(context.dependencies()));
360        Box::pin(async move {
361            let Some(client) = client else {
362                return Ok(());
363            };
364            match client?
365                .probe(ProbeRequest {
366                    value: "activation".to_owned(),
367                })
368                .await
369            {
370                Ok(_) => Ok(()),
371                Err(ProbeInvocationError::Runtime(error)) => Err(error),
372                Err(ProbeInvocationError::Domain(error)) => Err(RuntimeFailure::ModuleFailure {
373                    detail: format!("probe activation dependency returned {error:?}"),
374                }),
375            }
376        })
377    }
378}
379
380/// Default provider used by the conformance suite.
381#[derive(Debug)]
382pub struct ProbeProviderFactory;
383
384impl ConformanceModuleFactory for ProbeProviderFactory {
385    fn package_id(&self) -> &'static str {
386        PROBE_PROVIDER_PACKAGE_ID
387    }
388
389    fn package_version(&self) -> &'static str {
390        env!("CARGO_PKG_VERSION")
391    }
392
393    fn instantiate(
394        &self,
395        _instance: &ModuleInstancePlan,
396    ) -> Result<ConformanceModule, RuntimeFailure> {
397        Ok(ConformanceModule::new(vec![Rc::new(ProbeEndpoint::new(
398            EchoProbe("Echo"),
399        ))]))
400    }
401}
402
403/// Alternate implementation used to prove that bindings do not name implementations.
404#[derive(Debug)]
405pub struct AlternateProbeProviderFactory;
406
407impl ConformanceModuleFactory for AlternateProbeProviderFactory {
408    fn package_id(&self) -> &'static str {
409        ALTERNATE_PROBE_PROVIDER_PACKAGE_ID
410    }
411
412    fn package_version(&self) -> &'static str {
413        env!("CARGO_PKG_VERSION")
414    }
415
416    fn instantiate(
417        &self,
418        _instance: &ModuleInstancePlan,
419    ) -> Result<ConformanceModule, RuntimeFailure> {
420        Ok(ConformanceModule::new(vec![Rc::new(ProbeEndpoint::new(
421            EchoProbe("Alternate"),
422        ))]))
423    }
424}
425
426#[derive(Debug)]
427struct EchoProbe(&'static str);
428
429impl ProbeProvider for EchoProbe {
430    fn probe(
431        &self,
432        _context: InvocationContext,
433        request: ProbeRequest,
434    ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>> {
435        let prefix = self.0;
436        Box::pin(async move {
437            if request.value.is_empty() {
438                Err(ProbeInvocationError::Domain(ProbeError::EmptyValue))
439            } else {
440                Ok(ProbeResponse {
441                    value: format!("{prefix}: {}", request.value),
442                })
443            }
444        })
445    }
446}