213
tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TsapModeTests.cs
Normal file
213
tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TsapModeTests.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
using System.Text.Json;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the PR-S7-C2 TSAP / Connection Type selector. Validates the
|
||||
/// <see cref="S7TsapDefaults"/> raw-byte mapping, the <see cref="TsapMode"/> →
|
||||
/// constructor branching in <see cref="S7Driver"/>, and JSON DTO round-trip on
|
||||
/// <see cref="S7DriverFactoryExtensions"/>. The live-PLC handshake is exercised
|
||||
/// separately (and is hardware-gated to the dev-box S7-1500 lab rig).
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class S7TsapModeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Default_TsapMode_is_Auto_to_preserve_existing_behaviour()
|
||||
{
|
||||
new S7DriverOptions().TsapMode.ShouldBe(TsapMode.Auto);
|
||||
new S7DriverOptions().LocalTsap.ShouldBeNull();
|
||||
new S7DriverOptions().RemoteTsap.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ---- High-byte mapping ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(TsapMode.Pg, S7TsapDefaults.PgClassHighByte)]
|
||||
[InlineData(TsapMode.Op, S7TsapDefaults.OpClassHighByte)]
|
||||
[InlineData(TsapMode.S7Basic, S7TsapDefaults.S7BasicClassHighByte)]
|
||||
public void HighByteFor_returns_the_class_selector_for_each_named_mode(TsapMode mode, byte expected)
|
||||
{
|
||||
S7TsapDefaults.HighByteFor(mode).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TsapMode.Auto)]
|
||||
[InlineData(TsapMode.Other)]
|
||||
public void HighByteFor_throws_for_Auto_and_Other(TsapMode mode)
|
||||
{
|
||||
Should.Throw<ArgumentOutOfRangeException>(() => S7TsapDefaults.HighByteFor(mode));
|
||||
}
|
||||
|
||||
// ---- Raw-byte construction ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(S7TsapDefaults.PgClassHighByte, 0x0100)]
|
||||
[InlineData(S7TsapDefaults.OpClassHighByte, 0x0200)]
|
||||
[InlineData(S7TsapDefaults.S7BasicClassHighByte, 0x0300)]
|
||||
public void BuildLocalTsap_uses_class_high_byte_with_zero_low_byte(byte cls, ushort expected)
|
||||
{
|
||||
S7TsapDefaults.BuildLocalTsap(cls).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// PG / S7-300 default — rack=0 slot=2 → 0x0102 (matches docs/v2/s7.md table).
|
||||
[InlineData(S7TsapDefaults.PgClassHighByte, 0, 2, 0x0102)]
|
||||
// OP / S7-300 default — rack=0 slot=2 → 0x0202.
|
||||
[InlineData(S7TsapDefaults.OpClassHighByte, 0, 2, 0x0202)]
|
||||
// S7-Basic / S7-300 default — rack=0 slot=2 → 0x0302.
|
||||
[InlineData(S7TsapDefaults.S7BasicClassHighByte, 0, 2, 0x0302)]
|
||||
// S7-1200 / S7-1500 onboard PN — rack=0 slot=0 → low byte 0x00.
|
||||
[InlineData(S7TsapDefaults.PgClassHighByte, 0, 0, 0x0100)]
|
||||
// S7-400 distributed — rack=1 slot=3 → low byte = (1<<5)|3 = 0x23.
|
||||
[InlineData(S7TsapDefaults.OpClassHighByte, 1, 3, 0x0223)]
|
||||
public void BuildRemoteTsap_packs_class_rack_and_slot_per_S7_spec(byte cls, int rack, int slot, ushort expected)
|
||||
{
|
||||
S7TsapDefaults.BuildRemoteTsap(cls, rack, slot).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1, 0)]
|
||||
[InlineData(16, 0)]
|
||||
[InlineData(0, -1)]
|
||||
[InlineData(0, 32)]
|
||||
public void BuildRemoteTsap_throws_on_out_of_range_rack_or_slot(int rack, int slot)
|
||||
{
|
||||
Should.Throw<ArgumentOutOfRangeException>(() =>
|
||||
S7TsapDefaults.BuildRemoteTsap(S7TsapDefaults.PgClassHighByte, rack, slot));
|
||||
}
|
||||
|
||||
// ---- Driver construction (Other-mode missing-overrides validation) ----
|
||||
|
||||
[Fact]
|
||||
public async Task Other_mode_without_LocalTsap_or_RemoteTsap_throws_at_initialize()
|
||||
{
|
||||
// No LocalTsap / RemoteTsap → BuildPlc should refuse before opening the socket.
|
||||
var opts = new S7DriverOptions
|
||||
{
|
||||
Host = "192.0.2.1",
|
||||
Timeout = TimeSpan.FromMilliseconds(250),
|
||||
TsapMode = TsapMode.Other,
|
||||
};
|
||||
using var drv = new S7Driver(opts, "s7-other-missing");
|
||||
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
|
||||
ex.Message.ShouldContain("LocalTsap");
|
||||
ex.Message.ShouldContain("RemoteTsap");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Other_mode_with_only_LocalTsap_set_still_throws()
|
||||
{
|
||||
var opts = new S7DriverOptions
|
||||
{
|
||||
Host = "192.0.2.1",
|
||||
Timeout = TimeSpan.FromMilliseconds(250),
|
||||
TsapMode = TsapMode.Other,
|
||||
LocalTsap = 0x4D57,
|
||||
// RemoteTsap intentionally null
|
||||
};
|
||||
using var drv = new S7Driver(opts, "s7-other-half");
|
||||
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Other_mode_with_both_overrides_does_not_fail_at_BuildPlc_stage()
|
||||
{
|
||||
// With Other-mode + both overrides, BuildPlc must succeed; the connect itself fails
|
||||
// because 192.0.2.1 is unroutable (RFC 5737), so we expect a transport-shaped Exception
|
||||
// (NOT InvalidOperationException complaining about missing TSAPs).
|
||||
var opts = new S7DriverOptions
|
||||
{
|
||||
Host = "192.0.2.1",
|
||||
Timeout = TimeSpan.FromMilliseconds(250),
|
||||
TsapMode = TsapMode.Other,
|
||||
LocalTsap = 0x4D57,
|
||||
RemoteTsap = 0x4D58,
|
||||
};
|
||||
using var drv = new S7Driver(opts, "s7-other-ok");
|
||||
|
||||
var ex = await Should.ThrowAsync<Exception>(async () =>
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
|
||||
// Should not be the "missing LocalTsap/RemoteTsap" guard — that guard rejects on
|
||||
// Type, this should be a transport / timeout failure shape.
|
||||
(ex is InvalidOperationException io && io.Message.Contains("LocalTsap"))
|
||||
.ShouldBeFalse("Other-mode with both overrides set must pass the BuildPlc guard");
|
||||
}
|
||||
|
||||
// ---- DTO round-trip ----
|
||||
|
||||
[Fact]
|
||||
public void DTO_round_trip_with_TsapMode_Op_and_explicit_overrides()
|
||||
{
|
||||
// Mirrors what the central config DB hands to S7DriverFactoryExtensions.CreateInstance.
|
||||
var json = """
|
||||
{
|
||||
"Host": "192.168.1.30",
|
||||
"CpuType": "S71500",
|
||||
"Rack": 0,
|
||||
"Slot": 0,
|
||||
"TsapMode": "Op",
|
||||
"LocalTsap": 768,
|
||||
"RemoteTsap": 770,
|
||||
"Tags": []
|
||||
}
|
||||
""";
|
||||
var drv = (S7Driver)S7DriverFactoryExtensions.CreateInstance("s7-dto", json);
|
||||
drv.ShouldNotBeNull();
|
||||
drv.DriverInstanceId.ShouldBe("s7-dto");
|
||||
drv.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DTO_unknown_TsapMode_is_rejected()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Host": "192.168.1.30",
|
||||
"TsapMode": "ProfiNet",
|
||||
"Tags": []
|
||||
}
|
||||
""";
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
S7DriverFactoryExtensions.CreateInstance("s7-bad-mode", json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DTO_omitting_TsapMode_falls_back_to_Auto()
|
||||
{
|
||||
// Critical for backwards-compat: pre-PR-S7-C2 configs must still load.
|
||||
var json = """
|
||||
{
|
||||
"Host": "192.168.1.30",
|
||||
"Tags": []
|
||||
}
|
||||
""";
|
||||
var drv = S7DriverFactoryExtensions.CreateInstance("s7-legacy", json);
|
||||
drv.ShouldNotBeNull();
|
||||
drv.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DTO_round_trip_serialise_then_deserialise_preserves_TSAP_fields()
|
||||
{
|
||||
// Round-trip through the DTO type the factory uses, to catch property-name mismatches.
|
||||
var dto = new S7DriverFactoryExtensions.S7DriverConfigDto
|
||||
{
|
||||
Host = "10.0.0.5",
|
||||
TsapMode = "S7Basic",
|
||||
LocalTsap = 0x0300,
|
||||
RemoteTsap = 0x0302,
|
||||
};
|
||||
var json = JsonSerializer.Serialize(dto);
|
||||
var back = JsonSerializer.Deserialize<S7DriverFactoryExtensions.S7DriverConfigDto>(json)!;
|
||||
back.TsapMode.ShouldBe("S7Basic");
|
||||
back.LocalTsap.ShouldBe((ushort)0x0300);
|
||||
back.RemoteTsap.ShouldBe((ushort)0x0302);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user