Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205StringQuirkTests.cs
Joseph Doherty fdb268cee0 Docs + code-comment sweep — remove stale Pymodbus/ + PythonSnap7/ + LocateBinary references left behind by the native-fallback removal PR. Answer to "is the dev inventory + documentation updated": it was partial; this PR finishes the job.
Files touched — docs/drivers/Modbus-Test-Fixture.md dropped the key-files pointer at deleted Pymodbus/ + flipped "primary launcher is Docker, native fallback retained" framing to "Docker is the only supported launch path" (matching the code). docs/v2/dev-environment.md dropped the "skips both Docker + native-binary paths" parenthetical from AB_SERVER_ENDPOINT + flipped the "Native fallbacks" subsection to a one-liner that says Docker is the only supported path. docs/v2/modbus-test-plan.md rewrote §Harness from "pip install pymodbus + serve.ps1" setup pattern to "docker compose --profile <…> up" + updated the §PR 43 status bullet to point at Docker/profiles/. docs/v2/test-data-sources.md §"CI fixture (task #180)" rewrote the AB CIP section from "LocateBinary() picks binary off PATH" + GitHub Actions zip-download step to "Docker is the only supported reproducible build path" + docker compose GitHub Actions step; dropped the pinned-version SHA256 table + lock-file reference because the Dockerfile's LIBPLCTAG_TAG build-arg is the new pin.

Code docstrings + error messages — these are developer-facing operational text too. ModbusSimulatorFixture SkipReason strings (both branches) now point at `docker compose -f Docker/docker-compose.yml --profile standard up -d` instead of the deleted `Pymodbus\serve.ps1`; doc-comment at the top references Docker/docker-compose.yml. Snap7ServerFixture SkipReason strings + doc-comment point at Docker/docker-compose.yml instead of PythonSnap7/serve.ps1. S7_1500Profile.cs docstring updated. Modbus Dockerfile comment pointing at deleted tests/.../Pymodbus/README.md redirected to docs/drivers/Modbus-Test-Fixture.md. DL205Profile.cs + DL205StringQuirkTests.cs + S7_1500Profile.cs (in Modbus project) docstrings flipped from Pymodbus/*.json references to Docker/profiles/*.json.

Left untouched deliberately: docs/v2/implementation/exit-gate-phase-2-closed.md — that's a historical as-of-2026-04-18 snapshot documenting what was skipped at Phase 2 closure; rewriting would lose the date-stamped context. Its "oitc/modbus-server Docker container not started" + "ab_server binary not on PATH" lines describe the fixture landscape that existed at close time, not current operational guidance.

Final sweep confirms zero remaining `Pymodbus/` / `PythonSnap7/` / `LocateBinary` / `AbServerSeedTag` / `BuildCliArgs` / `AbServerPlcArg` mentions anywhere in tracked files outside that historical exit-gate doc. Whole-solution build still 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:36:19 -04:00

82 lines
3.7 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
/// <summary>
/// Verifies the DL205/DL260 low-byte-first ASCII string packing quirk against the
/// <c>dl205.json</c> pymodbus profile. Standard Modbus packs the first char of each pair
/// in the high byte of the register; DirectLOGIC packs it in the low byte instead. Without
/// <see cref="ModbusStringByteOrder.LowByteFirst"/> the driver decodes "eHllo" garbage
/// even though the bytes on the wire are identical.
/// </summary>
/// <remarks>
/// <para>
/// Requires the dl205 profile (<c>docker compose -f Docker/docker-compose.yml --profile dl205 up</c>). The standard
/// profile does not seed HR[1040..1042] with string bytes, so running this against the
/// standard profile returns <c>"\0\0\0\0\0"</c> and the test fails. Skip when the env
/// var <c>MODBUS_SIM_PROFILE</c> is not set to <c>dl205</c>.
/// </para>
/// </remarks>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "DL205")]
public sealed class DL205StringQuirkTests(ModbusSimulatorFixture sim)
{
[Fact]
public async Task DL205_string_low_byte_first_decodes_Hello_from_HR1040()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
if (!string.Equals(Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE"), "dl205",
StringComparison.OrdinalIgnoreCase))
{
Assert.Skip("MODBUS_SIM_PROFILE != dl205 — skipping (standard profile does not seed HR[1040..1042]).");
}
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition(
Name: "DL205_Hello_Low",
Region: ModbusRegion.HoldingRegisters,
Address: 1040,
DataType: ModbusDataType.String,
Writable: false,
StringLength: 5,
StringByteOrder: ModbusStringByteOrder.LowByteFirst),
// Control: same address, HighByteFirst, to prove the driver would have decoded
// garbage without the quirk flag.
new ModbusTagDefinition(
Name: "DL205_Hello_High",
Region: ModbusRegion.HoldingRegisters,
Address: 1040,
DataType: ModbusDataType.String,
Writable: false,
StringLength: 5,
StringByteOrder: ModbusStringByteOrder.HighByteFirst),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-string");
await driver.InitializeAsync(driverConfigJson: "{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(["DL205_Hello_Low", "DL205_Hello_High"],
TestContext.Current.CancellationToken);
results.Count.ShouldBe(2);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe("Hello", "DL205 low-byte-first ordering must produce 'Hello' from HR[1040..1042]");
// The high-byte-first read of the same wire bytes should differ — not asserting the
// exact garbage string (that would couple the test to the ASCII byte math) but the two
// decodes MUST disagree, otherwise the quirk flag is a no-op.
results[1].StatusCode.ShouldBe(0u);
results[1].Value.ShouldNotBe("Hello");
}
}