mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-10 02:59:02 +08:00
- 新增标准 Architecture 启动路径下自定义 notification publisher 的集成回归,验证 Configurator 注册的策略会被真实 publish 路径复用 - 更新 cqrs-rewrite active tracking 与 trace,推进恢复点到 RP-121 并记录当前验证结果与下一步边界
368 lines
14 KiB
C#
368 lines
14 KiB
C#
// Copyright (c) 2025-2026 GeWuYou
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
using System.Reflection;
|
||
using GFramework.Core.Abstractions.Architectures;
|
||
using GFramework.Core.Abstractions.Logging;
|
||
using GFramework.Core.Abstractions.Utility;
|
||
using GFramework.Core.Architectures;
|
||
using GFramework.Core.Logging;
|
||
using GFramework.Cqrs;
|
||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||
using GFramework.Cqrs.Notification;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
|
||
namespace GFramework.Core.Tests.Architectures;
|
||
|
||
/// <summary>
|
||
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
|
||
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
|
||
/// </summary>
|
||
[NonParallelizable]
|
||
[TestFixture]
|
||
public class ArchitectureModulesBehaviorTests
|
||
{
|
||
/// <summary>
|
||
/// 初始化日志工厂和全局上下文状态。
|
||
/// </summary>
|
||
[SetUp]
|
||
public void SetUp()
|
||
{
|
||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||
GameContext.Clear();
|
||
AdditionalAssemblyNotificationHandlerState.Reset();
|
||
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
|
||
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理测试过程中写入的全局上下文状态。
|
||
/// </summary>
|
||
[TearDown]
|
||
public void TearDown()
|
||
{
|
||
AdditionalAssemblyNotificationHandlerState.Reset();
|
||
GameContext.Clear();
|
||
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
|
||
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
|
||
LegacyBridgePipelineTracker.Reset();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证安装模块时会把当前架构实例传给模块,并允许模块在安装阶段注册组件。
|
||
/// </summary>
|
||
[Test]
|
||
public async Task InstallModule_Should_Invoke_Module_Install_With_Current_Architecture()
|
||
{
|
||
var module = new TrackingArchitectureModule();
|
||
var architecture = new ModuleTestArchitecture(target => target.InstallModule(module));
|
||
|
||
await architecture.InitializeAsync();
|
||
try
|
||
{
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
|
||
Assert.That(module.InstallCallCount, Is.EqualTo(1));
|
||
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
await architecture.DestroyAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证注册的 CQRS 行为会参与请求管道执行。
|
||
/// </summary>
|
||
[Test]
|
||
public async Task RegisterCqrsPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Request()
|
||
{
|
||
var architecture = new ModuleTestArchitecture(target =>
|
||
target.RegisterCqrsPipelineBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
|
||
|
||
await architecture.InitializeAsync();
|
||
try
|
||
{
|
||
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(response, Is.EqualTo("handled"));
|
||
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
await architecture.DestroyAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证注册的 CQRS stream 行为会参与建流处理流程。
|
||
/// </summary>
|
||
[Test]
|
||
public async Task RegisterCqrsStreamPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Stream_Request()
|
||
{
|
||
var architecture = new ModuleTestArchitecture(target =>
|
||
target.RegisterCqrsStreamPipelineBehavior<TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>>());
|
||
|
||
await architecture.InitializeAsync();
|
||
try
|
||
{
|
||
var response = await DrainAsync(architecture.Context.CreateStream(new ModuleStreamBehaviorRequest()));
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(response, Is.EqualTo([7]));
|
||
Assert.That(
|
||
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount,
|
||
Is.EqualTo(1));
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
await architecture.DestroyAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler,
|
||
/// 使旧 <c>SendCommand</c> / <c>SendQuery</c> 入口也能进入统一 CQRS pipeline。
|
||
/// </summary>
|
||
[Test]
|
||
public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies()
|
||
{
|
||
LegacyBridgePipelineTracker.Reset();
|
||
var architecture = new LegacyBridgeArchitecture();
|
||
|
||
await architecture.InitializeAsync();
|
||
try
|
||
{
|
||
var query = new LegacyArchitectureBridgeQuery();
|
||
var command = new LegacyArchitectureBridgeCommand();
|
||
|
||
var queryResult = architecture.Context.SendQuery(query);
|
||
architecture.Context.SendCommand(command);
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(queryResult, Is.EqualTo(24));
|
||
Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
|
||
Assert.That(command.Executed, Is.True);
|
||
Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
|
||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
await architecture.DestroyAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证标准架构启动路径会复用通过 <see cref="Architecture.Configurator" /> 声明的自定义 notification publisher,
|
||
/// 而不是在 <see cref="GFramework.Core.Services.Modules.CqrsRuntimeModule" /> 创建 runtime 时提前固化默认顺序策略。
|
||
/// </summary>
|
||
[Test]
|
||
public async Task InitializeAsync_Should_Reuse_Custom_NotificationPublisher_From_Configurator()
|
||
{
|
||
var generatedAssembly = CreateGeneratedHandlerAssembly();
|
||
var architecture = new ConfiguredNotificationPublisherArchitecture(generatedAssembly.Object);
|
||
|
||
await architecture.InitializeAsync();
|
||
try
|
||
{
|
||
var probe = architecture.Context.GetService<ArchitectureNotificationPublisherProbe>();
|
||
|
||
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(probe.WasCalled, Is.True);
|
||
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
await architecture.DestroyAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用于测试模块行为的最小架构实现。
|
||
/// </summary>
|
||
private sealed class ModuleTestArchitecture(Action<ModuleTestArchitecture> registrationAction) : Architecture
|
||
{
|
||
/// <summary>
|
||
/// 在初始化阶段执行测试注入的模块注册逻辑。
|
||
/// </summary>
|
||
protected override void OnInitialize()
|
||
{
|
||
registrationAction(this);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过公开初始化入口注册测试 pipeline behavior 的最小架构,
|
||
/// 用于验证默认 Core 程序集扫描是否会自动接入 legacy bridge handler。
|
||
/// </summary>
|
||
private sealed class LegacyBridgeArchitecture : Architecture
|
||
{
|
||
/// <summary>
|
||
/// 在容器钩子阶段注册 open-generic pipeline behavior,
|
||
/// 以便 bridge request 走真实的架构初始化与 handler 自动扫描链路。
|
||
/// </summary>
|
||
public override Action<IServiceCollection>? Configurator => services =>
|
||
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>));
|
||
|
||
/// <summary>
|
||
/// 保持空初始化,让测试只聚焦默认 CQRS 接线与 legacy bridge handler 自动发现。
|
||
/// </summary>
|
||
protected override void OnInitialize()
|
||
{
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过标准架构启动路径声明自定义 notification publisher 的最小架构。
|
||
/// </summary>
|
||
private sealed class ConfiguredNotificationPublisherArchitecture(Assembly generatedAssembly) : Architecture
|
||
{
|
||
/// <summary>
|
||
/// 在服务钩子阶段注册 probe 与自定义 publisher,
|
||
/// 以模拟真实项目在组合根里通过 <see cref="IServiceCollection" /> 覆盖默认策略的路径。
|
||
/// </summary>
|
||
public override Action<IServiceCollection>? Configurator => services =>
|
||
{
|
||
services.AddSingleton<ArchitectureNotificationPublisherProbe>();
|
||
services.AddSingleton<INotificationPublisher, ArchitectureTrackingNotificationPublisher>();
|
||
};
|
||
|
||
/// <summary>
|
||
/// 在用户初始化阶段显式接入额外程序集里的 notification handler,
|
||
/// 让测试聚焦“publisher 是否被复用”,而不是依赖当前测试文件自己的 handler 扫描形状。
|
||
/// </summary>
|
||
protected override void OnInitialize()
|
||
{
|
||
RegisterCqrsHandlersFromAssembly(generatedAssembly);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 记录模块安装调用情况的测试模块。
|
||
/// </summary>
|
||
private sealed class TrackingArchitectureModule : IArchitectureModule
|
||
{
|
||
/// <summary>
|
||
/// 获取模块安装调用次数。
|
||
/// </summary>
|
||
public int InstallCallCount { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 获取最近一次接收到的架构实例。
|
||
/// </summary>
|
||
public IArchitecture? InstalledArchitecture { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 记录安装调用,并在安装阶段注册一个工具验证调用链可用。
|
||
/// </summary>
|
||
/// <param name="architecture">目标架构实例。</param>
|
||
public void Install(IArchitecture architecture)
|
||
{
|
||
InstallCallCount++;
|
||
InstalledArchitecture = architecture;
|
||
architecture.RegisterUtility(new InstalledByModuleUtility());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 由测试模块安装时注册的简单工具。
|
||
/// </summary>
|
||
private sealed class InstalledByModuleUtility : IUtility
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。
|
||
/// 该测试替身模拟扩展程序集已经提供 notification handler registry,而架构只需在初始化时显式接入该程序集。
|
||
/// </summary>
|
||
/// <returns>包含程序集级 notification handler registry 元数据的 mocked Assembly。</returns>
|
||
private static Mock<Assembly> CreateGeneratedHandlerAssembly()
|
||
{
|
||
var generatedAssembly = new Mock<Assembly>();
|
||
generatedAssembly
|
||
.SetupGet(static assembly => assembly.FullName)
|
||
.Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0");
|
||
generatedAssembly
|
||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||
.Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]);
|
||
return generatedAssembly;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 记录自定义 notification publisher 是否真正参与了标准架构启动路径下的 publish 调用。
|
||
/// </summary>
|
||
private sealed class ArchitectureNotificationPublisherProbe
|
||
{
|
||
/// <summary>
|
||
/// 获取 probe 是否已被 publisher 标记为执行过。
|
||
/// </summary>
|
||
public bool WasCalled { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 记录当前 publish 调用已经命中了自定义 publisher。
|
||
/// </summary>
|
||
public void MarkCalled()
|
||
{
|
||
WasCalled = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 依赖容器内 probe 的自定义 notification publisher。
|
||
/// 该类型通过显式标记 + 正常转发处理器执行,验证标准架构启动路径不会把自定义策略短路成默认顺序发布器。
|
||
/// </summary>
|
||
private sealed class ArchitectureTrackingNotificationPublisher(
|
||
ArchitectureNotificationPublisherProbe probe) : INotificationPublisher
|
||
{
|
||
/// <summary>
|
||
/// 记录自定义 publisher 已参与当前发布调用,并继续按处理器解析顺序转发执行。
|
||
/// </summary>
|
||
public async ValueTask PublishAsync<TNotification>(
|
||
NotificationPublishContext<TNotification> context,
|
||
CancellationToken cancellationToken = default)
|
||
where TNotification : INotification
|
||
{
|
||
ArgumentNullException.ThrowIfNull(context);
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
|
||
probe.MarkCalled();
|
||
|
||
foreach (var handler in context.Handlers)
|
||
{
|
||
await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 物化异步流为只读列表,便于断言 stream pipeline 行为的最终可观察结果。
|
||
/// </summary>
|
||
/// <typeparam name="T">流元素类型。</typeparam>
|
||
/// <param name="stream">要物化的异步流。</param>
|
||
/// <returns>按枚举顺序收集的元素列表。</returns>
|
||
private static async Task<IReadOnlyList<T>> DrainAsync<T>(IAsyncEnumerable<T> stream)
|
||
{
|
||
var results = new List<T>();
|
||
|
||
await foreach (var item in stream.ConfigureAwait(false))
|
||
{
|
||
results.Add(item);
|
||
}
|
||
|
||
return results;
|
||
}
|
||
}
|