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:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions

View File

@@ -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));
}
}