219 lines
7.5 KiB
C#
219 lines
7.5 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FlowScope.Resources;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
|
|
namespace FlowScope.Tests.PlayMode.Resources
|
|
{
|
|
public sealed class AddressablesResourceServiceTests
|
|
{
|
|
[UnityTest]
|
|
public IEnumerator LoadAsync_WhenSameKeyLoadsConcurrently_SharesOneBackendLoad()
|
|
{
|
|
var backendLoads = 0;
|
|
var releaseCount = 0;
|
|
var gate = new TaskCompletionSource<object>();
|
|
var service = new AddressablesResourceService(
|
|
async (_, _, _) =>
|
|
{
|
|
backendLoads++;
|
|
return await gate.Task;
|
|
},
|
|
(_, _) => releaseCount++);
|
|
|
|
var first = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
|
|
var second = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
|
|
|
|
gate.SetResult(new TestAsset());
|
|
IResourceHandle<TestAsset> firstHandle = null;
|
|
IResourceHandle<TestAsset> secondHandle = null;
|
|
yield return AwaitTask(first, handle => firstHandle = handle);
|
|
yield return AwaitTask(second, handle => secondHandle = handle);
|
|
|
|
Assert.AreEqual(1, backendLoads);
|
|
Assert.AreSame(firstHandle.Asset, secondHandle.Asset);
|
|
|
|
firstHandle.Dispose();
|
|
Assert.AreEqual(0, releaseCount);
|
|
secondHandle.Dispose();
|
|
Assert.AreEqual(1, releaseCount);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator LoadAsync_WhenBackendFails_IncludesKeyAndBackend()
|
|
{
|
|
var service = new AddressablesResourceService(
|
|
(_, _, _) => throw new InvalidOperationException("missing"),
|
|
(_, _) => { });
|
|
|
|
InvalidOperationException exception = null;
|
|
yield return ThrowsAsync<InvalidOperationException>(
|
|
() => service.LoadAsync<TestAsset>("missing-key", CancellationToken.None),
|
|
caught => exception = caught);
|
|
|
|
StringAssert.Contains("missing-key", exception.Message);
|
|
StringAssert.Contains("Addressables", exception.Message);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator LoadAsync_WhenOneConcurrentCallerCancels_OtherCallerStillReceivesSharedLoad()
|
|
{
|
|
var backendLoads = 0;
|
|
var gate = new TaskCompletionSource<object>();
|
|
var service = new AddressablesResourceService(
|
|
async (_, _, cancellationToken) =>
|
|
{
|
|
backendLoads++;
|
|
Assert.That(cancellationToken.CanBeCanceled, Is.False);
|
|
return await gate.Task;
|
|
},
|
|
(_, _) => { });
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
var canceled = service.LoadAsync<TestAsset>("shared", cts.Token);
|
|
var survivor = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
|
|
cts.Cancel();
|
|
|
|
yield return ThrowsAsync<OperationCanceledException>(canceled);
|
|
gate.SetResult(new TestAsset());
|
|
IResourceHandle<TestAsset> survivorHandle = null;
|
|
yield return AwaitTask(survivor, handle => survivorHandle = handle);
|
|
|
|
Assert.AreEqual(1, backendLoads);
|
|
Assert.IsNotNull(survivorHandle.Asset);
|
|
survivorHandle.Dispose();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator LoadAsync_WhenOnlyCallerCancels_ReleasesCompletedBackendAsset()
|
|
{
|
|
var backendLoads = 0;
|
|
var releaseCount = 0;
|
|
var gate = new TaskCompletionSource<object>();
|
|
var service = new AddressablesResourceService(
|
|
async (_, _, cancellationToken) =>
|
|
{
|
|
backendLoads++;
|
|
Assert.That(cancellationToken.CanBeCanceled, Is.False);
|
|
if (backendLoads == 1)
|
|
{
|
|
return await gate.Task;
|
|
}
|
|
|
|
return new TestAsset();
|
|
},
|
|
(_, _) => releaseCount++);
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
var canceled = service.LoadAsync<TestAsset>("orphan", cts.Token);
|
|
cts.Cancel();
|
|
yield return ThrowsAsync<OperationCanceledException>(canceled);
|
|
gate.SetResult(new TestAsset());
|
|
|
|
yield return new WaitUntil(() => releaseCount == 1);
|
|
Assert.IsFalse(service.TryLoadCompleted<TestAsset>("orphan", out _));
|
|
|
|
IResourceHandle<TestAsset> nextHandle = null;
|
|
yield return AwaitTask(
|
|
service.LoadAsync<TestAsset>("orphan", CancellationToken.None),
|
|
handle => nextHandle = handle);
|
|
|
|
Assert.AreEqual(2, backendLoads);
|
|
Assert.IsNotNull(nextHandle.Asset);
|
|
nextHandle.Dispose();
|
|
Assert.AreEqual(2, releaseCount);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator TryLoadCompleted_AfterAsyncLoad_ReturnsCachedHandleWithoutBackendLoad()
|
|
{
|
|
var backendLoads = 0;
|
|
var service = new AddressablesResourceService(
|
|
(_, _, _) =>
|
|
{
|
|
backendLoads++;
|
|
return Task.FromResult<object>(new TestAsset());
|
|
},
|
|
(_, _) => { });
|
|
|
|
IResourceHandle<TestAsset> first = null;
|
|
yield return AwaitTask(
|
|
service.LoadAsync<TestAsset>("cached", CancellationToken.None),
|
|
handle => first = handle);
|
|
Assert.IsTrue(service.TryLoadCompleted<TestAsset>("cached", out var second));
|
|
|
|
Assert.AreEqual(1, backendLoads);
|
|
Assert.AreSame(first.Asset, second.Asset);
|
|
first.Dispose();
|
|
second.Dispose();
|
|
}
|
|
|
|
private static IEnumerator ThrowsAsync<TException>(Func<Task> action, Action<TException> onCaught)
|
|
where TException : Exception
|
|
{
|
|
var task = action();
|
|
while (!task.IsCompleted)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
if (task.IsFaulted && task.Exception?.GetBaseException() is TException exception)
|
|
{
|
|
onCaught(exception);
|
|
yield break;
|
|
}
|
|
|
|
Assert.Fail($"Expected {typeof(TException).Name}.");
|
|
}
|
|
|
|
private static IEnumerator ThrowsAsync<TException>(Task task)
|
|
where TException : Exception
|
|
{
|
|
while (!task.IsCompleted)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
if (task.IsFaulted && task.Exception?.GetBaseException() is TException)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
if (task.IsCanceled && typeof(TException) == typeof(OperationCanceledException))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
Assert.Fail($"Expected {typeof(TException).Name}.");
|
|
}
|
|
|
|
private static IEnumerator AwaitTask<T>(Task<T> task, Action<T> onCompleted)
|
|
{
|
|
while (!task.IsCompleted)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
if (task.IsFaulted)
|
|
{
|
|
throw task.Exception.GetBaseException();
|
|
}
|
|
|
|
if (task.IsCanceled)
|
|
{
|
|
throw new OperationCanceledException();
|
|
}
|
|
|
|
onCompleted(task.Result);
|
|
}
|
|
|
|
private sealed class TestAsset
|
|
{
|
|
}
|
|
}
|
|
}
|