1/*
2 * SPDX-FileCopyrightText: 2026 Copyright (c) Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14//! CDA startup builder.
15//!
16//! [`Setup`] is the public entry-point for applications that need to customize how the CDA
17//! boots. The default startup path uses [`Setup::new`] with the standard update plugin; a
18//! custom path calls [`Setup::with_update_plugin`] to inject any [`RuntimeFilesUpdatePlugin`]
19//! implementation and [`Setup::with_communication_plugin`] to replace the default
20//! `init_mode`-aware communication plugin, before handing the `Setup` to one of the
21//! `run_*` functions.
22//! Lock-priority policies are selected with [`Setup::with_lock_priority_plugin`].
23//!
24//! [`RuntimeFilesUpdatePlugin`]: cda_interfaces::runtime_update_api::RuntimeFilesUpdatePlugin
25
26use std::{sync::Arc, time::Duration};
27
28use cda_comm_can::CanDiagGateway;
29use cda_comm_doip::DoipDiagGateway;
30use cda_core::EcuManager;
31use cda_interfaces::{
32 HashMap, ShutdownSignal,
33 communication_control::{
34 ActivationCause, CommunicationAccess, CommunicationInitMode, CommunicationLifecycle,
35 CommunicationVariantDetection, PostUpdateCommunicationMode, VariantDetectionMode,
36 error::CommControlError,
37 },
38 component_slot::{ComponentSlot, ReplaceComponent},
39 health::HealthProvider,
40 http_protection::registry::HttpProtectionRegistry,
41 lock_priority_api::LockPriorityPolicy,
42};
43use cda_plugin_communication_management::{
44 lifecycle::{
45 CommunicationRuntime,
46 access::CommunicationAccessView,
47 build_communication_runtime,
48 disable::{CommunicationDisableView, DisableCommunication},
49 },
50 plugin::{
51 CommunicationPlugin, CommunicationPluginBuilder, default::DefaultCommunicationPluginBuilder,
52 },
53};
54use cda_plugin_security::{SecurityPlugin, SecurityPluginLoader};
55use cda_storage::LocalStorage;
56use cda_transport_router::DiagnosticTransportRouter;
57use futures::future::BoxFuture;
58use tokio::sync::RwLock;
59
60use crate::{
61 ApplicationState,
62 config::configfile::Configuration,
63 error::AppError,
64 update::{UpdatePluginBuilder, add_runtime_update_routes},
65 vehicle::{UdsManagerType, VehicleData},
66};
67
68// Type alias
69
70/// Boxed async callback executed after the webserver starts but **before** vehicle data is
71/// loaded. Receives the dynamic router so it can register early routes.
72pub(crate) type PreLoadHook = Box<
73 dyn FnOnce(cda_sovd::dynamic_router::DynamicRouter) -> BoxFuture<'static, Result<(), AppError>>
74 + Send,
75>;
76
77/// Runtime context produced during CDA initialization and provided to update-plugin builders.
78///
79/// `gateway_replacer` and `uds_manager_replacer` are replace-only. An update plugin can
80/// install a freshly built component, but has no read access, and therefore no operational
81/// authority, over the live gateway or UDS manager.
82pub struct CdaRuntime<SP: SecurityPlugin> {
83 pub config: Arc<RwLock<Configuration>>,
84 pub uds_manager_replacer: Arc<dyn ReplaceComponent<UdsManagerType<SP>>>,
85 pub gateway_replacer: Arc<
86 dyn ReplaceComponent<
87 DiagnosticTransportRouter<DoipDiagGateway<EcuManager<SP>>, CanDiagGateway>,
88 >,
89 >,
90 pub dynamic_router: cda_sovd::dynamic_router::DynamicRouter,
91 pub vehicle_route_handle: cda_sovd::RouteHandle,
92 pub lock_provider: Arc<cda_sovd::SovdLockStateProvider>,
93 pub flash_files_path: String,
94 pub flash_functional_class_default: String,
95 pub components_config: cda_interfaces::datatypes::ComponentsConfig,
96 pub health: Option<HashMap<String, Arc<dyn HealthProvider>>>,
97 pub storage: Arc<LocalStorage>,
98 pub mdd_decompress: bool,
99 pub shutdown_signal: ShutdownSignal,
100 /// Transport behavior to restore after a runtime database update completes.
101 pub post_update_mode: PostUpdateCommunicationMode,
102 /// Narrow handle used by update plugins to request activation without
103 /// gaining the full [`CommunicationPlugin`](CommunicationPlugin) lifecycle authority.
104 pub communication_access: Arc<dyn CommunicationAccess>,
105 /// Narrow handle used by update plugins to take exclusive ownership of
106 /// communication for the duration of a runtime file update.
107 pub communication_disable: Arc<dyn DisableCommunication>,
108 /// Shared registry of HTTP request restrictions installed by update plugins.
109 pub http_protections: HttpProtectionRegistry,
110 /// Retry hint surfaced on the HTTP `Retry-After` header while a runtime
111 /// update holds communication exclusively.
112 pub update_retry_after: Duration,
113}
114
115/// Bridges the `RwLock`-wrapped `UdsManager` to both communication-framework
116/// hook traits. The `UdsManager` implements each half itself, so this only
117/// forwards through the lock.
118struct UdsCommunicationHooks<SP: SecurityPlugin> {
119 uds_manager: ComponentSlot<UdsManagerType<SP>>,
120}
121
122#[async_trait::async_trait]
123impl<SP: SecurityPlugin> CommunicationLifecycle for UdsCommunicationHooks<SP> {
124 fn name(&self) -> &'static str {
125 "variant-detection-listener"
126 }
127
128 async fn initialize(&self) -> Result<(), CommControlError> {
129 self.uds_manager.read().await.initialize().await
130 }
131
132 async fn on_enabled(&self) {
133 self.uds_manager.read().await.on_enabled().await;
134 }
135
136 async fn deinitialize(&self) {
137 self.uds_manager.read().await.deinitialize().await;
138 }
139}
140
141#[async_trait::async_trait]
142impl<SP: SecurityPlugin> CommunicationVariantDetection for UdsCommunicationHooks<SP> {
143 fn name(&self) -> &'static str {
144 "variant-detection"
145 }
146
147 async fn detect(&self) -> Result<(), CommControlError> {
148 self.uds_manager.read().await.detect().await
149 }
150}
151
152/// Builder for customizing the CDA startup sequence.
153///
154/// Use [`Setup::new`] to start with the defaults and then chain optional
155/// configuration methods:
156///
157/// ```rust,ignore
158/// use opensovd_cda_lib::{
159/// setup::Setup,
160/// update::update_plugin_fn,
161/// };
162///
163/// let setup = Setup::<MySecurityPlugin, MySecurityLoader>::new()
164/// .with_preload(|router| async move {
165/// // register additional routes before databases load
166/// Ok(())
167/// })
168/// .with_update_plugin(update_plugin_fn(|infra| async move {
169/// Ok(MyCustomUpdatePlugin::new(infra))
170/// }))
171/// .with_communication_plugin(MyCommunicationPluginBuilder);
172///
173/// opensovd_cda_lib::run_with_ext_from_config(config, setup).await?;
174/// ```
175///
176/// When no update plugin is configured (`UpdatePluginBuilder = ()`) the runtime-update routes
177/// are **not** mounted. Pass a builder, typically via [`update_plugin_fn`] or by implementing
178/// [`UpdatePluginBuilder`], to enable them.
179///
180/// [`update_plugin_fn`]: crate::update::update_plugin_fn
181/// [`UpdatePluginBuilder`]: UpdatePluginBuilder
182///
183/// Type parameters:
184/// - `SP`: security plugin
185/// - `SL`: security plugin loader
186/// - `UPB`: update plugin builder, defaults to `()`
187/// - `CPB`: communication plugin builder, defaults to [`DefaultCommunicationPluginBuilder`]
188pub struct Setup<
189 SP: SecurityPlugin,
190 SL: SecurityPluginLoader,
191 UPB = (),
192 CPB = DefaultCommunicationPluginBuilder,
193> {
194 pub(crate) _phantom: std::marker::PhantomData<(SP, SL)>,
195 /// Optional callback run after the webserver starts but before vehicle data is loaded.
196 pub(crate) pre_load: Option<PreLoadHook>,
197 /// Optional update-plugin builder. When `None` (or `UPB = ()`) the runtime-update
198 /// routes are not registered.
199 pub(crate) build_update_plugin: Option<UPB>,
200 pub(crate) build_communication_plugin: CPB,
201 pub(crate) initialize_tracing: bool,
202 pub(crate) shutdown_signal: Option<ShutdownSignal>,
203 /// Lock-priority policy fixed for the lifetime of the lock store.
204 pub(crate) lock_priority_policy: Arc<dyn LockPriorityPolicy>,
205}
206
207impl<SP: SecurityPlugin, SL: SecurityPluginLoader> Default for Setup<SP, SL> {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213impl<SP: SecurityPlugin, SL: SecurityPluginLoader> Setup<SP, SL> {
214 /// Creates a new `Setup` with no preload hook or update plugin and no lock preemption.
215 #[must_use]
216 pub fn new() -> Self {
217 Self {
218 _phantom: std::marker::PhantomData,
219 pre_load: None,
220 build_update_plugin: None,
221 build_communication_plugin: DefaultCommunicationPluginBuilder,
222 initialize_tracing: true,
223 shutdown_signal: None,
224 lock_priority_policy: Arc::new(cda_plugin_lock_priority::NoPreemptionPolicy),
225 }
226 }
227}
228
229impl<SP: SecurityPlugin, SL: SecurityPluginLoader, UPB, CPB> Setup<SP, SL, UPB, CPB> {
230 /// Uses the tracing subscriber already installed by the embedding process.
231 #[must_use]
232 pub fn with_existing_tracing(mut self) -> Self {
233 self.initialize_tracing = false;
234 self
235 }
236
237 /// Uses an externally managed shutdown signal instead of process signals.
238 #[must_use]
239 pub fn with_shutdown_signal(mut self, shutdown_signal: ShutdownSignal) -> Self {
240 self.shutdown_signal = Some(shutdown_signal);
241 self
242 }
243
244 /// Registers a preload hook.
245 ///
246 /// The hook is called after the webserver starts, health state and sd-notify are
247 /// configured, but **before** vehicle data (MDD databases) are loaded. Use it to
248 /// mount additional routes that should be reachable during the (potentially slow)
249 /// database load.
250 ///
251 /// The hook must return `Ok(())` to allow startup to continue; returning `Err` aborts
252 /// the entire startup.
253 #[must_use]
254 pub fn with_preload<F, Fut>(mut self, f: F) -> Self
255 where
256 F: FnOnce(cda_sovd::dynamic_router::DynamicRouter) -> Fut + Send + 'static,
257 Fut: Future<Output = Result<(), AppError>> + Send + 'static,
258 {
259 self.pre_load = Some(Box::new(|router| Box::pin(f(router))));
260 self
261 }
262
263 /// Configures a custom runtime update plugin.
264 ///
265 /// `builder` will be called after vehicle data is loaded and routes are registered. It
266 /// receives the complete [`CdaRuntime`] context so it can access the UDS manager, `DoIP`
267 /// gateway, storage directory, lock provider, and every other piece of infrastructure it
268 /// needs.
269 ///
270 /// The returned plugin is wrapped in [`ExclusiveRuntimePlugin`] (read/write mutual
271 /// exclusion) and mounted on the standard runtime-update HTTP endpoints automatically.
272 ///
273 /// Use [`update_plugin_fn`] to wrap a plain async closure without implementing the trait
274 /// manually:
275 ///
276 /// ```rust,ignore
277 /// setup.with_update_plugin(update_plugin_fn(|infra| async move {
278 /// Ok(MyPlugin::new(infra))
279 /// }))
280 /// ```
281 ///
282 /// [`ExclusiveRuntimePlugin`]: cda_interfaces::runtime_update_api::ExclusiveRuntimePlugin
283 /// [`update_plugin_fn`]: crate::update::update_plugin_fn
284 pub fn with_update_plugin<UPB2: UpdatePluginBuilder<SP>>(
285 self,
286 builder: UPB2,
287 ) -> Setup<SP, SL, UPB2, CPB> {
288 Setup {
289 _phantom: self._phantom,
290 pre_load: self.pre_load,
291 build_update_plugin: Some(builder),
292 build_communication_plugin: self.build_communication_plugin,
293 initialize_tracing: self.initialize_tracing,
294 shutdown_signal: self.shutdown_signal,
295 lock_priority_policy: self.lock_priority_policy,
296 }
297 }
298
299 /// Replaces the default communication plugin factory.
300 ///
301 /// The factory is invoked after the passive transport and authoritative manager exist.
302 pub fn with_communication_plugin<CPB2: CommunicationPluginBuilder>(
303 self,
304 plugin: CPB2,
305 ) -> Setup<SP, SL, UPB, CPB2> {
306 Setup {
307 _phantom: self._phantom,
308 pre_load: self.pre_load,
309 build_update_plugin: self.build_update_plugin,
310 build_communication_plugin: plugin,
311 initialize_tracing: self.initialize_tracing,
312 shutdown_signal: self.shutdown_signal,
313 lock_priority_policy: self.lock_priority_policy,
314 }
315 }
316
317 /// Registers a lock-priority policy.
318 ///
319 /// Policy implementations receive revisioned
320 /// [`LockPriorityEvaluation`](cda_interfaces::lock_priority_api::LockPriorityEvaluation)
321 /// snapshots and return a
322 /// [`LockPriorityDecision`](cda_interfaces::lock_priority_api::LockPriorityDecision). Policy
323 /// calls are bounded by configured timeout and stale-state retries; returning `Preempt` also
324 /// requires the request's `break_lock` flag.
325 #[must_use]
326 pub fn with_lock_priority_plugin(mut self, plugin: Arc<dyn LockPriorityPolicy>) -> Self {
327 self.lock_priority_policy = plugin;
328 self
329 }
330}
331
332/// Installs the global HTTP protection guard, builds the authoritative communication plugin
333/// runtime, and derives a narrow access view for UDS manager construction.
334///
335/// Diagnostic operations request activation directly (see
336/// `CommunicationAccess::request_activate`), so `http_protections` is left to the
337/// runtime-update plugin's own protection.
338async fn build_communication_runtime_and_access<SP, CPB>(
339 ws: &ApplicationState,
340 gateway: ComponentSlot<
341 DiagnosticTransportRouter<DoipDiagGateway<EcuManager<SP>>, CanDiagGateway>,
342 >,
343 communication_plugin: CPB,
344 init_mode: CommunicationInitMode,
345 variant_detection: VariantDetectionMode,
346 retry_after: Duration,
347 http_protections: &HttpProtectionRegistry,
348) -> Result<(CommunicationRuntime, Arc<dyn CommunicationAccess>), AppError>
349where
350 SP: SecurityPlugin,
351 CPB: CommunicationPluginBuilder,
352{
353 cda_sovd::install_http_restriction_guard(
354 &ws.dynamic_router,
355 Arc::new(http_protections.clone()),
356 )
357 .await;
358
359 let transport_control = gateway.transport_control();
360 let communication_runtime = build_communication_runtime(
361 communication_plugin,
362 transport_control,
363 init_mode,
364 variant_detection,
365 )
366 .await
367 .map_err(|error| AppError::InitializationFailed(error.to_string()))?;
368
369 let communication_access: Arc<dyn CommunicationAccess> = Arc::new(
370 CommunicationAccessView::new(Arc::clone(&communication_runtime.plugin), retry_after),
371 );
372
373 Ok((communication_runtime, communication_access))
374}
375
376/// Registers the UDS manager's two communication hooks through the authoritative
377/// plugin.
378///
379/// Both halves come from one adapter but register separately. The listener runs
380/// on every transport enable, while detection is the optional last stage that
381/// `trigger_detection()` can run on its own.
382async fn register_communication_hooks<SP>(
383 plugin: &Arc<dyn CommunicationPlugin>,
384 uds_manager: &ComponentSlot<UdsManagerType<SP>>,
385) -> Result<(), AppError>
386where
387 SP: SecurityPlugin,
388{
389 let hooks = Arc::new(UdsCommunicationHooks {
390 uds_manager: uds_manager.clone(),
391 });
392 plugin
393 .register_lifecycle_hook(Arc::clone(&hooks) as Arc<dyn CommunicationLifecycle>)
394 .await
395 .map_err(|error| AppError::InitializationFailed(error.to_string()))?;
396 plugin
397 .register_variant_detection(hooks as Arc<dyn CommunicationVariantDetection>)
398 .await
399 .map_err(|error| AppError::InitializationFailed(error.to_string()))
400}
401
402/// `Always` initializes whole-vehicle communication eagerly at startup and propagates
403/// failure according to existing application-start semantics. `OnDemand` and `Disabled`
404/// leave it uninitialized, with HTTP/SOVD already served by the routes registered
405/// beforehand. Under `OnDemand` an explicit `activate()` or a qualifying ECU request
406/// initializes it. The default plugin offers no activation path under `Disabled`.
[docs]407/// [[ dimpl~deferred-communication-startup, Deferred communication startup wiring, dimpl ]]
408async fn activate_communication_per_init_mode(
409 plugin: &Arc<dyn CommunicationPlugin>,
410 init_mode: CommunicationInitMode,
411) -> Result<(), AppError> {
412 match init_mode {
413 CommunicationInitMode::Always => plugin
414 .activate(ActivationCause::Startup)
415 .await
416 .map(|_| ())
417 .map_err(AppError::from),
418 CommunicationInitMode::OnDemand | CommunicationInitMode::Disabled => Ok(()),
419 }
420}
421
422/// Builds the caller-supplied update plugin, if any, and mounts its HTTP routes.
423async fn setup_update_plugin<SP, SL, UPB>(
424 ws: &ApplicationState,
425 build_update_plugin: Option<UPB>,
426 infra: CdaRuntime<SP>,
427 lock_provider: Arc<cda_sovd::SovdLockStateProvider>,
428 upload_body_limit_bytes: usize,
429 update_retry_after: Duration,
430) -> Result<(), AppError>
431where
432 SP: SecurityPlugin,
433 SL: SecurityPluginLoader,
434 UPB: UpdatePluginBuilder<SP>,
435{
436 let Some(builder) = build_update_plugin else {
437 return Ok(());
438 };
439
440 let plugin = builder.build(infra).await?;
441 add_runtime_update_routes::<SL, _>(
442 &ws.dynamic_router,
443 plugin,
444 lock_provider,
445 upload_body_limit_bytes,
446 update_retry_after,
447 )
448 .await;
449
450 Ok(())
451}
452
453/// Registers all SOVD routes, the runtime-update plugin, `OpenAPI` routes, and the update guard.
454///
455/// This is called after vehicle data has been loaded. The `build_update_plugin` optional
456/// builder is consumed here; when `None` the runtime-update endpoints are simply not mounted.
457pub(crate) async fn setup_runtime_routes<SP, SL, UPB, CPB>(
458 config: Configuration,
459 vehicle_data: VehicleData<SP>,
460 ws: &ApplicationState,
461 build_update_plugin: Option<UPB>,
462 communication_plugin: CPB,
463 storage: Arc<LocalStorage>,
464) -> Result<CommunicationRuntime, AppError>
465where
466 SP: SecurityPlugin,
467 SL: SecurityPluginLoader,
468 UPB: UpdatePluginBuilder<SP>,
469 CPB: CommunicationPluginBuilder,
470{
471 let flash_files_path = config.flash_files_path.clone();
472 let flash_functional_class_default = config.flash_functional_class_default.clone();
473 let components_config = config.components.clone();
474 let mdd_decompress = config.flat_buf.mdd_decompress;
475
476 let runtime_update_config = config.runtime_update_config.clone();
477 let update_retry_after = Duration::from_secs(runtime_update_config.retry_after_seconds);
478 let communication_retry_after =
479 Duration::from_secs(config.communication.deferred_retry_after_seconds);
480 let post_update_mode = config.communication.post_update_mode.clone();
481
482 let lock_provider: Arc<cda_sovd::SovdLockStateProvider> = Arc::new(
483 cda_sovd::SovdLockStateProvider::new(Arc::clone(&vehicle_data.locks)),
484 );
485
486 let shutdown_signal = cda_interfaces::shutdown_signal(ws.shutdown_signal.clone());
487 let gateway = vehicle_data.diagnostic_gateway.clone();
488 let http_protections = HttpProtectionRegistry::new();
489
490 let (communication_runtime, communication_access) = build_communication_runtime_and_access(
491 ws,
492 vehicle_data.diagnostic_gateway.clone(),
493 communication_plugin,
494 config.communication.init_mode,
495 config.communication.variant_detection,
496 communication_retry_after,
497 &http_protections,
498 )
499 .await?;
500 let plugin = Arc::clone(&communication_runtime.plugin);
501
502 let components = crate::vehicle::finish_vehicle_components(
503 vehicle_data.prepared,
504 &config,
505 Arc::clone(&lock_provider)
506 as Arc<dyn cda_interfaces::request_authorization::RequestLockProvider>,
507 Arc::clone(&communication_access),
508 );
509 let uds_manager = ComponentSlot::new(components.uds_manager);
510 let file_managers = components.file_managers;
511
512 register_communication_hooks(&plugin, &uds_manager).await?;
513
514 let communication_disable: Arc<dyn DisableCommunication> =
515 Arc::new(CommunicationDisableView::new(Arc::clone(&plugin)));
516
517 // Routes are published only after the plugin, initializers, and narrow views are ready.
518 let vehicle_route_handle = cda_sovd::add_vehicle_routes::<_, _, SL>(
519 &ws.dynamic_router,
520 cda_sovd::VehicleConfig {
521 flash_files_path: config.flash_files_path.clone(),
522 flash_functional_class_default: config.flash_functional_class_default.clone(),
523 functional_group_config: config.functional_description.clone(),
524 components_config: config.components.clone(),
525 },
526 cda_sovd::VehicleResources {
527 ecu_uds: uds_manager.read().await.clone(),
528 file_managers,
529 locks: Arc::clone(&vehicle_data.locks),
530 communication_access: Arc::clone(&communication_access),
531 },
532 )
533 .await?;
534
535 activate_communication_per_init_mode(&plugin, config.communication.init_mode).await?;
536
537 let infra = CdaRuntime {
538 config: Arc::new(RwLock::new(config)),
539 dynamic_router: ws.dynamic_router.clone(),
540 vehicle_route_handle,
541 flash_files_path,
542 flash_functional_class_default,
543 components_config,
544 lock_provider: Arc::clone(&lock_provider),
545 shutdown_signal,
546 post_update_mode,
547 communication_access,
548 communication_disable,
549 http_protections,
550 update_retry_after,
551 uds_manager_replacer: uds_manager.replacer(),
552 gateway_replacer: gateway.replacer(),
553 health: vehicle_data.health_providers,
554 storage,
555 mdd_decompress,
556 };
557
558 setup_update_plugin::<SP, SL, _>(
559 ws,
560 build_update_plugin,
561 infra,
562 lock_provider,
563 runtime_update_config.upload_body_limit_bytes,
564 update_retry_after,
565 )
566 .await?;
567
568 Ok(communication_runtime)
569}
570
571#[cfg(test)]
572mod tests {
573 use std::sync::Arc;
574
575 use async_trait::async_trait;
576 use cda_interfaces::{
577 communication_control::{
578 CommControlError, CommunicationState, TransportControl, TransportState,
579 },
580 runtime_update_api::{
581 BulkDataCreatedList, BulkDataList, ExecutionMode, RuntimeFilesQuery,
582 RuntimeFilesUpdatePlugin, RuntimeUpdateError, UpdateExecution,
583 },
584 };
585 use cda_plugin_communication_management::{
586 lifecycle::build_communication_runtime, plugin::default::DefaultCommunicationPluginBuilder,
587 };
588 use cda_plugin_security::{DefaultSecurityPlugin, DefaultSecurityPluginData};
589
590 use super::*;
591 use crate::update::{UpdatePluginFn, update_plugin_fn};
592
593 // Minimal no-op plugin for type-checking.
594 struct NoOpPlugin;
595
596 #[async_trait::async_trait]
597 impl RuntimeFilesUpdatePlugin for NoOpPlugin {
598 async fn list_current(
599 &self,
600 _q: &RuntimeFilesQuery,
601 ) -> Result<BulkDataList, RuntimeUpdateError> {
602 Ok(BulkDataList::default())
603 }
604
605 async fn list_nextupdate(
606 &self,
607 _q: &RuntimeFilesQuery,
608 ) -> Result<BulkDataList, RuntimeUpdateError> {
609 Ok(BulkDataList::default())
610 }
611
612 async fn list_backup(
613 &self,
614 _q: &RuntimeFilesQuery,
615 ) -> Result<BulkDataList, RuntimeUpdateError> {
616 Ok(BulkDataList::default())
617 }
618
619 async fn upload(
620 &self,
621 _files: Vec<cda_interfaces::runtime_update_api::UploadFile>,
622 ) -> Result<BulkDataCreatedList, RuntimeUpdateError> {
623 Ok(BulkDataCreatedList::default())
624 }
625
626 async fn delete_nextupdate(&self) -> Result<Vec<String>, RuntimeUpdateError> {
627 Ok(vec![])
628 }
629
630 async fn delete_nextupdate_by_id(&self, _id: &str) -> Result<(), RuntimeUpdateError> {
631 Ok(())
632 }
633
634 async fn delete_backup(&self) -> Result<Vec<String>, RuntimeUpdateError> {
635 Ok(vec![])
636 }
637
638 async fn start_execution(
639 &self,
640 _mode: ExecutionMode,
641 ) -> Result<String, RuntimeUpdateError> {
642 Ok(String::new())
643 }
644
645 async fn list_executions(&self) -> Vec<UpdateExecution> {
646 vec![]
647 }
648
649 async fn get_execution_status(&self, _id: &str) -> Option<UpdateExecution> {
650 None
651 }
652 }
653
654 type TestSetup = Setup<DefaultSecurityPluginData, DefaultSecurityPlugin>;
655
656 struct NoopTransport;
657
658 #[async_trait]
659 impl TransportControl for NoopTransport {
660 async fn enable(&self) -> Result<(), CommControlError> {
661 Ok(())
662 }
663
664 async fn disable(&self) -> Result<(), CommControlError> {
665 Ok(())
666 }
667
668 async fn state(&self) -> TransportState {
669 TransportState::Disabled
670 }
671 }
672
[docs]673 /// [[ test~deferred-startup-policy, Startup activates communication only in Always mode, test ]]
674 #[tokio::test]
675 async fn startup_activation_follows_init_mode() {
676 for (mode, expected) in [
677 (CommunicationInitMode::Always, CommunicationState::Enabled),
678 (
679 CommunicationInitMode::OnDemand,
680 CommunicationState::Disabled,
681 ),
682 (
683 CommunicationInitMode::Disabled,
684 CommunicationState::Disabled,
685 ),
686 ] {
687 let runtime = build_communication_runtime(
688 DefaultCommunicationPluginBuilder,
689 Arc::new(NoopTransport),
690 mode,
691 VariantDetectionMode::Always,
692 )
693 .await
694 .unwrap();
695
696 activate_communication_per_init_mode(&runtime.plugin, mode)
697 .await
698 .unwrap();
699 assert_eq!(runtime.plugin.state(), expected, "{mode:?}");
700 runtime.plugin.shutdown().await;
701 }
702 }
703
704 #[test]
705 fn documented_public_api_type_checks() {
706 let _: Option<CdaRuntime<DefaultSecurityPluginData>> = None;
707 }
708
709 #[test]
710 fn new_has_no_preload_or_update_plugin() {
711 let s = TestSetup::new();
712 assert!(
713 s.pre_load.is_none(),
714 "fresh Setup must have no preload hook"
715 );
716 assert!(
717 s.build_update_plugin.is_none(),
718 "fresh Setup must have no update plugin"
719 );
720 }
721
722 #[test]
723 fn with_preload_stores_hook() {
724 let s = TestSetup::new().with_preload(|_router| async { Ok(()) });
725 assert!(
726 s.pre_load.is_some(),
727 "with_preload must store the provided hook"
728 );
729 }
730
731 #[test]
732 fn with_update_plugin_stores_builder() {
733 // Use `update_plugin_fn` as a convenient closure adapter.
734 let builder: UpdatePluginFn<_> =
735 update_plugin_fn(|_infra: CdaRuntime<DefaultSecurityPluginData>| async {
736 Ok(NoOpPlugin)
737 });
738
739 let s: Setup<DefaultSecurityPluginData, DefaultSecurityPlugin, _> =
740 TestSetup::new().with_update_plugin(builder);
741
742 assert!(
743 s.build_update_plugin.is_some(),
744 "with_update_plugin must store the provided builder"
745 );
746 }
747
748 #[test]
749 fn chaining_preload_then_plugin_retains_both() {
750 let builder: UpdatePluginFn<_> =
751 update_plugin_fn(|_infra: CdaRuntime<DefaultSecurityPluginData>| async {
752 Ok(NoOpPlugin)
753 });
754
755 let s = TestSetup::new()
756 .with_preload(|_| async { Ok(()) })
757 .with_update_plugin(builder);
758
759 assert!(s.pre_load.is_some(), "preload hook must survive chaining");
760 assert!(
761 s.build_update_plugin.is_some(),
762 "plugin builder must be stored after chaining"
763 );
764 }
765
766 #[test]
767 fn chaining_plugin_then_preload_retains_both() {
768 let builder: UpdatePluginFn<_> =
769 update_plugin_fn(|_infra: CdaRuntime<DefaultSecurityPluginData>| async {
770 Ok(NoOpPlugin)
771 });
772
773 let s = TestSetup::new()
774 .with_update_plugin(builder)
775 .with_preload(|_| async { Ok(()) });
776
777 assert!(s.pre_load.is_some(), "preload hook must survive chaining");
778 assert!(
779 s.build_update_plugin.is_some(),
780 "plugin builder must survive chaining"
781 );
782 }
783}