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