Files
FlowScope/My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs
2026-05-19 21:20:52 +08:00

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
{
}
}
}