Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests/WriteCommandParseValueTests.cs
Joseph Doherty 4dc685a365 Task #251 — S7 + TwinCAT test-client CLIs (driver CLI suite complete)
Final two of the five driver test clients. Pattern carried forward from
#249 (Modbus) + #250 (AB CIP, AB Legacy) — each CLI inherits Driver.Cli.Common
for DriverCommandBase + SnapshotFormatter and adds a protocol-specific
CommandBase + 4 commands (probe / read / write / subscribe).

New projects:
  - src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ — otopcua-s7-cli.
    S7CommandBase carries host/port/cpu/rack/slot/timeout. Handles all S7
    atomic types (Bool, Byte, Int16..UInt64, Float32/64, String, DateTime).
    DateTime parses via RoundtripKind so "2026-04-21T12:34:56Z" works.
  - src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ — otopcua-twincat-cli.
    TwinCATCommandBase carries ams-net-id + ams-port + --poll-only toggle
    (flips UseNativeNotifications=false). Covers the full IEC 61131-3
    atomic set: Bool, SInt/USInt, Int/UInt, DInt/UDInt, LInt/ULInt, Real,
    LReal, String, WString, Time/Date/DateTime/TimeOfDay. Structure writes
    refused as out-of-scope (same as AB CIP). IEC time/date variants marshal
    as UDINT on the wire per IEC spec. Subscribe banner announces "ADS
    notification" vs "polling" so the mechanism is obvious in bug reports.

Tests (49 new, 122 cumulative driver-CLI):
  - S7: 22 tests. Every S7DataType has a happy-path + bounds case. DateTime
    round-trips an ISO-8601 string. Tag-name synthesis round-trips every
    S7 address form (DB / M / I / Q, bit/word/dword, strings).
  - TwinCAT: 27 tests. Full IEC type matrix including WString UTF-8 pass-
    through + the four IEC time/date variants landing on UDINT. Structure
    rejection case. Tag-name synthesis for Program scope, GVL scope, nested
    UDT members, and array elements.

Docs:
  - docs/Driver.S7.Cli.md — address grammar cheat sheet + the PUT/GET-must-
    be-enabled gotcha every S7-1200/1500 operator hits.
  - docs/Driver.TwinCAT.Cli.md — AMS router prerequisite (XAR / standalone
    Router NuGet / remote AMS route) + per-command examples.

Wiring:
  - ZB.MOM.WW.OtOpcUa.slnx grew 4 entries (2 src + 2 tests).

Full-solution build clean. Both --help outputs verified end-to-end.

Driver CLI suite complete: 5 CLIs (otopcua-{modbus,abcip,ablegacy,s7,twincat}-cli)
sharing a common base + formatter. 122 CLI tests cumulative. Every driver family
shipped in v2 now has a shell-level ad-hoc validation tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:44:53 -04:00

