fix(twincat): TOD write bound, historian note, driver-level refused-write test (#306)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:26:32 -04:00
parent 90e3fe1cf7
commit 05ed039c73
4 changed files with 46 additions and 1 deletions
@@ -191,6 +191,35 @@ public sealed class TwinCATReadWriteTests
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadNotWritable);
}
/// <summary>A refused temporal write (#306) surfaces a status through WriteAsync, never an exception.</summary>
[Theory]
[InlineData(TwinCATDataType.TimeOfDay, 86_400_000d, TwinCATStatusMapper.BadOutOfRange)]
[InlineData(TwinCATDataType.Time, -1d, TwinCATStatusMapper.BadOutOfRange)]
[InlineData(TwinCATDataType.DateTime, "TimeSpan", TwinCATStatusMapper.BadTypeMismatch)]
public async Task Refused_temporal_write_surfaces_status_not_exception(
TwinCATDataType type, object value, uint expected)
{
var (drv, factory) = NewDriver(new TwinCATTagDefinition("T", "ads://5.23.91.23.1.1:851", "MAIN.T", type));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = () => new ConvertingFake();
if (value is "TimeSpan") value = TimeSpan.FromSeconds(1);
var results = await drv.WriteAsync([new WriteRequest("T", value)], CancellationToken.None);
results.Single().StatusCode.ShouldBe(expected);
}
// Runs the production write conversion the fake otherwise skips.
private sealed class ConvertingFake : FakeTwinCATClient
{
public override Task<uint> WriteValueAsync(
string symbolPath, TwinCATDataType type, int? bitIndex, object? value, CancellationToken ct)
{
AdsTwinCATClient.ConvertForWrite(type, value);
return base.WriteValueAsync(symbolPath, type, bitIndex, value, ct);
}
}
/// <summary>Verifies that write exceptions surface as BadCommunicationError status.</summary>
[Fact]
public async Task Write_exception_surfaces_BadCommunicationError()
@@ -53,6 +53,12 @@ public sealed class TwinCATTemporalCodecTests
public void Time_encode_refuses_out_of_range(double value)
=> Should.Throw<OverflowException>(() => TwinCATTemporalCodec.Encode(TwinCATDataType.Time, value));
[Theory]
[InlineData(86_400_000d)]
[InlineData(-1d)]
public void Tod_encode_refuses_a_full_day_or_more(double value)
=> Should.Throw<OverflowException>(() => TwinCATTemporalCodec.Encode(TwinCATDataType.TimeOfDay, value));
[Fact]
public void Dt_before_1970_is_refused()
=> Should.Throw<OverflowException>(() =>