1use std::collections::{HashMap, VecDeque};
2use std::fmt;
3use std::sync::Arc;
4use std::sync::{Mutex, RwLock};
5
6use fret_assets::{
7 AssetBundleId, AssetCapabilities, AssetLoadError, AssetLocator, AssetLocatorKind, AssetRequest,
8 AssetResolver, AssetRevision, InMemoryAssetResolver, ResolvedAssetBytes,
9 ResolvedAssetReference, StaticAssetEntry,
10};
11
12use crate::GlobalsHost;
13use crate::asset_reload::asset_reload_support;
14
15const MAX_ASSET_LOAD_RECENT_EVENTS: usize = 16;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AssetLoadAccessKind {
19 Bytes,
20 ExternalReference,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AssetLoadOutcomeKind {
25 Resolved,
26 Missing,
27 StaleManifest,
28 UnsupportedLocatorKind,
29 ExternalReferenceUnavailable,
30 ReferenceOnlyLocator,
31 ResolverUnavailable,
32 AccessDenied,
33 Io,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum AssetRevisionTransitionKind {
38 Initial,
39 Stable,
40 Changed,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct AssetLoadDiagnosticEvent {
45 pub access_kind: AssetLoadAccessKind,
46 pub locator_kind: AssetLocatorKind,
47 pub locator_debug: String,
48 pub outcome_kind: AssetLoadOutcomeKind,
49 pub revision: Option<AssetRevision>,
50 pub previous_revision: Option<AssetRevision>,
51 pub revision_transition: Option<AssetRevisionTransitionKind>,
52 pub message: Option<String>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Default)]
56pub struct AssetLoadDiagnosticsSnapshot {
57 pub total_requests: u64,
58 pub bytes_requests: u64,
59 pub reference_requests: u64,
60 pub missing_bundle_asset_requests: u64,
61 pub stale_manifest_requests: u64,
62 pub io_requests: u64,
63 pub unsupported_file_requests: u64,
64 pub unsupported_url_requests: u64,
65 pub external_reference_unavailable_requests: u64,
66 pub revision_change_requests: u64,
67 pub recent: Vec<AssetLoadDiagnosticEvent>,
68}
69
70#[derive(Default)]
71struct AssetLoadDiagnosticsState {
72 snapshot: AssetLoadDiagnosticsSnapshot,
73 recent: VecDeque<AssetLoadDiagnosticEvent>,
74 last_seen_revisions: HashMap<AssetLocator, AssetRevision>,
75}
76
77#[derive(Default)]
78struct AssetLoadDiagnosticsStore {
79 state: Mutex<AssetLoadDiagnosticsState>,
80}
81
82trait AssetLoadResolvedMetadata {
83 fn revision(&self) -> AssetRevision;
84}
85
86impl AssetLoadResolvedMetadata for ResolvedAssetBytes {
87 fn revision(&self) -> AssetRevision {
88 self.revision
89 }
90}
91
92impl AssetLoadResolvedMetadata for ResolvedAssetReference {
93 fn revision(&self) -> AssetRevision {
94 self.revision
95 }
96}
97
98impl AssetLoadDiagnosticsStore {
99 fn snapshot(&self) -> AssetLoadDiagnosticsSnapshot {
100 let state = self
101 .state
102 .lock()
103 .expect("poisoned AssetLoadDiagnosticsStore state lock");
104 let mut snapshot = state.snapshot.clone();
105 snapshot.recent = state.recent.iter().cloned().collect();
106 snapshot
107 }
108
109 fn record_bytes_result(
110 &self,
111 request: &AssetRequest,
112 result: &Result<ResolvedAssetBytes, AssetLoadError>,
113 ) {
114 self.record_result(AssetLoadAccessKind::Bytes, &request.locator, result);
115 }
116
117 fn record_reference_result(
118 &self,
119 request: &AssetRequest,
120 result: &Result<ResolvedAssetReference, AssetLoadError>,
121 ) {
122 self.record_result(
123 AssetLoadAccessKind::ExternalReference,
124 &request.locator,
125 result,
126 );
127 }
128
129 fn record_result<T>(
130 &self,
131 access_kind: AssetLoadAccessKind,
132 locator: &AssetLocator,
133 result: &Result<T, AssetLoadError>,
134 ) where
135 T: AssetLoadResolvedMetadata,
136 {
137 let mut state = self
138 .state
139 .lock()
140 .expect("poisoned AssetLoadDiagnosticsStore state lock");
141 state.snapshot.total_requests = state.snapshot.total_requests.saturating_add(1);
142 match access_kind {
143 AssetLoadAccessKind::Bytes => {
144 state.snapshot.bytes_requests = state.snapshot.bytes_requests.saturating_add(1);
145 }
146 AssetLoadAccessKind::ExternalReference => {
147 state.snapshot.reference_requests =
148 state.snapshot.reference_requests.saturating_add(1);
149 }
150 }
151
152 let (outcome_kind, revision, previous_revision, revision_transition, message) = match result
153 {
154 Ok(resolved) => {
155 let revision = resolved.revision();
156 let previous_revision = state.last_seen_revisions.insert(locator.clone(), revision);
157 let revision_transition = match previous_revision {
158 None => Some(AssetRevisionTransitionKind::Initial),
159 Some(prev) if prev == revision => Some(AssetRevisionTransitionKind::Stable),
160 Some(_) => {
161 state.snapshot.revision_change_requests =
162 state.snapshot.revision_change_requests.saturating_add(1);
163 Some(AssetRevisionTransitionKind::Changed)
164 }
165 };
166 (
167 AssetLoadOutcomeKind::Resolved,
168 Some(revision),
169 previous_revision,
170 revision_transition,
171 None,
172 )
173 }
174 Err(err) => {
175 let outcome_kind = match err {
176 AssetLoadError::NotFound => AssetLoadOutcomeKind::Missing,
177 AssetLoadError::StaleManifestMapping { .. } => {
178 state.snapshot.stale_manifest_requests =
179 state.snapshot.stale_manifest_requests.saturating_add(1);
180 AssetLoadOutcomeKind::StaleManifest
181 }
182 AssetLoadError::UnsupportedLocatorKind { kind } => {
183 match kind {
184 AssetLocatorKind::File => {
185 state.snapshot.unsupported_file_requests =
186 state.snapshot.unsupported_file_requests.saturating_add(1);
187 }
188 AssetLocatorKind::Url => {
189 state.snapshot.unsupported_url_requests =
190 state.snapshot.unsupported_url_requests.saturating_add(1);
191 }
192 _ => {}
193 }
194 AssetLoadOutcomeKind::UnsupportedLocatorKind
195 }
196 AssetLoadError::ExternalReferenceUnavailable { .. } => {
197 state.snapshot.external_reference_unavailable_requests = state
198 .snapshot
199 .external_reference_unavailable_requests
200 .saturating_add(1);
201 AssetLoadOutcomeKind::ExternalReferenceUnavailable
202 }
203 AssetLoadError::ReferenceOnlyLocator { .. } => {
204 AssetLoadOutcomeKind::ReferenceOnlyLocator
205 }
206 AssetLoadError::ResolverUnavailable => {
207 AssetLoadOutcomeKind::ResolverUnavailable
208 }
209 AssetLoadError::AccessDenied => AssetLoadOutcomeKind::AccessDenied,
210 AssetLoadError::Io { .. } => {
211 state.snapshot.io_requests = state.snapshot.io_requests.saturating_add(1);
212 AssetLoadOutcomeKind::Io
213 }
214 };
215
216 if matches!(err, AssetLoadError::NotFound)
217 && locator.kind() == AssetLocatorKind::BundleAsset
218 {
219 state.snapshot.missing_bundle_asset_requests = state
220 .snapshot
221 .missing_bundle_asset_requests
222 .saturating_add(1);
223 }
224
225 (
226 outcome_kind,
227 None,
228 state.last_seen_revisions.get(locator).copied(),
229 None,
230 match err {
231 AssetLoadError::StaleManifestMapping { path } => Some(path.to_string()),
232 AssetLoadError::ReferenceOnlyLocator { kind } => Some(format!(
233 "asset locator kind {kind:?} is reference-only on this path; resolve_reference(...) instead"
234 )),
235 AssetLoadError::Io {
236 operation,
237 path,
238 message,
239 } => Some(format!("{operation} {path}: {message}")),
240 _ => None,
241 },
242 )
243 }
244 };
245
246 push_recent_event(
247 &mut state.recent,
248 AssetLoadDiagnosticEvent {
249 access_kind,
250 locator_kind: locator.kind(),
251 locator_debug: debug_asset_locator(locator),
252 outcome_kind,
253 revision,
254 previous_revision,
255 revision_transition,
256 message,
257 },
258 );
259 }
260}
261
262fn push_recent_event(
263 recent: &mut VecDeque<AssetLoadDiagnosticEvent>,
264 event: AssetLoadDiagnosticEvent,
265) {
266 if recent.len() >= MAX_ASSET_LOAD_RECENT_EVENTS {
267 let _ = recent.pop_front();
268 }
269 recent.push_back(event);
270}
271
272fn debug_asset_locator(locator: &AssetLocator) -> String {
273 match locator {
274 AssetLocator::Memory(key) => format!("memory:{}", key.as_str()),
275 AssetLocator::Embedded(locator) => {
276 format!(
277 "embedded:{}:{}",
278 locator.owner.as_str(),
279 locator.key.as_str()
280 )
281 }
282 AssetLocator::BundleAsset(locator) => {
283 format!(
284 "bundle:{}:{}",
285 locator.bundle.as_str(),
286 locator.key.as_str()
287 )
288 }
289 AssetLocator::File(locator) => format!("file:{}", locator.path.to_string_lossy()),
290 AssetLocator::Url(locator) => format!("url:{}", locator.as_str()),
291 }
292}
293
294#[derive(Default)]
295struct AssetResolverServiceState {
296 layers: RwLock<Vec<AssetResolverLayer>>,
297 diagnostics: AssetLoadDiagnosticsStore,
298}
299
300#[derive(Clone)]
301enum AssetResolverLayer {
302 Primary(Arc<dyn AssetResolver>),
303 Registered(Arc<dyn AssetResolver>),
304}
305
306impl AssetResolverLayer {
307 fn resolver(&self) -> &Arc<dyn AssetResolver> {
308 match self {
309 Self::Primary(resolver) | Self::Registered(resolver) => resolver,
310 }
311 }
312}
313
314#[derive(Clone)]
315pub struct AssetResolverService {
316 state: Arc<AssetResolverServiceState>,
317}
318
319impl AssetResolverService {
320 pub fn new(resolver: Arc<dyn AssetResolver>) -> Self {
321 let service = Self::default();
322 service.set_primary_resolver(resolver);
323 service
324 }
325
326 pub fn primary_resolver(&self) -> Option<Arc<dyn AssetResolver>> {
327 self.state
328 .layers
329 .read()
330 .expect("poisoned AssetResolverService layers lock")
331 .iter()
332 .find_map(|layer| match layer {
333 AssetResolverLayer::Primary(resolver) => Some(resolver.clone()),
334 AssetResolverLayer::Registered(_) => None,
335 })
336 }
337
338 pub fn layered_resolvers(&self) -> Vec<Arc<dyn AssetResolver>> {
339 self.state
340 .layers
341 .read()
342 .expect("poisoned AssetResolverService layers lock")
343 .iter()
344 .filter_map(|layer| match layer {
345 AssetResolverLayer::Primary(_) => None,
346 AssetResolverLayer::Registered(resolver) => Some(resolver.clone()),
347 })
348 .collect()
349 }
350
351 pub fn set_primary_resolver(&self, resolver: Arc<dyn AssetResolver>) {
352 let mut layers = self
353 .state
354 .layers
355 .write()
356 .expect("poisoned AssetResolverService layers lock");
357 if let Some(layer) = layers
358 .iter_mut()
359 .find(|layer| matches!(layer, AssetResolverLayer::Primary(_)))
360 {
361 *layer = AssetResolverLayer::Primary(resolver);
362 } else {
363 layers.push(AssetResolverLayer::Primary(resolver));
364 }
365 }
366
367 pub fn register_resolver(&self, resolver: Arc<dyn AssetResolver>) {
368 self.state
369 .layers
370 .write()
371 .expect("poisoned AssetResolverService layers lock")
372 .push(AssetResolverLayer::Registered(resolver));
373 }
374
375 pub fn register_bundle_entries(
376 &self,
377 bundle: impl Into<AssetBundleId>,
378 entries: impl IntoIterator<Item = StaticAssetEntry>,
379 ) {
380 let mut resolver = InMemoryAssetResolver::new();
381 resolver.insert_bundle_entries(bundle, entries);
382 self.register_resolver(Arc::new(resolver));
383 }
384
385 pub fn register_embedded_entries(
386 &self,
387 owner: impl Into<AssetBundleId>,
388 entries: impl IntoIterator<Item = StaticAssetEntry>,
389 ) {
390 let mut resolver = InMemoryAssetResolver::new();
391 resolver.insert_embedded_entries(owner, entries);
392 self.register_resolver(Arc::new(resolver));
393 }
394
395 fn resolver_layers(&self) -> Vec<AssetResolverLayer> {
396 self.state
397 .layers
398 .read()
399 .expect("poisoned AssetResolverService layers lock")
400 .clone()
401 }
402
403 pub fn capabilities(&self) -> AssetCapabilities {
404 let mut caps = AssetCapabilities::default();
405
406 for layer in self.resolver_layers() {
407 union_capabilities(&mut caps, layer.resolver().capabilities());
408 }
409 caps
410 }
411
412 pub fn supports(&self, locator: &AssetLocator) -> bool {
413 self.capabilities().supports(locator)
414 }
415
416 pub fn diagnostics_snapshot(&self) -> AssetLoadDiagnosticsSnapshot {
417 self.state.diagnostics.snapshot()
418 }
419
420 pub fn resolve_bytes(
421 &self,
422 request: &AssetRequest,
423 ) -> Result<ResolvedAssetBytes, AssetLoadError> {
424 let mut saw_supported = false;
425
426 for layer in self.resolver_layers().into_iter().rev() {
427 let resolver = layer.resolver();
428 match try_resolver_layer(resolver.as_ref(), request) {
429 Ok(Some(resolved)) => {
430 let result = Ok(resolved);
431 self.state.diagnostics.record_bytes_result(request, &result);
432 return result;
433 }
434 Ok(None) => saw_supported |= resolver.supports(&request.locator),
435 Err(err) => {
436 let result = Err(err);
437 self.state.diagnostics.record_bytes_result(request, &result);
438 return result;
439 }
440 }
441 }
442
443 let result = if saw_supported {
444 Err(AssetLoadError::NotFound)
445 } else {
446 Err(AssetLoadError::UnsupportedLocatorKind {
447 kind: request.locator.kind(),
448 })
449 };
450 self.state.diagnostics.record_bytes_result(request, &result);
451 result
452 }
453
454 pub fn resolve_locator_bytes(
455 &self,
456 locator: AssetLocator,
457 ) -> Result<ResolvedAssetBytes, AssetLoadError> {
458 self.resolve_bytes(&AssetRequest::new(locator))
459 }
460
461 pub fn resolve_reference(
462 &self,
463 request: &AssetRequest,
464 ) -> Result<ResolvedAssetReference, AssetLoadError> {
465 let mut saw_supported = false;
466
467 for layer in self.resolver_layers().into_iter().rev() {
468 let resolver = layer.resolver();
469 match try_resolver_reference_layer(resolver.as_ref(), request) {
470 Ok(Some(resolved)) => {
471 let result = Ok(resolved);
472 self.state
473 .diagnostics
474 .record_reference_result(request, &result);
475 return result;
476 }
477 Ok(None) => saw_supported |= resolver.supports(&request.locator),
478 Err(err) => {
479 let result = Err(err);
480 self.state
481 .diagnostics
482 .record_reference_result(request, &result);
483 return result;
484 }
485 }
486 }
487
488 let result = if saw_supported {
489 Err(AssetLoadError::NotFound)
490 } else {
491 Err(AssetLoadError::UnsupportedLocatorKind {
492 kind: request.locator.kind(),
493 })
494 };
495 self.state
496 .diagnostics
497 .record_reference_result(request, &result);
498 result
499 }
500
501 pub fn resolve_locator_reference(
502 &self,
503 locator: AssetLocator,
504 ) -> Result<ResolvedAssetReference, AssetLoadError> {
505 self.resolve_reference(&AssetRequest::new(locator))
506 }
507}
508
509impl fmt::Debug for AssetResolverService {
510 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511 f.debug_struct("AssetResolverService")
512 .field("capabilities", &self.capabilities())
513 .field("has_primary", &self.primary_resolver().is_some())
514 .field("layered_resolvers", &self.layered_resolvers().len())
515 .finish_non_exhaustive()
516 }
517}
518
519impl Default for AssetResolverService {
520 fn default() -> Self {
521 Self {
522 state: Arc::new(AssetResolverServiceState::default()),
523 }
524 }
525}
526
527impl From<Arc<dyn AssetResolver>> for AssetResolverService {
528 fn from(resolver: Arc<dyn AssetResolver>) -> Self {
529 Self::new(resolver)
530 }
531}
532
533pub fn set_asset_resolver(host: &mut impl GlobalsHost, resolver: Arc<dyn AssetResolver>) {
534 host.with_global_mut(AssetResolverService::default, |service, _host| {
535 service.set_primary_resolver(resolver);
536 });
537}
538
539pub fn register_asset_resolver(host: &mut impl GlobalsHost, resolver: Arc<dyn AssetResolver>) {
540 host.with_global_mut(AssetResolverService::default, |service, _host| {
541 service.register_resolver(resolver);
542 });
543}
544
545pub fn register_bundle_asset_entries(
546 host: &mut impl GlobalsHost,
547 bundle: impl Into<AssetBundleId>,
548 entries: impl IntoIterator<Item = StaticAssetEntry>,
549) {
550 let bundle = bundle.into();
551 let entries = entries.into_iter().collect::<Vec<_>>();
552 host.with_global_mut(AssetResolverService::default, move |service, _host| {
553 service.register_bundle_entries(bundle, entries);
554 });
555}
556
557pub fn register_embedded_asset_entries(
558 host: &mut impl GlobalsHost,
559 owner: impl Into<AssetBundleId>,
560 entries: impl IntoIterator<Item = StaticAssetEntry>,
561) {
562 let owner = owner.into();
563 let entries = entries.into_iter().collect::<Vec<_>>();
564 host.with_global_mut(AssetResolverService::default, move |service, _host| {
565 service.register_embedded_entries(owner, entries);
566 });
567}
568
569pub fn asset_resolver(host: &impl GlobalsHost) -> Option<&AssetResolverService> {
570 host.global::<AssetResolverService>()
571}
572
573pub fn asset_capabilities(host: &impl GlobalsHost) -> Option<AssetCapabilities> {
574 let mut caps = asset_resolver(host)
575 .map(AssetResolverService::capabilities)
576 .unwrap_or_default();
577 let mut has_any = asset_resolver(host).is_some();
578
579 if let Some(reload_support) = asset_reload_support(host) {
580 caps.file_watch |= reload_support.file_watch;
581 has_any |= reload_support.file_watch;
582 }
583
584 has_any.then_some(caps)
585}
586
587pub fn resolve_asset_bytes(
588 host: &impl GlobalsHost,
589 request: &AssetRequest,
590) -> Result<ResolvedAssetBytes, AssetLoadError> {
591 asset_resolver(host)
592 .ok_or(AssetLoadError::ResolverUnavailable)?
593 .resolve_bytes(request)
594}
595
596pub fn resolve_asset_locator_bytes(
597 host: &impl GlobalsHost,
598 locator: AssetLocator,
599) -> Result<ResolvedAssetBytes, AssetLoadError> {
600 resolve_asset_bytes(host, &AssetRequest::new(locator))
601}
602
603pub fn resolve_asset_reference(
604 host: &impl GlobalsHost,
605 request: &AssetRequest,
606) -> Result<ResolvedAssetReference, AssetLoadError> {
607 asset_resolver(host)
608 .ok_or(AssetLoadError::ResolverUnavailable)?
609 .resolve_reference(request)
610}
611
612pub fn resolve_asset_locator_reference(
613 host: &impl GlobalsHost,
614 locator: AssetLocator,
615) -> Result<ResolvedAssetReference, AssetLoadError> {
616 resolve_asset_reference(host, &AssetRequest::new(locator))
617}
618
619fn union_capabilities(dst: &mut AssetCapabilities, src: AssetCapabilities) {
620 dst.memory |= src.memory;
621 dst.embedded |= src.embedded;
622 dst.bundle_asset |= src.bundle_asset;
623 dst.file |= src.file;
624 dst.url |= src.url;
625 dst.file_watch |= src.file_watch;
626 dst.system_font_scan |= src.system_font_scan;
627}
628
629fn try_resolver_layer(
630 resolver: &dyn AssetResolver,
631 request: &AssetRequest,
632) -> Result<Option<ResolvedAssetBytes>, AssetLoadError> {
633 if !resolver.supports(&request.locator) {
634 return Ok(None);
635 }
636
637 match resolver.resolve_bytes(request) {
638 Ok(resolved) => Ok(Some(resolved)),
639 Err(AssetLoadError::NotFound) => Ok(None),
640 Err(AssetLoadError::UnsupportedLocatorKind { .. }) => Ok(None),
641 Err(err) => Err(err),
642 }
643}
644
645fn try_resolver_reference_layer(
646 resolver: &dyn AssetResolver,
647 request: &AssetRequest,
648) -> Result<Option<ResolvedAssetReference>, AssetLoadError> {
649 if !resolver.supports(&request.locator) {
650 return Ok(None);
651 }
652
653 match resolver.resolve_reference(request) {
654 Ok(resolved) => Ok(Some(resolved)),
655 Err(AssetLoadError::NotFound) => Ok(None),
656 Err(AssetLoadError::UnsupportedLocatorKind { .. }) => Ok(None),
657 Err(err) => Err(err),
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use std::any::{Any, TypeId};
664 use std::collections::HashMap;
665
666 use fret_assets::UrlPassthroughAssetResolver;
667 use fret_assets::{AssetLocator, AssetRevision, InMemoryAssetResolver};
668
669 use super::*;
670
671 #[derive(Default)]
672 struct TestHost {
673 globals: HashMap<TypeId, Box<dyn Any>>,
674 }
675
676 impl GlobalsHost for TestHost {
677 fn set_global<T: Any>(&mut self, value: T) {
678 self.globals.insert(TypeId::of::<T>(), Box::new(value));
679 }
680
681 fn global<T: Any>(&self) -> Option<&T> {
682 self.globals.get(&TypeId::of::<T>())?.downcast_ref::<T>()
683 }
684
685 fn with_global_mut<T: Any, R>(
686 &mut self,
687 init: impl FnOnce() -> T,
688 f: impl FnOnce(&mut T, &mut Self) -> R,
689 ) -> R {
690 let type_id = TypeId::of::<T>();
691 let mut value = match self.globals.remove(&type_id) {
692 None => init(),
693 Some(value) => *value.downcast::<T>().expect("global type id must match"),
694 };
695 let out = f(&mut value, self);
696 self.globals.insert(type_id, Box::new(value));
697 out
698 }
699 }
700
701 #[test]
702 fn resolve_asset_bytes_requires_installed_service() {
703 let host = TestHost::default();
704 let err =
705 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
706 .expect_err("missing service should fail");
707
708 assert_eq!(err, AssetLoadError::ResolverUnavailable);
709 }
710
711 #[test]
712 fn installed_service_resolves_bundle_assets() {
713 let mut host = TestHost::default();
714 let mut resolver = InMemoryAssetResolver::new();
715 resolver.insert_bundle("app", "images/logo.png", AssetRevision(7), [1u8, 2, 3]);
716 set_asset_resolver(&mut host, Arc::new(resolver));
717
718 let caps = asset_capabilities(&host).expect("resolver caps should exist");
719 let resolved =
720 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
721 .expect("bundle asset should resolve");
722
723 assert!(caps.bundle_asset);
724 assert_eq!(resolved.revision, AssetRevision(7));
725 assert_eq!(resolved.bytes.as_ref(), &[1, 2, 3]);
726 }
727
728 #[test]
729 fn diagnostics_snapshot_records_initial_stable_and_changed_revisions() {
730 let mut host = TestHost::default();
731 register_bundle_asset_entries(
732 &mut host,
733 "app",
734 [StaticAssetEntry::new(
735 "images/logo.png",
736 AssetRevision(1),
737 b"v1",
738 )],
739 );
740
741 let _ = resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
742 .expect("first bundle resolution should succeed");
743 let _ = resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
744 .expect("second bundle resolution should succeed");
745 register_bundle_asset_entries(
746 &mut host,
747 "app",
748 [StaticAssetEntry::new(
749 "images/logo.png",
750 AssetRevision(9),
751 b"v9",
752 )],
753 );
754 let _ = resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
755 .expect("updated bundle resolution should succeed");
756
757 let snapshot = asset_resolver(&host)
758 .expect("resolver service")
759 .diagnostics_snapshot();
760
761 assert_eq!(snapshot.total_requests, 3);
762 assert_eq!(snapshot.bytes_requests, 3);
763 assert_eq!(snapshot.revision_change_requests, 1);
764 assert_eq!(snapshot.recent.len(), 3);
765 assert_eq!(
766 snapshot.recent[0].revision_transition,
767 Some(AssetRevisionTransitionKind::Initial)
768 );
769 assert_eq!(
770 snapshot.recent[1].revision_transition,
771 Some(AssetRevisionTransitionKind::Stable)
772 );
773 assert_eq!(
774 snapshot.recent[2].revision_transition,
775 Some(AssetRevisionTransitionKind::Changed)
776 );
777 assert_eq!(snapshot.recent[2].previous_revision, Some(AssetRevision(1)));
778 assert_eq!(snapshot.recent[2].revision, Some(AssetRevision(9)));
779 }
780
781 #[test]
782 fn diagnostics_snapshot_counts_missing_bundle_assets() {
783 let mut host = TestHost::default();
784 let resolver = InMemoryAssetResolver::new().with_capabilities(AssetCapabilities {
785 memory: false,
786 embedded: false,
787 bundle_asset: true,
788 file: false,
789 url: false,
790 file_watch: false,
791 system_font_scan: false,
792 });
793 set_asset_resolver(&mut host, Arc::new(resolver));
794
795 let err =
796 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/missing.png"))
797 .expect_err("missing bundle asset should fail");
798
799 assert_eq!(err, AssetLoadError::NotFound);
800 let snapshot = asset_resolver(&host)
801 .expect("resolver service")
802 .diagnostics_snapshot();
803 assert_eq!(snapshot.missing_bundle_asset_requests, 1);
804 assert_eq!(snapshot.recent.len(), 1);
805 assert_eq!(
806 snapshot.recent[0].outcome_kind,
807 AssetLoadOutcomeKind::Missing
808 );
809 assert_eq!(
810 snapshot.recent[0].locator_kind,
811 AssetLocatorKind::BundleAsset
812 );
813 }
814
815 #[test]
816 fn diagnostics_snapshot_counts_unsupported_file_capability_requests() {
817 let mut host = TestHost::default();
818 register_bundle_asset_entries(
819 &mut host,
820 "app",
821 [StaticAssetEntry::new(
822 "images/logo.png",
823 AssetRevision(1),
824 b"png",
825 )],
826 );
827
828 let err = resolve_asset_locator_bytes(&host, AssetLocator::file("assets/logo.png"))
829 .expect_err("unsupported file locator should fail");
830
831 assert_eq!(
832 err,
833 AssetLoadError::UnsupportedLocatorKind {
834 kind: AssetLocatorKind::File,
835 }
836 );
837 let snapshot = asset_resolver(&host)
838 .expect("resolver service")
839 .diagnostics_snapshot();
840 assert_eq!(snapshot.unsupported_file_requests, 1);
841 assert_eq!(
842 snapshot.recent[0].outcome_kind,
843 AssetLoadOutcomeKind::UnsupportedLocatorKind
844 );
845 assert_eq!(snapshot.recent[0].locator_kind, AssetLocatorKind::File);
846 }
847
848 #[test]
849 fn diagnostics_snapshot_counts_external_reference_unavailable_requests() {
850 let mut host = TestHost::default();
851 register_bundle_asset_entries(
852 &mut host,
853 "app",
854 [StaticAssetEntry::new(
855 "icons/search.svg",
856 AssetRevision(2),
857 br#"<svg viewBox="0 0 1 1"></svg>"#,
858 )],
859 );
860
861 let err =
862 resolve_asset_locator_reference(&host, AssetLocator::bundle("app", "icons/search.svg"))
863 .expect_err("byte-only bundle asset should not resolve to external reference");
864
865 assert_eq!(
866 err,
867 AssetLoadError::ExternalReferenceUnavailable {
868 kind: AssetLocatorKind::BundleAsset,
869 }
870 );
871 let snapshot = asset_resolver(&host)
872 .expect("resolver service")
873 .diagnostics_snapshot();
874 assert_eq!(snapshot.reference_requests, 1);
875 assert_eq!(snapshot.external_reference_unavailable_requests, 1);
876 assert_eq!(
877 snapshot.recent[0].access_kind,
878 AssetLoadAccessKind::ExternalReference
879 );
880 assert_eq!(
881 snapshot.recent[0].outcome_kind,
882 AssetLoadOutcomeKind::ExternalReferenceUnavailable
883 );
884 }
885
886 #[test]
887 fn diagnostics_snapshot_records_reference_only_locator_requests() {
888 let mut host = TestHost::default();
889 set_asset_resolver(&mut host, Arc::new(UrlPassthroughAssetResolver::new()));
890
891 let err =
892 resolve_asset_locator_bytes(&host, AssetLocator::url("https://example.com/logo.png"))
893 .expect_err(
894 "url passthrough bytes lane should report a typed reference-only error",
895 );
896
897 assert_eq!(
898 err,
899 AssetLoadError::ReferenceOnlyLocator {
900 kind: AssetLocatorKind::Url,
901 }
902 );
903 let snapshot = asset_resolver(&host)
904 .expect("resolver service")
905 .diagnostics_snapshot();
906 assert_eq!(snapshot.bytes_requests, 1);
907 assert_eq!(
908 snapshot.recent[0].outcome_kind,
909 AssetLoadOutcomeKind::ReferenceOnlyLocator
910 );
911 assert_eq!(snapshot.recent[0].locator_kind, AssetLocatorKind::Url);
912 assert_eq!(
913 snapshot.recent[0].message.as_deref(),
914 Some(
915 "asset locator kind Url is reference-only on this path; resolve_reference(...) instead"
916 )
917 );
918 }
919
920 #[test]
921 fn diagnostics_snapshot_counts_typed_io_requests() {
922 let mut host = TestHost::default();
923
924 struct IoResolver;
925
926 impl AssetResolver for IoResolver {
927 fn capabilities(&self) -> AssetCapabilities {
928 AssetCapabilities {
929 memory: false,
930 embedded: false,
931 bundle_asset: true,
932 file: false,
933 url: false,
934 file_watch: false,
935 system_font_scan: false,
936 }
937 }
938
939 fn resolve_bytes(
940 &self,
941 request: &AssetRequest,
942 ) -> Result<ResolvedAssetBytes, AssetLoadError> {
943 Err(AssetLoadError::Io {
944 operation: fret_assets::AssetIoOperation::Read,
945 path: format!("/tmp/dev-assets/{}", debug_asset_locator(&request.locator))
946 .into(),
947 message: "device reset".into(),
948 })
949 }
950 }
951
952 set_asset_resolver(&mut host, Arc::new(IoResolver));
953
954 let err =
955 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
956 .expect_err("typed io failures should surface through the shared asset contract");
957
958 assert_eq!(
959 err,
960 AssetLoadError::Io {
961 operation: fret_assets::AssetIoOperation::Read,
962 path: "/tmp/dev-assets/bundle:app:images/logo.png".into(),
963 message: "device reset".into(),
964 }
965 );
966 let snapshot = asset_resolver(&host)
967 .expect("resolver service")
968 .diagnostics_snapshot();
969 assert_eq!(snapshot.io_requests, 1);
970 assert_eq!(snapshot.bytes_requests, 1);
971 assert_eq!(snapshot.recent[0].outcome_kind, AssetLoadOutcomeKind::Io);
972 assert_eq!(
973 snapshot.recent[0].message.as_deref(),
974 Some("read /tmp/dev-assets/bundle:app:images/logo.png: device reset")
975 );
976 }
977
978 #[test]
979 fn asset_capabilities_union_optional_escape_hatches_across_layers() {
980 let mut host = TestHost::default();
981 let primary = InMemoryAssetResolver::new().with_capabilities(AssetCapabilities {
982 memory: false,
983 embedded: false,
984 bundle_asset: false,
985 file: true,
986 url: false,
987 file_watch: true,
988 system_font_scan: false,
989 });
990 let secondary = InMemoryAssetResolver::new().with_capabilities(AssetCapabilities {
991 memory: false,
992 embedded: false,
993 bundle_asset: false,
994 file: false,
995 url: true,
996 file_watch: false,
997 system_font_scan: true,
998 });
999
1000 set_asset_resolver(&mut host, Arc::new(primary));
1001 register_asset_resolver(&mut host, Arc::new(secondary));
1002
1003 assert_eq!(
1004 asset_capabilities(&host).expect("resolver caps should exist"),
1005 AssetCapabilities {
1006 memory: false,
1007 embedded: false,
1008 bundle_asset: false,
1009 file: true,
1010 url: true,
1011 file_watch: true,
1012 system_font_scan: true,
1013 }
1014 );
1015 }
1016
1017 #[test]
1018 fn unsupported_file_locator_kind_stays_unsupported_even_when_other_locators_exist() {
1019 let mut host = TestHost::default();
1020 register_bundle_asset_entries(
1021 &mut host,
1022 "app",
1023 [StaticAssetEntry::new(
1024 "images/logo.png",
1025 AssetRevision(1),
1026 b"png",
1027 )],
1028 );
1029
1030 let err = resolve_asset_locator_bytes(&host, AssetLocator::file("assets/logo.png"))
1031 .expect_err("unsupported file locator should not be downgraded to not-found");
1032
1033 assert_eq!(
1034 err,
1035 AssetLoadError::UnsupportedLocatorKind {
1036 kind: fret_assets::AssetLocatorKind::File,
1037 }
1038 );
1039 }
1040
1041 #[test]
1042 fn supported_but_missing_file_locator_returns_not_found() {
1043 let mut host = TestHost::default();
1044 let resolver = InMemoryAssetResolver::new().with_capabilities(AssetCapabilities {
1045 memory: false,
1046 embedded: false,
1047 bundle_asset: false,
1048 file: true,
1049 url: false,
1050 file_watch: true,
1051 system_font_scan: false,
1052 });
1053 set_asset_resolver(&mut host, Arc::new(resolver));
1054
1055 let err = resolve_asset_locator_bytes(&host, AssetLocator::file("assets/missing.png"))
1056 .expect_err("supported but missing file locator should report not-found");
1057
1058 assert_eq!(err, AssetLoadError::NotFound);
1059 }
1060
1061 #[test]
1062 fn register_bundle_asset_entries_adds_composable_static_assets() {
1063 let mut host = TestHost::default();
1064 register_bundle_asset_entries(
1065 &mut host,
1066 "app",
1067 [
1068 StaticAssetEntry::new("images/logo.png", AssetRevision(2), b"png")
1069 .with_media_type("image/png"),
1070 ],
1071 );
1072
1073 let resolved =
1074 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1075 .expect("bundle asset should resolve");
1076
1077 assert_eq!(resolved.revision, AssetRevision(2));
1078 assert_eq!(
1079 resolved.media_type.as_ref().map(|v| v.as_str()),
1080 Some("image/png")
1081 );
1082 }
1083
1084 #[test]
1085 fn register_embedded_asset_entries_adds_namespaced_assets() {
1086 let mut host = TestHost::default();
1087 register_embedded_asset_entries(
1088 &mut host,
1089 "fret-ui-shadcn",
1090 [StaticAssetEntry::new(
1091 "icons/search.svg",
1092 AssetRevision(5),
1093 br#"<svg viewBox="0 0 1 1"></svg>"#,
1094 )
1095 .with_media_type("image/svg+xml")],
1096 );
1097
1098 let resolved = resolve_asset_locator_bytes(
1099 &host,
1100 AssetLocator::embedded("fret-ui-shadcn", "icons/search.svg"),
1101 )
1102 .expect("embedded asset should resolve");
1103
1104 assert_eq!(resolved.revision, AssetRevision(5));
1105 assert_eq!(
1106 resolved.media_type.as_ref().map(|v| v.as_str()),
1107 Some("image/svg+xml")
1108 );
1109 }
1110
1111 #[test]
1112 fn resolve_asset_reference_requires_installed_service() {
1113 let host = TestHost::default();
1114 let err = resolve_asset_locator_reference(&host, AssetLocator::bundle("app", "logo.png"))
1115 .expect_err("missing service should fail");
1116
1117 assert_eq!(err, AssetLoadError::ResolverUnavailable);
1118 }
1119
1120 #[test]
1121 fn layered_resolvers_preserve_existing_sources() {
1122 let mut host = TestHost::default();
1123 register_bundle_asset_entries(
1124 &mut host,
1125 "app",
1126 [StaticAssetEntry::new(
1127 "images/logo.png",
1128 AssetRevision(1),
1129 b"png",
1130 )],
1131 );
1132
1133 let mut embedded = InMemoryAssetResolver::new();
1134 embedded.insert_embedded(
1135 "fret-ui-shadcn",
1136 "icons/search.svg",
1137 AssetRevision(4),
1138 [9u8, 8, 7],
1139 );
1140 register_asset_resolver(&mut host, Arc::new(embedded));
1141
1142 let bundle =
1143 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1144 .expect("bundle asset should resolve");
1145 let embedded = resolve_asset_locator_bytes(
1146 &host,
1147 AssetLocator::embedded("fret-ui-shadcn", "icons/search.svg"),
1148 )
1149 .expect("embedded asset should resolve");
1150
1151 assert_eq!(bundle.revision, AssetRevision(1));
1152 assert_eq!(embedded.revision, AssetRevision(4));
1153 }
1154
1155 #[test]
1156 fn later_missing_bundle_layer_falls_back_to_earlier_bundle_asset() {
1157 let mut host = TestHost::default();
1158
1159 let mut earlier = InMemoryAssetResolver::new();
1160 earlier.insert_bundle("app", "images/logo.png", AssetRevision(1), [1u8, 2, 3]);
1161 register_asset_resolver(&mut host, Arc::new(earlier));
1162
1163 let later = InMemoryAssetResolver::new().with_capabilities(AssetCapabilities {
1164 memory: false,
1165 embedded: false,
1166 bundle_asset: true,
1167 file: false,
1168 url: false,
1169 file_watch: false,
1170 system_font_scan: false,
1171 });
1172 register_asset_resolver(&mut host, Arc::new(later));
1173
1174 let resolved =
1175 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1176 .expect("later missing layer should fall back to earlier bytes");
1177
1178 assert_eq!(resolved.revision, AssetRevision(1));
1179 assert_eq!(resolved.bytes.as_ref(), &[1, 2, 3]);
1180 }
1181
1182 #[test]
1183 fn later_missing_reference_layer_falls_back_to_earlier_reference_handoff() {
1184 let mut host = TestHost::default();
1185
1186 struct FileReferenceResolver;
1187
1188 impl AssetResolver for FileReferenceResolver {
1189 fn capabilities(&self) -> AssetCapabilities {
1190 AssetCapabilities {
1191 memory: false,
1192 embedded: false,
1193 bundle_asset: true,
1194 file: false,
1195 url: false,
1196 file_watch: false,
1197 system_font_scan: false,
1198 }
1199 }
1200
1201 fn resolve_bytes(
1202 &self,
1203 request: &AssetRequest,
1204 ) -> Result<ResolvedAssetBytes, AssetLoadError> {
1205 Ok(ResolvedAssetBytes::new(
1206 request.locator.clone(),
1207 AssetRevision(1),
1208 b"earlier".as_slice(),
1209 ))
1210 }
1211
1212 fn resolve_reference(
1213 &self,
1214 request: &AssetRequest,
1215 ) -> Result<ResolvedAssetReference, AssetLoadError> {
1216 Ok(ResolvedAssetReference::new(
1217 request.locator.clone(),
1218 AssetRevision(1),
1219 fret_assets::AssetExternalReference::file_path("assets/earlier.png"),
1220 ))
1221 }
1222 }
1223
1224 register_asset_resolver(&mut host, Arc::new(FileReferenceResolver));
1225 register_asset_resolver(
1226 &mut host,
1227 Arc::new(
1228 InMemoryAssetResolver::new().with_capabilities(AssetCapabilities {
1229 memory: false,
1230 embedded: false,
1231 bundle_asset: true,
1232 file: false,
1233 url: false,
1234 file_watch: false,
1235 system_font_scan: false,
1236 }),
1237 ),
1238 );
1239
1240 let resolved =
1241 resolve_asset_locator_reference(&host, AssetLocator::bundle("app", "images/logo.png"))
1242 .expect("later missing reference layer should fall back to earlier handoff");
1243
1244 assert_eq!(resolved.revision, AssetRevision(1));
1245 assert_eq!(
1246 resolved.reference,
1247 fret_assets::AssetExternalReference::file_path("assets/earlier.png")
1248 );
1249 }
1250
1251 #[test]
1252 fn stale_manifest_layer_blocks_earlier_bundle_fallback_and_is_counted() {
1253 let mut host = TestHost::default();
1254
1255 let mut earlier = InMemoryAssetResolver::new();
1256 earlier.insert_bundle("app", "images/logo.png", AssetRevision(1), [1u8, 2, 3]);
1257 register_asset_resolver(&mut host, Arc::new(earlier));
1258
1259 struct StaleManifestResolver;
1260
1261 impl AssetResolver for StaleManifestResolver {
1262 fn capabilities(&self) -> AssetCapabilities {
1263 AssetCapabilities {
1264 memory: false,
1265 embedded: false,
1266 bundle_asset: true,
1267 file: false,
1268 url: false,
1269 file_watch: false,
1270 system_font_scan: false,
1271 }
1272 }
1273
1274 fn resolve_bytes(
1275 &self,
1276 request: &AssetRequest,
1277 ) -> Result<ResolvedAssetBytes, AssetLoadError> {
1278 Err(AssetLoadError::StaleManifestMapping {
1279 path: format!("/tmp/stale/{}", debug_asset_locator(&request.locator)).into(),
1280 })
1281 }
1282 }
1283
1284 register_asset_resolver(&mut host, Arc::new(StaleManifestResolver));
1285
1286 let err =
1287 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1288 .expect_err("stale manifest layer should block fallback");
1289
1290 assert!(matches!(err, AssetLoadError::StaleManifestMapping { .. }));
1291 let snapshot = asset_resolver(&host)
1292 .expect("resolver service")
1293 .diagnostics_snapshot();
1294 assert_eq!(snapshot.stale_manifest_requests, 1);
1295 assert_eq!(snapshot.missing_bundle_asset_requests, 0);
1296 assert_eq!(
1297 snapshot.recent[0].outcome_kind,
1298 AssetLoadOutcomeKind::StaleManifest
1299 );
1300 }
1301
1302 #[test]
1303 fn later_layer_without_reference_blocks_earlier_reference_handoff() {
1304 let mut host = TestHost::default();
1305
1306 struct FileReferenceResolver;
1307
1308 impl AssetResolver for FileReferenceResolver {
1309 fn capabilities(&self) -> AssetCapabilities {
1310 AssetCapabilities {
1311 memory: false,
1312 embedded: false,
1313 bundle_asset: true,
1314 file: false,
1315 url: false,
1316 file_watch: false,
1317 system_font_scan: false,
1318 }
1319 }
1320
1321 fn resolve_bytes(
1322 &self,
1323 request: &AssetRequest,
1324 ) -> Result<ResolvedAssetBytes, AssetLoadError> {
1325 Ok(ResolvedAssetBytes::new(
1326 request.locator.clone(),
1327 AssetRevision(1),
1328 b"earlier".as_slice(),
1329 ))
1330 }
1331
1332 fn resolve_reference(
1333 &self,
1334 request: &AssetRequest,
1335 ) -> Result<ResolvedAssetReference, AssetLoadError> {
1336 Ok(ResolvedAssetReference::new(
1337 request.locator.clone(),
1338 AssetRevision(1),
1339 fret_assets::AssetExternalReference::file_path("assets/earlier.png"),
1340 ))
1341 }
1342 }
1343
1344 register_asset_resolver(&mut host, Arc::new(FileReferenceResolver));
1345 register_bundle_asset_entries(
1346 &mut host,
1347 "app",
1348 [StaticAssetEntry::new(
1349 "images/logo.png",
1350 AssetRevision(9),
1351 b"override",
1352 )],
1353 );
1354
1355 let err =
1356 resolve_asset_locator_reference(&host, AssetLocator::bundle("app", "images/logo.png"))
1357 .expect_err("later static entry should shadow earlier file reference");
1358
1359 assert_eq!(
1360 err,
1361 AssetLoadError::ExternalReferenceUnavailable {
1362 kind: fret_assets::AssetLocatorKind::BundleAsset,
1363 }
1364 );
1365 }
1366
1367 #[test]
1368 fn later_layered_resolvers_override_earlier_layers_for_the_same_locator() {
1369 let mut host = TestHost::default();
1370
1371 let mut earlier = InMemoryAssetResolver::new();
1372 earlier.insert_bundle("app", "images/logo.png", AssetRevision(1), [1u8, 2, 3]);
1373 register_asset_resolver(&mut host, Arc::new(earlier));
1374
1375 let mut later = InMemoryAssetResolver::new();
1376 later.insert_bundle("app", "images/logo.png", AssetRevision(9), [9u8, 8, 7]);
1377 register_asset_resolver(&mut host, Arc::new(later));
1378
1379 let resolved =
1380 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1381 .expect("later layered resolver should win");
1382
1383 assert_eq!(resolved.revision, AssetRevision(9));
1384 assert_eq!(resolved.bytes.as_ref(), &[9, 8, 7]);
1385 }
1386
1387 #[test]
1388 fn later_static_entry_layers_override_earlier_resolver_layers_for_the_same_locator() {
1389 let mut host = TestHost::default();
1390
1391 let mut earlier = InMemoryAssetResolver::new();
1392 earlier.insert_bundle("app", "images/logo.png", AssetRevision(1), [1u8, 2, 3]);
1393 register_asset_resolver(&mut host, Arc::new(earlier));
1394
1395 register_bundle_asset_entries(
1396 &mut host,
1397 "app",
1398 [StaticAssetEntry::new(
1399 "images/logo.png",
1400 AssetRevision(9),
1401 b"override",
1402 )],
1403 );
1404
1405 let resolved =
1406 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1407 .expect("later static entry layer should win");
1408
1409 assert_eq!(resolved.revision, AssetRevision(9));
1410 assert_eq!(resolved.bytes.as_ref(), b"override");
1411 }
1412
1413 #[test]
1414 fn later_resolver_layers_override_earlier_static_entry_layers_for_the_same_locator() {
1415 let mut host = TestHost::default();
1416
1417 register_bundle_asset_entries(
1418 &mut host,
1419 "app",
1420 [StaticAssetEntry::new(
1421 "images/logo.png",
1422 AssetRevision(1),
1423 b"earlier",
1424 )],
1425 );
1426
1427 let mut later = InMemoryAssetResolver::new();
1428 later.insert_bundle("app", "images/logo.png", AssetRevision(9), [9u8, 8, 7]);
1429 register_asset_resolver(&mut host, Arc::new(later));
1430
1431 let resolved =
1432 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1433 .expect("later resolver layer should win");
1434
1435 assert_eq!(resolved.revision, AssetRevision(9));
1436 assert_eq!(resolved.bytes.as_ref(), &[9, 8, 7]);
1437 }
1438
1439 #[test]
1440 fn primary_resolver_replacement_keeps_its_existing_layer_position() {
1441 let mut host = TestHost::default();
1442
1443 let mut earlier = InMemoryAssetResolver::new();
1444 earlier.insert_bundle("app", "images/logo.png", AssetRevision(1), [1u8, 2, 3]);
1445 register_asset_resolver(&mut host, Arc::new(earlier));
1446
1447 let mut first_primary = InMemoryAssetResolver::new();
1448 first_primary.insert_bundle("app", "images/logo.png", AssetRevision(4), [4u8, 4, 4]);
1449 set_asset_resolver(&mut host, Arc::new(first_primary));
1450
1451 let resolved =
1452 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1453 .expect("first primary should win when it is the latest registration");
1454 assert_eq!(resolved.revision, AssetRevision(4));
1455
1456 let mut later = InMemoryAssetResolver::new();
1457 later.insert_bundle("app", "images/logo.png", AssetRevision(9), [9u8, 8, 7]);
1458 register_asset_resolver(&mut host, Arc::new(later));
1459
1460 let resolved =
1461 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1462 .expect("later layered resolver should win");
1463 assert_eq!(resolved.revision, AssetRevision(9));
1464
1465 let mut replacement_primary = InMemoryAssetResolver::new();
1466 replacement_primary.insert_bundle("app", "images/logo.png", AssetRevision(7), [7u8, 7, 7]);
1467 set_asset_resolver(&mut host, Arc::new(replacement_primary));
1468
1469 let resolved =
1470 resolve_asset_locator_bytes(&host, AssetLocator::bundle("app", "images/logo.png"))
1471 .expect("replacing primary should not jump ahead of later layers");
1472
1473 assert_eq!(resolved.revision, AssetRevision(9));
1474 assert_eq!(resolved.bytes.as_ref(), &[9, 8, 7]);
1475 }
1476}