use ahash::HashMap;
use educe::Educe;
use parking_lot::{RwLock, RwLockReadGuard};
use smol::prelude::*;
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
use crate::resource::id::ResourceId;
use crate::resource::manager::dependency::DependencyGraph;
use crate::resource::manager::load::{Cache, ResourceLoadFuture, ResourceLoadOperation, ResourceLoadParams};
use crate::resource::manager::source::Sources;
use crate::resource::manager::task::ManagerExecutor;
use crate::resource::manager::update::{ResourceUpdateFuture, UpdateManager};
use crate::resource::source::ResourceSource;
use crate::resource::watcher::ResourceWatcherConfig;
use crate::resource::{ResourceDefaultLoader, ResourceLoadError, ResourceLoadResult, ResourceLoader};
use crate::util::priority::HasStaticPriority;
use crate::worker::WorkerPool;
pub mod dependency;
mod load;
mod source;
mod task;
mod update;
pub use load::{
LoadPriority,
ResourceLoadContext,
};
pub use task::{FallibleManagerTask, ManagerTask};
enum ResourceFutureInner<T: ?Sized + Send + Sync + 'static> {
Cached(ResourceLoadResult<T>),
Loading(ResourceLoadFuture<T>),
Updating(ResourceUpdateFuture<T>),
Or {
base: Box<ResourceFutureInner<T>>,
id: ResourceId,
loader: Arc<dyn ResourceLoader<T>>,
},
}
impl<T: ?Sized + Send + Sync + 'static> Debug for ResourceFutureInner<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cached(Ok(res)) => write!(f, "Cached(Ok({}))", res.id()),
Self::Cached(Err(e)) => {
write!(f, "Cached(Err(")?;
Debug::fmt(e, f)?;
write!(f, "))")
},
Self::Loading(_) => write!(f, "Loading(<load-future>)"),
Self::Updating(_) => write!(f, "Updating(<update-future>)"),
Self::Or { base, id, .. } => {
Debug::fmt(base, f)?;
write!(f, " | {}", id)
}
}
}
}
impl<T: ?Sized + Send + Sync + 'static> ResourceFutureInner<T> {
fn poll(
&mut self,
cx: &mut Context<'_>,
cache: Arc<Cache>,
load_params: Arc<ResourceLoadParams<T>>,
) -> Result<Poll<ResourceLoadResult<T>>, Self> {
match self {
Self::Cached(result) => Ok(Poll::Ready(result.clone())),
Self::Loading(load_fut) => Ok(load_fut.poll(cx)),
Self::Updating(update_fut) => Ok(update_fut.poll(cx)),
Self::Or { base, id, loader } => {
let poll_result = match base.poll(cx, cache.clone(), load_params.clone()) {
Ok(poll_result) => poll_result,
Err(new_inner) => {
*base = Box::new(new_inner);
return self.poll(cx, cache, load_params)
}
};
match poll_result {
Poll::Ready(result) => {
match result {
Err(ResourceLoadError::NotFound(_)) => {
let mut load_params = (*load_params).clone();
load_params.loader = loader.clone();
Err(cache.get_or_load(id.clone(), load_params))
},
result => Ok(Poll::Ready(result)),
}
},
poll_result => Ok(poll_result),
}
}
}
}
}
#[derive(Educe)]
#[educe(Debug)]
pub struct ResourceFuture<'ctx, T: ?Sized + Send + Sync + 'static> {
inner: ResourceFutureInner<T>,
#[educe(Debug(ignore))]
cache: Arc<Cache>,
load_params: Arc<ResourceLoadParams<T>>,
#[educe(Debug(ignore))]
ctx: Option<&'ctx ResourceLoadContext>,
}
impl<T: ?Sized + Send + Sync + 'static> ResourceFuture<'static, T> {
#[inline]
fn new(
inner: impl Into<ResourceFutureInner<T>>,
cache: Arc<Cache>,
load_params: ResourceLoadParams<T>,
) -> Self {
Self {
inner: inner.into(),
cache,
load_params: Arc::new(load_params),
ctx: None,
}
}
}
impl<'ctx, T: ?Sized + Send + Sync + 'static> ResourceFuture<'ctx, T> {
#[inline]
fn with_context(
inner: impl Into<ResourceFutureInner<T>>,
cache: Arc<Cache>,
load_params: ResourceLoadParams<T>,
ctx: &'ctx ResourceLoadContext,
) -> Self {
Self {
inner: inner.into(),
cache,
load_params: Arc::new(load_params),
ctx: Some(ctx),
}
}
#[inline]
pub fn check(mut self) -> Result<ResourceLoadResult<T>, Self> {
let mut cx = Context::from_waker(Waker::noop());
match self.poll(&mut cx) {
Poll::Ready(result) => Ok(result),
Poll::Pending => Err(self),
}
}
#[inline]
pub fn or_get(
self,
id: impl Into<ResourceId>,
) -> Self
where
T: ResourceDefaultLoader,
{
self.or_get_with_loader(id, T::default_loader())
}
#[inline]
pub fn or_get_with_loader(
mut self,
id: impl Into<ResourceId>,
loader: impl ResourceLoader<T>,
) -> Self {
self.inner = ResourceFutureInner::Or {
base: Box::new(self.inner),
id: id.into(),
loader: Arc::new(loader),
};
self
}
}
impl<'ctx, T: ?Sized + Send + Sync + 'static> Future for ResourceFuture<'ctx, T> {
type Output = ResourceLoadResult<T>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let cache = self.cache.clone();
let load_params = self.load_params.clone();
let poll_result = match self.inner.poll(cx, cache, load_params) {
Ok(poll_result) => poll_result,
Err(new_inner) => {
self.inner = new_inner;
return self.poll(cx)
}
};
if let Poll::Ready(result) = &poll_result {
if let Ok(resource) = &result {
if let Some(ctx) = self.ctx {
ctx.add_dependency(resource.id().clone());
ctx.cache_resource(resource.clone());
}
}
}
poll_result
}
}
pub struct ResourceManager {
#[allow(unused)] executor: Arc<ManagerExecutor>,
cache: Arc<Cache>,
dependencies: Arc<RwLock<DependencyGraph>>,
update_manager: UpdateManager,
#[cfg(test)]
test_ctx: tests::ResourceManagerTestContext,
}
impl ResourceManager {
#[inline]
pub fn builder<'a>() -> ResourceManagerBuilder<'a> {
ResourceManagerBuilder::default()
}
#[cfg(test)]
#[inline]
pub fn test_ctx(&self) -> &tests::ResourceManagerTestContext { &self.test_ctx }
#[inline]
pub fn dependencies(&self) -> RwLockReadGuard<'_, DependencyGraph> {
self.dependencies.read()
}
#[inline]
pub fn get<T>(&self, id: impl Into<ResourceId>) -> ResourceFuture<'static, T>
where
T: ResourceDefaultLoader,
{
self.get_with_priority(id, LoadPriority::immediate())
}
#[inline]
pub fn get_with_priority<T>(
&self,
id: impl Into<ResourceId>,
load_priority: LoadPriority,
) -> ResourceFuture<'static, T>
where
T: ResourceDefaultLoader,
{
self.get_with_loader_priority(id, T::default_loader(), load_priority)
}
#[inline]
pub fn get_with_loader<T>(
&self,
id: impl Into<ResourceId>,
loader: impl ResourceLoader<T>,
) -> ResourceFuture<'static, T>
where
T: ?Sized + Send + Sync + 'static,
{
self.get_with_loader_priority(id, loader, LoadPriority::immediate())
}
pub fn get_with_loader_priority<T>(
&self,
id: impl Into<ResourceId>,
loader: impl ResourceLoader<T>,
load_priority: LoadPriority,
) -> ResourceFuture<'static, T>
where
T: ?Sized + Send + Sync + 'static,
{
let load_params = ResourceLoadParams {
loader: Arc::new(loader),
priority: load_priority.into(),
parents: vec![],
resource_cache: Arc::new(RwLock::new(HashMap::default())),
operation: ResourceLoadOperation::Load,
};
let inner = self.cache.get_or_load(
id.into(),
load_params.clone(),
);
ResourceFuture::new(inner, self.cache.clone(), load_params)
}
}
impl Drop for ResourceManager {
fn drop(&mut self) {
self.update_manager.stop();
self.cache.clear();
}
}
#[derive(Default, Debug)]
pub struct ResourceManagerWorkerPriorityConfig<PP: HasStaticPriority> {
pub immediate: PP,
pub delayed: PP,
pub update: PP,
}
impl<PP: HasStaticPriority + Clone> From<PP> for ResourceManagerWorkerPriorityConfig<PP> {
#[inline]
fn from(priority: PP) -> Self {
Self {
immediate: priority.clone(),
delayed: priority.clone(),
update: priority,
}
}
}
#[derive(Default)]
pub struct ResourceManagerBuilder<'a> {
sources: Vec<Box<dyn ResourceSource>>,
executor_builder: Option<Box<dyn (FnOnce() -> Arc<ManagerExecutor>) + 'a>>,
}
impl<'a> ResourceManagerBuilder<'a> {
#[inline]
pub fn source(mut self, source: impl ResourceSource) -> Self {
self.sources.push(Box::new(source));
self
}
pub fn worker_config<PP>(
mut self,
priority_config: impl Into<ResourceManagerWorkerPriorityConfig<PP>>,
workers: &'a WorkerPool<PP>,
) -> Self
where
PP: HasStaticPriority + Send + Sync + 'static,
{
let priority_config = priority_config.into();
self.executor_builder = Some(Box::new(move || {
assert!(
priority_config.immediate >= priority_config.delayed,
"Immediate priority should be at least as high as Delayed priority"
);
assert!(
priority_config.delayed >= priority_config.update,
"Delayed priority should be at least as high as Update priority"
);
Arc::new(ManagerExecutor::new(
priority_config,
workers,
))
}));
self
}
#[inline]
pub fn build(self) -> Arc<ResourceManager> {
let executor_builder = self.executor_builder
.expect(".worker_config() is required");
let executor = executor_builder();
let mut update_manager = UpdateManager::new();
let mut sources = self.sources;
for (idx, source) in sources.iter_mut().enumerate() {
source.watcher().configure(ResourceWatcherConfig {
src_idx: idx.into(),
sender: update_manager.watcher_sender().clone(),
});
}
let sources = Arc::new(Sources::new(sources));
let dependencies = Arc::new(RwLock::new(DependencyGraph::default()));
#[cfg(test)]
let test_ctx = tests::ResourceManagerTestContext::default();
let cache = Cache::new(
executor.clone(),
sources.clone(),
dependencies.clone(),
update_manager.updates().clone(),
#[cfg(test)]
test_ctx.sync_load.clone(),
);
update_manager.start(
sources.clone(),
cache.clone(),
dependencies.clone(),
#[cfg(test)]
test_ctx.sync_update.clone(),
);
Arc::new(ResourceManager {
executor,
cache,
dependencies,
update_manager,
#[cfg(test)]
test_ctx,
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use ahash::HashMap;
use async_trait::async_trait;
use itertools::Itertools;
use macro_rules_attribute::apply;
use parking_lot::{Mutex, RwLock, RwLockWriteGuard};
use rstest::{fixture, rstest};
use smol::Timer;
use smol_macros::test as smol_test;
use std::marker::PhantomData;
use std::time::Duration;
use test_log::test as test_log;
use crate::resource::source::{ResourceData, ResourceDataResult, ResourceDataSource, ResourceSource, ResourceUpdate};
use crate::resource::watcher::{ResourceHashProvider, ResourceWatcher};
use crate::resource::{Resource, ResourceReadData};
pub(in crate::resource) async fn timeout<T>(fut: impl Future<Output = T>, time: Duration) -> T {
let timeout_fn = async move {
Timer::after(time).await;
panic!("Timeout reached: {time:?}");
};
fut.or(timeout_fn).await
}
pub(in crate::resource) async fn assert_manager_drops(manager: Arc<ResourceManager>) {
let weak_dependencies = Arc::downgrade(&manager.dependencies);
let weak_cache = Arc::downgrade(&manager.cache);
let weak_executor = Arc::downgrade(&manager.executor);
let executor = manager.executor.clone();
drop(manager);
while !executor.is_empty() {
Timer::after(Duration::from_millis(10)).await;
}
drop(executor);
assert_eq!(weak_dependencies.strong_count(), 0);
assert_eq!(weak_cache.strong_count(), 0);
assert_eq!(weak_executor.strong_count(), 0);
}
pub struct SyncContextReadGuard<'a>{
#[allow(unused)]
inner: smol::lock::RwLockReadGuard<'a, ()>,
count: &'a Mutex<(usize, usize)>,
}
impl Drop for SyncContextReadGuard<'_> {
fn drop(&mut self) {
let mut count = self.count.lock();
count.0 += 1;
count.1 += 1;
}
}
pub struct SyncContextWriteGuard<'a>(
#[allow(unused)]
smol::lock::RwLockWriteGuard<'a, ()>
);
pub struct SyncContext {
inner: smol::lock::RwLock<()>,
count: Mutex<(usize, usize)>,
}
impl Default for SyncContext {
fn default() -> Self {
Self {
inner: smol::lock::RwLock::new(()),
count: Mutex::new((0, 0)),
}
}
}
impl SyncContext {
pub async fn run(&self) -> SyncContextReadGuard<'_> {
SyncContextReadGuard {
inner: self.inner.read().await,
count: &self.count,
}
}
#[inline]
pub async fn block(&self) -> SyncContextWriteGuard<'_> {
SyncContextWriteGuard(self.inner.write().await)
}
#[inline]
pub fn mark_attempt(&self) {
self.count.lock().1 += 1;
}
#[inline]
pub fn assert_count(&self, count: usize) {
let actual = self.count.lock().0;
assert_eq!(self.count.lock().0, count, "Expected {count} executions; was actually {actual}");
}
#[inline]
pub fn assert_attempts(&self, attempts: usize) {
assert_eq!(self.count.lock().1, attempts);
}
pub async fn wait_count(&self, count: usize) {
while self.count.lock().0 < count {
Timer::after(Duration::from_millis(50)).await;
}
}
pub async fn wait_attempts(&self, attempts: usize) {
while self.count.lock().1 < attempts {
Timer::after(Duration::from_millis(50)).await;
}
}
#[inline]
pub fn clear(&self) {
*self.count.lock() = (0, 0);
}
}
#[derive(Default)]
pub struct ResourceManagerTestContext {
pub sync_load: Arc<SyncContext>,
pub sync_update: Arc<SyncContext>,
}
impl ResourceManagerTestContext {
pub fn clear(&self) {
self.sync_load.clear();
self.sync_update.clear();
}
}
#[derive(Default, Clone)]
pub(in crate::resource) struct StringLoader(());
#[async_trait]
impl ResourceLoader<String> for StringLoader {
async fn load(
&self,
mut data: ResourceReadData,
_ctx: &ResourceLoadContext,
) -> Result<Box<String>, ResourceLoadError> {
let mut output = String::new();
data.read_to_string(&mut output).await?;
Ok(Box::new(output))
}
}
#[derive(Clone)]
pub(in crate::resource) struct TestResLoader {
expected_id: ResourceId,
}
impl TestResLoader {
#[inline]
pub fn new(expected_id: impl Into<ResourceId>) -> Self {
Self {
expected_id: expected_id.into(),
}
}
}
#[async_trait]
impl ResourceLoader<String> for TestResLoader {
async fn load(
&self,
data: ResourceReadData,
ctx: &ResourceLoadContext,
) -> Result<Box<String>, ResourceLoadError> {
assert_eq!(ctx.id(), self.expected_id);
StringLoader::default().load(data, ctx).await
}
}
#[derive(Debug)]
pub(in crate::resource) struct TestResRef<T: Send + Sync + 'static> {
pub resources: Vec<Arc<Resource<T>>>,
}
#[derive(Clone)]
pub(in crate::resource) struct TestRefLoader<T, L>
where
T: Send + Sync + 'static,
L: ResourceLoader<T>,
{
loader: L,
_phantom: PhantomData<T>,
}
impl<T, L> TestRefLoader<T, L>
where
T: Send + Sync + 'static,
L: ResourceLoader<T>,
{
#[inline]
pub fn new(loader: L) -> Self {
Self {
loader,
_phantom: PhantomData::default(),
}
}
}
#[async_trait]
impl<T, L> ResourceLoader<TestResRef<T>> for TestRefLoader<T, L>
where
T: Send + Sync + 'static,
L: ResourceLoader<T> + Clone,
{
async fn load(
&self,
mut data: ResourceReadData,
ctx: &ResourceLoadContext,
) -> Result<Box<TestResRef<T>>, ResourceLoadError> {
let mut ids = String::new();
data.read_to_string(&mut ids).await?;
let futs = ids.split(':')
.map(|id| ctx.get_with_loader(id, self.loader.clone()))
.collect::<Vec<_>>();
let mut resources = Vec::with_capacity(futs.len());
for fut in futs {
resources.push(fut.await?);
}
Ok(Box::new(TestResRef {
resources,
}))
}
}
#[derive(Default, Clone)]
pub(in crate::resource) struct TestResChainLoader(());
#[async_trait]
impl ResourceLoader<Vec<String>> for TestResChainLoader {
async fn load(
&self,
mut data: ResourceReadData,
ctx: &ResourceLoadContext,
) -> Result<Box<Vec<String>>, ResourceLoadError> {
let mut values_str = String::new();
data.read_to_string(&mut values_str).await?;
let mut values = Vec::new();
for value in values_str.split(':') {
match ctx.get_with_loader(value, Self::default()).await {
Ok(resource) => {
for sub_value in &*resource.read() {
values.push(ctx.id().to_string() + ":" + sub_value);
}
},
Err(ResourceLoadError::NotFound(_)) => {
values.push(ctx.id().to_string() + ":" + value);
},
Err(error) => return Err(error),
}
}
Ok(Box::new(values))
}
}
#[derive(Debug)]
pub(in crate::resource) struct ExpectedResourceRef<A> {
pub expected_resources: Vec<A>,
}
impl<A> ExpectedResourceRef<A> {
#[inline]
pub fn new(expected_resources: impl IntoIterator<Item=A>) -> Self {
Self {
expected_resources: expected_resources.into_iter().collect(),
}
}
}
impl<T: Send + Sync + 'static, A: PartialEq<T>> PartialEq<TestResRef<T>> for ExpectedResourceRef<A> {
fn eq(&self, other: &TestResRef<T>) -> bool {
if other.resources.len() != self.expected_resources.len() {
return false
}
for (idx, res) in other.resources.iter().enumerate() {
let expected = &self.expected_resources[idx];
let lock = res.read();
if expected != &*lock {
return false
}
}
true
}
}
#[derive(Educe)]
#[educe(Deref, DerefMut, Default)]
struct ResourceDataMapRaw(HashMap<ResourceId, (Vec<u8>, String)>);
impl ResourceHashProvider for ResourceDataMapRaw {
#[inline]
fn hash(&self, id: &ResourceId) -> Option<String> {
self.0.get(id).map(|(_, hash)| hash.clone())
}
}
pub struct ResourceDataMap {
raw: Arc<RwLock<ResourceDataMapRaw>>,
watcher: ResourceWatcher,
}
impl ResourceDataMap {
fn new() -> Self {
let raw = Arc::new(RwLock::new(ResourceDataMapRaw::default()));
let watcher = ResourceWatcher::new(raw.clone());
Self {
raw,
watcher,
}
}
fn get(&self, id: &ResourceId) -> Option<(Vec<u8>, String)> {
self.raw.read().get(id).map(|(r, h)| (r.clone(), h.clone()))
}
pub fn lock(&self) -> ResourceDataMapLock<'_> {
let raw = self.raw.write();
ResourceDataMapLock {
raw,
watcher: &self.watcher,
updates: HashMap::default(),
}
}
pub fn assert_watch(&self, id: impl Into<ResourceId>, should_watch: bool) {
let m = match should_watch {
true => "",
false => "NOT ",
};
let id = id.into();
assert_eq!(
self.watcher.is_watched(&id),
should_watch,
"Resource '{id}' should {m}be watched by: {:?}", self.watcher,
);
}
}
pub struct ResourceDataMapLock<'a> {
raw: RwLockWriteGuard<'a, ResourceDataMapRaw>,
watcher: &'a ResourceWatcher,
updates: HashMap<ResourceId, ResourceUpdate>,
}
impl<'a> ResourceDataMapLock<'a> {
pub fn insert(
&mut self,
id: impl Into<ResourceId>,
data: &[u8],
hash: impl Into<String>,
) {
let id = id.into();
let hash = hash.into();
let update = match self.raw.insert(id.clone(), (Vec::from(data), hash.clone())).is_some() {
true => ResourceUpdate::Modified(hash),
false => ResourceUpdate::Added(hash),
};
if self.watcher.is_watched(&id) {
self.updates.insert(id, update);
}
}
pub fn remove(&mut self, id: impl Into<ResourceId>) {
let id = id.into();
if self.raw.remove(&id).is_some() {
if self.watcher.is_watched(&id) {
self.updates.insert(id, ResourceUpdate::Removed);
}
}
}
}
impl<'a> Drop for ResourceDataMapLock<'a> {
fn drop(&mut self) {
let mut updates = HashMap::default();
std::mem::swap(&mut updates, &mut self.updates);
self.watcher.notify_update(updates.into());
}
}
pub struct TestResourceSource {
inner: Arc<ResourceDataMap>,
}
impl TestResourceSource {
#[inline]
pub fn new() -> (Self, Arc<ResourceDataMap>) {
let data_map = Arc::new(ResourceDataMap::new());
let source = Self {
inner: data_map.clone(),
};
(source, data_map)
}
}
#[async_trait]
impl ResourceSource for TestResourceSource {
fn hash(&self, id: &ResourceId) -> Option<ResourceDataSource> {
if let Some((_, hash)) = self.inner.get(id) {
Some(ResourceDataSource::new(hash))
} else {
None
}
}
async fn load(&self, id: &ResourceId) -> ResourceDataResult {
if let Some((data, hash)) = self.inner.get(id) {
Ok(ResourceData::new(Box::new(smol::io::Cursor::new(data)), hash))
} else {
Err(ResourceLoadError::NotFound(id.clone()))
}
}
fn watcher(&self) -> &ResourceWatcher {
&self.inner.watcher
}
}
pub struct TestResourceContext<const N: usize> {
pub manager: Arc<ResourceManager>,
pub data_maps: [Arc<ResourceDataMap>; N],
pub worker_pool: WorkerPool<isize>,
}
#[fixture]
pub fn test_resource_ctx<const N: usize>() -> TestResourceContext<N> {
let worker_pool = WorkerPool::<isize>::builder()
.worker_count(1.try_into().unwrap())
.start();
let mut builder = ResourceManager::builder()
.worker_config(
ResourceManagerWorkerPriorityConfig {
immediate: 2,
delayed: 1,
update: 0,
},
&worker_pool,
);
let tuples = core::array::from_fn::<_, N, _>(|_| TestResourceSource::new());
let data_maps = core::array::from_fn::<_, N, _>(|idx| {
tuples[idx].1.clone()
});
for tuple in tuples {
builder = builder.source(tuple.0);
}
let manager = builder.build();
TestResourceContext {
manager,
data_maps,
worker_pool,
}
}
#[derive(Clone)]
pub(in crate::resource) struct TestDataEntry {
pub id: ResourceId,
pub data: &'static [u8],
pub hash: String,
}
impl TestDataEntry {
#[inline]
pub fn new(
id: impl Into<ResourceId>,
data: &'static [u8],
hash: impl Into<String>,
) -> Self {
Self {
id: id.into(),
data,
hash: hash.into(),
}
}
}
pub(in crate::resource) struct TestLoadInfo<T: ?Sized + Send + Sync + 'static, L: ResourceLoader<T>> {
pub key: ResourceId,
pub loader: L,
_phantom: PhantomData<T>,
}
impl<T: ?Sized + Send + Sync + 'static, L: ResourceLoader<T>> TestLoadInfo<T, L> {
#[inline]
pub fn new(key: impl Into<ResourceId>, loader: L) -> Self {
Self {
key: key.into(),
loader,
_phantom: PhantomData::default(),
}
}
}
pub(in crate::resource) enum ExpectedLoadResult<T> {
Ok(T),
NotFound(ResourceId),
ReadErr,
}
impl<T> ExpectedLoadResult<T> {
pub fn assert_matches<V>(&self, result: ResourceLoadResult<V>) -> Option<Arc<Resource<V>>>
where
T: Debug + PartialEq<V>,
V: Debug + Send + Sync + 'static,
{
match self {
Self::Ok(expected_value) => {
let value = result
.expect("Resource should load");
assert_eq!(expected_value, &*value.read());
Some(value)
},
Self::NotFound(expected_id) => {
let error = result
.expect_err("Resource should fail to load");
assert_matches!(error, ResourceLoadError::NotFound(id) => {
assert_eq!(&id, expected_id);
});
None
}
Self::ReadErr => {
let error = result
.expect_err("Resource should fail to load");
assert_matches!(error, ResourceLoadError::ReadError(_));
None
}
}
}
}
pub(in crate::resource) struct ExpectedDataEntry<T> {
pub entry: TestDataEntry,
pub expected: ExpectedLoadResult<T>,
}
impl ExpectedDataEntry<String> {
#[inline]
pub fn ok(id: impl Into<ResourceId>) -> Self {
Self {
entry: TestDataEntry::new(id, b"value", "h_value"),
expected: ExpectedLoadResult::Ok("value".to_string()),
}
}
#[inline]
pub fn ok_new(id: impl Into<ResourceId>) -> Self {
Self {
entry: TestDataEntry::new(id, b"new_value", "h_new_value"),
expected: ExpectedLoadResult::Ok("new_value".to_string()),
}
}
#[inline]
pub fn read_error(id: impl Into<ResourceId>) -> Self {
Self {
entry: TestDataEntry::new(id, b"\xC0", "h_invalid"),
expected: ExpectedLoadResult::ReadErr,
}
}
#[inline]
pub fn read_error_new(id: impl Into<ResourceId>) -> Self {
Self {
entry: TestDataEntry::new(id, b"new_\xC0", "h_new_invalid"),
expected: ExpectedLoadResult::ReadErr,
}
}
}
pub(in crate::resource) struct ExpectedLoadInfo<T, L, A>
where
T: ?Sized + Send + Sync + 'static,
L: ResourceLoader<T>,
A: PartialEq<T>,
{
pub load_info: TestLoadInfo<T, L>,
pub expected: ExpectedLoadResult<A>,
}
pub(in crate::resource) fn setup_dependency_data(
data_map: &Arc<ResourceDataMap>,
dependency_graph: &DependencyGraph,
) {
let mut data_map = data_map.lock();
for node in dependency_graph.iter() {
let dep_str = node.dependencies()
.map(|n| n.id())
.sorted()
.join(":");
data_map.insert(node.id().clone(), dep_str.as_bytes(), format!("h_{}", node.id()));
}
}
#[derive(Clone)]
pub(in crate::resource) enum Update {
Insert(TestDataEntry),
Remove(ResourceId),
}
impl Update {
pub fn apply(self, data_map: &mut ResourceDataMapLock<'_>) {
match self {
Self::Insert(entry) =>
data_map.insert(entry.id, entry.data, entry.hash),
Self::Remove(id) =>
data_map.remove(id),
}
}
}
pub(in crate::resource) struct BulkUpdate {
pub idx: usize,
pub updates: Vec<Update>,
}
impl BulkUpdate {
pub fn new(idx: usize, updates: impl IntoIterator<Item=Update>) -> Self {
Self {
idx,
updates: updates.into_iter().collect(),
}
}
pub fn apply(self, data_maps: &[Arc<ResourceDataMap>]) {
let mut data_map = data_maps[self.idx].lock();
for update in self.updates {
update.apply(&mut data_map);
}
}
}
#[rstest]
#[test_attr(test_log(apply(smol_test)))]
async fn test_manager_can_drop(
test_resource_ctx: TestResourceContext<1>,
) {
timeout(
assert_manager_drops(test_resource_ctx.manager),
Duration::from_secs(5),
).await;
}
#[rstest]
#[case::first_ok(
[
TestDataEntry::new("a", b"value1", "h_a"),
TestDataEntry::new("b", b"value2", "h_b"),
TestDataEntry::new("c", b"value3", "h_c"),
],
[
TestLoadInfo::new("a", TestResLoader::new("a")),
TestLoadInfo::new("b", TestResLoader::new("b")),
TestLoadInfo::new("c", TestResLoader::new("c")),
],
ExpectedLoadResult::Ok("value1".to_string()),
)]
#[case::first_not_found(
[
TestDataEntry::new("b", b"value2", "h_b"),
TestDataEntry::new("c", b"value3", "h_c"),
],
[
TestLoadInfo::new("a", TestResLoader::new("a")),
TestLoadInfo::new("b", TestResLoader::new("b")),
TestLoadInfo::new("c", TestResLoader::new("c")),
],
ExpectedLoadResult::Ok("value2".to_string()),
)]
#[case::nested_not_found(
[
TestDataEntry::new("c", b"value3", "h_c"),
],
[
TestLoadInfo::new("a", TestResLoader::new("a")),
TestLoadInfo::new("b", TestResLoader::new("b")),
TestLoadInfo::new("c", TestResLoader::new("c")),
],
ExpectedLoadResult::Ok("value3".to_string()),
)]
#[case::none_found(
[],
[
TestLoadInfo::new("a", TestResLoader::new("a")),
TestLoadInfo::new("b", TestResLoader::new("b")),
TestLoadInfo::new("c", TestResLoader::new("c")),
],
ExpectedLoadResult::NotFound(ResourceId::from("c")),
)]
#[test_attr(test_log(apply(smol_test)))]
async fn test_future_or_get(
test_resource_ctx: TestResourceContext<1>,
#[case] initial_data: impl IntoIterator<Item=TestDataEntry>,
#[case] load_info: impl IntoIterator<Item=TestLoadInfo<String, TestResLoader>>,
#[case] expected_load_result: ExpectedLoadResult<String>,
) {
{
let mut lock = test_resource_ctx.data_maps[0].lock();
for data in initial_data {
lock.insert(data.id, data.data, data.hash);
}
}
let mut load_info = load_info.into_iter();
let mut fut = {
let first = load_info.next().expect("'load_info' should have at least 1 item");
test_resource_ctx.manager.get_with_loader(first.key, first.loader)
};
for next in load_info {
fut = fut.or_get_with_loader(next.key, next.loader);
}
let result = fut.await;
expected_load_result.assert_matches(result);
}
pub mod prelude {
pub use super::{
test_resource_ctx,
ResourceDataMap,
ResourceDataMapLock,
TestResourceContext,
};
pub(in crate::resource) use super::{
assert_manager_drops,
setup_dependency_data,
timeout,
BulkUpdate,
ExpectedDataEntry,
ExpectedLoadInfo,
ExpectedLoadResult,
ExpectedResourceRef,
StringLoader,
TestDataEntry,
TestLoadInfo,
TestRefLoader,
TestResChainLoader,
TestResLoader,
Update,
};
}
}