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,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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user