Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions GFramework.Core.Abstractions/Ioc/IIocContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ void RegisterCqrsPipelineBehavior<TBehavior>()
/// </summary>
/// <typeparam name="T">期望获取的实例类型</typeparam>
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
/// </remarks>
T? Get<T>() where T : class;

/// <summary>
Expand All @@ -149,6 +153,10 @@ void RegisterCqrsPipelineBehavior<TBehavior>()
/// </summary>
/// <param name="type">期望获取的实例类型</param>
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
/// </remarks>
object? Get(Type type);


Expand All @@ -174,13 +182,19 @@ void RegisterCqrsPipelineBehavior<TBehavior>()
/// </summary>
/// <typeparam name="T">期望获取的实例类型</typeparam>
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
/// </remarks>
IReadOnlyList<T> GetAll<T>() where T : class;

/// <summary>
/// 获取指定类型的所有实例
/// </summary>
/// <param name="type">期望获取的实例类型</param>
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
/// </remarks>
IReadOnlyList<object> GetAll(Type type);


Expand Down Expand Up @@ -219,6 +233,9 @@ void RegisterCqrsPipelineBehavior<TBehavior>()
/// </summary>
/// <typeparam name="T">要检查的类型</typeparam>
/// <returns>如果容器中包含指定类型的实例则返回true,否则返回false</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该方法更接近“是否存在对应注册”的检查,而不是完整的 DI 可解析性判断。
/// </remarks>
bool Contains<T>() where T : class;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using GFramework.Core.Rule;

namespace GFramework.Core.Tests.Architectures;

Expand Down Expand Up @@ -181,6 +183,80 @@ public async Task DestroyAsync_Should_Notify_Container_Phase_Listeners_About_Des
}));
}

/// <summary>
/// 验证架构销毁后会解除全局 GameContext 绑定。
/// 该回归测试用于防止已销毁架构继续充当默认上下文回退入口。
/// </summary>
[Test]
public async Task DestroyAsync_Should_Unbind_Context_From_GameContext()
{
var architecture = new PhaseTrackingArchitecture();

await architecture.InitializeAsync();

Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));

await architecture.DestroyAsync();

Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}

/// <summary>
/// 验证失败初始化后的销毁同样会解除全局上下文绑定。
/// </summary>
[Test]
public async Task DestroyAsync_After_FailedInitialization_Should_Unbind_Context_From_GameContext()
{
var destroyOrder = new List<string>();
var architecture = new FailingInitializationArchitecture(destroyOrder);

var exception = Assert.ThrowsAsync<InvalidOperationException>(() => architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));

await architecture.DestroyAsync();

Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}

/// <summary>
/// 验证销毁后的新 ContextAware 实例不会再通过全局回退命中过期上下文。
/// </summary>
[Test]
public async Task DestroyAsync_Should_Prevent_New_ContextAware_Fallback_From_Using_Destroyed_Context()
{
var architecture = new PhaseTrackingArchitecture();

await architecture.InitializeAsync();
await architecture.DestroyAsync();

IContextAware probe = new LifecycleContextAwareProbe();

Assert.Throws<InvalidOperationException>(() => probe.GetContext());
}

/// <summary>
/// 验证同步兼容销毁入口同样会解除全局 GameContext 绑定。
/// </summary>
[Test]
public async Task Destroy_Should_Unbind_Context_From_GameContext()
{
var architecture = new PhaseTrackingArchitecture();

await architecture.InitializeAsync();

Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));

#pragma warning disable CS0618
architecture.Destroy();
#pragma warning restore CS0618

Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}

/// <summary>
/// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。
/// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。
Expand Down Expand Up @@ -232,6 +308,13 @@ protected override void OnInitialize()
}
}

/// <summary>
/// 仅用于验证销毁后全局上下文回退是否仍然泄漏的最小 ContextAware 探针。
/// </summary>
private sealed class LifecycleContextAwareProbe : ContextAwareBase
{
}

/// <summary>
/// 在初始化时注册可销毁组件的测试架构。
/// </summary>
Expand Down
12 changes: 6 additions & 6 deletions GFramework.Core.Tests/Architectures/ContextProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// ContextProvider 相关类的单元测试
/// 测试内容包括:
/// - GameContextProvider 获取第一个架构上下文
/// - GameContextProvider 获取当前活动架构上下文
/// - GameContextProvider 尝试获取指定类型的上下文
/// - ScopedContextProvider 获取绑定的上下文
/// - ScopedContextProvider 尝试获取指定类型的上下文
Expand Down Expand Up @@ -37,10 +37,10 @@ public void TearDown()
}

/// <summary>
/// 测试 GameContextProvider 是否能正确获取第一个架构上下文
/// 测试 GameContextProvider 是否能正确获取当前活动架构上下文
/// </summary>
[Test]
public void GameContextProvider_GetContext_Should_Return_First_Context()
public void GameContextProvider_GetContext_Should_Return_Current_Context()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
Expand All @@ -63,13 +63,13 @@ public void GameContextProvider_GetContext_Should_Throw_When_Empty()
}

/// <summary>
/// 测试 GameContextProvider 的 TryGetContext 方法在找到上下文时返回 true
/// 测试 GameContextProvider 的 TryGetContext 方法在仅绑定架构类型时也能返回 true
/// </summary>
[Test]
public void GameContextProvider_TryGetContext_Should_Return_True_When_Found()
public void GameContextProvider_TryGetContext_Should_Return_True_When_Current_Context_Matches()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Bind(typeof(TestArchitecture), context);

