Harden Surreal migration with retry/coverage fixes and XML docs cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m17s
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m17s
This commit is contained in:
87
tests/ZB.MOM.WW.CBDDC.Core.Tests/DocumentCacheTests.cs
Normal file
87
tests/ZB.MOM.WW.CBDDC.Core.Tests/DocumentCacheTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.CBDDC.Core.Cache;
|
||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
|
||||
public class DocumentCacheTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies cache hit/miss statistics after get and set operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAndSet_ShouldTrackCacheHitsAndMisses()
|
||||
{
|
||||
var cache = new DocumentCache(CreateConfigProvider(maxDocumentCacheSize: 2));
|
||||
|
||||
Document? missing = await cache.Get("users", "1");
|
||||
missing.ShouldBeNull();
|
||||
|
||||
var document = CreateDocument("users", "1");
|
||||
await cache.Set("users", "1", document);
|
||||
Document? hit = await cache.Get("users", "1");
|
||||
|
||||
hit.ShouldNotBeNull();
|
||||
hit.Key.ShouldBe("1");
|
||||
var stats = cache.GetStatistics();
|
||||
stats.Hits.ShouldBe(1);
|
||||
stats.Misses.ShouldBe(1);
|
||||
stats.Size.ShouldBe(1);
|
||||
stats.HitRate.ShouldBe(0.5d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies least-recently-used eviction when cache capacity is reached.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Set_WhenCacheIsFull_EvictsLeastRecentlyUsedEntry()
|
||||
{
|
||||
var cache = new DocumentCache(CreateConfigProvider(maxDocumentCacheSize: 2));
|
||||
await cache.Set("users", "1", CreateDocument("users", "1"));
|
||||
await cache.Set("users", "2", CreateDocument("users", "2"));
|
||||
|
||||
// Touch key 1 so key 2 becomes the LRU entry.
|
||||
(await cache.Get("users", "1")).ShouldNotBeNull();
|
||||
|
||||
await cache.Set("users", "3", CreateDocument("users", "3"));
|
||||
|
||||
(await cache.Get("users", "2")).ShouldBeNull();
|
||||
(await cache.Get("users", "1")).ShouldNotBeNull();
|
||||
(await cache.Get("users", "3")).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies remove and clear operations delete entries from the cache.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RemoveAndClear_ShouldDeleteEntriesFromCache()
|
||||
{
|
||||
var cache = new DocumentCache(CreateConfigProvider(maxDocumentCacheSize: 3));
|
||||
await cache.Set("users", "1", CreateDocument("users", "1"));
|
||||
await cache.Set("users", "2", CreateDocument("users", "2"));
|
||||
cache.GetStatistics().Size.ShouldBe(2);
|
||||
|
||||
cache.Remove("users", "1");
|
||||
(await cache.Get("users", "1")).ShouldBeNull();
|
||||
cache.GetStatistics().Size.ShouldBe(1);
|
||||
|
||||
cache.Clear();
|
||||
cache.GetStatistics().Size.ShouldBe(0);
|
||||
}
|
||||
|
||||
private static Document CreateDocument(string collection, string key)
|
||||
{
|
||||
using var json = JsonDocument.Parse("""{"name":"test"}""");
|
||||
return new Document(collection, key, json.RootElement.Clone(), new HlcTimestamp(1, 0, "node-a"), false);
|
||||
}
|
||||
|
||||
private static IPeerNodeConfigurationProvider CreateConfigProvider(int maxDocumentCacheSize)
|
||||
{
|
||||
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
|
||||
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
|
||||
{
|
||||
MaxDocumentCacheSize = maxDocumentCacheSize
|
||||
});
|
||||
return configProvider;
|
||||
}
|
||||
}
|
||||
92
tests/ZB.MOM.WW.CBDDC.Core.Tests/OfflineQueueTests.cs
Normal file
92
tests/ZB.MOM.WW.CBDDC.Core.Tests/OfflineQueueTests.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||
using ZB.MOM.WW.CBDDC.Core.Sync;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
|
||||
public class OfflineQueueTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that enqueuing beyond capacity drops the oldest operation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Enqueue_WhenQueueIsFull_DropsOldestOperation()
|
||||
{
|
||||
var queue = new OfflineQueue(CreateConfigProvider(maxQueueSize: 2));
|
||||
await queue.Enqueue(CreateOperation("1"));
|
||||
await queue.Enqueue(CreateOperation("2"));
|
||||
await queue.Enqueue(CreateOperation("3"));
|
||||
|
||||
var flushed = new List<string>();
|
||||
(int successful, int failed) = await queue.FlushAsync(op =>
|
||||
{
|
||||
flushed.Add(op.Key);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
successful.ShouldBe(2);
|
||||
failed.ShouldBe(0);
|
||||
flushed.ShouldBe(["2", "3"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that flush continues when an executor throws and returns the failure count.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FlushAsync_WhenExecutorThrows_ContinuesAndReturnsFailureCount()
|
||||
{
|
||||
var queue = new OfflineQueue(CreateConfigProvider(maxQueueSize: 10));
|
||||
await queue.Enqueue(CreateOperation("1"));
|
||||
await queue.Enqueue(CreateOperation("2"));
|
||||
|
||||
(int successful, int failed) = await queue.FlushAsync(op =>
|
||||
{
|
||||
if (op.Key == "1") throw new InvalidOperationException("boom");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
successful.ShouldBe(1);
|
||||
failed.ShouldBe(1);
|
||||
queue.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that clear removes all queued operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllQueuedOperations()
|
||||
{
|
||||
var queue = new OfflineQueue(CreateConfigProvider(maxQueueSize: 10));
|
||||
await queue.Enqueue(CreateOperation("1"));
|
||||
await queue.Enqueue(CreateOperation("2"));
|
||||
queue.Count.ShouldBe(2);
|
||||
|
||||
await queue.Clear();
|
||||
|
||||
queue.Count.ShouldBe(0);
|
||||
(int successful, int failed) = await queue.FlushAsync(_ => Task.CompletedTask);
|
||||
successful.ShouldBe(0);
|
||||
failed.ShouldBe(0);
|
||||
}
|
||||
|
||||
private static PendingOperation CreateOperation(string key)
|
||||
{
|
||||
return new PendingOperation
|
||||
{
|
||||
Type = "upsert",
|
||||
Collection = "users",
|
||||
Key = key,
|
||||
Data = new { Value = key },
|
||||
QueuedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static IPeerNodeConfigurationProvider CreateConfigProvider(int maxQueueSize)
|
||||
{
|
||||
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
|
||||
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
|
||||
{
|
||||
MaxQueueSize = maxQueueSize
|
||||
});
|
||||
return configProvider;
|
||||
}
|
||||
}
|
||||
78
tests/ZB.MOM.WW.CBDDC.Core.Tests/RetryPolicyTests.cs
Normal file
78
tests/ZB.MOM.WW.CBDDC.Core.Tests/RetryPolicyTests.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using ZB.MOM.WW.CBDDC.Core.Exceptions;
|
||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||
using ZB.MOM.WW.CBDDC.Core.Resilience;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
|
||||
public class RetryPolicyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies transient failures are retried until a successful result is returned.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenTransientFailureEventuallySucceeds_RetriesAndReturnsResult()
|
||||
{
|
||||
var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 3, retryDelayMs: 1));
|
||||
var attempts = 0;
|
||||
|
||||
int result = await policy.ExecuteAsync(async () =>
|
||||
{
|
||||
attempts++;
|
||||
if (attempts < 3) throw new NetworkException("transient");
|
||||
await Task.CompletedTask;
|
||||
return 42;
|
||||
}, "test-op");
|
||||
|
||||
result.ShouldBe(42);
|
||||
attempts.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies transient failures throw retry exhausted when all retries are consumed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenTransientFailureExhausted_ThrowsRetryExhaustedException()
|
||||
{
|
||||
var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 2, retryDelayMs: 1));
|
||||
var attempts = 0;
|
||||
|
||||
var ex = await Should.ThrowAsync<CBDDCException>(() => policy.ExecuteAsync<int>(() =>
|
||||
{
|
||||
attempts++;
|
||||
throw new NetworkException("still transient");
|
||||
}, "test-op"));
|
||||
|
||||
ex.ErrorCode.ShouldBe("RETRY_EXHAUSTED");
|
||||
ex.InnerException.ShouldBeOfType<NetworkException>();
|
||||
attempts.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies non-transient failures are not retried.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenFailureIsNonTransient_DoesNotRetry()
|
||||
{
|
||||
var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 3, retryDelayMs: 1));
|
||||
var attempts = 0;
|
||||
|
||||
await Should.ThrowAsync<InvalidOperationException>(() => policy.ExecuteAsync<int>(() =>
|
||||
{
|
||||
attempts++;
|
||||
throw new InvalidOperationException("non-transient");
|
||||
}, "test-op"));
|
||||
|
||||
attempts.ShouldBe(1);
|
||||
}
|
||||
|
||||
private static IPeerNodeConfigurationProvider CreateConfigProvider(int retryAttempts, int retryDelayMs)
|
||||
{
|
||||
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
|
||||
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
|
||||
{
|
||||
RetryAttempts = retryAttempts,
|
||||
RetryDelayMs = retryDelayMs
|
||||
});
|
||||
return configProvider;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user