From 5693ab7e6f1839a29c7dd5ecb2f378e18cdd7987 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:26:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(test-helpers):=20=E6=94=B6=E6=95=9BPR300?= =?UTF-8?q?=E8=AF=84=E5=AE=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复测试架构上下文、生命周期钩子与注册表初始化钩子的评审问题,避免静默成功或错误共享状态 - 补充 TestResourceLoader、TestLogger、CapturingLoggerFactoryProvider 与 CQRS 测试辅助类型的契约文档和并发语义 - 新增测试覆盖并更新 analyzer-warning-reduction 活跃跟踪,记录 PR #300 跟进验证与现存 Cqrs warning blocker --- .../Architectures/TestArchitectureContext.cs | 45 +++-- .../TestArchitectureContextBehaviorTests.cs | 156 ++++++++++++++++++ .../TestArchitectureContextV3.cs | 24 +-- .../TestArchitectureWithRegistry.cs | 3 +- .../TestArchitectureWithoutRegistry.cs | 3 +- .../Coroutine/WaitForMultipleEventsTests.cs | 40 +++-- GFramework.Core.Tests/Logging/TestLogger.cs | 25 ++- .../Resource/TestResourceLoader.cs | 56 +++++-- .../RegistryInitializationHookBase.cs | 26 ++- .../Cqrs/CapturingLoggerFactoryProvider.cs | 23 ++- .../DeterministicNotificationHandlerState.cs | 7 + ...ialGeneratedNotificationHandlerRegistry.cs | 3 + .../analyzer-warning-reduction-tracking.md | 48 +++--- .../analyzer-warning-reduction-trace.md | 32 ++++ 14 files changed, 403 insertions(+), 88 deletions(-) create mode 100644 GFramework.Core.Tests/Architectures/TestArchitectureContextBehaviorTests.cs diff --git a/GFramework.Core.Tests/Architectures/TestArchitectureContext.cs b/GFramework.Core.Tests/Architectures/TestArchitectureContext.cs index cda590fd..a1d85189 100644 --- a/GFramework.Core.Tests/Architectures/TestArchitectureContext.cs +++ b/GFramework.Core.Tests/Architectures/TestArchitectureContext.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; using GFramework.Core.Abstractions.Environment; @@ -27,6 +31,7 @@ namespace GFramework.Core.Tests.Architectures; public class TestArchitectureContext : IArchitectureContext { private readonly MicrosoftDiContainer _container = new(); + private readonly EventBus _eventBus = new(); /// /// 获取用于解析测试服务的依赖注入容器。 @@ -36,7 +41,11 @@ public class TestArchitectureContext : IArchitectureContext /// /// 获取测试事件总线实例。 /// - public IEventBus EventBus => new EventBus(); + /// + /// 返回同一个缓存事件总线,以便 与 + /// 在同一份订阅状态上协作。 + /// + public IEventBus EventBus => _eventBus; /// /// 获取测试命令执行器实例。 @@ -183,6 +192,7 @@ public class TestArchitectureContext : IArchitectureContext /// 事件类型。 public void SendEvent() where TEvent : new() { + _eventBus.Send(); } /// @@ -192,6 +202,8 @@ public class TestArchitectureContext : IArchitectureContext /// 事件实例。 public void SendEvent(TEvent e) where TEvent : class { + ArgumentNullException.ThrowIfNull(e); + _eventBus.Send(e); } /// @@ -199,10 +211,11 @@ public class TestArchitectureContext : IArchitectureContext /// /// 事件类型。 /// 事件处理委托。 - /// 用于测试的空注销句柄。 + /// 用于测试的事件注销句柄。 public IUnRegister RegisterEvent(Action handler) { - return new DefaultUnRegister(() => { }); + ArgumentNullException.ThrowIfNull(handler); + return _eventBus.Register(handler); } /// @@ -212,6 +225,8 @@ public class TestArchitectureContext : IArchitectureContext /// 事件处理委托。 public void UnRegisterEvent(Action onEvent) { + ArgumentNullException.ThrowIfNull(onEvent); + _eventBus.UnRegister(onEvent); } /// @@ -358,8 +373,10 @@ public class TestArchitectureContext : IArchitectureContext /// 发送旧版命令。 /// /// 命令对象。 + /// 该测试桩不支持旧版命令执行入口。 public void SendCommand(ICommand command) { + throw new NotSupportedException(); } /// @@ -367,20 +384,21 @@ public class TestArchitectureContext : IArchitectureContext /// /// 返回值类型。 /// 命令对象。 - /// 测试桩默认返回值。 + /// 此方法始终抛出异常,不返回结果。 + /// 该测试桩不支持旧版命令执行入口。 public TResult SendCommand(ICommand command) { - return default!; + throw new NotSupportedException(); } /// /// 异步发送旧版命令。 /// /// 命令对象。 - /// 已完成任务。 + /// 已失败的任务。 public Task SendCommandAsync(IAsyncCommand command) { - return Task.CompletedTask; + return Task.FromException(new NotSupportedException()); } /// @@ -388,10 +406,10 @@ public class TestArchitectureContext : IArchitectureContext /// /// 返回值类型。 /// 命令对象。 - /// 包含测试桩默认返回值的任务。 + /// 已失败的任务。 public Task SendCommandAsync(IAsyncCommand command) { - return Task.FromResult(default(TResult)!); + return Task.FromException(new NotSupportedException()); } /// @@ -399,10 +417,11 @@ public class TestArchitectureContext : IArchitectureContext /// /// 查询结果类型。 /// 查询对象。 - /// 测试桩默认返回值。 + /// 此方法始终抛出异常,不返回结果。 + /// 该测试桩不支持旧版查询执行入口。 public TResult SendQuery(IQuery query) { - return default!; + throw new NotSupportedException(); } /// @@ -410,10 +429,10 @@ public class TestArchitectureContext : IArchitectureContext /// /// 查询结果类型。 /// 异步查询对象。 - /// 包含测试桩默认返回值的任务。 + /// 已失败的任务。 public Task SendQueryAsync(IAsyncQuery query) { - return Task.FromResult(default(TResult)!); + return Task.FromException(new NotSupportedException()); } /// diff --git a/GFramework.Core.Tests/Architectures/TestArchitectureContextBehaviorTests.cs b/GFramework.Core.Tests/Architectures/TestArchitectureContextBehaviorTests.cs new file mode 100644 index 00000000..a4532a0f --- /dev/null +++ b/GFramework.Core.Tests/Architectures/TestArchitectureContextBehaviorTests.cs @@ -0,0 +1,156 @@ +using System; +using System.Threading.Tasks; +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Enums; +using GFramework.Core.Abstractions.Query; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 覆盖测试架构上下文替身的共享事件与显式失败契约。 +/// +[TestFixture] +public class TestArchitectureContextBehaviorTests +{ + /// + /// 验证测试上下文会把事件注册与发送委托到同一个事件总线实例。 + /// + [Test] + public void RegisterEvent_And_SendEvent_Should_Use_Shared_EventBus() + { + var context = new TestArchitectureContext(); + var eventReceived = false; + + context.RegisterEvent(_ => eventReceived = true); + context.SendEvent(); + + Assert.That(eventReceived, Is.True); + } + + /// + /// 验证测试上下文的旧版命令与查询入口会显式抛出未支持异常。 + /// + [Test] + public async Task Legacy_Entries_Should_Throw_Or_Return_Faulted_Tasks() + { + var context = new TestArchitectureContext(); + + Assert.That(() => context.SendCommand(new TestCommandV2()), Throws.TypeOf()); + Assert.That( + () => context.SendCommand(new TestCommandWithResultV2 { Result = 1 }), + Throws.TypeOf()); + Assert.That(() => context.SendQuery(new TestQueryV2 { Result = 1 }), Throws.TypeOf()); + Assert.That( + async () => await context.SendCommandAsync(new TestAsyncCommand()).ConfigureAwait(false), + Throws.TypeOf()); + Assert.That( + async () => await context.SendCommandAsync(new TestAsyncCommandWithResult()).ConfigureAwait(false), + Throws.TypeOf()); + Assert.That( + async () => await context.SendQueryAsync(new TestAsyncQuery()).ConfigureAwait(false), + Throws.TypeOf()); + } + + /// + /// 验证用于 ArchitectureServices 的上下文替身也会把旧版入口显式标记为不支持。 + /// + [Test] + public async Task Legacy_Entries_On_TestArchitectureContextV3_Should_Throw_Or_Return_Faulted_Tasks() + { + var context = new TestArchitectureContextV3(); + + Assert.That(() => context.SendCommand(new TestCommandV2()), Throws.TypeOf()); + Assert.That( + () => context.SendCommand(new TestCommandWithResultV2 { Result = 1 }), + Throws.TypeOf()); + Assert.That(() => context.SendQuery(new TestQueryV2 { Result = 1 }), Throws.TypeOf()); + Assert.That( + async () => await context.SendCommandAsync(new TestAsyncCommand()).ConfigureAwait(false), + Throws.TypeOf()); + Assert.That( + async () => await context.SendCommandAsync(new TestAsyncCommandWithResult()).ConfigureAwait(false), + Throws.TypeOf()); + Assert.That( + async () => await context.SendQueryAsync(new TestAsyncQuery()).ConfigureAwait(false), + Throws.TypeOf()); + } + + /// + /// 验证两类架构测试替身在接口视角下都会以 no-op 方式接受生命周期钩子。 + /// + [Test] + public void RegisterLifecycleHook_Via_Interface_Should_Return_Original_Hook() + { + IArchitecture withRegistry = new TestArchitectureWithRegistry(new TestRegistry()); + IArchitecture withoutRegistry = new TestArchitectureWithoutRegistry(); + var hook = new NoOpLifecycleHook(); + + Assert.That(withRegistry.RegisterLifecycleHook(hook), Is.SameAs(hook)); + Assert.That(withoutRegistry.RegisterLifecycleHook(hook), Is.SameAs(hook)); + } + + /// + /// 为旧版异步命令入口提供最小实现的测试命令。 + /// + private sealed class TestAsyncCommand : IAsyncCommand + { + public Task ExecuteAsync() + { + return Task.CompletedTask; + } + + public IArchitectureContext GetContext() + { + throw new NotSupportedException(); + } + + public void SetContext(IArchitectureContext context) + { + ArgumentNullException.ThrowIfNull(context); + } + } + + /// + /// 为旧版异步命令入口提供最小实现的带结果测试命令。 + /// + private sealed class TestAsyncCommandWithResult : IAsyncCommand + { + public Task ExecuteAsync() + { + return Task.FromResult(1); + } + + public IArchitectureContext GetContext() + { + throw new NotSupportedException(); + } + + public void SetContext(IArchitectureContext context) + { + ArgumentNullException.ThrowIfNull(context); + } + } + + /// + /// 为旧版异步查询入口提供最小实现的测试查询。 + /// + private sealed class TestAsyncQuery : IAsyncQuery + { + public Task DoAsync() + { + return Task.FromResult(1); + } + } + + /// + /// 为生命周期钩子接口提供空实现的测试替身。 + /// + private sealed class NoOpLifecycleHook : IArchitectureLifecycleHook + { + public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) + { + ArgumentNullException.ThrowIfNull(architecture); + } + } +} diff --git a/GFramework.Core.Tests/Architectures/TestArchitectureContextV3.cs b/GFramework.Core.Tests/Architectures/TestArchitectureContextV3.cs index 6fda6f2d..7cdd8a57 100644 --- a/GFramework.Core.Tests/Architectures/TestArchitectureContextV3.cs +++ b/GFramework.Core.Tests/Architectures/TestArchitectureContextV3.cs @@ -343,8 +343,10 @@ public class TestArchitectureContextV3 : IArchitectureContext /// 发送旧版无返回值命令。 /// /// 要发送的命令。 + /// 该测试桩不支持旧版命令执行入口。 public void SendCommand(ICommand command) { + throw new NotSupportedException(); } /// @@ -352,20 +354,21 @@ public class TestArchitectureContextV3 : IArchitectureContext /// /// 命令响应类型。 /// 要发送的命令。 - /// 默认响应值。 + /// 此方法始终抛出异常,不返回结果。 + /// 该测试桩不支持旧版命令执行入口。 public TResult SendCommand(GFramework.Core.Abstractions.Command.ICommand command) { - return default!; + throw new NotSupportedException(); } /// /// 异步发送旧版无返回值命令。 /// /// 要发送的命令。 - /// 已完成任务。 + /// 已失败的任务。 public Task SendCommandAsync(IAsyncCommand command) { - return Task.CompletedTask; + return Task.FromException(new NotSupportedException()); } /// @@ -373,10 +376,10 @@ public class TestArchitectureContextV3 : IArchitectureContext /// /// 命令响应类型。 /// 要发送的命令。 - /// 占位任务。 + /// 已失败的任务。 public Task SendCommandAsync(IAsyncCommand command) { - return (Task)Task.CompletedTask; + return Task.FromException(new NotSupportedException()); } /// @@ -384,10 +387,11 @@ public class TestArchitectureContextV3 : IArchitectureContext /// /// 查询结果类型。 /// 要发送的查询。 - /// 默认查询结果。 + /// 此方法始终抛出异常,不返回结果。 + /// 该测试桩不支持旧版查询执行入口。 public TResult SendQuery(GFramework.Core.Abstractions.Query.IQuery query) { - return default!; + throw new NotSupportedException(); } /// @@ -395,10 +399,10 @@ public class TestArchitectureContextV3 : IArchitectureContext /// /// 查询结果类型。 /// 要发送的查询。 - /// 占位任务。 + /// 已失败的任务。 public Task SendQueryAsync(IAsyncQuery query) { - return (Task)Task.CompletedTask; + return Task.FromException(new NotSupportedException()); } /// diff --git a/GFramework.Core.Tests/Architectures/TestArchitectureWithRegistry.cs b/GFramework.Core.Tests/Architectures/TestArchitectureWithRegistry.cs index cdb52e81..df0f93e5 100644 --- a/GFramework.Core.Tests/Architectures/TestArchitectureWithRegistry.cs +++ b/GFramework.Core.Tests/Architectures/TestArchitectureWithRegistry.cs @@ -95,7 +95,8 @@ public class TestArchitectureWithRegistry : IArchitecture IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook) { - throw new NotSupportedException(); + RegisterLifecycleHook(hook); + return hook; } Task IArchitecture.WaitUntilReadyAsync() diff --git a/GFramework.Core.Tests/Architectures/TestArchitectureWithoutRegistry.cs b/GFramework.Core.Tests/Architectures/TestArchitectureWithoutRegistry.cs index f4bd61ec..b9387e54 100644 --- a/GFramework.Core.Tests/Architectures/TestArchitectureWithoutRegistry.cs +++ b/GFramework.Core.Tests/Architectures/TestArchitectureWithoutRegistry.cs @@ -93,7 +93,8 @@ public class TestArchitectureWithoutRegistry : IArchitecture IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook) { - throw new NotSupportedException(); + RegisterLifecycleHook(hook); + return hook; } /// diff --git a/GFramework.Core.Tests/Coroutine/WaitForMultipleEventsTests.cs b/GFramework.Core.Tests/Coroutine/WaitForMultipleEventsTests.cs index 88baf123..e35b9a6c 100644 --- a/GFramework.Core.Tests/Coroutine/WaitForMultipleEventsTests.cs +++ b/GFramework.Core.Tests/Coroutine/WaitForMultipleEventsTests.cs @@ -1,3 +1,4 @@ +using System; using GFramework.Core.Abstractions.Events; using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Events; @@ -8,25 +9,28 @@ namespace GFramework.Core.Tests.Coroutine [TestFixture] public class WaitForMultipleEventsTests { + private IEventBus? _eventBus; + + private IEventBus EventBus => _eventBus ?? throw new InvalidOperationException("EventBus has not been initialized."); + [SetUp] public void SetUp() { - eventBus = new EventBus(); + _eventBus = new EventBus(); } [TearDown] public void TearDown() { - (eventBus as IDisposable)?.Dispose(); + (EventBus as IDisposable)?.Dispose(); + _eventBus = null; } - private IEventBus eventBus = null!; - [Test] public void Constructor_RegistersBothEventTypes() { // Arrange & Act - var waitForMultipleEvents = new WaitForMultipleEvents(eventBus); + var waitForMultipleEvents = new WaitForMultipleEvents(EventBus); // Assert Assert.That(waitForMultipleEvents.IsDone, Is.False); @@ -37,11 +41,11 @@ namespace GFramework.Core.Tests.Coroutine public async Task FirstEventWins_WhenBothEventsFired() { // Arrange - var waitForMultipleEvents = new WaitForMultipleEvents(eventBus); + var waitForMultipleEvents = new WaitForMultipleEvents(EventBus); // Act - eventBus.Send(new TestEvent1 { Data = "first_event" }); - eventBus.Send(new TestEvent2 { Data = "second_event" }); + EventBus.Send(new TestEvent1 { Data = "first_event" }); + EventBus.Send(new TestEvent2 { Data = "second_event" }); // Assert Assert.That(waitForMultipleEvents.IsDone, Is.True); @@ -54,10 +58,10 @@ namespace GFramework.Core.Tests.Coroutine public async Task SecondEventWins_WhenOnlySecondEventFired() { // Arrange - var waitForMultipleEvents = new WaitForMultipleEvents(eventBus); + var waitForMultipleEvents = new WaitForMultipleEvents(EventBus); // Act - eventBus.Send(new TestEvent2 { Data = "second_event" }); + EventBus.Send(new TestEvent2 { Data = "second_event" }); // Assert Assert.That(waitForMultipleEvents.IsDone, Is.True); @@ -70,11 +74,11 @@ namespace GFramework.Core.Tests.Coroutine public async Task FirstEventWins_WhenBothEventsFiredInReverseOrder() { // Arrange - var waitForMultipleEvents = new WaitForMultipleEvents(eventBus); + var waitForMultipleEvents = new WaitForMultipleEvents(EventBus); // Act - eventBus.Send(new TestEvent2 { Data = "second_event" }); - eventBus.Send(new TestEvent1 { Data = "first_event" }); + EventBus.Send(new TestEvent2 { Data = "second_event" }); + EventBus.Send(new TestEvent1 { Data = "first_event" }); // Assert Assert.That(waitForMultipleEvents.IsDone, Is.True); @@ -88,10 +92,10 @@ namespace GFramework.Core.Tests.Coroutine public async Task MultipleEvents_AfterCompletion_DoNotOverrideState() { // Arrange - var waitForMultipleEvents = new WaitForMultipleEvents(eventBus); + var waitForMultipleEvents = new WaitForMultipleEvents(EventBus); // Act - Fire first event - eventBus.Send(new TestEvent1 { Data = "first_event" }); + EventBus.Send(new TestEvent1 { Data = "first_event" }); // Verify first event was processed Assert.That(waitForMultipleEvents.IsDone, Is.True); @@ -99,7 +103,7 @@ namespace GFramework.Core.Tests.Coroutine Assert.That(waitForMultipleEvents.FirstEventData?.Data, Is.EqualTo("first_event")); // Fire second event after completion - eventBus.Send(new TestEvent2 { Data = "second_event" }); + EventBus.Send(new TestEvent2 { Data = "second_event" }); // Assert - The state should not change Assert.That(waitForMultipleEvents.IsDone, Is.True); @@ -113,13 +117,13 @@ namespace GFramework.Core.Tests.Coroutine public async Task Disposal_PreventsFurtherEventHandling() { // Arrange - var waitForMultipleEvents = new WaitForMultipleEvents(eventBus); + var waitForMultipleEvents = new WaitForMultipleEvents(EventBus); // Act - Dispose the instance waitForMultipleEvents.Dispose(); // Fire an event after disposal - eventBus.Send(new TestEvent1 { Data = "after_disposal" }); + EventBus.Send(new TestEvent1 { Data = "after_disposal" }); // Assert - Event should not be processed due to disposal // Since we disposed, no event data should be captured diff --git a/GFramework.Core.Tests/Logging/TestLogger.cs b/GFramework.Core.Tests/Logging/TestLogger.cs index 594b6de7..317123e8 100644 --- a/GFramework.Core.Tests/Logging/TestLogger.cs +++ b/GFramework.Core.Tests/Logging/TestLogger.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Threading; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; @@ -6,9 +9,13 @@ namespace GFramework.Core.Tests.Logging; /// /// 表示供日志相关测试复用的内存日志记录器。 /// +/// +/// 并发写入会通过内部锁串行化; 每次返回快照,避免断言观察到正在被修改的可变集合。 +/// public sealed class TestLogger : AbstractLogger { private readonly List _logs = new(); + private readonly Lock _sync = new(); /// /// 初始化 的新实例。 @@ -20,9 +27,18 @@ public sealed class TestLogger : AbstractLogger } /// - /// 获取按写入顺序保存的日志条目只读视图。 + /// 获取按写入顺序保存的日志条目快照。 /// - public IReadOnlyList Logs => _logs; + public IReadOnlyList Logs + { + get + { + lock (_sync) + { + return _logs.ToArray(); + } + } + } /// /// 将日志信息追加到内存列表,供断言读取。 @@ -32,7 +48,10 @@ public sealed class TestLogger : AbstractLogger /// 相关异常;没有异常时为 。 protected override void Write(LogLevel level, string message, Exception? exception) { - _logs.Add(new LogEntry(level, message, exception)); + lock (_sync) + { + _logs.Add(new LogEntry(level, message, exception)); + } } /// diff --git a/GFramework.Core.Tests/Resource/TestResourceLoader.cs b/GFramework.Core.Tests/Resource/TestResourceLoader.cs index 5dcf6a14..05ef4d13 100644 --- a/GFramework.Core.Tests/Resource/TestResourceLoader.cs +++ b/GFramework.Core.Tests/Resource/TestResourceLoader.cs @@ -1,18 +1,30 @@ +using System; +using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using GFramework.Core.Abstractions.Resource; namespace GFramework.Core.Tests.Resource; -/// -/// 为 ResourceManager 测试提供可控数据源的资源加载器。 -/// + /// + /// 为 ResourceManager 测试提供可控数据源的资源加载器。 + /// public class TestResourceLoader : IResourceLoader { private readonly Dictionary _resourceData = new(StringComparer.Ordinal); - /// + /// + /// 同步加载指定路径的测试资源。 + /// + /// 资源路径。 + /// 加载得到的测试资源。 + /// + /// 为空字符串。 + /// 指定路径的测试资源不存在。 public TestResource Load(string path) { + ArgumentException.ThrowIfNullOrEmpty(path); + if (_resourceData.TryGetValue(path, out var content)) { return new TestResource { Content = content }; @@ -21,22 +33,40 @@ public class TestResourceLoader : IResourceLoader throw new FileNotFoundException($"Resource not found: {path}"); } - /// - public async Task LoadAsync(string path) + /// + /// 异步加载指定路径的测试资源。 + /// + /// 资源路径。 + /// 加载得到的测试资源任务。 + /// + /// 为空字符串。 + /// 指定路径的测试资源不存在。 + public Task LoadAsync(string path) { - await Task.Delay(10).ConfigureAwait(false); // 模拟异步加载 - return Load(path); + return Task.FromResult(Load(path)); } - /// + /// + /// 卸载已加载的测试资源。 + /// + /// 要标记为已释放的资源。 + /// public void Unload(TestResource resource) { + ArgumentNullException.ThrowIfNull(resource); resource.IsDisposed = true; } - /// + /// + /// 判断当前加载器是否包含指定路径的测试资源。 + /// + /// 资源路径。 + /// 存在对应测试资源时返回 ;否则返回 + /// + /// 为空字符串。 public bool CanLoad(string path) { + ArgumentException.ThrowIfNullOrEmpty(path); return _resourceData.ContainsKey(path); } @@ -45,8 +75,14 @@ public class TestResourceLoader : IResourceLoader /// /// 资源路径。 /// 资源内容。 + /// + /// 。 + /// + /// 为空字符串。 public void AddTestData(string path, string content) { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(content); _resourceData[path] = content; } } diff --git a/GFramework.Core/Architectures/RegistryInitializationHookBase.cs b/GFramework.Core/Architectures/RegistryInitializationHookBase.cs index fe746dbc..49b99c18 100644 --- a/GFramework.Core/Architectures/RegistryInitializationHookBase.cs +++ b/GFramework.Core/Architectures/RegistryInitializationHookBase.cs @@ -1,3 +1,4 @@ +using System; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Utility; @@ -33,12 +34,29 @@ public abstract class RegistryInitializationHookBase : IArch /// /// 当前的架构阶段 /// 相关的架构实例 + /// + /// 当目标注册表未被装入当前架构上下文时,该钩子会保持 no-op, + /// 以便同一组配置可以安全复用于不包含该注册表的测试或裁剪场景。 + /// public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) { - if (phase != _targetPhase) return; + ArgumentNullException.ThrowIfNull(architecture); - var registry = architecture.Context.GetUtility(); - if (registry == null) return; + if (phase != _targetPhase) + { + return; + } + + TRegistry registry; + + try + { + registry = architecture.Context.GetUtility(); + } + catch (InvalidOperationException) + { + return; + } foreach (var config in _configs) { @@ -52,4 +70,4 @@ public abstract class RegistryInitializationHookBase : IArch /// 注册表实例 /// 配置项 protected abstract void RegisterConfig(TRegistry registry, TConfig config); -} \ No newline at end of file +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CapturingLoggerFactoryProvider.cs b/GFramework.Cqrs.Tests/Cqrs/CapturingLoggerFactoryProvider.cs index 00f3df65..dfb8dff4 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CapturingLoggerFactoryProvider.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CapturingLoggerFactoryProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; using GFramework.Cqrs.Tests.Logging; @@ -11,10 +12,12 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// /// 处理器注册入口会分别为测试运行时、容器和注册器创建日志器。 /// 该提供程序统一保留这些测试日志器,以便断言警告是否经由公开入口真正发出。 +/// 并发创建日志器时会通过内部锁串行化, 每次返回快照,避免调用方观察到可变集合。 /// internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider { private readonly List _loggers = []; + private readonly Lock _sync = new(); /// /// 使用指定的最小日志级别初始化一个新的捕获型日志工厂提供程序。 @@ -26,9 +29,18 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider } /// - /// 获取通过当前提供程序创建的全部测试日志器。 + /// 获取通过当前提供程序创建的全部测试日志器快照。 /// - public IReadOnlyList Loggers => _loggers; + public IReadOnlyList Loggers + { + get + { + lock (_sync) + { + return _loggers.ToArray(); + } + } + } /// /// 获取或设置新建测试日志器的最小日志级别。 @@ -43,7 +55,12 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider public ILogger CreateLogger(string name) { var logger = new TestLogger(name, MinLevel); - _loggers.Add(logger); + + lock (_sync) + { + _loggers.Add(logger); + } + return logger; } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DeterministicNotificationHandlerState.cs b/GFramework.Cqrs.Tests/Cqrs/DeterministicNotificationHandlerState.cs index 957eda1d..451b011c 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DeterministicNotificationHandlerState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DeterministicNotificationHandlerState.cs @@ -10,11 +10,18 @@ internal static class DeterministicNotificationHandlerState /// /// 获取当前测试中的通知处理器执行顺序。 /// + /// + /// 该集合仅供顺序测试断言使用,不提供并发安全保证。 + /// 若多个处理器在并行测试中同时写入,调用方可能观察到竞争条件或未定义顺序。 + /// public static List InvocationOrder { get; } = []; /// /// 重置共享的执行顺序状态。 /// + /// + /// 该方法只支持在单线程测试准备阶段调用;并发调用会与 的直接写入互相竞争。 + /// public static void Reset() { InvocationOrder.Clear(); diff --git a/GFramework.Cqrs.Tests/Cqrs/PartialGeneratedNotificationHandlerRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/PartialGeneratedNotificationHandlerRegistry.cs index 6bbcfb36..8e7d47ea 100644 --- a/GFramework.Cqrs.Tests/Cqrs/PartialGeneratedNotificationHandlerRegistry.cs +++ b/GFramework.Cqrs.Tests/Cqrs/PartialGeneratedNotificationHandlerRegistry.cs @@ -16,6 +16,9 @@ internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandler /// /// 承载处理器映射的服务集合。 /// 用于记录注册诊断的日志器。 + /// + /// 。 + /// public void Register(IServiceCollection services, ILogger logger) { ArgumentNullException.ThrowIfNull(services); diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 4c823765..441e2b3e 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,41 +6,38 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-087` -- 当前阶段:`Phase 87` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-088` +- 当前阶段:`Phase 88` - 当前焦点: - - `2026-04-28` 已按 `$gframework-batch-boot 50` 先执行仓库根 `dotnet clean` + `dotnet build`,建立本轮权威基线 `288 Warning(s)` / `214` 个唯一位点 - - 本轮已并行收敛 `GameContextTests.cs`、`ArchitectureServicesTests.cs`、`RegistryInitializationHookBaseTests.cs`、`CqrsDispatcherCacheTests.cs` 与 `CqrsHandlerRegistrarTests.cs` - - 主线程已补齐 `ResourceManagerTests.cs`、`TestEvent.cs`、`LoggerTests.cs`、`ContextProviderTests.cs`、`TestArchitectureBase.cs`、`CommandCoroutineExtensionsTests.cs` 等 `Core.Tests` 零散 warning - - 当前 `GFramework.Core.Tests` 与 `GFramework.Cqrs.Tests` 的受影响项目 Release 构建都已恢复到 `0 Warning(s)` / `0 Error(s)` - - 当前仓库根权威基线已从本轮开始时的 `288 Warning(s)` / `214` 个唯一位点下降到 `236 Warning(s)` / `162` 个唯一位点;剩余 warning 只集中在 `Mediator/*` 与 `YamlConfigSchemaValidator*` + - `2026-04-28` 已执行 `$gframework-pr-review`,确认 `PR #300` 最新 head 上仍有 `8` 条 CodeRabbit open threads、`1` 个 failed test,以及 `dotnet-format restore failed` 的 CI 噪音 + - 本轮已核对并收敛仍然成立的 review comments:`TestArchitectureContext*` 旧入口显式失败、共享事件总线、`RegisterLifecycleHook` 语义统一、`TestResourceLoader` 契约、`TestLogger` / `CapturingLoggerFactoryProvider` 快照访问、`DeterministicNotificationHandlerState` 并发说明与 `PartialGeneratedNotificationHandlerRegistry` XML 异常文档 + - 已新增 `TestArchitectureContextBehaviorTests.cs`,直接覆盖共享事件总线、旧入口失败契约与接口视角生命周期钩子行为 + - `RegistryInitializationHookBase` 现已在注册表缺失时保持 no-op,修复了 PR 上报的失败测试 `OnPhase_Should_Not_Throw_When_Registry_Not_Found` ## 当前活跃事实 - 当前 `origin/main` 基线提交为 `6cc87a9`(`2026-04-27T20:28:50+08:00`)。 - 当前直接验证结果: - - `dotnet clean` - - 最新结果:成功;已刷新本轮 final non-incremental 仓库根基线 - - `dotnet build` - - 最新结果:成功;`236 Warning(s)`、`0 Error(s)`,唯一位点 `162` - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 最新结果:成功;`125 Warning(s)`、`0 Error(s)`;warning 仍集中在既有 `Mediator/*` 文件,不在本轮 PR review 修复写集内 + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~WaitForMultipleEventsTests|FullyQualifiedName~ResourceManagerTests|FullyQualifiedName~LoggerTests|FullyQualifiedName~TestArchitectureContextBehaviorTests"` + - 最新结果:成功;`97` 通过、`0` 失败 + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsHandlerRegistrarTests"` + - 最新结果:成功;`11` 通过、`0` 失败 - 当前批次摘要: - - 本轮接受并集成 `GameContextTests.cs`、`ArchitectureServicesTests.cs`、`RegistryInitializationHookBaseTests.cs`、`CqrsDispatcherCacheTests.cs`、`CqrsHandlerRegistrarTests.cs` 五个并行 worker 切片 - - 主线程补齐 `Core.Tests` 内剩余零散 warning,使 `GFramework.Core.Tests` 项目级 Release 构建回到 `0 Warning(s)` / `0 Error(s)` - - 当前 `origin/main...HEAD` 已提交 branch diff 仍为 `21` 个文件;计入当前待提交工作树后的并集 footprint 为 `45 / 50` 个文件,已接近本轮停止线 -- 当前建议保留到下一波次的候选: - - `GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs`、`MediatorComprehensiveTests.cs`、`MediatorAdvancedFeaturesTests.cs` 的高密度 `MA0048` / `MA0004` - - `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 与 `YamlConfigSchemaValidator.ObjectKeywords.cs` 的高耦合 warning 热点 + - 当前工作树包含 `11` 个已修改文件和 `1` 个新增测试文件,全部来自 `Core` / `Core.Tests` / `Cqrs.Tests` 的 PR review follow-up + - 本轮没有触碰 `Mediator/*` 或 `YamlConfigSchemaValidator*` 的高耦合 warning 波次 ## 当前风险 -- `GFramework.Cqrs.Tests/Mediator/*` 仍有 `94` / `88` / `68` 条输出 warning,属于高 changed-file 风险的 `MA0048` 大波次。 - - 缓解措施:当前 footprint 已到 `45 / 50`,下一轮应在新提交基础上单独规划 `Mediator*` 波次,而不是继续叠在本轮工作树上。 -- `YamlConfigSchemaValidator*` 仍然聚集 `222` 条输出 warning,且同时混有 `MA0048`、`MA0009`、`MA0051`、`MA0006`。 - - 缓解措施:保持为独立高耦合波次,不与测试项目拆分混提。 +- `GFramework.Cqrs.Tests` 当前项目级 Release 构建仍有 `125` 条既有 warning,主要集中在 `MediatorArchitectureIntegrationTests.cs`、`MediatorAdvancedFeaturesTests.cs` 与 `MediatorComprehensiveTests.cs`。 + - 缓解措施:本轮仅记录为现存 blocker,不在 PR #300 的 review follow-up 里扩展到 `Mediator/*` warning reduction 波次。 +- `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,但与本轮 review 修复无交集。 + - 缓解措施:继续保持为独立高耦合波次。 ## 活跃文档 @@ -60,11 +57,12 @@ ## 验证说明 - 权威验证结果统一维护在“当前活跃事实”。 -- `GFramework.Core.Tests` 与 `GFramework.Cqrs.Tests` 的当前受影响项目 Release 构建都已在本轮清零,但仓库根 non-incremental 构建仍保留 `Mediator/*` 与 `YamlConfigSchemaValidator*` 既有 warning。 +- `GFramework.Core` 与 `GFramework.Core.Tests` 的当前受影响项目 Release 构建都已清零,并通过对应定向测试回归。 +- `GFramework.Cqrs.Tests` 的本轮 helper 改动已由 `CqrsHandlerRegistrarTests` 回归覆盖,但项目级 Release 构建仍暴露 `Mediator/*` 的既有 warning。 - warning reduction 的仓库级真值只以同轮 `dotnet clean` 后的 `dotnet build` 为准。 ## 下一步建议 -1. 提交本轮 `Core.Tests` / `Cqrs.Tests` warning reduction 与 `ai-plan` 同步。 -2. 下一轮在新提交基础上单独规划 `Mediator/*` 波次,避免在 `45 / 50` footprint 状态继续扩批。 -3. 将 `YamlConfigSchemaValidator*` 保持为独立高耦合波次,必要时先由主线程局部切分再决定是否并行。 +1. 提交本轮 `PR #300` review follow-up 与 `ai-plan` 同步。 +2. 若继续处理 `GFramework.Cqrs.Tests` warning,下一轮单独切到 `Mediator/*` 波次,并先接受当前 `125` 条 warning 作为显式基线。 +3. `YamlConfigSchemaValidator*` 继续保持为独立高耦合波次,不与 `Mediator/*` 混提。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index edbc3a06..334eb204 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,37 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-28 — RP-088 + +### 阶段:收敛 PR #300 的 open review threads 与 failed-test follow-up + +- 触发背景: + - 用户执行 `$gframework-pr-review`,要求以当前分支 PR 真值为准核对 AI review / failed-test / linter 信号 + - `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 返回 `PR #300`,latest head 上仍有 `8` 条 CodeRabbit open threads、`1` 个失败测试 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,以及 `dotnet-format` restore failed 的 CI 噪音 +- 主线程实施: + - 核对 `TestArchitectureContext` / `TestArchitectureContextV3` 后,修复共享事件总线与旧版命令/查询入口的静默成功问题,统一改为显式 `NotSupportedException` 或 faulted task + - 校正 `TestArchitectureWithRegistry` / `TestArchitectureWithoutRegistry` 的显式接口 `RegisterLifecycleHook`,使接口视角与公开 no-op 语义一致 + - 修复 `RegistryInitializationHookBase` 在注册表缺失场景下的 no-op 行为,并保持有注册表路径继续使用单实例 `GetUtility()` + - 收敛 `TestResourceLoader`、`TestLogger`、`CapturingLoggerFactoryProvider`、`DeterministicNotificationHandlerState`、`PartialGeneratedNotificationHandlerRegistry` 的判空、XML 文档、快照访问与并发语义说明 + - 新增 `TestArchitectureContextBehaviorTests.cs`,覆盖共享事件总线、旧入口失败契约与 `RegisterLifecycleHook` 接口行为 +- 验证里程碑: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~WaitForMultipleEventsTests|FullyQualifiedName~ResourceManagerTests|FullyQualifiedName~LoggerTests|FullyQualifiedName~TestArchitectureContextBehaviorTests"` + - 结果:成功;`97` 通过、`0` 失败 + - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:成功;`125 Warning(s)`、`0 Error(s)`;warning 全部来自既有 `Mediator/*` 文件,当前 helper 改动未新增 warning + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsHandlerRegistrarTests"` + - 结果:成功;`11` 通过、`0` 失败 +- 当前结论: + - `PR #300` 当前仍然成立的 open review threads 已在本地收敛,且 failed-test 信号已被现有测试回归覆盖 + - `Core` / `Core.Tests` 的本轮受影响项目均保持 `0 Warning(s)`,新增测试也覆盖了此前没有直接回归的测试替身行为 + - `GFramework.Cqrs.Tests` 仍保留 `125` 条既有 `Mediator/*` warning;这属于下一轮 warning reduction 波次,而不是本轮 PR review follow-up 的直接写集 +- 下一步: + 1. 提交本轮 PR review follow-up 与 `ai-plan` 同步。 + 2. 若继续处理 `Cqrs.Tests` warning,以下一轮单独规划 `Mediator/*` 波次为起点。 + ## 2026-04-28 — RP-087 ### 阶段:按 `$gframework-batch-boot 50` 并行收敛 `Core.Tests` / `Cqrs.Tests` 低风险切片