57b9506d01
Live-probed StorageService.OpenStorageConnection against the 2023 R2 server over a
write-enabled (0x401) session. Every attempt — sweeping ConnectionMode (0x401/0x402/0x1),
StorageSessionId-in (Open2-GUID / empty), and FreeDiskSpace — returns the IDENTICAL native
error type=4 code=85 ("session not registered"), so it's a structural refusal, not a bad
field value.
Decode (two corroborating facts):
- Error 85 is the same code the event read returns before RegisterTags2 (see
HistorianWcfEventOrchestrator) — a generic "session not registered for this op".
- The 2023 R2 decompile shows OpenStorageConnection lives on a SEPARATE GrpcStorageClient
(the storage engine's SF/snapshot channel, own port + service identity); HistorianAccess
drives non-streamed writes through the native C++ HistorianClient, never this op.
So the roadmap's mapped "missing console session" step was wrong. The real non-streamed-write
precondition is the front-door HistoryService.RegisterTags (RTag2-family) for the target tag —
which is exactly why the R3.1 batch failed at AddNonStreamValues (no tag registered ->
StoreNonStreamValues had no route). Matches the original 2020-WCF D2 hypothesis.
Remaining (both need a native gRPC capture; do not guess bytes): the regular-tag RegisterTags
btTagInfos (only CM_EVENT's tag-GUID form is known) and the AddNonStreamValues btInput.
- HistorianGrpcStorageConnectionProbe + grpc-open-storage-connection CLI (opens nothing
persistent; CloseStorageConnection on success)
- corrected revision-write-path.md §R3.1 follow-up + hcal-roadmap R3.1/R3.2 rows
- gated regression test pinning the error-85 refusal
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
200 lines
8.9 KiB
C#
200 lines
8.9 KiB
C#
using AVEVA.Historian.Client.Grpc;
|
|
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 GetServerTimeZoneAsync_OverGrpc_ReturnsZone()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// R1.3: gRPC StatusService.GetSystemTimeZoneName returns the real server zone (the 2020 WCF
|
|
// op is a stub). Live-verified value: "Eastern Daylight Time".
|
|
HistorianClient client = new(BuildOptions(host));
|
|
string? zone = await client.GetServerTimeZoneAsync(CancellationToken.None);
|
|
Assert.False(string.IsNullOrWhiteSpace(zone));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NonStreamedWriteTransaction_OverGrpc_BeginsAndDiscards()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// M3 reachability probe: on 2020 WCF this op group is walled (TransactionService relay
|
|
// returns UnknownClient(51) — the storage-engine-pipe requirement, see
|
|
// docs/plans/revision-write-path.md). On the 2023 R2 gRPC front door the native client
|
|
// passes the Open2 storage-session GUID straight to TransactionService and it works.
|
|
// This asserts the wall is gone: a write-enabled session opens and AddNonStreamValuesBegin
|
|
// returns a transaction id, which we immediately End with bCommit=false (writes nothing).
|
|
var probe = new HistorianGrpcRevisionProbe(BuildOptions(host));
|
|
HistorianGrpcRevisionProbeResult result = await probe.ProbeBeginAsync(CancellationToken.None);
|
|
|
|
Assert.True(result.OpenSucceeded);
|
|
Assert.True(result.BeginSucceeded, "AddNonStreamValuesBegin should return a transaction id over gRPC.");
|
|
Assert.False(string.IsNullOrEmpty(result.BeginTransactionId));
|
|
Assert.True(result.EndDiscardSucceeded, "AddNonStreamValuesEnd(bCommit:false) should discard cleanly.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenStorageConnection_OverGrpc_RefusedAsNotRegistered()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// M3 R3.1 follow-up finding (2026-06-21): StorageService.OpenStorageConnection is NOT the
|
|
// missing non-streamed-write precondition. It's the storage engine's SF/snapshot channel
|
|
// (separate GrpcStorageClient / service identity), and on the Historian front door it is
|
|
// refused with native type=4 code=85 ("session not registered") for every parameter combo —
|
|
// the same code the event read returns before RegisterTags2. The real precondition is the
|
|
// front-door HistoryService.RegisterTags (RTag2-family). See docs/plans/revision-write-path.md
|
|
// §"R3.1 follow-up". This test pins the refusal so a future server/behaviour change is noticed.
|
|
var probe = new HistorianGrpcStorageConnectionProbe(BuildOptions(host));
|
|
HistorianGrpcOpenStorageConnectionResult result = await probe.ProbeAsync(CancellationToken.None);
|
|
|
|
Assert.True(result.OpenSucceeded, "the write-enabled gRPC session itself should still open.");
|
|
Assert.False(result.OpenStorageSucceeded, "OpenStorageConnection is not a front-door client op (error 85).");
|
|
Assert.NotEmpty(result.Attempts);
|
|
Assert.All(result.Attempts, a => Assert.False(a.Succeeded));
|
|
}
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|