0e19adae68
Routes HistorianClient.GetTagMetadataAsync over gRPC when Transport==RemoteGrpc,
via the new Grpc/HistorianGrpcTagClient calling RetrievalService.GetTagInfosFromName
(the plural string-handle metadata op).
- String handle = the Open2 storage-session GUID formatted uppercase (the format
that resolves the native string-handle path); threaded out of the shared handshake
via a new HistorianGrpcHandshake.Session { ClientHandle, StorageSessionId, StringHandle }.
- Request btTagNames = uint count + per-name(uint charCount + UTF-16LE) — golden-byte
unit-tested (BuildTagNamesBuffer).
- Response btTagInfos = uint count + CTagMetadata records — decoded by the existing
HistorianTagQueryProtocol.ParseGetTagInfoResponse; data type via the shared MapDataType.
The 2020 WCF string-handle wall does NOT apply on the gRPC front door, as the
string-handle-wall RE note predicted. LIVE-VERIFIED against a real 2023 R2 server:
GetTagMetadataAsync returns the requested tag with a valid decoded data type.
216 unit tests pass. Captured framing confirmed live then discarded; no tag names
or identities committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
167 lines
7.2 KiB
C#
167 lines
7.2 KiB
C#
using AVEVA.Historian.Client.Grpc;
|
|
using AVEVA.Historian.Client.Models;
|
|
using AVEVA.Historian.Client.Wcf;
|
|
using Google.Protobuf;
|
|
using ArchestrA.Grpc.Contract.Retrieval;
|
|
using GrpcHistory = ArchestrA.Grpc.Contract.History;
|
|
|
|
namespace AVEVA.Historian.Client.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit coverage for the 2023 R2 RemoteGrpc transport — the parts that do not require a live
|
|
/// server: channel address/port resolution, metadata, transport routing, and the invariant that
|
|
/// gRPC request messages carry the same native byte buffers the WCF path uses.
|
|
/// </summary>
|
|
public sealed class HistorianGrpcTransportTests
|
|
{
|
|
private static HistorianClientOptions Options(
|
|
string host = "histserver",
|
|
int port = HistorianClientOptions.DefaultPort,
|
|
bool tls = false,
|
|
string? dnsIdentity = null,
|
|
bool compression = false) => new()
|
|
{
|
|
Host = host,
|
|
Port = port,
|
|
Transport = HistorianTransport.RemoteGrpc,
|
|
GrpcUseTls = tls,
|
|
ServerDnsIdentity = dnsIdentity,
|
|
Compression = compression,
|
|
IntegratedSecurity = true
|
|
};
|
|
|
|
[Fact]
|
|
public void ResolvePort_DefaultWcfPort_SubstitutesGrpcDefault()
|
|
{
|
|
Assert.Equal(HistorianClientOptions.DefaultGrpcPort, HistorianGrpcChannelFactory.ResolvePort(Options(port: HistorianClientOptions.DefaultPort)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolvePort_ExplicitPort_IsHonoured()
|
|
{
|
|
Assert.Equal(443, HistorianGrpcChannelFactory.ResolvePort(Options(port: 443)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAddress_Plaintext_UsesHttpAndHost()
|
|
{
|
|
Assert.Equal("http://histserver:32565", HistorianGrpcChannelFactory.ResolveAddress(Options()));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAddress_Tls_UsesHttpsAndHostWhenNoDnsIdentity()
|
|
{
|
|
Assert.Equal("https://histserver:32565", HistorianGrpcChannelFactory.ResolveAddress(Options(tls: true)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAddress_Tls_PrefersDnsIdentityForCertMatch()
|
|
{
|
|
string address = HistorianGrpcChannelFactory.ResolveAddress(Options(host: "10.0.0.5", tls: true, dnsIdentity: "localhost"));
|
|
Assert.Equal("https://localhost:32565", address);
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_CompressionDisabled_EmitsNoEncodingHeader()
|
|
{
|
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(Options(compression: false));
|
|
Assert.DoesNotContain(connection.Metadata, e => e.Key == "grpc-internal-encoding-request");
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_CompressionEnabled_AdvertisesGzipRequestEncoding()
|
|
{
|
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(Options(compression: true));
|
|
global::Grpc.Core.Metadata.Entry entry = Assert.Single(connection.Metadata, e => e.Key == "grpc-internal-encoding-request");
|
|
Assert.Equal("gzip", entry.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void StartQueryRequest_CarriesNativeDataQueryBufferUnchanged()
|
|
{
|
|
// The gRPC envelope must wrap the exact bytes the WCF StartQuery2 path sends, so the
|
|
// already-reverse-engineered DataQueryRequest serializer is reused verbatim.
|
|
HistorianDataQueryRequest request = HistorianWcfReadOrchestrator.BuildDataQueryRequest(
|
|
"Tag.Counter", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc), 100);
|
|
byte[] nativeBuffer = HistorianDataQueryProtocol.SerializeFullHistoryRequest(request);
|
|
|
|
var message = new StartQueryRequest
|
|
{
|
|
UiHandle = 7,
|
|
UiQueryRequestType = HistorianDataQueryProtocol.QueryRequestTypeData,
|
|
BtRequestBuffer = ByteString.CopyFrom(nativeBuffer)
|
|
};
|
|
|
|
// Round-trip through protobuf and confirm the native buffer survives byte-for-byte.
|
|
byte[] wire = message.ToByteArray();
|
|
var decoded = StartQueryRequest.Parser.ParseFrom(wire);
|
|
Assert.Equal(nativeBuffer, decoded.BtRequestBuffer.ToByteArray());
|
|
Assert.Equal(7u, decoded.UiHandle);
|
|
Assert.Equal((uint)HistorianDataQueryProtocol.QueryRequestTypeData, decoded.UiQueryRequestType);
|
|
}
|
|
|
|
[Fact]
|
|
public void InterfaceVersionResponses_ExposeErrorAndVersion_AsProbeExpects()
|
|
{
|
|
// R0.4 ProbeAsync reads uiError/uiVersion off each service's GetInterfaceVersion response.
|
|
// Pin that field mapping (success = uiError 0 + uiVersion > 0) via a protobuf round-trip.
|
|
var history = GrpcHistory.GetInterfaceVersionResponse.Parser.ParseFrom(
|
|
new GrpcHistory.GetInterfaceVersionResponse { UiError = 0, UiVersion = 12 }.ToByteArray());
|
|
var retrieval = GetRetrievalInterfaceVersionResponse.Parser.ParseFrom(
|
|
new GetRetrievalInterfaceVersionResponse { UiError = 0, UiVersion = 4 }.ToByteArray());
|
|
|
|
Assert.Equal(0u, history.UiError);
|
|
Assert.Equal(12u, history.UiVersion);
|
|
Assert.Equal(0u, retrieval.UiError);
|
|
Assert.Equal(4u, retrieval.UiVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSystemParameterMessages_CarryHandleNameAndValue_AsStatusClientExpects()
|
|
{
|
|
// R0.3 sends {uiHandle, strParameterName} and reads strParameterValue when status succeeds.
|
|
var request = ArchestrA.Grpc.Contract.Status.GetSystemParameterRequest.Parser.ParseFrom(
|
|
new ArchestrA.Grpc.Contract.Status.GetSystemParameterRequest { UiHandle = 9, StrParameterName = "HistorianVersion" }.ToByteArray());
|
|
Assert.Equal(9u, request.UiHandle);
|
|
Assert.Equal("HistorianVersion", request.StrParameterName);
|
|
|
|
var response = ArchestrA.Grpc.Contract.Status.GetSystemParameterResponse.Parser.ParseFrom(
|
|
new ArchestrA.Grpc.Contract.Status.GetSystemParameterResponse
|
|
{
|
|
Status = new ArchestrA.Grpc.Contract.RequestStatus.Status { BSuccess = true },
|
|
StrParameterValue = "20.0.000"
|
|
}.ToByteArray());
|
|
Assert.True(response.Status.BSuccess);
|
|
Assert.Equal("20.0.000", response.StrParameterValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildTagNamesBuffer_EncodesCountThenLengthPrefixedUtf16Names()
|
|
{
|
|
// R0.2 request framing: uint count + per-name(uint charCount + UTF-16LE). Golden bytes.
|
|
byte[] buffer = AVEVA.Historian.Client.Grpc.HistorianGrpcTagClient.BuildTagNamesBuffer(["AB", "C"]);
|
|
|
|
byte[] expected =
|
|
[
|
|
0x02, 0x00, 0x00, 0x00, // count = 2
|
|
0x02, 0x00, 0x00, 0x00, // "AB" char count = 2
|
|
0x41, 0x00, 0x42, 0x00, // 'A','B' UTF-16LE
|
|
0x01, 0x00, 0x00, 0x00, // "C" char count = 1
|
|
0x43, 0x00 // 'C' UTF-16LE
|
|
];
|
|
Assert.Equal(expected, buffer);
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenConnectionRequest_CarriesNativeOpen2BufferUnchanged()
|
|
{
|
|
byte[] open2 = HistorianNativeHandshake.BuildOpenConnection3Request(
|
|
"histserver", Guid.NewGuid(), HistorianWcfAuthChainHelper.NativeIntegratedReadOnlyConnectionMode);
|
|
|
|
var message = new GrpcHistory.OpenConnectionRequest { BtConnectionRequest = ByteString.CopyFrom(open2) };
|
|
var decoded = GrpcHistory.OpenConnectionRequest.Parser.ParseFrom(message.ToByteArray());
|
|
|
|
Assert.Equal(open2, decoded.BtConnectionRequest.ToByteArray());
|
|
}
|
|
}
|