var provider = new GameContextProvider();
var result = provider.TryGetContext<TestArchitectureContext>(out var foundContext);
Expand Down
89 changes: 64 additions & 25 deletions GFramework.Core.Tests/Architectures/GameContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,12 @@
namespace GFramework.Core.Tests.Architectures;

/// <summary>
/// GameContext类的单元测试
/// GameContext 类的单元测试
/// 测试内容包括:
/// - ArchitectureReadOnlyDictionary在启动时为空
/// - Bind方法添加上下文到字典
/// - Bind重复类型时抛出异常
/// - GetByType返回正确的上下文
/// - GetByType未找到时抛出异常
/// - Get泛型方法返回正确的上下文
/// - TryGet方法在找到时返回true
/// - TryGet方法在未找到时返回false
/// - GetFirstArchitectureContext在存在时返回
/// - GetFirstArchitectureContext为空时抛出异常
/// - Unbind移除上下文
/// - Clear移除所有上下文
/// - 初始状态为空
/// - 绑定后可通过架构类型和上下文类型回查
/// - 不允许并存绑定两个不同上下文实例
/// - 清理和解绑会同步更新当前活动上下文
/// </summary>
[TestFixture]
public class GameContextTests
Expand Down Expand Up @@ -81,6 +73,21 @@ public void Bind_WithDuplicateType_Should_ThrowInvalidOperationException()
GameContext.Bind(typeof(TestArchitecture), context2));
}

/// <summary>
/// 测试绑定第二个不同的上下文实例时会被拒绝。
/// </summary>
[Test]
public void Bind_WithDifferentContextInstance_Should_ThrowInvalidOperationException()
{
var firstContext = new TestArchitectureContext();
var secondContext = new TestArchitectureContext();

GameContext.Bind(typeof(TestArchitecture), firstContext);

Assert.Throws<InvalidOperationException>(() =>
GameContext.Bind(typeof(AnotherTestArchitectureContext), secondContext));
}

/// <summary>
/// 测试GetByType方法是否返回正确的上下文
/// </summary>
Expand All @@ -106,27 +113,41 @@ public void GetByType_Should_Throw_When_Not_Found()
}

/// <summary>
/// 测试Get泛型方法是否返回正确的上下文
/// 测试 GetByType 支持按当前活动上下文的具体类型回查。
/// </summary>
[Test]
public void GetGeneric_Should_Return_Correct_Context()
public void GetByType_Should_Return_Current_Context_When_Requested_By_Context_Type()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Bind(typeof(TestArchitecture), context);

var result = GameContext.GetByType(typeof(TestArchitectureContext));

Assert.That(result, Is.SameAs(context));
}

/// <summary>
/// 测试 Get 泛型方法在仅绑定架构类型时也能返回当前上下文
/// </summary>
[Test]
public void GetGeneric_Should_Return_Current_Context_When_Bound_By_Architecture_Type()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);

var result = GameContext.Get<TestArchitectureContext>();

Assert.That(result, Is.SameAs(context));
}

/// <summary>
/// 测试TryGet方法在找到上下文时是否返回true并正确设置输出参数
/// 测试 TryGet 方法在仅绑定架构类型时也能找到当前上下文
/// </summary>
[Test]
public void TryGet_Should_ReturnTrue_When_Found()
public void TryGet_Should_ReturnTrue_When_Bound_By_Architecture_Type()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Bind(typeof(TestArchitecture), context);

var result = GameContext.TryGet(out TestArchitectureContext? foundContext);

Expand All @@ -135,7 +156,7 @@ public void TryGet_Should_ReturnTrue_When_Found()
}

/// <summary>
/// 测试TryGet方法在未找到上下文时是否返回false且输出参数为null
/// 测试 TryGet 方法在未找到上下文时是否返回 false 且输出参数为 null
/// </summary>
[Test]
public void TryGet_Should_ReturnFalse_When_Not_Found()
Expand Down Expand Up @@ -171,10 +192,10 @@ public void GetFirstArchitectureContext_Should_Throw_When_Empty()
}

/// <summary>
/// 测试Unbind方法是否正确移除指定类型的上下文
/// 测试 Unbind 方法在移除最后一个别名时会清空当前活动上下文
/// </summary>
[Test]
public void Unbind_Should_Remove_Context()
public void Unbind_Should_Remove_Context_When_Last_Alias_Is_Removed()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
Expand All @@ -185,16 +206,34 @@ public void Unbind_Should_Remove_Context()
}

/// <summary>
/// 测试Clear方法是否正确移除所有上下文
/// 测试 Unbind 方法在仍有其他别名时保留当前活动上下文
/// </summary>
[Test]
public void Unbind_Should_Keep_Current_Context_When_Another_Alias_Remains()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
GameContext.Bind(typeof(TestArchitectureContext), context);

GameContext.Unbind(typeof(TestArchitecture));

Assert.That(GameContext.GetFirstArchitectureContext(), Is.SameAs(context));
Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(1));
}

/// <summary>
/// 测试 Clear 方法是否正确移除所有上下文
/// </summary>
[Test]
public void Clear_Should_Remove_All_Contexts()
{
GameContext.Bind(typeof(TestArchitecture), new TestArchitectureContext());
GameContext.Bind(typeof(TestArchitectureContext), new TestArchitectureContext());
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
GameContext.Bind(typeof(TestArchitectureContext), context);

GameContext.Clear();

Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(0));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}
}
Loading
Loading