fix(s7): numeric TSAP loads in form, shared TSAP conflict error, Test Connect read detects PUT/GET disabled, pin Int32 ms rounding (#632 #638)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:38:49 -04:00
parent 34c52defb3
commit ff055ebd79
10 changed files with 171 additions and 3 deletions
@@ -188,4 +188,46 @@ public sealed class S7TsapAndPduTests
public void Dispose() => IsConnected = false;
}
}
[Fact]
public void Factory_conflict_message_matches_the_shared_detail()
{
var ex = Should.Throw<InvalidOperationException>(() => S7DriverFactoryExtensions.ParseOptions("d",
"""{"host":"h","remoteTsap":"03.01","connectionType":"OP"}"""));
ex.Message.ShouldContain(S7DriverOptions.TsapConflictDetail);
}
// ---- Test Connect data-access check (PUT/GET) ----
[Fact]
public async Task Probe_data_access_succeeds_when_the_read_works() =>
(await S7DriverProbe.CheckDataAccessAsync(_ => Task.FromResult(new byte[2]), TestContext.Current.CancellationToken))
.ShouldBeNull();
[Fact]
public async Task Probe_fails_with_actionable_message_when_put_get_is_disabled()
{
var denied = new global::S7.Net.PlcException(global::S7.Net.ErrorCode.ReadData,
new Exception("Accessing object not allowed"));
var r = await S7DriverProbe.CheckDataAccessAsync(_ => throw denied, TestContext.Current.CancellationToken);
r.ShouldNotBeNull().Ok.ShouldBeFalse();
r.Message!.ShouldContain("PUT/GET");
}
[Fact]
public async Task Probe_tolerates_an_address_not_available_read_error()
{
var missing = new global::S7.Net.PlcException(global::S7.Net.ErrorCode.ReadData, new Exception("Object does not exist"));
(await S7DriverProbe.CheckDataAccessAsync(_ => throw missing, TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
public async Task Probe_reports_any_other_read_fault_as_a_failure()
{
var r = await S7DriverProbe.CheckDataAccessAsync(
_ => throw new global::S7.Net.PlcException(global::S7.Net.ErrorCode.ConnectionError, "socket dropped"),
TestContext.Current.CancellationToken);
r.ShouldNotBeNull().Ok.ShouldBeFalse();
r.Message!.ShouldContain("socket dropped");
}
}
@@ -221,6 +221,16 @@ public sealed class S7ValueCodecTests
Encode(S7DataType.Tod, TimeSpan.FromHours(1)).ShouldBe([0, 0x36, 0xEE, 0x80]);
}
[Fact]
public void Fractional_millisecond_double_rounds_like_the_other_int32_writes()
{
// Pinned: matches BoxValueForWrite's Convert.ToInt32 behaviour (banker's), see docs.
Encode(S7DataType.Tod, 100.7).ShouldBe(Encode(S7DataType.Tod, 101));
Encode(S7DataType.Time, 100.7).ShouldBe(Encode(S7DataType.Time, 101));
Encode(S7DataType.S5Time, 99.7).ShouldBe(Encode(S7DataType.S5Time, 100));
Should.Throw<S7ValueRangeException>(() => Encode(S7DataType.S5Time, 100.7)); // rounds to 101: off the grid
}
// ---- TIME ----
[Theory]
@@ -138,4 +138,28 @@ public sealed class S7DriverPageFormSerializationTests
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter() } });
json.ShouldContain("\"connectionType\":\"PG\"");
}
[Fact]
public void Numeric_tsap_loads_into_the_form_model_instead_of_falling_back_to_defaults()
{
// The factory accepts a JSON number; the form binds the same document the same way, so a Save
// cannot silently replace the authored config with defaults.
var opts = JsonSerializer.Deserialize<S7DriverOptions>(
"""{"remoteTsap":769,"localTsap":"01.00","cpuType":"S7300"}""",
new JsonSerializerOptions { PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter() } });
opts.ShouldNotBeNull();
var back = S7DriverForm.FormModel.FromOptions(opts).ToOptions();
back.RemoteTsap.ShouldBe("03.01");
back.LocalTsap.ShouldBe("01.00");
back.CpuType.ShouldBe(S7CpuType.S7300);
}
[Fact]
public void Form_reports_the_factory_conflict_error_for_remote_tsap_plus_connection_type()
{
var form = new S7DriverForm.FormModel { RemoteTsap = "03.01", ConnectionTypeText = "PG" };
form.TsapConflictError().ShouldNotBeNull().ShouldContain(S7DriverOptions.TsapConflictDetail);
new S7DriverForm.FormModel { RemoteTsap = "03.01" }.TsapConflictError().ShouldBeNull();
new S7DriverForm.FormModel { ConnectionTypeText = "PG" }.TsapConflictError().ShouldBeNull();
}
}