chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"otopcua-cache-test-{Guid.NewGuid():N}.db");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
private GenerationSnapshot Snapshot(string cluster, long gen) => new()
|
||||
{
|
||||
ClusterId = cluster,
|
||||
GenerationId = gen,
|
||||
CachedAt = DateTime.UtcNow,
|
||||
PayloadJson = $"{{\"g\":{gen}}}",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task Roundtrip_preserves_payload()
|
||||
{
|
||||
using var cache = new LiteDbConfigCache(_dbPath);
|
||||
var put = Snapshot("c-1", 42);
|
||||
await cache.PutAsync(put);
|
||||
|
||||
var got = await cache.GetMostRecentAsync("c-1");
|
||||
got.ShouldNotBeNull();
|
||||
got!.GenerationId.ShouldBe(42);
|
||||
got.PayloadJson.ShouldBe(put.PayloadJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMostRecent_returns_latest_when_multiple_generations_present()
|
||||
{
|
||||
using var cache = new LiteDbConfigCache(_dbPath);
|
||||
foreach (var g in new long[] { 10, 20, 15 })
|
||||
await cache.PutAsync(Snapshot("c-1", g));
|
||||
|
||||
var got = await cache.GetMostRecentAsync("c-1");
|
||||
got!.GenerationId.ShouldBe(20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMostRecent_returns_null_for_unknown_cluster()
|
||||
{
|
||||
using var cache = new LiteDbConfigCache(_dbPath);
|
||||
(await cache.GetMostRecentAsync("ghost")).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Prune_keeps_latest_N_and_drops_older()
|
||||
{
|
||||
using var cache = new LiteDbConfigCache(_dbPath);
|
||||
for (long g = 1; g <= 15; g++)
|
||||
await cache.PutAsync(Snapshot("c-1", g));
|
||||
|
||||
await cache.PruneOldGenerationsAsync("c-1", keepLatest: 10);
|
||||
|
||||
(await cache.GetMostRecentAsync("c-1"))!.GenerationId.ShouldBe(15);
|
||||
|
||||
// Drop them one by one and count — should be exactly 10 remaining
|
||||
var count = 0;
|
||||
while (await cache.GetMostRecentAsync("c-1") is not null)
|
||||
{
|
||||
count++;
|
||||
await cache.PruneOldGenerationsAsync("c-1", keepLatest: Math.Max(0, 10 - count));
|
||||
if (count > 20) break; // safety
|
||||
}
|
||||
count.ShouldBe(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_same_cluster_generation_twice_replaces_not_duplicates()
|
||||
{
|
||||
using var cache = new LiteDbConfigCache(_dbPath);
|
||||
var first = Snapshot("c-1", 1);
|
||||
first.PayloadJson = "{\"v\":1}";
|
||||
await cache.PutAsync(first);
|
||||
|
||||
var second = Snapshot("c-1", 1);
|
||||
second.PayloadJson = "{\"v\":2}";
|
||||
await cache.PutAsync(second);
|
||||
|
||||
(await cache.GetMostRecentAsync("c-1"))!.PayloadJson.ShouldBe("{\"v\":2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Corrupt_file_surfaces_as_LocalConfigCacheCorruptException()
|
||||
{
|
||||
// Write a file large enough to look like a LiteDB page but with garbage contents so page
|
||||
// deserialization fails on the first read probe.
|
||||
File.WriteAllBytes(_dbPath, new byte[8192]);
|
||||
Array.Fill<byte>(File.ReadAllBytes(_dbPath), 0xAB);
|
||||
using (var fs = File.OpenWrite(_dbPath))
|
||||
{
|
||||
fs.Write(new byte[8192].Select(_ => (byte)0xAB).ToArray());
|
||||
}
|
||||
|
||||
Should.Throw<LocalConfigCacheCorruptException>(() => new LiteDbConfigCache(_dbPath));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user