143 lines
4.4 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests;
/// <summary>
/// Covers <see cref="WriteCommand.ParseValue"/> for the IEC 61131-3 atomic types
/// TwinCAT exposes. Wider matrix than AB CIP because IEC adds WSTRING + the four
/// TIME/DATE variants that all marshal as UDINT on the wire.
/// </summary>
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
[Theory]
[InlineData("true", true)]
[InlineData("0", false)]
[InlineData("on", true)]
[InlineData("NO", false)]
public void ParseValue_Bool_accepts_common_aliases(string raw, bool expected)
{
WriteCommand.ParseValue(raw, TwinCATDataType.Bool).ShouldBe(expected);
}
[Fact]
public void ParseValue_Bool_rejects_garbage()
{
Should.Throw<CliFx.Exceptions.CommandException>(
() => WriteCommand.ParseValue("maybe", TwinCATDataType.Bool));
}
[Fact]
public void ParseValue_SInt_signed_byte()
{
WriteCommand.ParseValue("-128", TwinCATDataType.SInt).ShouldBe((sbyte)-128);
}
[Fact]
public void ParseValue_USInt_unsigned_byte()
{
WriteCommand.ParseValue("255", TwinCATDataType.USInt).ShouldBe((byte)255);
}
[Fact]
public void ParseValue_Int_signed_16bit()
{
WriteCommand.ParseValue("-32768", TwinCATDataType.Int).ShouldBe((short)-32768);
}
[Fact]
public void ParseValue_UInt_unsigned_16bit()
{
WriteCommand.ParseValue("65535", TwinCATDataType.UInt).ShouldBe((ushort)65535);
}
[Fact]
public void ParseValue_DInt_int32_bounds()
{
WriteCommand.ParseValue("-2147483648", TwinCATDataType.DInt).ShouldBe(int.MinValue);
}
[Fact]
public void ParseValue_UDInt_uint32_max()
{
WriteCommand.ParseValue("4294967295", TwinCATDataType.UDInt).ShouldBe(uint.MaxValue);
}
[Fact]
public void ParseValue_LInt_int64_min()
{
WriteCommand.ParseValue("-9223372036854775808", TwinCATDataType.LInt).ShouldBe(long.MinValue);
}
[Fact]
public void ParseValue_ULInt_uint64_max()
{
WriteCommand.ParseValue("18446744073709551615", TwinCATDataType.ULInt).ShouldBe(ulong.MaxValue);
}
[Fact]
public void ParseValue_Real_invariant_culture()
{
WriteCommand.ParseValue("3.14", TwinCATDataType.Real).ShouldBe(3.14f);
}
[Fact]
public void ParseValue_LReal_higher_precision()
{
WriteCommand.ParseValue("2.718281828", TwinCATDataType.LReal).ShouldBeOfType<double>();
}
[Fact]
public void ParseValue_String_passthrough()
{
WriteCommand.ParseValue("hallo beckhoff", TwinCATDataType.String).ShouldBe("hallo beckhoff");
}
[Fact]
public void ParseValue_WString_passthrough()
{
// CLI layer doesn't distinguish UTF-8 input; the driver handles the WSTRING
// encoding on the wire.
WriteCommand.ParseValue("überstall", TwinCATDataType.WString).ShouldBe("überstall");
}
[Theory]
[InlineData(TwinCATDataType.Time)]
[InlineData(TwinCATDataType.Date)]
[InlineData(TwinCATDataType.DateTime)]
[InlineData(TwinCATDataType.TimeOfDay)]
public void ParseValue_IEC_date_time_variants_land_on_uint32(TwinCATDataType type)
{
// IEC 61131-3 TIME / DATE / DT / TOD all marshal as UDINT on the wire; the CLI
// accepts a numeric raw value and lets the caller handle the encoding.
WriteCommand.ParseValue("1234567", type).ShouldBeOfType<uint>();
}
[Fact]
public void ParseValue_Structure_refused()
{
Should.Throw<CliFx.Exceptions.CommandException>(
() => WriteCommand.ParseValue("42", TwinCATDataType.Structure));
}
[Fact]
public void ParseValue_non_numeric_for_numeric_types_throws()
{
Should.Throw<FormatException>(
() => WriteCommand.ParseValue("xyz", TwinCATDataType.DInt));
}
[Theory]
[InlineData("MAIN.bStart", TwinCATDataType.Bool, "MAIN.bStart:Bool")]
[InlineData("GVL.Counter", TwinCATDataType.DInt, "GVL.Counter:DInt")]
[InlineData("Motor1.Status.Running", TwinCATDataType.Bool, "Motor1.Status.Running:Bool")]
[InlineData("Recipe[3]", TwinCATDataType.Real, "Recipe[3]:Real")]
public void SynthesiseTagName_preserves_symbolic_path_verbatim(
string symbol, TwinCATDataType type, string expected)
{
ReadCommand.SynthesiseTagName(symbol, type).ShouldBe(expected);
}
}