using GFramework.Core.Abstractions.Property; using GFramework.Core.Abstractions.StateManagement; using GFramework.Core.Extensions; using GFramework.Core.Property; using GFramework.Core.StateManagement; namespace GFramework.Core.Tests.StateManagement; /// /// Store 状态管理能力的单元测试。 /// 这些测试覆盖集中式状态容器的核心职责:状态归约、订阅通知、选择器桥接和诊断行为。 /// [TestFixture] public class StoreTests { /// /// 测试 Store 在创建后能够暴露初始状态。 /// [Test] public void State_Should_Return_Initial_State() { var store = CreateStore(new CounterState(1, "Player")); Assert.That(store.State.Count, Is.EqualTo(1)); Assert.That(store.State.Name, Is.EqualTo("Player")); } /// /// 测试 Dispatch 能够执行 reducer 并向订阅者广播新状态。 /// [Test] public void Dispatch_Should_Update_State_And_Notify_Subscribers() { var store = CreateStore(); var receivedStates = new List(); store.Subscribe(receivedStates.Add); store.Dispatch(new IncrementAction(2)); Assert.That(store.State.Count, Is.EqualTo(2)); Assert.That(receivedStates.Count, Is.EqualTo(1)); Assert.That(receivedStates[0].Count, Is.EqualTo(2)); } /// /// 测试当 reducer 返回逻辑相等状态时不会触发通知。 /// [Test] public void Dispatch_Should_Not_Notify_When_State_Does_Not_Change() { var store = CreateStore(); var notifyCount = 0; store.Subscribe(_ => notifyCount++); store.Dispatch(new RenameAction("Player")); Assert.That(store.State.Name, Is.EqualTo("Player")); Assert.That(notifyCount, Is.EqualTo(0)); } /// /// 测试同一 action 类型的多个 reducer 会按注册顺序执行。 /// [Test] public void Dispatch_Should_Run_Multiple_Reducers_In_Registration_Order() { var store = CreateStore(); store.RegisterReducer((state, action) => state with { Count = state.Count + action.Amount * 10 }); store.Dispatch(new IncrementAction(1)); Assert.That(store.State.Count, Is.EqualTo(11)); } /// /// 测试 SubscribeWithInitValue 会立即回放当前状态并继续接收后续变化。 /// [Test] public void SubscribeWithInitValue_Should_Replay_Current_State_And_Future_Changes() { var store = CreateStore(new CounterState(5, "Player")); var receivedCounts = new List(); store.SubscribeWithInitValue(state => receivedCounts.Add(state.Count)); store.Dispatch(new IncrementAction(3)); Assert.That(receivedCounts, Is.EqualTo(new[] { 5, 8 })); } /// /// 测试 Store 的 SubscribeWithInitValue 在初始化回放期间不会漏掉后续状态变化。 /// [Test] public void SubscribeWithInitValue_Should_Not_Miss_Changes_During_Init_Callback() { var store = CreateStore(); var receivedCounts = new List(); store.SubscribeWithInitValue(state => { receivedCounts.Add(state.Count); if (receivedCounts.Count == 1) { store.Dispatch(new IncrementAction(1)); } }); Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 })); } /// /// 测试注销订阅后不会再收到后续通知。 /// [Test] public void UnRegister_Handle_Should_Stop_Future_Notifications() { var store = CreateStore(); var notifyCount = 0; var unRegister = store.Subscribe(_ => notifyCount++); store.Dispatch(new IncrementAction(1)); unRegister.UnRegister(); store.Dispatch(new IncrementAction(1)); Assert.That(notifyCount, Is.EqualTo(1)); } /// /// 测试选择器仅在所选状态片段变化时触发通知。 /// [Test] public void Select_Should_Only_Notify_When_Selected_Slice_Changes() { var store = CreateStore(); var selectedCounts = new List(); var selection = store.Select(state => state.Count); selection.Register(selectedCounts.Add); store.Dispatch(new RenameAction("Renamed")); store.Dispatch(new IncrementAction(2)); Assert.That(selectedCounts, Is.EqualTo(new[] { 2 })); } /// /// 测试选择器支持自定义比较器,从而抑制无意义的局部状态通知。 /// [Test] public void Select_Should_Respect_Custom_Selected_Value_Comparer() { var store = CreateStore(); var selectedCounts = new List(); var selection = store.Select( state => state.Count, new TensBucketEqualityComparer()); selection.Register(selectedCounts.Add); store.Dispatch(new IncrementAction(5)); store.Dispatch(new IncrementAction(6)); Assert.That(selectedCounts, Is.EqualTo(new[] { 11 })); } /// /// 测试 StoreSelection 的 RegisterWithInitValue 在初始化回放期间不会漏掉后续局部状态变化。 /// [Test] public void Selection_RegisterWithInitValue_Should_Not_Miss_Changes_During_Init_Callback() { var store = CreateStore(); var selection = store.Select(state => state.Count); var receivedCounts = new List(); selection.RegisterWithInitValue(value => { receivedCounts.Add(value); if (receivedCounts.Count == 1) { store.Dispatch(new IncrementAction(1)); } }); Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 })); } /// /// 测试 ToBindableProperty 可桥接到现有 BindableProperty 风格用法,并与旧属性系统共存。 /// [Test] public void ToBindableProperty_Should_Work_With_Existing_BindableProperty_Pattern() { var store = CreateStore(); var mirror = new BindableProperty(0); IReadonlyBindableProperty bindableProperty = store.ToBindableProperty(state => state.Count); bindableProperty.Register(value => mirror.Value = value); store.Dispatch(new IncrementAction(3)); Assert.That(mirror.Value, Is.EqualTo(3)); } /// /// 测试 IStateSelector 接口重载能够复用显式选择逻辑。 /// [Test] public void Select_With_IStateSelector_Should_Project_Selected_Value() { var store = CreateStore(); var selection = store.Select(new CounterNameSelector()); Assert.That(selection.Value, Is.EqualTo("Player")); } /// /// 测试 Store 在中间件内部发生同一实例的嵌套分发时会抛出异常。 /// [Test] public void Dispatch_Should_Throw_When_Nested_Dispatch_Happens_On_Same_Store() { var store = CreateStore(); store.UseMiddleware(new NestedDispatchMiddleware(store)); Assert.That( () => store.Dispatch(new IncrementAction(1)), Throws.InvalidOperationException.With.Message.Contain("Nested dispatch")); } /// /// 测试中间件链执行顺序和 Store 诊断信息更新。 /// [Test] public void Dispatch_Should_Run_Middlewares_In_Order_And_Update_Diagnostics() { var store = CreateStore(); var logs = new List(); store.UseMiddleware(new RecordingMiddleware(logs, "first")); store.UseMiddleware(new RecordingMiddleware(logs, "second")); store.Dispatch(new IncrementAction(2)); Assert.That(logs, Is.EqualTo(new[] { "first:before", "second:before", "second:after", "first:after" })); Assert.That(store.LastActionType, Is.EqualTo(typeof(IncrementAction))); Assert.That(store.LastStateChangedAt, Is.Not.Null); Assert.That(store.LastDispatchRecord, Is.Not.Null); Assert.That(store.LastDispatchRecord!.HasStateChanged, Is.True); Assert.That(store.LastDispatchRecord.NextState.Count, Is.EqualTo(2)); } /// /// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。 /// [Test] public void Dispatch_Without_Matching_Reducer_Should_Update_Record_Without_Changing_State() { var store = CreateStore(); store.Dispatch(new NoopAction()); Assert.That(store.State.Count, Is.EqualTo(0)); Assert.That(store.LastActionType, Is.EqualTo(typeof(NoopAction))); Assert.That(store.LastDispatchRecord, Is.Not.Null); Assert.That(store.LastDispatchRecord!.HasStateChanged, Is.False); Assert.That(store.LastStateChangedAt, Is.Null); } /// /// 测试 Store 能够复用同一个缓存选择视图实例。 /// [Test] public void GetOrCreateSelection_Should_Return_Cached_Instance_For_Same_Key() { var store = CreateStore(); var first = store.GetOrCreateSelection("count", state => state.Count); var second = store.GetOrCreateSelection("count", state => state.Count); Assert.That(second, Is.SameAs(first)); } /// /// 测试 StoreBuilder 能够应用 reducer、中间件和状态比较器配置。 /// [Test] public void StoreBuilder_Should_Apply_Configured_Reducers_Middlewares_And_Comparer() { var logs = new List(); var store = (Store)Store .CreateBuilder() .WithComparer(new CounterStateNameInsensitiveComparer()) .AddReducer((state, action) => state with { Count = state.Count + action.Amount }) .AddReducer((state, action) => state with { Name = action.Name }) .UseMiddleware(new RecordingMiddleware(logs, "builder")) .Build(new CounterState(0, "Player")); var notifyCount = 0; store.Subscribe(_ => notifyCount++); store.Dispatch(new RenameAction("player")); store.Dispatch(new IncrementAction(2)); Assert.That(notifyCount, Is.EqualTo(1)); Assert.That(store.State.Count, Is.EqualTo(2)); Assert.That(logs, Is.EqualTo(new[] { "builder:before", "builder:after", "builder:before", "builder:after" })); } /// /// 测试长时间运行的 middleware 不会长时间占用状态锁, /// 使读取状态和新增订阅仍能在 dispatch 进行期间完成。 /// [Test] public void Dispatch_Should_Not_Block_State_Read_Or_Subscribe_While_Middleware_Is_Running() { using var entered = new ManualResetEventSlim(false); using var release = new ManualResetEventSlim(false); var store = CreateStore(); store.UseMiddleware(new BlockingMiddleware(entered, release)); var dispatchTask = Task.Run(() => store.Dispatch(new IncrementAction(1))); Assert.That(entered.Wait(TimeSpan.FromSeconds(2)), Is.True, "middleware 未按预期进入阻塞阶段"); var stateReadTask = Task.Run(() => store.State.Count); Assert.That(stateReadTask.Wait(TimeSpan.FromMilliseconds(200)), Is.True, "State 读取被 dispatch 长时间阻塞"); Assert.That(stateReadTask.Result, Is.EqualTo(0), "middleware 执行期间应仍能读取到提交前的状态快照"); var subscribeTask = Task.Run(() => { var unRegister = store.Subscribe(_ => { }); unRegister.UnRegister(); }); Assert.That(subscribeTask.Wait(TimeSpan.FromMilliseconds(200)), Is.True, "Subscribe 被 dispatch 长时间阻塞"); release.Set(); Assert.That(dispatchTask.Wait(TimeSpan.FromSeconds(2)), Is.True, "dispatch 未在释放 middleware 后完成"); Assert.That(store.State.Count, Is.EqualTo(1)); } /// /// 创建一个带有基础 reducer 的测试 Store。 /// /// 可选初始状态。 /// 已配置基础 reducer 的 Store 实例。 private static Store CreateStore(CounterState? initialState = null) { var store = new Store(initialState ?? new CounterState(0, "Player")); store.RegisterReducer((state, action) => state with { Count = state.Count + action.Amount }); store.RegisterReducer((state, action) => state with { Name = action.Name }); return store; } /// /// 用于测试的计数器状态。 /// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。 /// /// 当前计数值。 /// 当前名称。 private sealed record CounterState(int Count, string Name); /// /// 表示增加计数的 action。 /// /// 要增加的数量。 private sealed record IncrementAction(int Amount); /// /// 表示修改名称的 action。 /// /// 新的名称。 private sealed record RenameAction(string Name); /// /// 表示没有匹配 reducer 的 action,用于验证无变更分发路径。 /// private sealed record NoopAction; /// /// 显式选择器实现,用于验证 IStateSelector 重载。 /// private sealed class CounterNameSelector : IStateSelector { /// /// 从状态中选择名称字段。 /// /// 完整状态。 /// 名称字段。 public string Select(CounterState state) { return state.Name; } } /// /// 将计数值按十位分桶比较的测试比较器。 /// 该比较器用于验证选择器只在局部状态“语义变化”时才触发通知。 /// private sealed class TensBucketEqualityComparer : IEqualityComparer { /// /// 判断两个值是否落在同一个十位分桶中。 /// /// 左侧值。 /// 右侧值。 /// 若位于同一分桶则返回 ,否则返回 public bool Equals(int x, int y) { return x / 10 == y / 10; } /// /// 返回基于十位分桶的哈希码。 /// /// 目标值。 /// 分桶哈希码。 public int GetHashCode(int obj) { return obj / 10; } } /// /// 用于测试 StoreBuilder 自定义状态比较器的比较器实现。 /// 该比较器忽略名称字段的大小写差异,并保持计数字段严格比较。 /// private sealed class CounterStateNameInsensitiveComparer : IEqualityComparer { /// /// 判断两个状态是否在业务语义上相等。 /// /// 左侧状态。 /// 右侧状态。 /// 若两个状态在计数相同且名称仅大小写不同,则返回 public bool Equals(CounterState? x, CounterState? y) { if (ReferenceEquals(x, y)) { return true; } if (x is null || y is null) { return false; } return x.Count == y.Count && string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } /// /// 返回与业务语义一致的哈希码。 /// /// 目标状态。 /// 忽略名称大小写后的哈希码。 public int GetHashCode(CounterState obj) { return HashCode.Combine(obj.Count, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name)); } } /// /// 记录中间件调用顺序的测试中间件。 /// private sealed class RecordingMiddleware(List logs, string name) : IStoreMiddleware { /// /// 记录当前中间件在分发前后的调用顺序。 /// /// 当前分发上下文。 /// 后续处理节点。 public void Invoke(StoreDispatchContext context, Action next) { logs.Add($"{name}:before"); next(); logs.Add($"{name}:after"); } } /// /// 用于验证 dispatch 管线在 middleware 执行期间不会占用状态锁的测试中间件。 /// private sealed class BlockingMiddleware(ManualResetEventSlim entered, ManualResetEventSlim release) : IStoreMiddleware { /// /// 通知测试线程 middleware 已进入阻塞点,并等待释放信号后继续执行。 /// /// 当前分发上下文。 /// 后续处理节点。 public void Invoke(StoreDispatchContext context, Action next) { entered.Set(); release.Wait(TimeSpan.FromSeconds(2)); next(); } } /// /// 在中间件阶段尝试二次分发的测试中间件,用于验证重入保护。 /// private sealed class NestedDispatchMiddleware(Store store) : IStoreMiddleware { /// /// 标记是否已经触发过一次嵌套分发,避免因测试实现本身导致无限递归。 /// private bool _hasTriggered; /// /// 在第一次进入中间件时执行嵌套分发。 /// /// 当前分发上下文。 /// 后续处理节点。 public void Invoke(StoreDispatchContext context, Action next) { if (!_hasTriggered) { _hasTriggered = true; store.Dispatch(new IncrementAction(1)); } next(); } } }