InventoryInboundLockGuardTests.cs 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  2. using Microsoft.Extensions.Logging.Abstractions;
  3. using SqlSugar;
  4. using Xunit;
  5. namespace Admin.NET.Plugin.AiDOP.Tests.MaterialWarehouse;
  6. /// <summary>
  7. /// 库存物化有界等待锁的进程内行为。不连真实库。
  8. /// 跨连接互斥留在 S5 集成测试,仅当 AIDOP_IT=1 时执行。
  9. /// </summary>
  10. public class InventoryInboundLockGuardAcquireTests
  11. {
  12. [Fact]
  13. public void ProductionDefaults_AreTenMinutesAndTwoSeconds()
  14. {
  15. Assert.Equal(TimeSpan.FromMinutes(10), InventoryInboundLockGuard.DefaultWaitTimeout);
  16. Assert.Equal(TimeSpan.FromSeconds(2), InventoryInboundLockGuard.DefaultPollInterval);
  17. }
  18. [Fact]
  19. public async Task AcquireAsync_PreCancelledToken_StopsBeforeWaiting()
  20. {
  21. using var db = ClosedClient();
  22. using var cts = new CancellationTokenSource();
  23. cts.Cancel();
  24. var started = DateTime.UtcNow;
  25. await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
  26. InventoryInboundLockGuard.AcquireAsync(
  27. db, NullLogger.Instance, "aidop:t:inv-in:cancel",
  28. TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(50), cts.Token));
  29. Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(2));
  30. }
  31. [Fact]
  32. public async Task AcquireAsync_UnreachableDatabase_TimesOutWithFixedMessage()
  33. {
  34. using var db = ClosedClient();
  35. var ex = await Assert.ThrowsAsync<TimeoutException>(() =>
  36. InventoryInboundLockGuard.AcquireAsync(
  37. db, NullLogger.Instance, "aidop:t:inv-in:timeout",
  38. TimeSpan.FromMilliseconds(200), TimeSpan.FromMilliseconds(50)));
  39. Assert.Equal("inventory materialization lock busy", ex.Message);
  40. }
  41. [Fact]
  42. public void TenantMaterializeEntry_WaitsForLock_BackgroundPathsStayNonBlocking()
  43. {
  44. var src = File.ReadAllText(Path.Combine(RepoRoot(),
  45. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "InventoryMdpSyncService.cs"));
  46. var method = Slice(src, "public async Task<int> TransformTransStdFromStgAsync(", "private async Task<InventorySyncResult> RunAsync");
  47. Assert.Contains("InventoryInboundLockGuard.AcquireAsync(", method, StringComparison.Ordinal);
  48. Assert.DoesNotContain("TryAcquireAsync", method, StringComparison.Ordinal);
  49. Assert.True(Count(src, "InventoryInboundLockGuard.TryAcquireAsync(") >= 3);
  50. }
  51. private static SqlSugarClient ClosedClient() => new(new ConnectionConfig
  52. {
  53. DbType = DbType.MySql,
  54. ConnectionString = "Server=127.0.0.1;Port=1;User ID=x;Password=x;Database=x;Connection Timeout=1;Pooling=false;",
  55. IsAutoCloseConnection = true,
  56. });
  57. private static string RepoRoot()
  58. {
  59. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  60. while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  61. dir = dir.Parent;
  62. return dir?.FullName ?? throw new InvalidOperationException("repo root not found");
  63. }
  64. private static string Slice(string src, string start, string end)
  65. {
  66. var i = src.IndexOf(start, StringComparison.Ordinal);
  67. var j = src.IndexOf(end, i, StringComparison.Ordinal);
  68. Assert.True(i >= 0 && j > i);
  69. return src[i..j];
  70. }
  71. private static int Count(string src, string token)
  72. {
  73. var n = 0;
  74. var i = 0;
  75. while ((i = src.IndexOf(token, i, StringComparison.Ordinal)) >= 0)
  76. {
  77. n++;
  78. i += token.Length;
  79. }
  80. return n;
  81. }
  82. }