Files
histsdk/tests/AVEVA.Historian.Client.Tests/WcfEventQueryProtocolTests.cs
T
Joseph Doherty 6d470eab4a R1.7: server-side event filters — ReadEventsAsync(HistorianEventFilter), live-honored
Roadmap M1 R1.7. Filters are set on the native EventQuery object via
AddEventFilter(property, HistorianComparisionType, value) — NOT EventQueryArgs
(time/count/order only). Found via a new harness --dump-type-members command.

Captured the native filtered StartEventQuery pRequestBuff (Capture-EventFilter.ps1 +
harness --event-filter knob) and diffed Equal(0) vs Contains(12) to isolate the
operator field. Filter block (decoded byte-for-byte):
  ushort 0 + uint filterCount + uint condCount + uint nameLen + name(UTF-16) +
  uint 1 + ushort op + uint 1 + value(0x09-LEN-0x00 compact-ASCII) + byte 0

The filter is REAL, not inert (unlike the analog-summary knobs): a non-matching
predicate returns 0 events; Type=Equal=User.Write returns only User.Write events.
Verified live via both the native harness and the SDK.

- HistorianClient.ReadEventsAsync(start, end, HistorianEventFilter, ct) overload
- HistorianEventFilter + HistorianEventComparison (18 ops, ordinals = native)
- Filter encoding in HistorianEventQueryProtocol (empty-filter path unchanged)
- Golden-byte tests (block match, op field, empty-filter regression) + gated live test

Single string-valued predicate only; multi-filter (OR) / multi-condition (AND via
AddEventFilterCondition) framing is partially captured and not shipped. 216 unit
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-20 18:32:03 -04:00

101 lines
4.8 KiB
C#

using AVEVA.Historian.Client.Models;
using AVEVA.Historian.Client.Wcf;
namespace AVEVA.Historian.Client.Tests;
public sealed class WcfEventQueryProtocolTests
{
// Filter block (offset 0x1E into pRequestBuff) captured from a native
// EventQuery.AddEventFilter("Area", Equal, "RetestFilterArea") StartEventQuery, via
// instrument-wcf-writemessage.
private const string CaptureFilterBlockHex =
"000001000000010000000400000041007200650061000100000000000100000009100052657465737446696c7465724172656100";
private const int FilterBlockOffset = 0x1E;
[Fact]
public void SerializerMatchesInstrumentedNativeEventFilterBlock()
{
HistorianEventQueryAttempt attempt = Assert.Single(HistorianEventQueryProtocol.CreateStartEventQueryAttempts(
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc),
5,
new HistorianEventFilter("Area", HistorianEventComparison.Equal, "RetestFilterArea")));
byte[] expectedBlock = Convert.FromHexString(CaptureFilterBlockHex);
byte[] actualBlock = attempt.RequestBuffer[FilterBlockOffset..(FilterBlockOffset + expectedBlock.Length)];
Assert.Equal("native-filter-version5", attempt.Name);
Assert.Equal(expectedBlock, actualBlock);
}
[Fact]
public void FilterBlockEncodesComparisonOperatorAsUInt16()
{
// Equal = 0, Contains = 12 — the op field is the only byte that differs between them
// (offset 0x38 in pRequestBuff = 0x1A into the filter block).
byte[] equal = HistorianEventQueryProtocol.CreateStartEventQueryAttempts(
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc),
5, new HistorianEventFilter("Area", HistorianEventComparison.Equal, "X"))[0].RequestBuffer;
byte[] contains = HistorianEventQueryProtocol.CreateStartEventQueryAttempts(
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc),
5, new HistorianEventFilter("Area", HistorianEventComparison.Contains, "X"))[0].RequestBuffer;
Assert.Equal(0x38, FilterBlockOffset + 0x1A);
Assert.Equal(0, BitConverter.ToUInt16(equal, 0x38));
Assert.Equal(12, BitConverter.ToUInt16(contains, 0x38));
}
[Fact]
public void NullFilterStillProducesTheEmptyFilterBuffer()
{
HistorianEventQueryAttempt withNull = HistorianEventQueryProtocol.CreateStartEventQueryAttempts(
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 1, 1, 0, 1, 0, DateTimeKind.Utc), 3)[0];
Assert.Equal("native-empty-filter-version5", withNull.Name);
Assert.Equal(65, withNull.RequestBuffer.Length);
}
[Fact]
public void SerializerMatchesInstrumentedNativeEventRequest()
{
HistorianEventQueryAttempt attempt = Assert.Single(HistorianEventQueryProtocol.CreateStartEventQueryAttempts(
new DateTime(2026, 4, 25, 14, 39, 36, 800, DateTimeKind.Utc).AddTicks(1646),
new DateTime(2026, 5, 2, 14, 39, 36, 800, DateTimeKind.Utc).AddTicks(1646),
3));
byte[] expected = Convert.FromBase64String(
"BQBuHAVXwdTcAW5c6X9B2twBAwAAAAAAAAAAAAEAAAAAAAAAAAAAAQADAAAAVQBUAEMAAQEAAAEAAAEAAAAAAAA=");
Assert.Equal(expected, attempt.RequestBuffer);
Assert.Equal("6b955b02087047a3199a8c74f3eee85c3b49aaa29b05de12eff2dd536f2da0d5", attempt.RequestSha256);
}
[Fact]
public void NativeEmptyFilterAttemptMatchesDecompiledSaveOrder()
{
HistorianEventQueryAttempt attempt = Assert.Single(HistorianEventQueryProtocol.CreateStartEventQueryAttempts(
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 1, 1, 0, 1, 0, DateTimeKind.Utc),
3));
byte[] actual = attempt.RequestBuffer;
Assert.Equal("native-empty-filter-version5", attempt.Name);
Assert.Equal(3, HistorianEventQueryProtocol.QueryRequestTypeEvent);
Assert.Equal(65, actual.Length);
Assert.Equal([0x05, 0x00], actual[..2]);
Assert.Equal(3u, BitConverter.ToUInt32(actual, 18));
Assert.Equal(0u, BitConverter.ToUInt32(actual, 22));
Assert.Equal(0, BitConverter.ToUInt16(actual, 26));
Assert.Equal(1, BitConverter.ToUInt16(actual, 28));
Assert.Equal([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], actual[30..37]);
Assert.Equal(65_536u, BitConverter.ToUInt32(actual, 37));
Assert.Equal([0x03, 0x00, 0x00, 0x00, 0x55, 0x00, 0x54, 0x00, 0x43, 0x00], actual[41..51]);
Assert.Equal([0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00], actual[51..61]);
Assert.Equal([0x00, 0x00, 0x00, 0x00], actual[^4..]);
}
}