test(grpc): multi-group ext-prop golden + ConnStatus + tightened write read-back
- WcfTagExtendedPropertyProtocolTests: add a multi-group golden test mirroring the live capture (one group per property + uint16 flags trailer) that the old parser failed; correct the synthetic builder to the uint16-flags trailer. - HistorianGrpcIntegrationTests: add GetConnectionStatusAsync_OverGrpc_ReportsConnected (plan #5); tighten the write-lifecycle read-back to a hard assert now that the parser is fixed; make sandbox cleanup generous best-effort (rename is async + the browse view is eventually consistent, so a hard absence assert was racy). 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:
@@ -440,20 +440,11 @@ public sealed class HistorianGrpcIntegrationTests
|
||||
bool propAdded = await client.AddTagExtendedPropertyAsync(sandbox!, "GrpcToolingTest", "ok", CancellationToken.None);
|
||||
Assert.True(propAdded, "AddTagExtendedProperties over gRPC should succeed.");
|
||||
|
||||
// Read-back is best-effort. The write is already confirmed by AddTagExtendedProperties
|
||||
// returning success above; the shared GetTepByNm parser has a known evidence gap for some
|
||||
// written value encodings (surfaced live 2026-06-22: value marker 0x01 where the parser
|
||||
// expects the compact-string 0x09). Don't let that read-side gap block verifying the
|
||||
// remaining write ops (rename + delete).
|
||||
try
|
||||
{
|
||||
IReadOnlyList<HistorianTagExtendedProperty> props = await client.GetTagExtendedPropertiesAsync(sandbox!, CancellationToken.None);
|
||||
Assert.Contains(props, p => string.Equals(p.Name, "GrpcToolingTest", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch (ProtocolEvidenceMissingException)
|
||||
{
|
||||
// Known extended-property read-back parser gap — write already confirmed above.
|
||||
}
|
||||
// Read the written property back: confirms AddTagExtendedProperties round-trips AND that the
|
||||
// shared GetTepByNm parser handles the multi-group / uint16-flags response shape captured live
|
||||
// 2026-06-22 (the earlier 0x01-vs-0x09 drift is fixed).
|
||||
IReadOnlyList<HistorianTagExtendedProperty> props = await client.GetTagExtendedPropertiesAsync(sandbox!, CancellationToken.None);
|
||||
Assert.Contains(props, p => string.Equals(p.Name, "GrpcToolingTest", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Rename is an async StartJob; the server can transiently reject it right after the create
|
||||
// commits. Retry a few times before asserting.
|
||||
@@ -471,24 +462,17 @@ public sealed class HistorianGrpcIntegrationTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup of whichever name survives (rename is an async server job). Retry both names a few
|
||||
// times so neither the pending rename job nor delete propagation leaves litter on the shared
|
||||
// server, then confirm absence.
|
||||
for (int attempt = 0; attempt < 5; attempt++)
|
||||
// Cleanup of whichever name survives. Rename is an async server job, so _R may only appear a
|
||||
// moment after the job runs; delete BOTH names across a generous window so neither the pending
|
||||
// rename nor metadata-server lag leaves litter on the shared server. Best-effort by design —
|
||||
// the browse/metadata view is eventually consistent, so a hard absence assert here would be
|
||||
// racy. The next run's pre-clean is the backstop.
|
||||
for (int attempt = 0; attempt < 8; attempt++)
|
||||
{
|
||||
try { await client.DeleteTagAsync(sandbox!, CancellationToken.None); } catch { /* ignore */ }
|
||||
try { await client.DeleteTagAsync(renamed, CancellationToken.None); } catch { /* ignore */ }
|
||||
|
||||
if (!await TagExistsAsync(client, sandbox!) && !await TagExistsAsync(client, renamed))
|
||||
{
|
||||
break;
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
// No litter must remain on the shared server.
|
||||
Assert.False(await TagExistsAsync(client, sandbox!), $"sandbox tag '{sandbox}' should be deleted.");
|
||||
Assert.False(await TagExistsAsync(client, renamed), $"renamed tag '{renamed}' should be deleted.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,18 +509,26 @@ public sealed class HistorianGrpcIntegrationTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>True if a tag with exactly <paramref name="name"/> is browsable on the server.</summary>
|
||||
private static async Task<bool> TagExistsAsync(HistorianClient client, string name)
|
||||
[Fact]
|
||||
public async Task GetConnectionStatusAsync_OverGrpc_ReportsConnected()
|
||||
{
|
||||
await foreach (string n in client.BrowseTagNamesAsync(name, CancellationToken.None))
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
|
||||
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
|
||||
{
|
||||
if (string.Equals(n, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Plan #5: GetConnectionStatus over gRPC is measured from a real handshake (OpenConnection
|
||||
// yields a storage-session GUID). Against a reachable server it reports connected with no error.
|
||||
HistorianClient client = new(BuildOptions(host));
|
||||
HistorianConnectionStatus status = await client.GetConnectionStatusAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(host, status.ServerName);
|
||||
Assert.True(status.ConnectedToServer, $"should be connected: {status.Error}");
|
||||
Assert.True(status.ConnectedToServerStorage);
|
||||
Assert.False(status.ErrorOccurred);
|
||||
Assert.Null(status.Error);
|
||||
Assert.False(status.ConnectedToStoreForward);
|
||||
}
|
||||
|
||||
private static HistorianClientOptions BuildOptions(string host)
|
||||
|
||||
Reference in New Issue
Block a user