实现 P0 生命周期编排
This commit is contained in:
31
My project/Assets/FlowScope/Runtime/Flow/FeatureBase.cs
Normal file
31
My project/Assets/FlowScope/Runtime/Flow/FeatureBase.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Flow
|
||||
{
|
||||
public abstract class FeatureBase : IFeature
|
||||
{
|
||||
protected FeatureContext Context { get; private set; }
|
||||
|
||||
public virtual Task LoadAsync(FeatureContext context)
|
||||
{
|
||||
Context = context;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task EnterAsync(FeatureContext context)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task ExitAsync(FeatureContext context)
|
||||
{
|
||||
context?.Disposables.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Context = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
My project/Assets/FlowScope/Runtime/Flow/FeatureBase.cs.meta
Normal file
11
My project/Assets/FlowScope/Runtime/Flow/FeatureBase.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5f4e3fb56394e719bff30e8edd4abaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
285
My project/Assets/FlowScope/Runtime/Flow/GameFlow.cs
Normal file
285
My project/Assets/FlowScope/Runtime/Flow/GameFlow.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Config;
|
||||
using FlowScope.Resources;
|
||||
using R3;
|
||||
using UnityEngine;
|
||||
using ContainerType = FlowScope.Container.Container;
|
||||
|
||||
namespace FlowScope.Flow
|
||||
{
|
||||
public sealed class GameFlow
|
||||
{
|
||||
private ContainerType _root;
|
||||
private IFeature _activeFeature;
|
||||
private FeatureContext _activeContext;
|
||||
|
||||
public GameFlowState State { get; private set; } = GameFlowState.Idle;
|
||||
|
||||
public async Task StartupAsync<TInitialFeature>(
|
||||
ContainerType root,
|
||||
CancellationToken cancellationToken)
|
||||
where TInitialFeature : IFeature
|
||||
{
|
||||
EnsureState(nameof(StartupAsync), GameFlowState.Idle);
|
||||
_root = root ?? throw new ArgumentNullException(nameof(root));
|
||||
State = GameFlowState.Starting;
|
||||
|
||||
IFeature feature = null;
|
||||
FeatureContext context = null;
|
||||
|
||||
try
|
||||
{
|
||||
var config = _root.Resolve<IConfigProvider>();
|
||||
await config.LoadAllAsync(cancellationToken);
|
||||
|
||||
(feature, context) = CreateFeature<TInitialFeature>(cancellationToken);
|
||||
await feature.LoadAsync(context);
|
||||
await feature.EnterAsync(context);
|
||||
|
||||
_activeFeature = feature;
|
||||
_activeContext = context;
|
||||
State = GameFlowState.Running;
|
||||
}
|
||||
catch
|
||||
{
|
||||
BestEffortDisposeFeature(feature);
|
||||
BestEffortDisposeContext(context);
|
||||
_activeFeature = null;
|
||||
_activeContext = null;
|
||||
State = GameFlowState.Idle;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SwitchToAsync<TFeature>(CancellationToken cancellationToken)
|
||||
where TFeature : IFeature
|
||||
{
|
||||
EnsureState(
|
||||
nameof(SwitchToAsync),
|
||||
GameFlowState.Running,
|
||||
GameFlowState.NoActiveFeature);
|
||||
|
||||
State = GameFlowState.Switching;
|
||||
await CleanupActiveFeatureAsync();
|
||||
|
||||
IFeature nextFeature = null;
|
||||
FeatureContext nextContext = null;
|
||||
|
||||
try
|
||||
{
|
||||
(nextFeature, nextContext) = CreateFeature<TFeature>(cancellationToken);
|
||||
await nextFeature.LoadAsync(nextContext);
|
||||
await nextFeature.EnterAsync(nextContext);
|
||||
|
||||
_activeFeature = nextFeature;
|
||||
_activeContext = nextContext;
|
||||
State = GameFlowState.Running;
|
||||
}
|
||||
catch
|
||||
{
|
||||
BestEffortDisposeFeature(nextFeature);
|
||||
BestEffortDisposeContext(nextContext);
|
||||
_activeFeature = null;
|
||||
_activeContext = null;
|
||||
State = GameFlowState.NoActiveFeature;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureState(
|
||||
nameof(ShutdownAsync),
|
||||
GameFlowState.Running,
|
||||
GameFlowState.NoActiveFeature);
|
||||
|
||||
State = GameFlowState.ShuttingDown;
|
||||
Exception cancellationException = null;
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException exception)
|
||||
{
|
||||
cancellationException = exception;
|
||||
}
|
||||
|
||||
await CleanupActiveFeatureAsync();
|
||||
BestEffortDisposeRoot();
|
||||
State = GameFlowState.Disposed;
|
||||
|
||||
if (cancellationException != null)
|
||||
{
|
||||
throw cancellationException;
|
||||
}
|
||||
}
|
||||
|
||||
private (IFeature Feature, FeatureContext Context) CreateFeature<TFeature>(
|
||||
CancellationToken cancellationToken)
|
||||
where TFeature : IFeature
|
||||
{
|
||||
ContainerType scope = null;
|
||||
IResourceGroup resources = null;
|
||||
FeatureContext context = null;
|
||||
|
||||
try
|
||||
{
|
||||
scope = _root.CreateScope();
|
||||
var resourceService = _root.Resolve<IResourceService>();
|
||||
resources = resourceService.CreateGroup();
|
||||
context = new FeatureContext(
|
||||
scope,
|
||||
resources,
|
||||
new CompositeDisposable(),
|
||||
cancellationToken);
|
||||
var feature = scope.Resolve<TFeature>();
|
||||
return (feature, context);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (context != null)
|
||||
{
|
||||
BestEffortDisposeContext(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
BestEffortDisposePartialContext(resources, scope);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupActiveFeatureAsync()
|
||||
{
|
||||
var feature = _activeFeature;
|
||||
var context = _activeContext;
|
||||
_activeFeature = null;
|
||||
_activeContext = null;
|
||||
|
||||
if (feature != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await feature.ExitAsync(context);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
|
||||
BestEffortDisposeFeature(feature);
|
||||
}
|
||||
|
||||
BestEffortDisposeContext(context);
|
||||
}
|
||||
|
||||
private void BestEffortDisposeFeature(IFeature feature)
|
||||
{
|
||||
if (feature == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
feature.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void BestEffortDisposeContext(FeatureContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context.Disposables.Clear();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context.Resources?.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context.Scope?.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void BestEffortDisposePartialContext(
|
||||
IResourceGroup resources,
|
||||
ContainerType scope)
|
||||
{
|
||||
try
|
||||
{
|
||||
resources?.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
scope?.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void BestEffortDisposeRoot()
|
||||
{
|
||||
try
|
||||
{
|
||||
_root?.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_root = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureState(string operation, params GameFlowState[] allowedStates)
|
||||
{
|
||||
foreach (var allowedState in allowedStates)
|
||||
{
|
||||
if (State == allowedState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"{operation} cannot run while GameFlow state is {State}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
My project/Assets/FlowScope/Runtime/Flow/GameFlow.cs.meta
Normal file
11
My project/Assets/FlowScope/Runtime/Flow/GameFlow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a533908611b643259dbfcb681369d012
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/FlowScope/Tests.meta
Normal file
8
My project/Assets/FlowScope/Tests.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59e5a255e19244928da66de10163b3d4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/FlowScope/Tests/EditMode.meta
Normal file
8
My project/Assets/FlowScope/Tests/EditMode.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 058b3cf72633450f9abc0d0eda0fe24b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/FlowScope/Tests/EditMode/Flow.meta
Normal file
8
My project/Assets/FlowScope/Tests/EditMode/Flow.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3be35091c79468180dfa23774e4ff2b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Container;
|
||||
using FlowScope.Flow;
|
||||
using FlowScope.Resources;
|
||||
using NUnit.Framework;
|
||||
using R3;
|
||||
|
||||
namespace FlowScope.Tests.EditMode.Flow
|
||||
{
|
||||
public sealed class FeatureBaseTests
|
||||
{
|
||||
[Test]
|
||||
public async Task LoadAsync_StoresContext()
|
||||
{
|
||||
var context = CreateContext();
|
||||
var feature = new TestFeature();
|
||||
|
||||
await feature.LoadAsync(context);
|
||||
|
||||
Assert.That(feature.ExposedContext, Is.SameAs(context));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExitAsync_ClearsContextDisposables()
|
||||
{
|
||||
var context = CreateContext();
|
||||
var disposable = new SpyDisposable();
|
||||
context.Disposables.Add(disposable);
|
||||
var feature = new TestFeature();
|
||||
|
||||
await feature.ExitAsync(context);
|
||||
|
||||
Assert.That(disposable.IsDisposed, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Dispose_IsIdempotentAndClearsContextReference()
|
||||
{
|
||||
var context = CreateContext();
|
||||
var feature = new TestFeature();
|
||||
feature.LoadAsync(context).GetAwaiter().GetResult();
|
||||
|
||||
feature.Dispose();
|
||||
feature.Dispose();
|
||||
|
||||
Assert.That(feature.ExposedContext, Is.Null);
|
||||
}
|
||||
|
||||
private static FeatureContext CreateContext()
|
||||
{
|
||||
return new FeatureContext(
|
||||
new Container(),
|
||||
new TestResourceGroup(),
|
||||
new CompositeDisposable(),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
private sealed class TestFeature : FeatureBase
|
||||
{
|
||||
public FeatureContext ExposedContext => Context;
|
||||
}
|
||||
|
||||
private sealed class SpyDisposable : IDisposable
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestResourceGroup : IResourceGroup
|
||||
{
|
||||
public int Count => 0;
|
||||
|
||||
public void Add<T>(IResourceHandle<T> handle)
|
||||
where T : class
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdd19af912bd408489e848bdfc88278d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
318
My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs
Normal file
318
My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Config;
|
||||
using FlowScope.Container;
|
||||
using FlowScope.Flow;
|
||||
using FlowScope.Resources;
|
||||
using FlowScope.Save;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace FlowScope.Tests.EditMode.Flow
|
||||
{
|
||||
public sealed class GameFlowTests
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
FirstFeature.Instance = null;
|
||||
SecondFeature.Instance = null;
|
||||
FailingLoadFeature.Instance = null;
|
||||
FailingEnterFeature.Instance = null;
|
||||
ThrowingExitAndDisposeFeature.Instance = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StartupAsync_LoadsConfigCreatesContextAndEntersInitialFeature()
|
||||
{
|
||||
var services = new TestServices();
|
||||
var root = services.CreateRoot();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var flow = new GameFlow();
|
||||
|
||||
await flow.StartupAsync<FirstFeature>(root, cts.Token);
|
||||
|
||||
Assert.That(flow.State, Is.EqualTo(GameFlowState.Running));
|
||||
Assert.That(services.Config.LoadCalls, Is.EqualTo(1));
|
||||
Assert.That(FirstFeature.Instance.Calls, Is.EqualTo(new[] { "Load", "Enter" }));
|
||||
Assert.That(FirstFeature.Instance.Context.CancellationToken, Is.EqualTo(cts.Token));
|
||||
Assert.That(FirstFeature.Instance.Context.Scope, Is.Not.SameAs(root));
|
||||
Assert.That(FirstFeature.Instance.Context.Resources, Is.SameAs(services.Resource.Groups[0]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SwitchToAsync_ExitsDisposesAndReleasesOldContextBeforeEnteringNextFeature()
|
||||
{
|
||||
var services = new TestServices();
|
||||
var root = services.CreateRoot();
|
||||
var flow = new GameFlow();
|
||||
await flow.StartupAsync<FirstFeature>(root, CancellationToken.None);
|
||||
|
||||
await flow.SwitchToAsync<SecondFeature>(CancellationToken.None);
|
||||
|
||||
Assert.That(flow.State, Is.EqualTo(GameFlowState.Running));
|
||||
Assert.That(FirstFeature.Instance.Calls, Is.EqualTo(new[] { "Load", "Enter", "Exit", "Dispose" }));
|
||||
Assert.That(services.Resource.Groups[0].IsDisposed, Is.True);
|
||||
Assert.That(SecondFeature.Instance.Calls, Is.EqualTo(new[] { "Load", "Enter" }));
|
||||
Assert.That(SecondFeature.Instance.Context.Resources, Is.SameAs(services.Resource.Groups[1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StartupAsync_WhenFeatureLoadFails_CleansCreatedFeatureAndReturnsIdle()
|
||||
{
|
||||
var services = new TestServices();
|
||||
var root = services.CreateRoot();
|
||||
var flow = new GameFlow();
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await flow.StartupAsync<FailingLoadFeature>(root, CancellationToken.None));
|
||||
|
||||
Assert.That(flow.State, Is.EqualTo(GameFlowState.Idle));
|
||||
Assert.That(FailingLoadFeature.Instance.Calls, Is.EqualTo(new[] { "Load", "Dispose" }));
|
||||
Assert.That(services.Resource.Groups[0].IsDisposed, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SwitchToAsync_WhenNextEnterFails_CleansNextFeatureAndLeavesNoActiveFeature()
|
||||
{
|
||||
var services = new TestServices();
|
||||
var root = services.CreateRoot();
|
||||
var flow = new GameFlow();
|
||||
await flow.StartupAsync<FirstFeature>(root, CancellationToken.None);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await flow.SwitchToAsync<FailingEnterFeature>(CancellationToken.None));
|
||||
|
||||
Assert.That(flow.State, Is.EqualTo(GameFlowState.NoActiveFeature));
|
||||
Assert.That(FirstFeature.Instance.Calls, Does.Contain("Dispose"));
|
||||
Assert.That(FailingEnterFeature.Instance.Calls, Is.EqualTo(new[] { "Load", "Enter", "Dispose" }));
|
||||
Assert.That(services.Resource.Groups[1].IsDisposed, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SwitchToAsync_WhenIdle_ThrowsWithOperationAndCurrentState()
|
||||
{
|
||||
var flow = new GameFlow();
|
||||
|
||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await flow.SwitchToAsync<FirstFeature>(CancellationToken.None));
|
||||
|
||||
Assert.That(exception.Message, Does.Contain(nameof(GameFlow.SwitchToAsync)));
|
||||
Assert.That(exception.Message, Does.Contain(GameFlowState.Idle.ToString()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ShutdownAsync_WhenFeatureCleanupThrows_StillReachesDisposed()
|
||||
{
|
||||
var services = new TestServices();
|
||||
var root = services.CreateRoot();
|
||||
var flow = new GameFlow();
|
||||
await flow.StartupAsync<ThrowingExitAndDisposeFeature>(root, CancellationToken.None);
|
||||
|
||||
await flow.ShutdownAsync(CancellationToken.None);
|
||||
|
||||
Assert.That(flow.State, Is.EqualTo(GameFlowState.Disposed));
|
||||
Assert.That(services.Resource.Groups[0].IsDisposed, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ShutdownAsync_WhenCancellationIsAlreadyRequested_CleansUpThenThrowsCancellation()
|
||||
{
|
||||
var services = new TestServices();
|
||||
var root = services.CreateRoot();
|
||||
var flow = new GameFlow();
|
||||
await flow.StartupAsync<FirstFeature>(root, CancellationToken.None);
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
Assert.ThrowsAsync<OperationCanceledException>(
|
||||
async () => await flow.ShutdownAsync(cts.Token));
|
||||
|
||||
Assert.That(flow.State, Is.EqualTo(GameFlowState.Disposed));
|
||||
Assert.That(FirstFeature.Instance.Calls, Does.Contain("Dispose"));
|
||||
Assert.That(services.Resource.Groups[0].IsDisposed, Is.True);
|
||||
}
|
||||
|
||||
private sealed class TestServices
|
||||
{
|
||||
public TestConfigProvider Config { get; } = new();
|
||||
public TestSaveService Save { get; } = new();
|
||||
public TestResourceService Resource { get; } = new();
|
||||
|
||||
public Container CreateRoot()
|
||||
{
|
||||
var root = new Container();
|
||||
root.RegisterInstance<IConfigProvider>(Config);
|
||||
root.RegisterInstance<ISaveService>(Save);
|
||||
root.RegisterInstance<IResourceService>(Resource);
|
||||
root.RegisterFactory(_ => new FirstFeature());
|
||||
root.RegisterFactory(_ => new SecondFeature());
|
||||
root.RegisterFactory(_ => new FailingLoadFeature());
|
||||
root.RegisterFactory(_ => new FailingEnterFeature());
|
||||
root.RegisterFactory(_ => new ThrowingExitAndDisposeFeature());
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestConfigProvider : IConfigProvider
|
||||
{
|
||||
public int LoadCalls { get; private set; }
|
||||
|
||||
public Task LoadAllAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
LoadCalls++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public T Get<T>(int id)
|
||||
where T : class, IConfigRow => null;
|
||||
|
||||
public IReadOnlyList<T> GetAll<T>()
|
||||
where T : class, IConfigRow => Array.Empty<T>();
|
||||
}
|
||||
|
||||
private sealed class TestSaveService : ISaveService
|
||||
{
|
||||
public Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken) => Task.FromResult(defaultValue);
|
||||
public void Delete(string key) { }
|
||||
public bool Exists(string key) => false;
|
||||
}
|
||||
|
||||
private sealed class TestResourceService : IResourceService
|
||||
{
|
||||
public List<TestResourceGroup> Groups { get; } = new();
|
||||
|
||||
public Task<IResourceHandle<T>> LoadAsync<T>(string key, CancellationToken cancellationToken)
|
||||
where T : class => Task.FromResult<IResourceHandle<T>>(null);
|
||||
|
||||
public IResourceGroup CreateGroup()
|
||||
{
|
||||
var group = new TestResourceGroup();
|
||||
Groups.Add(group);
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestResourceGroup : IResourceGroup
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
public int Count => 0;
|
||||
|
||||
public void Add<T>(IResourceHandle<T> handle)
|
||||
where T : class
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class RecordingFeature : IFeature
|
||||
{
|
||||
public List<string> Calls { get; } = new();
|
||||
public FeatureContext Context { get; private set; }
|
||||
|
||||
public virtual Task LoadAsync(FeatureContext context)
|
||||
{
|
||||
Context = context;
|
||||
Calls.Add("Load");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task EnterAsync(FeatureContext context)
|
||||
{
|
||||
Calls.Add("Enter");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task ExitAsync(FeatureContext context)
|
||||
{
|
||||
Calls.Add("Exit");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Calls.Add("Dispose");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FirstFeature : RecordingFeature
|
||||
{
|
||||
public static FirstFeature Instance { get; set; }
|
||||
|
||||
public FirstFeature()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SecondFeature : RecordingFeature
|
||||
{
|
||||
public static SecondFeature Instance { get; set; }
|
||||
|
||||
public SecondFeature()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FailingLoadFeature : RecordingFeature
|
||||
{
|
||||
public static FailingLoadFeature Instance { get; set; }
|
||||
|
||||
public FailingLoadFeature()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public override Task LoadAsync(FeatureContext context)
|
||||
{
|
||||
base.LoadAsync(context);
|
||||
throw new InvalidOperationException("load failed");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FailingEnterFeature : RecordingFeature
|
||||
{
|
||||
public static FailingEnterFeature Instance { get; set; }
|
||||
|
||||
public FailingEnterFeature()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public override Task EnterAsync(FeatureContext context)
|
||||
{
|
||||
base.EnterAsync(context);
|
||||
throw new InvalidOperationException("enter failed");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ThrowingExitAndDisposeFeature : RecordingFeature
|
||||
{
|
||||
public static ThrowingExitAndDisposeFeature Instance { get; set; }
|
||||
|
||||
public ThrowingExitAndDisposeFeature()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public override Task ExitAsync(FeatureContext context)
|
||||
{
|
||||
base.ExitAsync(context);
|
||||
throw new InvalidOperationException("exit failed");
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
throw new InvalidOperationException("dispose failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12268dd1cada4c7bbe174bd1d276b87a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user