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,148 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Security;
namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
/// <summary>
/// Unit tests for <see cref="ScopePathIndexBuilder"/> — the ADR-001 Task B builder that
/// produces the full-path <see cref="NodeScope"/> index consumed by
/// <see cref="NodeScopeResolver"/> in its indexed mode.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ScopePathIndexBuilderTests
{
[Fact]
public void Build_emits_full_hierarchy_for_well_formed_content()
{
var index = ScopePathIndexBuilder.Build("c1", "ns-eq", Content(
areas: [Area("area1")],
lines: [Line("line1", "area1")],
equipment: [Equip("eq1", "line1")],
tags: [TagRow("tag1", "eq1", tagConfig: "Eq1/Speed")]));
index.Count.ShouldBe(1);
var scope = index["Eq1/Speed"];
scope.ClusterId.ShouldBe("c1");
scope.NamespaceId.ShouldBe("ns-eq");
scope.UnsAreaId.ShouldBe("area1");
scope.UnsLineId.ShouldBe("line1");
scope.EquipmentId.ShouldBe("eq1");
scope.TagId.ShouldBe("Eq1/Speed");
scope.Kind.ShouldBe(NodeHierarchyKind.Equipment);
}
[Fact]
public void Build_skips_tags_with_null_EquipmentId()
{
// SystemPlatform-namespace tags (decision #110) — the cluster-only resolver
// fallback handles them; no index entry needed.
var index = ScopePathIndexBuilder.Build("c1", "ns-sp", Content(
tags: [TagRow("t", equipmentId: null, tagConfig: "Galaxy.Object.Attr")]));
index.Count.ShouldBe(0);
}
[Fact]
public void Build_skips_tags_with_broken_Equipment_FK()
{
// Tag references a missing Equipment row. sp_ValidateDraft should have caught this
// at publish; builder skips rather than crashes so startup stays bootable.
var index = ScopePathIndexBuilder.Build("c1", "ns", Content(
areas: [Area("area1")],
lines: [Line("line1", "area1")],
tags: [TagRow("t", "missing-eq", "missing/Speed")]));
index.Count.ShouldBe(0);
}
[Fact]
public void Build_skips_equipment_with_broken_line_FK()
{
var index = ScopePathIndexBuilder.Build("c1", "ns", Content(
areas: [Area("area1")],
lines: [], // no lines — equipment's UnsLineId misses
equipment: [Equip("eq1", "missing")],
tags: [TagRow("t", "eq1", "E/S")]));
index.Count.ShouldBe(0);
}
[Fact]
public void Build_throws_on_duplicate_TagConfig()
{
var ex = Should.Throw<InvalidOperationException>(() =>
ScopePathIndexBuilder.Build("c1", "ns", Content(
areas: [Area("area1")],
lines: [Line("line1", "area1")],
equipment: [Equip("eq1", "line1")],
tags:
[
TagRow("t1", "eq1", "E/DUP"),
TagRow("t2", "eq1", "E/DUP"),
])));
ex.Message.ShouldContain("Duplicate");
ex.Message.ShouldContain("E/DUP");
}
[Fact]
public void Resolver_with_index_returns_full_path_scope()
{
var index = ScopePathIndexBuilder.Build("c1", "ns", Content(
areas: [Area("area1")],
lines: [Line("line1", "area1")],
equipment: [Equip("eq1", "line1")],
tags: [TagRow("t", "eq1", "E/Speed")]));
var resolver = new NodeScopeResolver("c1", index);
var resolved = resolver.Resolve("E/Speed");
resolved.UnsAreaId.ShouldBe("area1");
resolved.UnsLineId.ShouldBe("line1");
resolved.EquipmentId.ShouldBe("eq1");
// Un-indexed ref falls through to cluster-only scope — pre-ADR-001 behaviour preserved.
var fallback = resolver.Resolve("Galaxy.Object.Attr");
fallback.ClusterId.ShouldBe("c1");
fallback.TagId.ShouldBe("Galaxy.Object.Attr");
fallback.UnsAreaId.ShouldBeNull();
}
// ---- fixture helpers ---------------------------------------------------
private static EquipmentNamespaceContent Content(
IReadOnlyList<UnsArea>? areas = null,
IReadOnlyList<UnsLine>? lines = null,
IReadOnlyList<Equipment>? equipment = null,
IReadOnlyList<Tag>? tags = null) =>
new(areas ?? [], lines ?? [], equipment ?? [], tags ?? []);
private static UnsArea Area(string id) => new()
{
UnsAreaId = id, ClusterId = "c1", Name = $"Area {id}",
};
private static UnsLine Line(string id, string areaId) => new()
{
UnsLineId = id, UnsAreaId = areaId, Name = $"Line {id}",
};
private static Equipment Equip(string id, string lineId) => new()
{
EquipmentId = id, UnsLineId = lineId, DriverInstanceId = "drv",
Name = $"Eq {id}", MachineCode = $"M{id}", ZTag = id,
};
private static Tag TagRow(string id, string? equipmentId, string tagConfig) => new()
{
TagId = id, EquipmentId = equipmentId,
DriverInstanceId = "drv",
Name = id, DataType = "Int32",
AccessLevel = TagAccessLevel.ReadWrite,
TagConfig = tagConfig,
};
}