收束 P1 资源后端适配层

This commit is contained in:
JSD\13999
2026-05-20 20:49:29 +08:00
parent 1c53319961
commit 6e45152250
4 changed files with 129 additions and 31 deletions

View File

@@ -2,7 +2,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Resources;
using UnityEngine.AddressableAssets;
using UnityAddressables = UnityEngine.AddressableAssets.Addressables;
namespace FlowScope.Addressables
{
@@ -13,7 +13,7 @@ namespace FlowScope.Addressables
public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class
{
var handle = Addressables.LoadAssetAsync<T>(key);
var handle = UnityAddressables.LoadAssetAsync<T>(key);
await WaitWithCancellation(handle.Task, cancellationToken);
return handle.Result;
}
@@ -21,7 +21,7 @@ namespace FlowScope.Addressables
public void Release<T>(string key, T asset)
where T : class
{
Addressables.Release(asset);
UnityAddressables.Release(asset);
}
private static async Task WaitWithCancellation(Task task, CancellationToken cancellationToken)

View File

@@ -3,7 +3,8 @@
"rootNamespace": "FlowScope.Addressables",
"references": [
"FlowScope.Runtime",
"Unity.Addressables"
"Unity.Addressables",
"Unity.ResourceManager"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -1,15 +1,18 @@
using System;
using System.Collections;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Resources;
using NUnit.Framework;
using UnityEngine.TestTools;
namespace FlowScope.Tests.EditMode.Resources
{
public sealed class ResourceServiceBackendTests
{
[Test]
public async Task LoadAsync_WhenSameKeyLoadsConcurrently_UsesOneBackendLoad()
[UnityTest]
public IEnumerator LoadAsync_WhenSameKeyLoadsConcurrently_UsesOneBackendLoad()
{
var backend = new FakeResourceBackend();
var service = new ResourceService(backend);
@@ -18,15 +21,17 @@ namespace FlowScope.Tests.EditMode.Resources
var second = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
backend.Gate.SetResult(new TestAsset());
using var firstHandle = await first;
using var secondHandle = await second;
yield return Await(first);
yield return Await(second);
using var firstHandle = Result(first);
using var secondHandle = Result(second);
Assert.AreEqual(1, backend.LoadCalls);
Assert.AreSame(firstHandle.Asset, secondHandle.Asset);
}
[Test]
public async Task LoadAsync_WhenCanceled_DoesNotCreateHandle()
[UnityTest]
public IEnumerator LoadAsync_WhenCanceled_DoesNotCreateHandle()
{
var backend = new FakeResourceBackend();
var service = new ResourceService(backend);
@@ -35,13 +40,20 @@ namespace FlowScope.Tests.EditMode.Resources
var task = service.LoadAsync<TestAsset>("canceled", cts.Token);
cts.Cancel();
await ExpectThrowsAsync<OperationCanceledException>(task);
try
{
yield return AwaitExpected<OperationCanceledException>(task);
Assert.IsFalse(service.TryLoadCompleted<TestAsset>("canceled", out var handle));
Assert.IsNull(handle);
}
finally
{
backend.Gate.TrySetResult(new TestAsset());
}
}
[Test]
public async Task DisposeLastHandle_ReleasesBackendAsset()
[UnityTest]
public IEnumerator DisposeLastHandle_ReleasesBackendAsset()
{
var backend = new FakeResourceBackend();
var service = new ResourceService(backend);
@@ -49,8 +61,10 @@ namespace FlowScope.Tests.EditMode.Resources
var first = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
var second = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
backend.Gate.SetResult(new TestAsset());
var firstHandle = await first;
var secondHandle = await second;
yield return Await(first);
yield return Await(second);
var firstHandle = Result(first);
var secondHandle = Result(second);
firstHandle.Dispose();
Assert.AreEqual(0, backend.ReleaseCalls);
@@ -59,15 +73,16 @@ namespace FlowScope.Tests.EditMode.Resources
Assert.AreEqual(1, backend.ReleaseCalls);
}
[Test]
public async Task DisposeLastHandle_RemovesCompletedCacheEntry()
[UnityTest]
public IEnumerator DisposeLastHandle_RemovesCompletedCacheEntry()
{
var backend = new FakeResourceBackend();
var service = new ResourceService(backend);
var load = service.LoadAsync<TestAsset>("cached", CancellationToken.None);
backend.Gate.SetResult(new TestAsset());
var handle = await load;
yield return Await(load);
var handle = Result(load);
Assert.IsTrue(service.TryLoadCompleted<TestAsset>("cached", out var cached));
cached.Dispose();
@@ -76,8 +91,8 @@ namespace FlowScope.Tests.EditMode.Resources
Assert.IsFalse(service.TryLoadCompleted<TestAsset>("cached", out _));
}
[Test]
public async Task CreateGroup_DisposesAllHandles()
[UnityTest]
public IEnumerator CreateGroup_DisposesAllHandles()
{
var backend = new FakeResourceBackend();
var service = new ResourceService(backend);
@@ -85,13 +100,15 @@ namespace FlowScope.Tests.EditMode.Resources
var first = service.LoadAsync<TestAsset>("first", CancellationToken.None);
backend.Gate.SetResult(new TestAsset());
var firstHandle = await first;
yield return Await(first);
var firstHandle = Result(first);
group.Add(firstHandle);
backend.Gate = new TaskCompletionSource<object>();
backend.ResetGate();
var second = service.LoadAsync<TestAsset>("second", CancellationToken.None);
backend.Gate.SetResult(new TestAsset());
var secondHandle = await second;
yield return Await(second);
var secondHandle = Result(second);
group.Add(secondHandle);
group.Dispose();
@@ -106,7 +123,13 @@ namespace FlowScope.Tests.EditMode.Resources
public string Name => "Fake";
public int LoadCalls { get; private set; }
public int ReleaseCalls { get; private set; }
public TaskCompletionSource<object> Gate { get; set; } = new();
public TaskCompletionSource<object> Gate { get; private set; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public void ResetGate()
{
Gate = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
}
public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class
@@ -126,19 +149,72 @@ namespace FlowScope.Tests.EditMode.Resources
{
}
private static async Task ExpectThrowsAsync<TException>(Task task)
private static IEnumerator Await(Task task)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (!task.IsCompleted)
{
if (DateTime.UtcNow > deadline)
{
Assert.Fail("Async operation did not complete within 2 seconds.");
}
yield return null;
}
ThrowIfFailed(task);
}
private static IEnumerator AwaitExpected<TException>(Task task)
where TException : Exception
{
try
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (!task.IsCompleted)
{
await task;
if (DateTime.UtcNow > deadline)
{
Assert.Fail("Async operation did not complete within 2 seconds.");
}
catch (TException)
yield return null;
}
if (task.IsCanceled && typeof(TException) == typeof(OperationCanceledException))
{
return;
yield break;
}
var exception = task.Exception?.InnerException;
if (exception is TException)
{
yield break;
}
if (exception != null)
{
ExceptionDispatchInfo.Capture(exception).Throw();
}
Assert.Fail($"Expected {typeof(TException).Name}.");
}
private static T Result<T>(Task<T> task)
{
ThrowIfFailed(task);
return task.Result;
}
private static void ThrowIfFailed(Task task)
{
if (task.IsCanceled)
{
throw new OperationCanceledException();
}
if (task.Exception?.InnerException != null)
{
ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw();
}
}
}
}

View File

@@ -9,6 +9,20 @@
},
"hash": "329ca7915713417f2e0837b0e0a80b4da074db4a"
},
"com.unity.addressables": {
"version": "1.21.21",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.scriptablebuildpipeline": "1.21.23",
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0"
},
"url": "https://packages.unity.cn"
},
"com.unity.collab-proxy": {
"version": "2.12.4",
"depth": 0,
@@ -76,6 +90,13 @@
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.scriptablebuildpipeline": {
"version": "1.21.25",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.settings-manager": {
"version": "2.1.0",
"depth": 2,