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>
118 lines
3.4 KiB
C#
118 lines
3.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Commands;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests;
|
|
|
|
/// <summary>
|
|
/// Covers <see cref="WriteCommand.ParseValue"/> across every S7 atomic type.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class WriteCommandParseValueTests
|
|
{
|
|
[Theory]
|
|
[InlineData("true", true)]
|
|
[InlineData("0", false)]
|
|
[InlineData("yes", true)]
|
|
[InlineData("OFF", false)]
|
|
public void ParseValue_Bool_accepts_common_aliases(string raw, bool expected)
|
|
{
|
|
WriteCommand.ParseValue(raw, S7DataType.Bool).ShouldBe(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_Bool_rejects_garbage()
|
|
{
|
|
Should.Throw<CliFx.Exceptions.CommandException>(
|
|
() => WriteCommand.ParseValue("maybe", S7DataType.Bool));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_Byte_ranges()
|
|
{
|
|
WriteCommand.ParseValue("0", S7DataType.Byte).ShouldBe((byte)0);
|
|
WriteCommand.ParseValue("255", S7DataType.Byte).ShouldBe((byte)255);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_Int16_signed_range()
|
|
{
|
|
WriteCommand.ParseValue("-32768", S7DataType.Int16).ShouldBe((short)-32768);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_UInt16_unsigned_max()
|
|
{
|
|
WriteCommand.ParseValue("65535", S7DataType.UInt16).ShouldBe((ushort)65535);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_Int32_parses_negative()
|
|
{
|
|
WriteCommand.ParseValue("-2147483648", S7DataType.Int32).ShouldBe(int.MinValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_UInt32_parses_max()
|
|
{
|
|
WriteCommand.ParseValue("4294967295", S7DataType.UInt32).ShouldBe(uint.MaxValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_Int64_parses_min()
|
|
{
|
|
WriteCommand.ParseValue("-9223372036854775808", S7DataType.Int64).ShouldBe(long.MinValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_UInt64_parses_max()
|
|
{
|
|
WriteCommand.ParseValue("18446744073709551615", S7DataType.UInt64).ShouldBe(ulong.MaxValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_Float32_invariant_culture()
|
|
{
|
|
WriteCommand.ParseValue("3.14", S7DataType.Float32).ShouldBe(3.14f);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_Float64_higher_precision()
|
|
{
|
|
WriteCommand.ParseValue("2.718281828", S7DataType.Float64).ShouldBeOfType<double>();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_String_passthrough()
|
|
{
|
|
WriteCommand.ParseValue("hallo siemens", S7DataType.String).ShouldBe("hallo siemens");
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_DateTime_parses_roundtrip_form()
|
|
{
|
|
var result = WriteCommand.ParseValue("2026-04-21T12:34:56Z", S7DataType.DateTime);
|
|
result.ShouldBeOfType<DateTime>();
|
|
((DateTime)result).Year.ShouldBe(2026);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseValue_non_numeric_for_numeric_types_throws()
|
|
{
|
|
Should.Throw<FormatException>(
|
|
() => WriteCommand.ParseValue("xyz", S7DataType.Int16));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("DB1.DBW0", S7DataType.Int16, "DB1.DBW0:Int16")]
|
|
[InlineData("M0.0", S7DataType.Bool, "M0.0:Bool")]
|
|
[InlineData("IW4", S7DataType.UInt16, "IW4:UInt16")]
|
|
[InlineData("QD8", S7DataType.UInt32, "QD8:UInt32")]
|
|
[InlineData("DB10.STRING[0]", S7DataType.String, "DB10.STRING[0]:String")]
|
|
public void SynthesiseTagName_preserves_S7_address_verbatim(
|
|
string address, S7DataType type, string expected)
|
|
{
|
|
ReadCommand.SynthesiseTagName(address, type).ShouldBe(expected);
|
|
}
|
|
}
|