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,130 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class UnsServiceMoveTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadSnapshotAsync_ReturnsAllAreasAndLines_WithEquipmentCounts()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
Seed(ctx, draftId: 1, areas: new[] { "area-1", "area-2" },
|
||||
lines: new[] { ("line-a", "area-1"), ("line-b", "area-1"), ("line-c", "area-2") },
|
||||
equipmentLines: new[] { "line-a", "line-a", "line-b" });
|
||||
var svc = new UnsService(ctx);
|
||||
|
||||
var snap = await svc.LoadSnapshotAsync(1, CancellationToken.None);
|
||||
|
||||
snap.Areas.Count.ShouldBe(2);
|
||||
snap.Lines.Count.ShouldBe(3);
|
||||
snap.FindLine("line-a")!.EquipmentCount.ShouldBe(2);
|
||||
snap.FindLine("line-b")!.EquipmentCount.ShouldBe(1);
|
||||
snap.FindLine("line-c")!.EquipmentCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadSnapshotAsync_RevisionToken_IsStable_BetweenTwoReads()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
Seed(ctx, draftId: 1, areas: new[] { "area-1" }, lines: new[] { ("line-a", "area-1") });
|
||||
var svc = new UnsService(ctx);
|
||||
|
||||
var first = await svc.LoadSnapshotAsync(1, CancellationToken.None);
|
||||
var second = await svc.LoadSnapshotAsync(1, CancellationToken.None);
|
||||
|
||||
second.RevisionToken.Matches(first.RevisionToken).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadSnapshotAsync_RevisionToken_Changes_When_LineAdded()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
Seed(ctx, draftId: 1, areas: new[] { "area-1" }, lines: new[] { ("line-a", "area-1") });
|
||||
var svc = new UnsService(ctx);
|
||||
|
||||
var before = await svc.LoadSnapshotAsync(1, CancellationToken.None);
|
||||
await svc.AddLineAsync(1, "area-1", "new-line", null, CancellationToken.None);
|
||||
var after = await svc.LoadSnapshotAsync(1, CancellationToken.None);
|
||||
|
||||
after.RevisionToken.Matches(before.RevisionToken).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MoveLineAsync_WithMatchingToken_Reparents_Line()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
Seed(ctx, draftId: 1, areas: new[] { "area-1", "area-2" },
|
||||
lines: new[] { ("line-a", "area-1") });
|
||||
var svc = new UnsService(ctx);
|
||||
|
||||
var snap = await svc.LoadSnapshotAsync(1, CancellationToken.None);
|
||||
await svc.MoveLineAsync(1, snap.RevisionToken, "line-a", "area-2", CancellationToken.None);
|
||||
|
||||
var moved = await ctx.UnsLines.AsNoTracking().FirstAsync(l => l.UnsLineId == "line-a");
|
||||
moved.UnsAreaId.ShouldBe("area-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MoveLineAsync_WithStaleToken_Throws_DraftRevisionConflict()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
Seed(ctx, draftId: 1, areas: new[] { "area-1", "area-2" },
|
||||
lines: new[] { ("line-a", "area-1") });
|
||||
var svc = new UnsService(ctx);
|
||||
|
||||
// Simulate a peer operator's concurrent edit between our preview + commit.
|
||||
var stale = new DraftRevisionToken("0000000000000000");
|
||||
|
||||
await Should.ThrowAsync<DraftRevisionConflictException>(() =>
|
||||
svc.MoveLineAsync(1, stale, "line-a", "area-2", CancellationToken.None));
|
||||
|
||||
var row = await ctx.UnsLines.AsNoTracking().FirstAsync(l => l.UnsLineId == "line-a");
|
||||
row.UnsAreaId.ShouldBe("area-1");
|
||||
}
|
||||
|
||||
private static void Seed(OtOpcUaConfigDbContext ctx, long draftId,
|
||||
IEnumerable<string> areas,
|
||||
IEnumerable<(string line, string area)> lines,
|
||||
IEnumerable<string>? equipmentLines = null)
|
||||
{
|
||||
foreach (var a in areas)
|
||||
{
|
||||
ctx.UnsAreas.Add(new UnsArea
|
||||
{
|
||||
GenerationId = draftId, UnsAreaId = a, ClusterId = "c1", Name = a,
|
||||
});
|
||||
}
|
||||
foreach (var (line, area) in lines)
|
||||
{
|
||||
ctx.UnsLines.Add(new UnsLine
|
||||
{
|
||||
GenerationId = draftId, UnsLineId = line, UnsAreaId = area, Name = line,
|
||||
});
|
||||
}
|
||||
foreach (var lineId in equipmentLines ?? [])
|
||||
{
|
||||
ctx.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentRowId = Guid.NewGuid(), GenerationId = draftId,
|
||||
EquipmentId = $"EQ-{Guid.NewGuid():N}"[..15],
|
||||
EquipmentUuid = Guid.NewGuid(), DriverInstanceId = "drv",
|
||||
UnsLineId = lineId, Name = "x", MachineCode = "m",
|
||||
});
|
||||
}
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
private static OtOpcUaConfigDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new OtOpcUaConfigDbContext(opts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user