fix(driver-historian-wonderware-client): resolve High code-review finding (Driver.Historian.Wonderware.Client-001)
WonderwareHistorianClient.ReadAtTimeAsync passed the sidecar's reply.Samples straight through ToSnapshots, which violated the IHistorianDataSource contract: the result MUST be the same length and order as the requested timestampsUtc, with gaps returned as Bad-quality snapshots. If the sidecar dropped or reordered samples, OPC UA HistoryReadAtTime would silently misalign values with timestamps. Add an AlignAtTimeSnapshots helper that indexes the returned samples by timestamp ticks, builds the result array at timestampsUtc.Count in request order, and emits a Bad-quality (0x80000000) snapshot for any requested timestamp the sidecar did not return. Add the ReadAtTimeAsync_PartialAndReorderedReply_AlignsByTimestamp_AndFillsGapsAsBad regression test where the fake returns a partial, reordered sample set. Update code-reviews/Driver.Historian.Wonderware.Client/findings.md: -001 Resolved, open-finding count 10 -> 9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,59 @@ public sealed class WonderwareHistorianClientTests
|
||||
result.Samples[1].SourceTimestampUtc.ShouldBe(t2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAtTimeAsync_PartialAndReorderedReply_AlignsByTimestamp_AndFillsGapsAsBad()
|
||||
{
|
||||
var pipe = UniquePipeName();
|
||||
var t1 = new DateTime(2026, 4, 29, 1, 0, 0, DateTimeKind.Utc);
|
||||
var t2 = new DateTime(2026, 4, 29, 2, 0, 0, DateTimeKind.Utc);
|
||||
var t3 = new DateTime(2026, 4, 29, 3, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
await using var server = new FakeSidecarServer(pipe, Secret)
|
||||
{
|
||||
// Sidecar returns only t3 and t1 (out of order), drops t2 entirely. A
|
||||
// contract-compliant client must realign by timestamp and synthesize a
|
||||
// Bad-quality snapshot for the missing t2.
|
||||
OnReadAtTime = _ => new ReadAtTimeReply
|
||||
{
|
||||
Success = true,
|
||||
Samples =
|
||||
[
|
||||
new HistorianSampleDto
|
||||
{
|
||||
ValueBytes = MessagePackSerializer.Serialize<object>(3.0),
|
||||
Quality = 192, TimestampUtcTicks = t3.Ticks,
|
||||
},
|
||||
new HistorianSampleDto
|
||||
{
|
||||
ValueBytes = MessagePackSerializer.Serialize<object>(1.0),
|
||||
Quality = 192, TimestampUtcTicks = t1.Ticks,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
await server.StartAsync();
|
||||
|
||||
await using var client = new WonderwareHistorianClient(OptsFor(pipe));
|
||||
var result = await client.ReadAtTimeAsync("Tank.Level", new[] { t1, t2, t3 }, CancellationToken.None);
|
||||
|
||||
// Result MUST be the same length and order as the request.
|
||||
result.Samples.Count.ShouldBe(3);
|
||||
|
||||
result.Samples[0].SourceTimestampUtc.ShouldBe(t1);
|
||||
result.Samples[0].StatusCode.ShouldBe(0x00000000u); // Good
|
||||
result.Samples[0].Value.ShouldBe(1.0);
|
||||
|
||||
// t2 was not returned by the sidecar → Bad-quality gap snapshot at the requested time.
|
||||
result.Samples[1].SourceTimestampUtc.ShouldBe(t2);
|
||||
result.Samples[1].StatusCode.ShouldBe(0x80000000u); // Bad
|
||||
result.Samples[1].Value.ShouldBeNull();
|
||||
|
||||
result.Samples[2].SourceTimestampUtc.ShouldBe(t3);
|
||||
result.Samples[2].StatusCode.ShouldBe(0x00000000u); // Good
|
||||
result.Samples[2].Value.ShouldBe(3.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadEventsAsync_PreservesEventFields()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user