| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- using System.Reactive.Linq;
- using Apq.Cfg.Changes;
- namespace Apq.Cfg.Tests;
- /// <summary>
- /// ConfigChanges 订阅测试
- /// </summary>
- public class ConfigChangesSubscriptionTests : IDisposable
- {
- private readonly string _testDir;
- public ConfigChangesSubscriptionTests()
- {
- _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgSubscriptionTests_{Guid.NewGuid():N}");
- Directory.CreateDirectory(_testDir);
- }
- public void Dispose()
- {
- if (Directory.Exists(_testDir))
- {
- try { Directory.Delete(_testDir, true); }
- catch { }
- }
- }
- #region 基本订阅测试
- [Fact]
- public void ConfigChanges_IsNotNull()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- // Act & Assert
- Assert.NotNull(cfg.ConfigChanges);
- }
- [Fact]
- public void ConfigChanges_CanSubscribe()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- // Act
- using var subscription = cfg.ConfigChanges.Subscribe(_ => { });
- // Assert - 订阅应该成功
- Assert.NotNull(subscription);
- }
- [Fact]
- public void ConfigChanges_MultipleSubscribers_AllSubscribeSuccessfully()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- // Act - 多个订阅者
- using var sub1 = cfg.ConfigChanges.Subscribe(_ => { });
- using var sub2 = cfg.ConfigChanges.Subscribe(_ => { });
- using var sub3 = cfg.ConfigChanges.Subscribe(_ => { });
- // Assert - 所有订阅都应该成功
- Assert.NotNull(sub1);
- Assert.NotNull(sub2);
- Assert.NotNull(sub3);
- }
- [Fact]
- public void ConfigChanges_Unsubscribe_Works()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- var eventCount = 0;
- // Act
- var subscription = cfg.ConfigChanges.Subscribe(_ => eventCount++);
- subscription.Dispose(); // 取消订阅
- // Assert - 取消订阅不应抛出异常
- }
- #endregion
- #region ConfigChangeEvent 测试
- [Fact]
- public void ConfigChangeEvent_HasCorrectTimestamp()
- {
- // Arrange
- var before = DateTimeOffset.Now;
- var changes = new Dictionary<string, ConfigChange>
- {
- ["Key"] = new ConfigChange("Key", "Old", "New", ChangeType.Modified)
- };
- // Act
- var evt = new ConfigChangeEvent(changes);
- var after = DateTimeOffset.Now;
- // Assert
- Assert.True(evt.Timestamp >= before);
- Assert.True(evt.Timestamp <= after);
- }
- [Fact]
- public void ConfigChangeEvent_WithExplicitTimestamp_Works()
- {
- // Arrange
- var explicitTime = new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero);
- var changes = new Dictionary<string, ConfigChange>
- {
- ["Key"] = new ConfigChange("Key", null, "Value", ChangeType.Added)
- };
- // Act
- var evt = new ConfigChangeEvent(changes, explicitTime);
- // Assert
- Assert.Equal(explicitTime, evt.Timestamp);
- }
- [Fact]
- public void ConfigChangeEvent_Changes_IsReadOnly()
- {
- // Arrange
- var changes = new Dictionary<string, ConfigChange>
- {
- ["Key1"] = new ConfigChange("Key1", null, "Value1", ChangeType.Added),
- ["Key2"] = new ConfigChange("Key2", "Old", "New", ChangeType.Modified)
- };
- // Act
- var evt = new ConfigChangeEvent(changes);
- // Assert
- Assert.Equal(2, evt.Changes.Count);
- Assert.True(evt.Changes.ContainsKey("Key1"));
- Assert.True(evt.Changes.ContainsKey("Key2"));
- }
- [Fact]
- public void ConfigChangeEvent_EmptyChanges_Works()
- {
- // Arrange
- var changes = new Dictionary<string, ConfigChange>();
- // Act
- var evt = new ConfigChangeEvent(changes);
- // Assert
- Assert.Empty(evt.Changes);
- }
- #endregion
- #region ConfigChange 测试
- [Fact]
- public void ConfigChange_Added_HasCorrectProperties()
- {
- // Arrange & Act
- var change = new ConfigChange("NewKey", null, "NewValue", ChangeType.Added);
- // Assert
- Assert.Equal("NewKey", change.Key);
- Assert.Null(change.OldValue);
- Assert.Equal("NewValue", change.NewValue);
- Assert.Equal(ChangeType.Added, change.Type);
- }
- [Fact]
- public void ConfigChange_Modified_HasCorrectProperties()
- {
- // Arrange & Act
- var change = new ConfigChange("Key", "OldValue", "NewValue", ChangeType.Modified);
- // Assert
- Assert.Equal("Key", change.Key);
- Assert.Equal("OldValue", change.OldValue);
- Assert.Equal("NewValue", change.NewValue);
- Assert.Equal(ChangeType.Modified, change.Type);
- }
- [Fact]
- public void ConfigChange_Removed_HasCorrectProperties()
- {
- // Arrange & Act
- var change = new ConfigChange("RemovedKey", "OldValue", null, ChangeType.Removed);
- // Assert
- Assert.Equal("RemovedKey", change.Key);
- Assert.Equal("OldValue", change.OldValue);
- Assert.Null(change.NewValue);
- Assert.Equal(ChangeType.Removed, change.Type);
- }
- [Fact]
- public void ConfigChange_ToString_ContainsAllInfo()
- {
- // Arrange
- var change = new ConfigChange("TestKey", "OldVal", "NewVal", ChangeType.Modified);
- // Act
- var str = change.ToString();
- // Assert
- Assert.Contains("TestKey", str);
- Assert.Contains("OldVal", str);
- Assert.Contains("NewVal", str);
- Assert.Contains("Modified", str);
- }
- [Fact]
- public void ConfigChange_ToString_HandlesNullValues()
- {
- // Arrange
- var change = new ConfigChange("Key", null, "Value", ChangeType.Added);
- // Act
- var str = change.ToString();
- // Assert
- Assert.Contains("(null)", str);
- Assert.Contains("Value", str);
- }
- [Fact]
- public void ConfigChange_Equality_WorksCorrectly()
- {
- // Arrange
- var change1 = new ConfigChange("Key", "Old", "New", ChangeType.Modified);
- var change2 = new ConfigChange("Key", "Old", "New", ChangeType.Modified);
- var change3 = new ConfigChange("Key", "Old", "Different", ChangeType.Modified);
- // Assert - struct 默认相等性比较
- Assert.Equal(change1.Key, change2.Key);
- Assert.Equal(change1.OldValue, change2.OldValue);
- Assert.Equal(change1.NewValue, change2.NewValue);
- Assert.NotEqual(change1.NewValue, change3.NewValue);
- }
- #endregion
- #region ChangeType 测试
- [Fact]
- public void ChangeType_AllValues_AreDefined()
- {
- // Assert
- Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Added));
- Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Modified));
- Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Removed));
- }
- [Fact]
- public void ChangeType_Values_AreDistinct()
- {
- // Assert
- Assert.NotEqual(ChangeType.Added, ChangeType.Modified);
- Assert.NotEqual(ChangeType.Modified, ChangeType.Removed);
- Assert.NotEqual(ChangeType.Added, ChangeType.Removed);
- }
- #endregion
- #region DynamicReloadOptions 测试
- [Fact]
- public void DynamicReloadOptions_DefaultValues()
- {
- // Arrange & Act
- var options = new DynamicReloadOptions();
- // Assert
- Assert.Equal(100, options.DebounceMs);
- Assert.True(options.EnableDynamicReload);
- }
- [Fact]
- public void DynamicReloadOptions_CanSetDebounceMs()
- {
- // Arrange & Act
- var options = new DynamicReloadOptions { DebounceMs = 500 };
- // Assert
- Assert.Equal(500, options.DebounceMs);
- }
- [Fact]
- public void DynamicReloadOptions_CanDisableDynamicReload()
- {
- // Arrange & Act
- var options = new DynamicReloadOptions { EnableDynamicReload = false };
- // Assert
- Assert.False(options.EnableDynamicReload);
- }
- [Fact]
- public void DynamicReloadOptions_ZeroDebounceMs_Works()
- {
- // Arrange & Act
- var options = new DynamicReloadOptions { DebounceMs = 0 };
- // Assert
- Assert.Equal(0, options.DebounceMs);
- }
- #endregion
- #region 动态配置集成测试
- [Fact]
- public void ToMicrosoftConfiguration_WithDynamicReload_ReturnsConfiguration()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- // Act
- var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
- {
- DebounceMs = 50,
- EnableDynamicReload = true
- });
- // Assert
- Assert.NotNull(msConfig);
- Assert.Equal("Value", msConfig["Key"]);
- }
- [Fact]
- public void ToMicrosoftConfiguration_DisabledReload_ReturnsSameAsStatic()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .Build();
- // Act
- var staticConfig = cfg.ToMicrosoftConfiguration();
- var dynamicConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
- {
- EnableDynamicReload = false
- });
- // Assert
- Assert.Same(staticConfig, dynamicConfig);
- }
- [Fact]
- public void ToMicrosoftConfiguration_CalledMultipleTimes_ReturnsSameInstance()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- var options = new DynamicReloadOptions { DebounceMs = 50 };
- // Act
- var config1 = cfg.ToMicrosoftConfiguration(options);
- var config2 = cfg.ToMicrosoftConfiguration(options);
- // Assert
- Assert.Same(config1, config2);
- }
- [Fact]
- public void ToMicrosoftConfiguration_NullOptions_UsesDefaults()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- // Act
- var msConfig = cfg.ToMicrosoftConfiguration(null);
- // Assert
- Assert.NotNull(msConfig);
- Assert.Equal("Value", msConfig["Key"]);
- }
- #endregion
- #region Dispose 后订阅测试
- [Fact]
- public void ConfigChanges_AfterDispose_CompletesObservable()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- var completed = false;
- using var subscription = cfg.ConfigChanges.Subscribe(
- onNext: _ => { },
- onCompleted: () => completed = true
- );
- // Act
- cfg.Dispose();
- // Assert - Observable 应该完成
- Assert.True(completed);
- }
- [Fact]
- public async Task ConfigChanges_AfterDisposeAsync_CompletesObservable()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- var completed = false;
- using var subscription = cfg.ConfigChanges.Subscribe(
- onNext: _ => { },
- onCompleted: () => completed = true
- );
- // Act
- await cfg.DisposeAsync();
- // Assert
- Assert.True(completed);
- }
- #endregion
- #region 文件变更检测测试
- [Fact]
- public void DynamicConfig_HasReloadToken()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
- {
- DebounceMs = 50
- });
- // Act
- var reloadToken = msConfig.GetReloadToken();
- // Assert
- Assert.NotNull(reloadToken);
- Assert.False(reloadToken.HasChanged); // 初始状态未变更
- }
- [Fact]
- public async Task DynamicConfig_FileChange_TriggersReload()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "OriginalValue"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
- .Build();
- var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
- {
- DebounceMs = 50
- });
- var changeDetected = new TaskCompletionSource<bool>();
- var token = msConfig.GetReloadToken();
- token.RegisterChangeCallback(_ => changeDetected.TrySetResult(true), null);
- // Act - 修改文件
- await Task.Delay(100); // 等待文件监视器初始化
- File.WriteAllText(jsonPath, """{"Key": "NewValue"}""");
- // Assert - 等待变更检测(最多 3 秒)
- var detected = await Task.WhenAny(changeDetected.Task, Task.Delay(3000)) == changeDetected.Task;
- // 注意:文件变更检测依赖于操作系统,可能不稳定
- // 主要验证 API 正常工作
- Assert.NotNull(msConfig.GetReloadToken());
- }
- #endregion
- }
|