PerformanceOptimizationTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. using Apq.Cfg.Internal;
  2. namespace Apq.Cfg.Tests;
  3. /// <summary>
  4. /// 性能优化相关测试
  5. /// </summary>
  6. public class PerformanceOptimizationTests : IDisposable
  7. {
  8. private readonly string _testDir;
  9. public PerformanceOptimizationTests()
  10. {
  11. _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgPerfTests_{Guid.NewGuid():N}");
  12. Directory.CreateDirectory(_testDir);
  13. }
  14. public void Dispose()
  15. {
  16. if (Directory.Exists(_testDir))
  17. {
  18. try { Directory.Delete(_testDir, true); }
  19. catch { }
  20. }
  21. }
  22. // ========== 批量操作 API 测试 ==========
  23. [Fact]
  24. public void GetMany_ReturnsAllRequestedKeys()
  25. {
  26. // Arrange
  27. var jsonPath = Path.Combine(_testDir, "config.json");
  28. File.WriteAllText(jsonPath, """
  29. {
  30. "Key1": "Value1",
  31. "Key2": "Value2",
  32. "Key3": "Value3"
  33. }
  34. """);
  35. using var cfg = new CfgBuilder()
  36. .AddJson(jsonPath, level: 0, writeable: true)
  37. .Build();
  38. // Act
  39. var result = cfg.GetMany(new[] { "Key1", "Key2", "Key3" });
  40. // Assert
  41. Assert.Equal(3, result.Count);
  42. Assert.Equal("Value1", result["Key1"]);
  43. Assert.Equal("Value2", result["Key2"]);
  44. Assert.Equal("Value3", result["Key3"]);
  45. }
  46. [Fact]
  47. public void GetMany_ReturnsNullForMissingKeys()
  48. {
  49. // Arrange
  50. var jsonPath = Path.Combine(_testDir, "config.json");
  51. File.WriteAllText(jsonPath, """{"Key1": "Value1"}""");
  52. using var cfg = new CfgBuilder()
  53. .AddJson(jsonPath, level: 0, writeable: true)
  54. .Build();
  55. // Act
  56. var result = cfg.GetMany(new[] { "Key1", "NonExistent" });
  57. // Assert
  58. Assert.Equal(2, result.Count);
  59. Assert.Equal("Value1", result["Key1"]);
  60. Assert.Null(result["NonExistent"]);
  61. }
  62. [Fact]
  63. public void GetMany_Generic_ConvertsTypes()
  64. {
  65. // Arrange
  66. var jsonPath = Path.Combine(_testDir, "config.json");
  67. File.WriteAllText(jsonPath, """
  68. {
  69. "Int1": "42",
  70. "Int2": "100",
  71. "Int3": "999"
  72. }
  73. """);
  74. using var cfg = new CfgBuilder()
  75. .AddJson(jsonPath, level: 0, writeable: true)
  76. .Build();
  77. // Act
  78. var result = cfg.GetMany<int>(new[] { "Int1", "Int2", "Int3" });
  79. // Assert
  80. Assert.Equal(3, result.Count);
  81. Assert.Equal(42, result["Int1"]);
  82. Assert.Equal(100, result["Int2"]);
  83. Assert.Equal(999, result["Int3"]);
  84. }
  85. [Fact]
  86. public void GetMany_IncludesPendingChanges()
  87. {
  88. // Arrange
  89. var jsonPath = Path.Combine(_testDir, "config.json");
  90. File.WriteAllText(jsonPath, """{"Key1": "Original"}""");
  91. using var cfg = new CfgBuilder()
  92. .AddJson(jsonPath, level: 0, writeable: true)
  93. .Build();
  94. cfg.Set("Key1", "Modified");
  95. cfg.Set("Key2", "NewValue");
  96. // Act
  97. var result = cfg.GetMany(new[] { "Key1", "Key2" });
  98. // Assert
  99. Assert.Equal("Modified", result["Key1"]);
  100. Assert.Equal("NewValue", result["Key2"]);
  101. }
  102. [Fact]
  103. public void SetMany_SetsMultipleValues()
  104. {
  105. // Arrange
  106. var jsonPath = Path.Combine(_testDir, "config.json");
  107. File.WriteAllText(jsonPath, """{}""");
  108. using var cfg = new CfgBuilder()
  109. .AddJson(jsonPath, level: 0, writeable: true)
  110. .Build();
  111. // Act
  112. cfg.SetMany(new Dictionary<string, string?>
  113. {
  114. ["Key1"] = "Value1",
  115. ["Key2"] = "Value2",
  116. ["Key3"] = "Value3"
  117. });
  118. // Assert
  119. Assert.Equal("Value1", cfg.Get("Key1"));
  120. Assert.Equal("Value2", cfg.Get("Key2"));
  121. Assert.Equal("Value3", cfg.Get("Key3"));
  122. }
  123. [Fact]
  124. public async Task SetMany_PersistsAfterSave()
  125. {
  126. // Arrange
  127. var jsonPath = Path.Combine(_testDir, "config.json");
  128. File.WriteAllText(jsonPath, "{}");
  129. using var cfg = new CfgBuilder()
  130. .AddJson(jsonPath, level: 0, writeable: true)
  131. .Build();
  132. // Act
  133. cfg.SetMany(new Dictionary<string, string?>
  134. {
  135. ["Key1"] = "Value1",
  136. ["Key2"] = "Value2"
  137. });
  138. await cfg.SaveAsync();
  139. // Assert - 重新读取文件验证
  140. var content = File.ReadAllText(jsonPath);
  141. Assert.Contains("Key1", content);
  142. Assert.Contains("Value1", content);
  143. Assert.Contains("Key2", content);
  144. Assert.Contains("Value2", content);
  145. }
  146. // ========== GetMany 回调方式测试(高性能 API)==========
  147. [Fact]
  148. public void GetMany_Callback_InvokesForEachKey()
  149. {
  150. // Arrange
  151. var jsonPath = Path.Combine(_testDir, "config.json");
  152. File.WriteAllText(jsonPath, """
  153. {
  154. "Key1": "Value1",
  155. "Key2": "Value2",
  156. "Key3": "Value3"
  157. }
  158. """);
  159. using var cfg = new CfgBuilder()
  160. .AddJson(jsonPath, level: 0, writeable: true)
  161. .Build();
  162. var results = new Dictionary<string, string?>();
  163. // Act
  164. cfg.GetMany(new[] { "Key1", "Key2", "Key3" }, (key, value) =>
  165. {
  166. results[key] = value;
  167. });
  168. // Assert
  169. Assert.Equal(3, results.Count);
  170. Assert.Equal("Value1", results["Key1"]);
  171. Assert.Equal("Value2", results["Key2"]);
  172. Assert.Equal("Value3", results["Key3"]);
  173. }
  174. [Fact]
  175. public void GetMany_Callback_ReturnsNullForMissingKeys()
  176. {
  177. // Arrange
  178. var jsonPath = Path.Combine(_testDir, "config.json");
  179. File.WriteAllText(jsonPath, """{"Key1": "Value1"}""");
  180. using var cfg = new CfgBuilder()
  181. .AddJson(jsonPath, level: 0, writeable: true)
  182. .Build();
  183. var results = new Dictionary<string, string?>();
  184. // Act
  185. cfg.GetMany(new[] { "Key1", "NonExistent" }, (key, value) =>
  186. {
  187. results[key] = value;
  188. });
  189. // Assert
  190. Assert.Equal(2, results.Count);
  191. Assert.Equal("Value1", results["Key1"]);
  192. Assert.Null(results["NonExistent"]);
  193. }
  194. [Fact]
  195. public void GetMany_Callback_IncludesPendingChanges()
  196. {
  197. // Arrange
  198. var jsonPath = Path.Combine(_testDir, "config.json");
  199. File.WriteAllText(jsonPath, """{"Key1": "Original"}""");
  200. using var cfg = new CfgBuilder()
  201. .AddJson(jsonPath, level: 0, writeable: true)
  202. .Build();
  203. cfg.Set("Key1", "Modified");
  204. cfg.Set("Key2", "NewValue");
  205. var results = new Dictionary<string, string?>();
  206. // Act
  207. cfg.GetMany(new[] { "Key1", "Key2" }, (key, value) =>
  208. {
  209. results[key] = value;
  210. });
  211. // Assert
  212. Assert.Equal("Modified", results["Key1"]);
  213. Assert.Equal("NewValue", results["Key2"]);
  214. }
  215. [Fact]
  216. public void GetMany_Callback_PreservesKeyOrder()
  217. {
  218. // Arrange
  219. var jsonPath = Path.Combine(_testDir, "config.json");
  220. File.WriteAllText(jsonPath, """
  221. {
  222. "A": "1",
  223. "B": "2",
  224. "C": "3"
  225. }
  226. """);
  227. using var cfg = new CfgBuilder()
  228. .AddJson(jsonPath, level: 0, writeable: true)
  229. .Build();
  230. var orderedKeys = new List<string>();
  231. // Act
  232. cfg.GetMany(new[] { "C", "A", "B" }, (key, value) =>
  233. {
  234. orderedKeys.Add(key);
  235. });
  236. // Assert - 回调顺序应与输入顺序一致
  237. Assert.Equal(new[] { "C", "A", "B" }, orderedKeys);
  238. }
  239. [Fact]
  240. public void GetMany_Generic_Callback_ConvertsTypes()
  241. {
  242. // Arrange
  243. var jsonPath = Path.Combine(_testDir, "config.json");
  244. File.WriteAllText(jsonPath, """
  245. {
  246. "Int1": "42",
  247. "Int2": "100",
  248. "Int3": "999"
  249. }
  250. """);
  251. using var cfg = new CfgBuilder()
  252. .AddJson(jsonPath, level: 0, writeable: true)
  253. .Build();
  254. var results = new Dictionary<string, int?>();
  255. // Act
  256. cfg.GetMany<int>(new[] { "Int1", "Int2", "Int3" }, (key, value) =>
  257. {
  258. results[key] = value;
  259. });
  260. // Assert
  261. Assert.Equal(3, results.Count);
  262. Assert.Equal(42, results["Int1"]);
  263. Assert.Equal(100, results["Int2"]);
  264. Assert.Equal(999, results["Int3"]);
  265. }
  266. [Fact]
  267. public void GetMany_Generic_Callback_ReturnsDefaultForMissingKeys()
  268. {
  269. // Arrange
  270. var jsonPath = Path.Combine(_testDir, "config.json");
  271. File.WriteAllText(jsonPath, """{"Int1": "42"}""");
  272. using var cfg = new CfgBuilder()
  273. .AddJson(jsonPath, level: 0, writeable: true)
  274. .Build();
  275. var results = new Dictionary<string, int?>();
  276. // Act
  277. cfg.GetMany<int>(new[] { "Int1", "NonExistent" }, (key, value) =>
  278. {
  279. results[key] = value;
  280. });
  281. // Assert
  282. Assert.Equal(2, results.Count);
  283. Assert.Equal(42, results["Int1"]);
  284. Assert.Equal(default(int), results["NonExistent"]);
  285. }
  286. [Fact]
  287. public void GetMany_Generic_Callback_IncludesPendingChanges()
  288. {
  289. // Arrange
  290. var jsonPath = Path.Combine(_testDir, "config.json");
  291. File.WriteAllText(jsonPath, """{"Int1": "10"}""");
  292. using var cfg = new CfgBuilder()
  293. .AddJson(jsonPath, level: 0, writeable: true)
  294. .Build();
  295. cfg.Set("Int1", "20");
  296. cfg.Set("Int2", "30");
  297. var results = new Dictionary<string, int?>();
  298. // Act
  299. cfg.GetMany<int>(new[] { "Int1", "Int2" }, (key, value) =>
  300. {
  301. results[key] = value;
  302. });
  303. // Assert
  304. Assert.Equal(20, results["Int1"]);
  305. Assert.Equal(30, results["Int2"]);
  306. }
  307. [Fact]
  308. public void GetMany_Callback_WithEmptyKeys_DoesNotInvokeCallback()
  309. {
  310. // Arrange
  311. var jsonPath = Path.Combine(_testDir, "config.json");
  312. File.WriteAllText(jsonPath, """{"Key1": "Value1"}""");
  313. using var cfg = new CfgBuilder()
  314. .AddJson(jsonPath, level: 0, writeable: true)
  315. .Build();
  316. var callCount = 0;
  317. // Act
  318. cfg.GetMany(Array.Empty<string>(), (key, value) =>
  319. {
  320. callCount++;
  321. });
  322. // Assert
  323. Assert.Equal(0, callCount);
  324. }
  325. // ========== KeyPathParser 测试 ==========
  326. [Fact]
  327. public void KeyPathParser_GetFirstSegment_ReturnsFirstPart()
  328. {
  329. // Act & Assert
  330. Assert.Equal("Database", KeyPathParser.GetFirstSegment("Database:ConnectionString").ToString());
  331. Assert.Equal("SingleKey", KeyPathParser.GetFirstSegment("SingleKey").ToString());
  332. Assert.Equal("A", KeyPathParser.GetFirstSegment("A:B:C").ToString());
  333. }
  334. [Fact]
  335. public void KeyPathParser_GetRemainder_ReturnsRestAfterFirstSegment()
  336. {
  337. // Act & Assert
  338. Assert.Equal("ConnectionString", KeyPathParser.GetRemainder("Database:ConnectionString").ToString());
  339. Assert.True(KeyPathParser.GetRemainder("SingleKey").IsEmpty);
  340. Assert.Equal("B:C", KeyPathParser.GetRemainder("A:B:C").ToString());
  341. }
  342. [Fact]
  343. public void KeyPathParser_GetLastSegment_ReturnsLastPart()
  344. {
  345. // Act & Assert
  346. Assert.Equal("ConnectionString", KeyPathParser.GetLastSegment("Database:ConnectionString").ToString());
  347. Assert.Equal("SingleKey", KeyPathParser.GetLastSegment("SingleKey").ToString());
  348. Assert.Equal("C", KeyPathParser.GetLastSegment("A:B:C").ToString());
  349. }
  350. [Fact]
  351. public void KeyPathParser_GetParentPath_ReturnsPathWithoutLastSegment()
  352. {
  353. // Act & Assert
  354. Assert.Equal("Database", KeyPathParser.GetParentPath("Database:ConnectionString").ToString());
  355. Assert.True(KeyPathParser.GetParentPath("SingleKey").IsEmpty);
  356. Assert.Equal("A:B", KeyPathParser.GetParentPath("A:B:C").ToString());
  357. }
  358. [Fact]
  359. public void KeyPathParser_GetDepth_ReturnsCorrectDepth()
  360. {
  361. // Act & Assert
  362. Assert.Equal(0, KeyPathParser.GetDepth(""));
  363. Assert.Equal(1, KeyPathParser.GetDepth("SingleKey"));
  364. Assert.Equal(2, KeyPathParser.GetDepth("Database:ConnectionString"));
  365. Assert.Equal(3, KeyPathParser.GetDepth("A:B:C"));
  366. }
  367. [Fact]
  368. public void KeyPathParser_StartsWithSegment_MatchesCorrectly()
  369. {
  370. // Act & Assert
  371. Assert.True(KeyPathParser.StartsWithSegment("Database:ConnectionString", "Database"));
  372. Assert.True(KeyPathParser.StartsWithSegment("Database", "Database"));
  373. Assert.False(KeyPathParser.StartsWithSegment("DatabaseBackup:Path", "Database"));
  374. Assert.False(KeyPathParser.StartsWithSegment("Other:Key", "Database"));
  375. }
  376. [Fact]
  377. public void KeyPathParser_Combine_JoinsPathsCorrectly()
  378. {
  379. // Act & Assert
  380. Assert.Equal("Database:ConnectionString", KeyPathParser.Combine("Database", "ConnectionString"));
  381. Assert.Equal("ConnectionString", KeyPathParser.Combine("", "ConnectionString"));
  382. Assert.Equal("Database", KeyPathParser.Combine("Database", ""));
  383. Assert.Equal("A:B:C", KeyPathParser.Combine("A:B", "C"));
  384. }
  385. [Fact]
  386. public void KeyPathParser_EnumerateSegments_IteratesAllSegments()
  387. {
  388. // Arrange
  389. var segments = new List<string>();
  390. // Act
  391. foreach (var segment in KeyPathParser.EnumerateSegments("A:B:C:D"))
  392. {
  393. segments.Add(segment.ToString());
  394. }
  395. // Assert
  396. Assert.Equal(4, segments.Count);
  397. Assert.Equal("A", segments[0]);
  398. Assert.Equal("B", segments[1]);
  399. Assert.Equal("C", segments[2]);
  400. Assert.Equal("D", segments[3]);
  401. }
  402. // ========== ValueCache 测试 ==========
  403. [Fact]
  404. public void ValueCache_SetAndGet_WorksCorrectly()
  405. {
  406. // Arrange
  407. var cache = new ValueCache();
  408. // Act
  409. cache.Set("Key1", 42);
  410. cache.Set("Key2", "StringValue");
  411. // Assert
  412. Assert.True(cache.TryGet<int>("Key1", out var intValue));
  413. Assert.Equal(42, intValue);
  414. Assert.True(cache.TryGet<string>("Key2", out var stringValue));
  415. Assert.Equal("StringValue", stringValue);
  416. }
  417. [Fact]
  418. public void ValueCache_TryGet_ReturnsFalseForMissingKey()
  419. {
  420. // Arrange
  421. var cache = new ValueCache();
  422. // Act & Assert
  423. Assert.False(cache.TryGet<int>("NonExistent", out _));
  424. }
  425. [Fact]
  426. public void ValueCache_DifferentTypes_CachedSeparately()
  427. {
  428. // Arrange
  429. var cache = new ValueCache();
  430. // Act
  431. cache.Set<int>("Key", 42);
  432. cache.Set<string>("Key", "42");
  433. // Assert
  434. Assert.True(cache.TryGet<int>("Key", out var intValue));
  435. Assert.Equal(42, intValue);
  436. Assert.True(cache.TryGet<string>("Key", out var stringValue));
  437. Assert.Equal("42", stringValue);
  438. }
  439. [Fact]
  440. public void ValueCache_Invalidate_RemovesKey()
  441. {
  442. // Arrange
  443. var cache = new ValueCache();
  444. cache.Set("Key1", 42);
  445. cache.Set("Key2", 100);
  446. // Act
  447. cache.Invalidate("Key1");
  448. // Assert
  449. Assert.False(cache.TryGet<int>("Key1", out _));
  450. Assert.True(cache.TryGet<int>("Key2", out _));
  451. }
  452. [Fact]
  453. public void ValueCache_Clear_RemovesAllKeys()
  454. {
  455. // Arrange
  456. var cache = new ValueCache();
  457. cache.Set("Key1", 42);
  458. cache.Set("Key2", 100);
  459. // Act
  460. cache.Clear();
  461. // Assert
  462. Assert.False(cache.TryGet<int>("Key1", out _));
  463. Assert.False(cache.TryGet<int>("Key2", out _));
  464. }
  465. [Fact]
  466. public void ValueCache_Version_IncrementsOnInvalidate()
  467. {
  468. // Arrange
  469. var cache = new ValueCache();
  470. var initialVersion = cache.Version;
  471. // Act
  472. cache.Set("Key", 42);
  473. var afterSetVersion = cache.Version;
  474. cache.Invalidate("Key");
  475. var afterInvalidateVersion = cache.Version;
  476. // Assert
  477. Assert.Equal(initialVersion, afterSetVersion); // Set 不改变版本
  478. Assert.Equal(initialVersion + 1, afterInvalidateVersion); // Invalidate 增加版本
  479. }
  480. // ========== FastCollections 测试 ==========
  481. [Fact]
  482. public void FastReadOnlyDictionary_WorksCorrectly()
  483. {
  484. // Arrange
  485. var source = new Dictionary<string, int>
  486. {
  487. ["A"] = 1,
  488. ["B"] = 2,
  489. ["C"] = 3
  490. };
  491. // Act
  492. var fast = source.ToFastReadOnly();
  493. // Assert
  494. Assert.Equal(3, fast.Count);
  495. Assert.Equal(1, fast["A"]);
  496. Assert.Equal(2, fast["B"]);
  497. Assert.Equal(3, fast["C"]);
  498. Assert.True(fast.ContainsKey("A"));
  499. Assert.False(fast.ContainsKey("D"));
  500. Assert.True(fast.TryGetValue("B", out var value));
  501. Assert.Equal(2, value);
  502. }
  503. [Fact]
  504. public void FastReadOnlySet_WorksCorrectly()
  505. {
  506. // Arrange
  507. var source = new[] { "A", "B", "C" };
  508. // Act
  509. var fast = source.ToFastReadOnlySet();
  510. // Assert
  511. Assert.Equal(3, fast.Count);
  512. Assert.True(fast.Contains("A"));
  513. Assert.True(fast.Contains("B"));
  514. Assert.True(fast.Contains("C"));
  515. Assert.False(fast.Contains("D"));
  516. }
  517. }