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,116 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
[Trait("Category", "Unit")]
public sealed class MelsecAddressTests
{
// --- X / Y hex vs octal family trap ---
[Theory]
[InlineData("X0", (ushort)0)]
[InlineData("X9", (ushort)9)]
[InlineData("XA", (ushort)10)] // hex
[InlineData("XF", (ushort)15)]
[InlineData("X10", (ushort)16)] // hex 0x10 = decimal 16
[InlineData("X20", (ushort)32)] // hex 0x20 = decimal 32 — the classic MELSEC-Q trap
[InlineData("X1FF", (ushort)511)]
[InlineData("x10", (ushort)16)] // lowercase prefix
public void XInputToDiscrete_QLiQR_parses_hex(string x, ushort expected)
=> MelsecAddress.XInputToDiscrete(x, MelsecFamily.Q_L_iQR).ShouldBe(expected);
[Theory]
[InlineData("X0", (ushort)0)]
[InlineData("X7", (ushort)7)]
[InlineData("X10", (ushort)8)] // octal 10 = decimal 8
[InlineData("X20", (ushort)16)] // octal 20 = decimal 16 — SAME string, DIFFERENT value on FX
[InlineData("X777", (ushort)511)]
public void XInputToDiscrete_FiQF_parses_octal(string x, ushort expected)
=> MelsecAddress.XInputToDiscrete(x, MelsecFamily.F_iQF).ShouldBe(expected);
[Theory]
[InlineData("Y0", (ushort)0)]
[InlineData("Y1F", (ushort)31)]
public void YOutputToCoil_QLiQR_parses_hex(string y, ushort expected)
=> MelsecAddress.YOutputToCoil(y, MelsecFamily.Q_L_iQR).ShouldBe(expected);
[Theory]
[InlineData("Y0", (ushort)0)]
[InlineData("Y17", (ushort)15)]
public void YOutputToCoil_FiQF_parses_octal(string y, ushort expected)
=> MelsecAddress.YOutputToCoil(y, MelsecFamily.F_iQF).ShouldBe(expected);
[Fact]
public void Same_address_string_decodes_differently_between_families()
{
// This is the headline quirk: "X20" in GX Works means one thing on Q-series and
// another on FX-series. The driver's family selector is the only defence.
MelsecAddress.XInputToDiscrete("X20", MelsecFamily.Q_L_iQR).ShouldBe((ushort)32);
MelsecAddress.XInputToDiscrete("X20", MelsecFamily.F_iQF).ShouldBe((ushort)16);
}
[Theory]
[InlineData("X8")] // 8 is non-octal
[InlineData("X12G")] // G is non-hex
public void XInputToDiscrete_FiQF_rejects_non_octal(string bad)
=> Should.Throw<ArgumentException>(() => MelsecAddress.XInputToDiscrete(bad, MelsecFamily.F_iQF));
[Theory]
[InlineData("X12G")]
public void XInputToDiscrete_QLiQR_rejects_non_hex(string bad)
=> Should.Throw<ArgumentException>(() => MelsecAddress.XInputToDiscrete(bad, MelsecFamily.Q_L_iQR));
[Fact]
public void XInputToDiscrete_honors_bank_base_from_assignment_block()
{
// Real-world QJ71MT91 assignment blocks commonly place X at DI 8192+ when other
// ranges take the low Modbus addresses. Helper must add the base cleanly.
MelsecAddress.XInputToDiscrete("X10", MelsecFamily.Q_L_iQR, xBankBase: 8192).ShouldBe((ushort)(8192 + 16));
}
// --- M-relay (decimal, both families) ---
[Theory]
[InlineData("M0", (ushort)0)]
[InlineData("M10", (ushort)10)] // M addresses are DECIMAL, not hex or octal
[InlineData("M511", (ushort)511)]
[InlineData("m99", (ushort)99)] // lowercase
public void MRelayToCoil_parses_decimal(string m, ushort expected)
=> MelsecAddress.MRelayToCoil(m).ShouldBe(expected);
[Fact]
public void MRelayToCoil_honors_bank_base()
=> MelsecAddress.MRelayToCoil("M0", mBankBase: 512).ShouldBe((ushort)512);
[Fact]
public void MRelayToCoil_rejects_non_numeric()
=> Should.Throw<ArgumentException>(() => MelsecAddress.MRelayToCoil("M1F"));
// --- D-register (decimal, both families) ---
[Theory]
[InlineData("D0", (ushort)0)]
[InlineData("D100", (ushort)100)]
[InlineData("d1023", (ushort)1023)]
public void DRegisterToHolding_parses_decimal(string d, ushort expected)
=> MelsecAddress.DRegisterToHolding(d).ShouldBe(expected);
[Fact]
public void DRegisterToHolding_honors_bank_base()
=> MelsecAddress.DRegisterToHolding("D10", dBankBase: 4096).ShouldBe((ushort)4106);
[Fact]
public void DRegisterToHolding_rejects_empty()
=> Should.Throw<ArgumentException>(() => MelsecAddress.DRegisterToHolding("D"));
// --- overflow ---
[Fact]
public void XInputToDiscrete_overflow_throws()
{
// 0xFFFF + base 1 = 0x10000 — past ushort.
Should.Throw<OverflowException>(() =>
MelsecAddress.XInputToDiscrete("XFFFF", MelsecFamily.Q_L_iQR, xBankBase: 1));
}
}