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
This commit is contained in:
@@ -745,6 +745,53 @@ public sealed class HistorianClientIntegrationTests
|
||||
Assert.False(string.IsNullOrWhiteSpace(metadata.EngineeringUnit));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadEventsAsync_WithFilter_IsHonoredByServer()
|
||||
{
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
||||
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HistorianClient client = new(new HistorianClientOptions
|
||||
{
|
||||
Host = host,
|
||||
IntegratedSecurity = true,
|
||||
Transport = HistorianTransport.LocalPipe
|
||||
});
|
||||
|
||||
DateTime endUtc = DateTime.UtcNow;
|
||||
DateTime startUtc = endUtc - TimeSpan.FromDays(30);
|
||||
|
||||
// A predicate that matches nothing must return zero events — proving the server applies
|
||||
// the filter (not inert), unlike e.g. the analog-summary knobs.
|
||||
List<AVEVA.Historian.Client.Models.HistorianEvent> noMatch = [];
|
||||
await foreach (var evt in client.ReadEventsAsync(startUtc, endUtc,
|
||||
new AVEVA.Historian.Client.Models.HistorianEventFilter("Type",
|
||||
AVEVA.Historian.Client.Models.HistorianEventComparison.Equal, "ZZZ_NoSuchEventType"),
|
||||
CancellationToken.None))
|
||||
{
|
||||
noMatch.Add(evt);
|
||||
}
|
||||
Assert.Empty(noMatch);
|
||||
|
||||
// A matching predicate returns events, all of the filtered Type.
|
||||
List<AVEVA.Historian.Client.Models.HistorianEvent> matched = [];
|
||||
await foreach (var evt in client.ReadEventsAsync(startUtc, endUtc,
|
||||
new AVEVA.Historian.Client.Models.HistorianEventFilter("Type",
|
||||
AVEVA.Historian.Client.Models.HistorianEventComparison.Equal, "User.Write"),
|
||||
CancellationToken.None))
|
||||
{
|
||||
matched.Add(evt);
|
||||
}
|
||||
|
||||
// Requires User.Write events in the window (present on a working Historian). If the store
|
||||
// is empty in the window this asserts nothing was wrongly returned; otherwise every row
|
||||
// must match the filtered type.
|
||||
Assert.All(matched, evt => Assert.Equal("User.Write", evt.Type));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendEventAsync_AgainstLocalHistorian_AcceptedByServer()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user