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,122 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;
/// <summary>
/// End-to-end verification that the driver's <c>MapModbusExceptionToStatus</c>
/// translation is wire-correct for every exception code in the mapping table —
/// not just 0x02, which is the only code the pymodbus simulator naturally emits.
/// Drives the standalone <c>exception_injector.py</c> server (<c>exception_injection</c>
/// compose profile) at each of the rule addresses in
/// <c>Docker/profiles/exception_injection.json</c> and asserts the driver surfaces
/// the expected OPC UA StatusCode.
/// </summary>
/// <remarks>
/// Why integration coverage on top of the unit tests: the unit tests prove the
/// translation function is correct; these prove the driver wires it through on
/// the read + write paths unchanged, after the MBAP header + PDU round-trip
/// (where a subtle framing bug could swallow or misclassify the exception).
/// </remarks>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "ExceptionInjection")]
public sealed class ExceptionInjectionTests(ModbusSimulatorFixture sim)
{
private const uint StatusGood = 0u;
private const uint StatusBadOutOfRange = 0x803C0000u;
private const uint StatusBadNotSupported = 0x803D0000u;
private const uint StatusBadDeviceFailure = 0x80550000u;
private const uint StatusBadCommunicationError = 0x80050000u;
private void SkipUnlessInjectorLive()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var profile = Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE");
if (!string.Equals(profile, "exception_injection", StringComparison.OrdinalIgnoreCase))
Assert.Skip("MODBUS_SIM_PROFILE != exception_injection — skipping. " +
"Start the fixture with --profile exception_injection.");
}
private async Task<IReadOnlyList<DataValueSnapshot>> ReadSingleAsync(int address, string tagName)
{
var opts = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition(tagName,
ModbusRegion.HoldingRegisters, Address: (ushort)address,
DataType: ModbusDataType.UInt16, Writable: false),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-exc");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
return await driver.ReadAsync([tagName], TestContext.Current.CancellationToken);
}
[Theory]
[InlineData(1000, StatusBadNotSupported, "exc 0x01 (Illegal Function) -> BadNotSupported")]
[InlineData(1001, StatusBadOutOfRange, "exc 0x02 (Illegal Data Address) -> BadOutOfRange")]
[InlineData(1002, StatusBadOutOfRange, "exc 0x03 (Illegal Data Value) -> BadOutOfRange")]
[InlineData(1003, StatusBadDeviceFailure, "exc 0x04 (Server Failure) -> BadDeviceFailure")]
[InlineData(1004, StatusBadDeviceFailure, "exc 0x05 (Acknowledge / long op) -> BadDeviceFailure")]
[InlineData(1005, StatusBadDeviceFailure, "exc 0x06 (Server Busy) -> BadDeviceFailure")]
[InlineData(1006, StatusBadCommunicationError, "exc 0x0A (Gateway Path Unavailable) -> BadCommunicationError")]
[InlineData(1007, StatusBadCommunicationError, "exc 0x0B (Gateway Target No Response) -> BadCommunicationError")]
public async Task FC03_read_at_injection_address_surfaces_expected_status(
int address, uint expectedStatus, string scenario)
{
SkipUnlessInjectorLive();
var results = await ReadSingleAsync(address, $"Injected_{address}");
results[0].StatusCode.ShouldBe(expectedStatus, scenario);
}
[Fact]
public async Task FC03_read_at_non_injected_address_returns_Good()
{
// Sanity: HR[0..31] are seeded with address-as-value in the profile. A read at
// one of those addresses must come back Good (0) — otherwise the injector is
// misbehaving and every other assertion in this class is uninformative.
SkipUnlessInjectorLive();
var results = await ReadSingleAsync(address: 5, tagName: "Healthy_5");
results[0].StatusCode.ShouldBe(StatusGood);
results[0].Value.ShouldBe((ushort)5);
}
[Theory]
[InlineData(2000, StatusBadDeviceFailure, "exc 0x04 on FC06 -> BadDeviceFailure (CPU in PROGRAM mode)")]
[InlineData(2001, StatusBadDeviceFailure, "exc 0x06 on FC06 -> BadDeviceFailure (Server Busy)")]
public async Task FC06_write_at_injection_address_surfaces_expected_status(
int address, uint expectedStatus, string scenario)
{
SkipUnlessInjectorLive();
var tag = $"InjectedWrite_{address}";
var opts = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition(tag,
ModbusRegion.HoldingRegisters, Address: (ushort)address,
DataType: ModbusDataType.UInt16, Writable: true),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-exc-write");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var writes = await driver.WriteAsync(
[new WriteRequest(tag, (ushort)42)],
TestContext.Current.CancellationToken);
writes[0].StatusCode.ShouldBe(expectedStatus, scenario);
}
}