Skip to main content

fret_runtime/
asset_resolver.rs

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}