Files
histsdk/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs
T
Joseph Doherty 85ff1b48df R0.1 browse over gRPC SHIPPED — QueryTag cracked, M0 gRPC parity complete
Wires HistorianClient.BrowseTagNamesAsync over gRPC (Transport==RemoteGrpc) via
Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync: StartTagQuery(OData) -> paged
QueryTag -> EndTagQuery. Live-verified against a real 2023 R2 server (returns Sys* tags).

QueryTag packet-id recovered WITHOUT native disassembly: a .rdata packet-descriptor
table in aahClientManaged.dll lists {0x6751,1}=StartTagQuery immediately followed by
{0x6752,1}=QueryTag (found via pefile byte-scan of .rdata), confirmed live.

Wire format (live-verified):
- request btRequest = u16 0x6752 + u16 version(1) + u16 queryType(1=names) + u32 startIndex + u32 count
- response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (NextIndex/metadata, ignored)
- new HistorianTagQueryProtocol.ParseTagNameQueryPage tolerates the trailer
- GlobToODataFilter translates the SDK glob filter to OData (Pre*->startswith, *suf->endswith,
  *mid*->contains, exact->eq); the 2023 R2 metadata-server parses filters as OData.

Replaces the earlier RE probe helpers with the shipped browse path. Adds golden-byte
(BuildQueryTagRequest) + 8 glob-translation unit tests + gated live browse test.
226 unit tests pass; 5/5 live gRPC tests pass (read, probe, system-param, metadata, browse).

Milestone 0 (full gRPC parity) is complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-21 16:01:15 -04:00

134 lines
5.3 KiB
C#

using AVEVA.Historian.Client.Models;
namespace AVEVA.Historian.Client.Tests;
/// <summary>
/// Live integration tests for the 2023 R2 RemoteGrpc transport. Gated on a dedicated
/// <c>HISTORIAN_GRPC_HOST</c> env var (plus <c>HISTORIAN_TEST_TAG</c>) so they skip cleanly until
/// a 2023 R2 Historian is available. Optional:
/// HISTORIAN_GRPC_PORT (default 32565), HISTORIAN_GRPC_TLS (true/false),
/// HISTORIAN_USER / HISTORIAN_PASSWORD (explicit creds; otherwise IntegratedSecurity),
/// HISTORIAN_GRPC_DNSID (server certificate name when connecting by IP over TLS).
/// </summary>
public sealed class HistorianGrpcIntegrationTests
{
[Fact]
public async Task ProbeAsync_OverGrpc_ReturnsTrue()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
if (string.IsNullOrWhiteSpace(host))
{
return;
}
// ProbeAsync calls the unauthenticated GetInterfaceVersion RPCs, so it succeeds even when
// credentials are unavailable — no HISTORIAN_USER/PASSWORD required.
HistorianClient client = new(BuildOptions(host));
Assert.True(await client.ProbeAsync(CancellationToken.None));
}
[Fact]
public async Task ReadRawAsync_OverGrpc_ReturnsAtLeastOneRow()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag))
{
return;
}
HistorianClient client = new(BuildOptions(host));
DateTime endUtc = DateTime.UtcNow;
DateTime startUtc = endUtc - TimeSpan.FromDays(7);
List<HistorianSample> samples = [];
await foreach (HistorianSample sample in client.ReadRawAsync(testTag, startUtc, endUtc, maxValues: 8, CancellationToken.None))
{
samples.Add(sample);
}
Assert.NotEmpty(samples);
Assert.All(samples, s => Assert.Equal(testTag, s.TagName));
}
[Fact]
public async Task GetSystemParameterAsync_OverGrpc_ReturnsValue()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
HistorianClient client = new(BuildOptions(host));
string? version = await client.GetSystemParameterAsync("HistorianVersion", CancellationToken.None);
Assert.False(string.IsNullOrWhiteSpace(version));
}
[Fact]
public async Task GetTagMetadataAsync_OverGrpc_ReturnsRequestedTag()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
string? tag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(tag)
|| string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
HistorianClient client = new(BuildOptions(host));
HistorianTagMetadata? metadata = await client.GetTagMetadataAsync(tag, CancellationToken.None);
Assert.NotNull(metadata);
Assert.Equal(tag, metadata!.Name);
// A real metadata record decodes to a known data type (descriptor passed MapDataType).
Assert.True(Enum.IsDefined(metadata.DataType));
}
[Fact]
public async Task BrowseTagNamesAsync_OverGrpc_ReturnsSystemTags()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
// Full R0.1 browse over gRPC: StartTagQuery(OData) -> paged QueryTag(0x6752) -> EndTagQuery.
HistorianClient client = new(BuildOptions(host));
List<string> names = [];
await foreach (string name in client.BrowseTagNamesAsync("Sys*", CancellationToken.None))
{
names.Add(name);
}
Assert.NotEmpty(names);
Assert.All(names, n => Assert.StartsWith("Sys", n, StringComparison.Ordinal));
}
private static HistorianClientOptions BuildOptions(string host)
{
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
bool explicitCreds = !string.IsNullOrEmpty(user);
int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_PORT"), out int parsed)
? parsed
: HistorianClientOptions.DefaultGrpcPort;
bool tls = string.Equals(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TLS"), "true", StringComparison.OrdinalIgnoreCase);
return new HistorianClientOptions
{
Host = host,
Port = port,
Transport = HistorianTransport.RemoteGrpc,
GrpcUseTls = tls,
AllowUntrustedServerCertificate = tls,
ServerDnsIdentity = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_DNSID"),
IntegratedSecurity = !explicitCreds,
UserName = user ?? string.Empty,
Password = password ?? string.Empty
};
}
}