ConfigTest.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. namespace MeloongCore.Tests;
  2. public class ConfigTest : TestWithFolder {
  3. #region JsonConfigProvider
  4. [Test]
  5. public async Task JsonConfigProvider_字符串列表() {
  6. string configFile = Path.Combine(tempFolder, "config.json");
  7. var provider = new JsonConfigProvider(configFile);
  8. var entry = new ConfigEntry<List<string>>("list", [], provider: provider);
  9. entry.Set(["Alpha", "中文", "123"]);
  10. provider.Save();
  11. var reloadedEntry = new ConfigEntry<List<string>>("list", [], provider: new JsonConfigProvider(configFile));
  12. var reloadedValue = reloadedEntry.Get();
  13. await Assert.That(reloadedValue is not null).IsTrue();
  14. await Assert.That(string.Join("|", reloadedValue!)).IsEqualTo("Alpha|中文|123");
  15. await Assert.That(FileUtils.ReadAsString(configFile)).Contains("\"list\": [");
  16. }
  17. [Test]
  18. public async Task JsonConfigProvider_自定义类列表() {
  19. string configFile = Path.Combine(tempFolder, "config.json");
  20. var provider = new JsonConfigProvider(configFile);
  21. var entry = new ConfigEntry<List<TestConfigItem>>("items", [], provider: provider);
  22. entry.Set([
  23. new() { Name = "Fabric", Count = 2, Enabled = true },
  24. new() { Name = "Forge 中文", Count = 5, Enabled = false }
  25. ]);
  26. provider.Save();
  27. var reloadedEntry = new ConfigEntry<List<TestConfigItem>>("items", [], provider: new JsonConfigProvider(configFile));
  28. var reloadedValue = reloadedEntry.Get();
  29. await Assert.That(reloadedValue is not null).IsTrue();
  30. await Assert.That(string.Join("|", reloadedValue!.Select(i => $"{i.Name}:{i.Count}:{i.Enabled}"))).IsEqualTo("Fabric:2:True|Forge 中文:5:False");
  31. string json = FileUtils.ReadAsString(configFile);
  32. await Assert.That(json).Contains("\"items\": [");
  33. await Assert.That(json).Contains("\"Name\": \"Fabric\"");
  34. await Assert.That(json).Contains("\"Count\": 2");
  35. await Assert.That(json).Contains("\"Enabled\": true");
  36. await Assert.That(json).Contains("\"Name\": \"Forge 中文\"");
  37. await Assert.That(json).Contains("\"Enabled\": false");
  38. }
  39. [Test]
  40. public async Task ConfigEntry_SetNull_RoundTripsThroughJson() {
  41. string configFile = Path.Combine(tempFolder, "config.json");
  42. var provider = new JsonConfigProvider(configFile);
  43. var entry = new ConfigEntry<string>("nullable", "fallback", provider: provider);
  44. entry.Set(null);
  45. provider.Save();
  46. var reloadedEntry = new ConfigEntry<string>("nullable", "fallback", provider: new JsonConfigProvider(configFile));
  47. await Assert.That(reloadedEntry.HasValue()).IsTrue();
  48. await Assert.That(reloadedEntry.Get() is null).IsTrue();
  49. await Assert.That(FileUtils.ReadAsString(configFile)).Contains("\"nullable\": null");
  50. }
  51. [Test]
  52. public async Task ConfigEntry_SetString_RoundTripsThroughJson() {
  53. string configFile = Path.Combine(tempFolder, "config.json");
  54. var provider = new JsonConfigProvider(configFile);
  55. var entry = new ConfigEntry<string>("text", "fallback", provider: provider);
  56. entry.Set("plain text 中文");
  57. provider.Save();
  58. var reloadedEntry = new ConfigEntry<string>("text", "fallback", provider: new JsonConfigProvider(configFile));
  59. await Assert.That(reloadedEntry.Get()).IsEqualTo("plain text 中文");
  60. await Assert.That(FileUtils.ReadAsString(configFile)).Contains("\"text\": \"plain text 中文\"");
  61. }
  62. [Test]
  63. public async Task ConfigUtils_SaveAll_WritesPendingChangesImmediately() {
  64. string configFile = Path.Combine(tempFolder, "config.json");
  65. var provider = new JsonConfigProvider(configFile);
  66. var entry = new ConfigEntry<string>("text", "fallback", provider: provider);
  67. entry.Set("pending 中文");
  68. ConfigUtils.SaveAll();
  69. ConfigUtils.SaveAll();
  70. var reloadedEntry = new ConfigEntry<string>("text", "fallback", provider: new JsonConfigProvider(configFile));
  71. await Assert.That(reloadedEntry.Get()).IsEqualTo("pending 中文");
  72. await Assert.That(FileUtils.ReadAsString(configFile)).Contains("\"text\": \"pending 中文\"");
  73. }
  74. [Test]
  75. public async Task JsonConfigProvider_Save_原子替换且不残留临时文件() {
  76. string configFile = Path.Combine(tempFolder, "config.json");
  77. FileUtils.Write(configFile, "{\"existing\":true}");
  78. var provider = new JsonConfigProvider(configFile);
  79. var entry = new ConfigEntry<string>("text", "", provider: provider);
  80. entry.Set("updated");
  81. provider.Save();
  82. Newtonsoft.Json.Linq.JObject config = FileUtils.ReadAsJson<Newtonsoft.Json.Linq.JObject>(configFile)!;
  83. await Assert.That(config.Value<bool>("existing")).IsTrue();
  84. await Assert.That(config.Value<string>("text")).IsEqualTo("updated");
  85. await Assert.That(DirectoryUtils.EnumerateFiles(tempFolder, searchPattern: "config.json.*.tmp")).IsEmpty();
  86. }
  87. [Test]
  88. public async Task JsonConfigProvider_Save_等待正在进行的保存() {
  89. string configFile = Path.Combine(tempFolder, "config.json");
  90. var provider = new JsonConfigProvider(configFile);
  91. var entry = new ConfigEntry<string>("text", "", provider: provider);
  92. using var firstStarted = new ManualResetEventSlim(false);
  93. using var secondStarted = new ManualResetEventSlim(false);
  94. Task firstSave, secondSave;
  95. bool firstBlocked, secondRunning, returnedEarly;
  96. lock (provider) {
  97. entry.Set("pending");
  98. Thread? firstThread = null;
  99. firstSave = Task.Factory.StartNew(() => {
  100. firstThread = Thread.CurrentThread;
  101. firstStarted.Set();
  102. provider.Save();
  103. }, TaskCreationOptions.LongRunning);
  104. // 阻止首次保存写盘,稳定复现第二次 Save 在写盘完成前返回的窗口。
  105. firstBlocked = firstStarted.Wait(3000) && SpinWait.SpinUntil(
  106. () => (firstThread!.ThreadState & ThreadState.WaitSleepJoin) != 0, 3000);
  107. secondSave = Task.Factory.StartNew(() => {
  108. secondStarted.Set();
  109. provider.Save();
  110. }, TaskCreationOptions.LongRunning);
  111. secondRunning = secondStarted.Wait(3000);
  112. returnedEarly = secondSave.Wait(200);
  113. }
  114. await Task.WhenAll(firstSave, secondSave);
  115. await Assert.That(firstBlocked).IsTrue();
  116. await Assert.That(secondRunning).IsTrue();
  117. await Assert.That(returnedEarly).IsFalse();
  118. var reloadedEntry = new ConfigEntry<string>("text", "", provider: new JsonConfigProvider(configFile));
  119. await Assert.That(reloadedEntry.Get()).IsEqualTo("pending");
  120. }
  121. [Test]
  122. public async Task JsonConfigProvider_Save_写入失败后可直接重试() {
  123. string configFile = Path.Combine(tempFolder, "config.json");
  124. var provider = new JsonConfigProvider(configFile);
  125. var entry = new ConfigEntry<string>("text", "", provider: provider);
  126. entry.Set("before");
  127. provider.Save();
  128. lock (provider) {
  129. entry.Set("after");
  130. using (var stream = new FileStream(PathUtils.ForApi(configFile), FileMode.Open, FileAccess.Read, FileShare.None)) {
  131. Assert.Throws<IOException>(() => provider.Save());
  132. }
  133. // 无需再次 Set,失败的内容仍应处于待保存状态。
  134. provider.Save();
  135. }
  136. var reloadedEntry = new ConfigEntry<string>("text", "", provider: new JsonConfigProvider(configFile));
  137. await Assert.That(reloadedEntry.Get()).IsEqualTo("after");
  138. }
  139. public class TestConfigItem {
  140. public string Name { get; set; } = "";
  141. public int Count { get; set; }
  142. public bool Enabled { get; set; }
  143. }
  144. #endregion
  145. }