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, NativeEndpointSet, NativeEventEndpoint,
13    NativeRequestEndpoint, NativeRequestHandle, NativeStreamEndpoint, NoopModuleLifecycle,
14    PreparedBinding, PreparedEventBinding, PreparedNativeApp, PreparedNativeModule,
15    PreparedStreamBinding, RequestCapability, RuntimeFailure,
16};
17
18mod interaction;
19
20pub use interaction::*;
21
22/// Stable identity used only by the runtime conformance suite.
23pub const PROBE_CAPABILITY_ID: &str = "lenso.runtime.conformance.probe@1";
24/// Exact conformance Descriptor version.
25pub const PROBE_DESCRIPTOR_VERSION: &str = "1.0.0";
26/// The request Operation exercised by the conformance suite.
27pub const PROBE_OPERATION: &str = "probe";
28
29/// Default provider package used by the conformance suite.
30pub const PROBE_PROVIDER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-provider";
31/// Replaceable provider package used to prove binding stability.
32pub const ALTERNATE_PROBE_PROVIDER_PACKAGE_ID: &str =
33    "lenso.runtime.conformance.alternate-probe-provider";
34/// Consumer package used by the conformance suite.
35pub const PROBE_CONSUMER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-consumer";
36
37/// Request value transferred through the runtime seam.
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct ProbeRequest {
40    pub value: String,
41}
42
43/// Success value transferred through the runtime seam.
44#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct ProbeResponse {
46    pub value: String,
47}
48
49/// Domain outcome used to prove that runtime and domain failures stay distinct.
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub enum ProbeError {
52    EmptyValue,
53}
54
55/// Typed conformance Capability.
56#[derive(Debug)]
57pub struct Probe;
58
59impl RequestCapability for Probe {
60    type Request = ProbeRequest;
61    type Response = ProbeResponse;
62    type DomainError = ProbeError;
63
64    const ID: &'static str = PROBE_CAPABILITY_ID;
65    const DESCRIPTOR_VERSION: &'static str = PROBE_DESCRIPTOR_VERSION;
66}
67
68/// Provider-side Interface for the conformance Capability.
69pub trait ProbeProvider: fmt::Debug + 'static {
70    fn probe(
71        &self,
72        context: InvocationContext,
73        request: ProbeRequest,
74    ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>>;
75}
76
77/// Typed endpoint backed by one conformance provider.
78#[derive(Debug)]
79pub struct ProbeEndpoint<P> {
80    provider: Rc<P>,
81}
82
83impl<P: ProbeProvider> ProbeEndpoint<P> {
84    pub fn new(provider: P) -> Self {
85        Self {
86            provider: Rc::new(provider),
87        }
88    }
89}
90
91impl<P: ProbeProvider> NativeRequestEndpoint for ProbeEndpoint<P> {
92    fn capability_id(&self) -> &'static str {
93        PROBE_CAPABILITY_ID
94    }
95
96    fn descriptor_version(&self) -> &'static str {
97        PROBE_DESCRIPTOR_VERSION
98    }
99
100    fn operations(&self) -> &'static [&'static str] {
101        &[PROBE_OPERATION]
102    }
103
104    fn invoke(
105        &self,
106        operation: &str,
107        request: Box<dyn std::any::Any>,
108        context: InvocationContext,
109    ) -> LocalBoxFuture<
110        'static,
111        Result<Result<Box<dyn std::any::Any>, Box<dyn std::any::Any>>, RuntimeFailure>,
112    > {
113        if operation != PROBE_OPERATION {
114            return Box::pin(futures::future::ready(Err(
115                RuntimeFailure::UnknownOperation {
116                    capability: PROBE_CAPABILITY_ID,
117                    operation: operation.to_owned(),
118                },
119            )));
120        }
121        let Ok(request) = request.downcast::<ProbeRequest>() else {
122            return Box::pin(futures::future::ready(Err(
123                RuntimeFailure::ProtocolViolation {
124                    capability: PROBE_CAPABILITY_ID,
125                },
126            )));
127        };
128        let provider = Rc::clone(&self.provider);
129        Box::pin(async move {
130            match provider.probe(context, *request).await {
131                Ok(value) => Ok(Ok(Box::new(value) as Box<dyn std::any::Any>)),
132                Err(ProbeInvocationError::Domain(error)) => {
133                    Ok(Err(Box::new(error) as Box<dyn std::any::Any>))
134                }
135                Err(ProbeInvocationError::Runtime(error)) => Err(error),
136            }
137        })
138    }
139}
140
141/// Consumer wrapper used by Driver and Adapter conformance tests.
142#[derive(Debug)]
143pub struct ProbeClient {
144    handle: NativeRequestHandle<Probe>,
145}
146
147impl ProbeClient {
148    pub fn new(handle: NativeRequestHandle<Probe>) -> Self {
149        Self { handle }
150    }
151
152    pub fn from_dependencies(
153        dependencies: &lenso_kernel::ModuleDependencies,
154    ) -> Result<Self, RuntimeFailure> {
155        Ok(Self::new(dependencies.one::<Probe>()?))
156    }
157
158    pub async fn probe(
159        &self,
160        request: ProbeRequest,
161    ) -> Result<ProbeResponse, ProbeInvocationError> {
162        self.handle
163            .invoke(PROBE_OPERATION, request)
164            .await
165            .map_err(ProbeInvocationError::Runtime)?
166            .map_err(ProbeInvocationError::Domain)
167    }
168}
169
170/// Keeps typed domain outcomes separate from runtime failures.
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub enum ProbeInvocationError {
173    Domain(ProbeError),
174    Runtime(RuntimeFailure),
175}
176
177/// One generation returned by the conformance Adapter.
178#[derive(Debug)]
179pub struct ConformanceModule {
180    endpoints: NativeEndpointSet,
181    lifecycle: Rc<dyn ModuleLifecycle>,
182}
183
184impl ConformanceModule {
185    pub fn new(endpoints: Vec<Rc<dyn NativeRequestEndpoint>>) -> Self {
186        Self::with_lifecycle(endpoints, NoopModuleLifecycle)
187    }
188
189    pub fn with_lifecycle(
190        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
191        lifecycle: impl ModuleLifecycle,
192    ) -> Self {
193        Self::with_all_endpoints(endpoints, Vec::new(), Vec::new(), lifecycle)
194    }
195
196    /// Creates one conformance generation with every native interaction kind.
197    pub fn with_all_endpoints(
198        request: Vec<Rc<dyn NativeRequestEndpoint>>,
199        stream: Vec<Rc<dyn NativeStreamEndpoint>>,
200        event: Vec<Rc<dyn NativeEventEndpoint>>,
201        lifecycle: impl ModuleLifecycle,
202    ) -> Self {
203        Self {
204            endpoints: NativeEndpointSet::new(request, stream, event),
205            lifecycle: Rc::new(lifecycle),
206        }
207    }
208
209    /// Creates one conformance generation containing bidirectional stream endpoints.
210    pub fn with_stream_endpoints(
211        endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
212        lifecycle: impl ModuleLifecycle,
213    ) -> Self {
214        Self::with_all_endpoints(Vec::new(), endpoints, Vec::new(), lifecycle)
215    }
216
217    /// Creates one conformance generation containing ephemeral Event endpoints.
218    pub fn with_event_endpoints(
219        endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
220        lifecycle: impl ModuleLifecycle,
221    ) -> Self {
222        Self::with_all_endpoints(Vec::new(), Vec::new(), endpoints, lifecycle)
223    }
224
225    fn prepared(&self) -> PreparedNativeModule {
226        PreparedNativeModule::with_endpoint_set_lifecycle(
227            self.endpoints.clone(),
228            self.lifecycle.clone(),
229        )
230    }
231}
232
233impl Default for ConformanceModule {
234    fn default() -> Self {
235        Self::new(Vec::new())
236    }
237}
238
239/// Adapter-specific factory used only by the runtime conformance suite.
240pub trait ConformanceModuleFactory: fmt::Debug + 'static {
241    fn package_id(&self) -> &'static str;
242
243    fn package_version(&self) -> &'static str {
244        ""
245    }
246
247    fn instantiate(
248        &self,
249        instance: &ModuleInstancePlan,
250    ) -> Result<ConformanceModule, RuntimeFailure>;
251}
252
253/// Interaction-complete Execution Adapter used to test the Kernel Interface directly.
254#[derive(Debug, Default)]
255pub struct ConformanceExecutionAdapter {
256    factories: Vec<Rc<dyn ConformanceModuleFactory>>,
257}
258
259impl ConformanceExecutionAdapter {
260    pub fn new() -> Self {
261        Self::default()
262    }
263
264    #[must_use]
265    pub fn with_factory(mut self, factory: impl ConformanceModuleFactory) -> Self {
266        self.factories.push(Rc::new(factory));
267        self
268    }
269
270    fn instantiate(
271        &self,
272        instance: &ModuleInstancePlan,
273    ) -> Result<ConformanceModule, RuntimeFailure> {
274        let matches = self
275            .factories
276            .iter()
277            .filter(|factory| {
278                factory.package_id() == instance.package_id()
279                    && (instance.package_revision().is_empty()
280                        || factory.package_version() == instance.package_revision())
281            })
282            .collect::<Vec<_>>();
283        match matches.as_slice() {
284            [] => Err(RuntimeFailure::MissingModuleFactory {
285                instance: instance.instance_key().to_owned(),
286                package_id: instance.package_id().to_owned(),
287            }),
288            [factory] => factory.instantiate(instance),
289            _ => Err(RuntimeFailure::InvalidResolvedPlan {
290                detail: format!(
291                    "multiple conformance factories declare package `{}`",
292                    instance.package_id()
293                ),
294            }),
295        }
296    }
297}
298
299impl lenso_kernel::NativeExecutionAdapter for ConformanceExecutionAdapter {
300    fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
301        plan.validate()
302            .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
303                detail: error.to_string(),
304            })?;
305
306        let mut modules = BTreeMap::new();
307        let mut generations = BTreeMap::new();
308        for instance in plan
309            .module_instances()
310            .iter()
311            .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
312        {
313            let module = self.instantiate(instance)?;
314            generations.insert(instance.instance_key().to_owned(), module.prepared());
315            modules.insert(instance.instance_key().to_owned(), module);
316        }
317
318        let mut bindings = Vec::new();
319        let mut stream_bindings = Vec::new();
320        let mut event_bindings = Vec::new();
321        for binding in plan.capability_bindings() {
322            let Some(module) = modules.get(binding.provider_instance()) else {
323                continue;
324            };
325            let provider = plan
326                .module_instance(binding.provider_instance())
327                .expect("validated binding provider should exist");
328            let descriptor = provider
329                .provided_capabilities()
330                .iter()
331                .find(|descriptor| descriptor.capability_id() == binding.capability_id())
332                .expect("validated binding descriptor should exist");
333
334            if !descriptor.request_operations().is_empty() {
335                let endpoint = find_endpoint(module.endpoints.request(), binding, "request")?;
336                bindings.push(PreparedBinding::new(
337                    binding.consumer_instance(),
338                    binding.provider_instance(),
339                    endpoint,
340                ));
341            }
342            if !descriptor.stream_operations().is_empty() {
343                let endpoint = find_endpoint(module.endpoints.stream(), binding, "stream")?;
344                stream_bindings.push(PreparedStreamBinding::new(
345                    binding.consumer_instance(),
346                    binding.provider_instance(),
347                    endpoint,
348                ));
349            }
350            if !descriptor.event_operations().is_empty() {
351                let endpoint = find_endpoint(module.endpoints.event(), binding, "Event")?;
352                event_bindings.push(PreparedEventBinding::new(
353                    binding.consumer_instance(),
354                    binding.provider_instance(),
355                    endpoint,
356                ));
357            }
358        }
359
360        Ok(PreparedNativeApp::new(bindings, generations)
361            .with_stream_bindings(stream_bindings)
362            .with_event_bindings(event_bindings))
363    }
364
365    fn recreate(
366        &self,
367        plan: &ResolvedAppPlan,
368        instance_key: &str,
369    ) -> Result<PreparedNativeModule, RuntimeFailure> {
370        let instance = plan.module_instance(instance_key).ok_or_else(|| {
371            RuntimeFailure::InvalidResolvedPlan {
372                detail: format!("unknown Module Instance `{instance_key}`"),
373            }
374        })?;
375        Ok(self.instantiate(instance)?.prepared())
376    }
377}
378
379trait ConformanceEndpoint {
380    fn capability_id(&self) -> &'static str;
381    fn descriptor_version(&self) -> &'static str;
382}
383
384impl ConformanceEndpoint for dyn NativeRequestEndpoint {
385    fn capability_id(&self) -> &'static str {
386        NativeRequestEndpoint::capability_id(self)
387    }
388
389    fn descriptor_version(&self) -> &'static str {
390        NativeRequestEndpoint::descriptor_version(self)
391    }
392}
393
394impl ConformanceEndpoint for dyn NativeStreamEndpoint {
395    fn capability_id(&self) -> &'static str {
396        NativeStreamEndpoint::capability_id(self)
397    }
398
399    fn descriptor_version(&self) -> &'static str {
400        NativeStreamEndpoint::descriptor_version(self)
401    }
402}
403
404impl ConformanceEndpoint for dyn NativeEventEndpoint {
405    fn capability_id(&self) -> &'static str {
406        NativeEventEndpoint::capability_id(self)
407    }
408
409    fn descriptor_version(&self) -> &'static str {
410        NativeEventEndpoint::descriptor_version(self)
411    }
412}
413
414fn find_endpoint<T>(
415    endpoints: &[Rc<T>],
416    binding: &lenso_app_plan::CapabilityBinding,
417    interaction: &str,
418) -> Result<Rc<T>, RuntimeFailure>
419where
420    T: ConformanceEndpoint + ?Sized,
421{
422    endpoints
423        .iter()
424        .find(|endpoint| {
425            endpoint.capability_id() == binding.capability_id()
426                && endpoint.descriptor_version() == binding.descriptor_version()
427        })
428        .cloned()
429        .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
430            detail: format!(
431                "Capability `{}` version `{}` has no {interaction} endpoint on provider `{}`",
432                binding.capability_id(),
433                binding.descriptor_version(),
434                binding.provider_instance()
435            ),
436        })
437}
438
439/// Default consumer factory. It verifies its singular dependency during activation.
440#[derive(Debug)]
441pub struct ProbeConsumerFactory;
442
443impl ConformanceModuleFactory for ProbeConsumerFactory {
444    fn package_id(&self) -> &'static str {
445        PROBE_CONSUMER_PACKAGE_ID
446    }
447
448    fn package_version(&self) -> &'static str {
449        env!("CARGO_PKG_VERSION")
450    }
451
452    fn instantiate(
453        &self,
454        _instance: &ModuleInstancePlan,
455    ) -> Result<ConformanceModule, RuntimeFailure> {
456        Ok(ConformanceModule::with_lifecycle(
457            Vec::new(),
458            ProbeConsumerLifecycle,
459        ))
460    }
461}
462
463#[derive(Debug)]
464struct ProbeConsumerLifecycle;
465
466impl ModuleLifecycle for ProbeConsumerLifecycle {
467    fn activate(&self, context: ActivateContext) -> lenso_kernel::ModuleFuture {
468        let client = (context.dependencies().len() == 1)
469            .then(|| ProbeClient::from_dependencies(context.dependencies()));
470        Box::pin(async move {
471            let Some(client) = client else {
472                return Ok(());
473            };
474            match client?
475                .probe(ProbeRequest {
476                    value: "activation".to_owned(),
477                })
478                .await
479            {
480                Ok(_) => Ok(()),
481                Err(ProbeInvocationError::Runtime(error)) => Err(error),
482                Err(ProbeInvocationError::Domain(error)) => Err(RuntimeFailure::ModuleFailure {
483                    detail: format!("probe activation dependency returned {error:?}"),
484                }),
485            }
486        })
487    }
488}
489
490/// Default provider used by the conformance suite.
491#[derive(Debug)]
492pub struct ProbeProviderFactory;
493
494impl ConformanceModuleFactory for ProbeProviderFactory {
495    fn package_id(&self) -> &'static str {
496        PROBE_PROVIDER_PACKAGE_ID
497    }
498
499    fn package_version(&self) -> &'static str {
500        env!("CARGO_PKG_VERSION")
501    }
502
503    fn instantiate(
504        &self,
505        _instance: &ModuleInstancePlan,
506    ) -> Result<ConformanceModule, RuntimeFailure> {
507        Ok(ConformanceModule::new(vec![Rc::new(ProbeEndpoint::new(
508            EchoProbe("Echo"),
509        ))]))
510    }
511}
512
513/// Alternate implementation used to prove that bindings do not name implementations.
514#[derive(Debug)]
515pub struct AlternateProbeProviderFactory;
516
517impl ConformanceModuleFactory for AlternateProbeProviderFactory {
518    fn package_id(&self) -> &'static str {
519        ALTERNATE_PROBE_PROVIDER_PACKAGE_ID
520    }
521
522    fn package_version(&self) -> &'static str {
523        env!("CARGO_PKG_VERSION")
524    }
525
526    fn instantiate(
527        &self,
528        _instance: &ModuleInstancePlan,
529    ) -> Result<ConformanceModule, RuntimeFailure> {
530        Ok(ConformanceModule::new(vec![Rc::new(ProbeEndpoint::new(
531            EchoProbe("Alternate"),
532        ))]))
533    }
534}
535
536#[derive(Debug)]
537struct EchoProbe(&'static str);
538
539impl ProbeProvider for EchoProbe {
540    fn probe(
541        &self,
542        _context: InvocationContext,
543        request: ProbeRequest,
544    ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>> {
545        let prefix = self.0;
546        Box::pin(async move {
547            if request.value.is_empty() {
548                Err(ProbeInvocationError::Domain(ProbeError::EmptyValue))
549            } else {
550                Ok(ProbeResponse {
551                    value: format!("{prefix}: {}", request.value),
552                })
553            }
554        })
555    }
556}