test(r2-06): HISTGW_BOOL_SANDBOX_TAG fixture + Boolean EnsureTags leg + retype probe (skip-clean offline) (06/U-7)
This commit is contained in:
+7
@@ -22,6 +22,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Live;
|
||||
/// <item><c>HISTGW_GATEWAY_APIKEY</c> — the <c>histgw_<id>_<secret></c> key (must carry <c>historian:read</c> + <c>historian:write</c> scopes). Required; absent ⇒ all tests skip.</item>
|
||||
/// <item><c>HISTGW_TEST_TAG</c> — an existing Galaxy / historian tag for the read round-trip.</item>
|
||||
/// <item><c>HISTGW_WRITE_SANDBOX_TAG</c> — a Float sandbox tag the write round-trip may <c>EnsureTags</c> + write (e.g. <c>HistGW.LiveTest.Sandbox</c>).</item>
|
||||
/// <item><c>HISTGW_BOOL_SANDBOX_TAG</c> — a writable Boolean sandbox tag (e.g. <c>HistGW.LiveTest.BoolSandbox</c>) for the 0.2.0 Boolean <c>EnsureTags</c> leg + the retype probe (archreview 06/U-7). Dedicated — never the shared write sandbox.</item>
|
||||
/// <item><c>HISTGW_ALARM_SOURCE</c> — a source name for the alarm <c>SendEvent</c> → <c>ReadEvents</c> round-trip.</item>
|
||||
/// <item><c>HISTGW_GATEWAY_ALLOW_UNTRUSTED</c> — <c>true</c> to accept a self-signed dev cert (optional).</item>
|
||||
/// </list>
|
||||
@@ -44,6 +45,7 @@ public sealed class GatewayLiveFixture
|
||||
private const string EnvApiKey = "HISTGW_GATEWAY_APIKEY";
|
||||
private const string EnvTestTag = "HISTGW_TEST_TAG";
|
||||
private const string EnvWriteSandboxTag = "HISTGW_WRITE_SANDBOX_TAG";
|
||||
private const string EnvBoolSandboxTag = "HISTGW_BOOL_SANDBOX_TAG";
|
||||
private const string EnvAlarmSource = "HISTGW_ALARM_SOURCE";
|
||||
private const string EnvAllowUntrusted = "HISTGW_GATEWAY_ALLOW_UNTRUSTED";
|
||||
|
||||
@@ -68,6 +70,7 @@ public sealed class GatewayLiveFixture
|
||||
_apiKey = Trimmed(EnvApiKey);
|
||||
TestTag = Trimmed(EnvTestTag);
|
||||
WriteSandboxTag = Trimmed(EnvWriteSandboxTag);
|
||||
BoolSandboxTag = Trimmed(EnvBoolSandboxTag);
|
||||
AlarmSource = Trimmed(EnvAlarmSource);
|
||||
_allowUntrusted = string.Equals(
|
||||
Trimmed(EnvAllowUntrusted), "true", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -105,6 +108,10 @@ public sealed class GatewayLiveFixture
|
||||
/// <summary>The Float sandbox tag for the write round-trip (<c>HISTGW_WRITE_SANDBOX_TAG</c>); null when unset.</summary>
|
||||
public string? WriteSandboxTag { get; }
|
||||
|
||||
/// <summary>The dedicated Boolean sandbox tag for the 0.2.0 Boolean EnsureTags leg + retype probe
|
||||
/// (<c>HISTGW_BOOL_SANDBOX_TAG</c>, archreview 06/U-7); null when unset.</summary>
|
||||
public string? BoolSandboxTag { get; }
|
||||
|
||||
/// <summary>The source name for the alarm round-trip (<c>HISTGW_ALARM_SOURCE</c>); null when unset.</summary>
|
||||
public string? AlarmSource { get; }
|
||||
|
||||
|
||||
+120
@@ -253,4 +253,124 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
HistorianWriteOutcome.Ack,
|
||||
"the alarm SendEvent must be acked (the AlarmEventMapper must NOT set the wire event Id — the gateway rejects a client-supplied id).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// archreview 06/U-7 — Boolean provisioning leg on the 0.2.0 contract. Through the <b>real</b>
|
||||
/// <see cref="GatewayTagProvisioner"/>, <c>EnsureTags</c> a <see cref="DriverDataType.Boolean"/>
|
||||
/// request for the dedicated Boolean sandbox tag (mapped to <c>Int1</c> with explicit proto3
|
||||
/// presence), assert it is ensured (not failed), then <c>WriteLiveValues</c> a <c>1.0</c> and read
|
||||
/// it back over a wide window. This retires "the live EnsureTags leg has never run on 0.2.0" for
|
||||
/// the Boolean path. The ±12h read window + timezone caveat mirror <see cref="Write_then_read_on_sandbox_tag"/>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", "LiveIntegration")]
|
||||
public async Task EnsureTags_boolean_sandbox_provisions_as_Int1()
|
||||
{
|
||||
if (_fx.NotConfigured) Assert.Skip(_fx.SkipReason!);
|
||||
if (_fx.BoolSandboxTag is null)
|
||||
Assert.Skip("Skipped: set HISTGW_BOOL_SANDBOX_TAG to a writable Boolean sandbox tag (e.g. HistGW.LiveTest.BoolSandbox) to run the 0.2.0 Boolean EnsureTags leg.");
|
||||
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var tag = _fx.BoolSandboxTag;
|
||||
|
||||
// EnsureTags (Boolean → Int1) through the REAL provisioner seam (the same code path deploys use).
|
||||
await using var writeClient = _fx.CreateClient();
|
||||
var provisioner = new GatewayTagProvisioner(writeClient, NullLogger<GatewayTagProvisioner>.Instance);
|
||||
var provision = await provisioner.EnsureTagsAsync(
|
||||
new[] { new HistorianTagProvisionRequest(tag, DriverDataType.Boolean, null, "OtOpcUa live validation Boolean sandbox") },
|
||||
ct);
|
||||
|
||||
provision.Ensured.ShouldBe(1,
|
||||
"the Boolean sandbox tag must be ensured (needs the gateway on 0.2.0 contracts + an API key carrying historian:tags:write).");
|
||||
provision.Failed.ShouldBe(0);
|
||||
|
||||
// WriteLiveValues a Boolean-as-1.0 and read it back (SQL live path can lag a flush cadence → poll).
|
||||
const ushort goodQuality = 192; // OPC-DA "Good" floor.
|
||||
var writeUtc = DateTime.UtcNow;
|
||||
const double written = 1.0;
|
||||
var valueWriter = new GatewayHistorianValueWriter(writeClient, NullLogger<GatewayHistorianValueWriter>.Instance);
|
||||
var acked = await valueWriter.WriteLiveValuesAsync(
|
||||
tag, new[] { new HistorizationValue(writeUtc, written, goodQuality) }, ct);
|
||||
acked.ShouldBeTrue("the Boolean live write must be acked (gateway RuntimeDb:Enabled=true + EnsureTags-provisioned tag).");
|
||||
|
||||
await using var dataSource = _fx.CreateDataSource();
|
||||
DataValueSnapshot? hit = null;
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(15);
|
||||
do
|
||||
{
|
||||
var read = await dataSource.ReadRawAsync(
|
||||
tag, writeUtc - TimeSpan.FromHours(12), writeUtc + TimeSpan.FromHours(12), maxValuesPerNode: 50_000, ct);
|
||||
hit = read.Samples.FirstOrDefault(s => s.Value is double d && Math.Abs(d - written) < 0.5);
|
||||
if (hit is not null) break;
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), ct);
|
||||
}
|
||||
while (DateTime.UtcNow < deadline);
|
||||
|
||||
hit.ShouldNotBeNull($"the written Boolean sample ({written}) should read back from '{tag}'.");
|
||||
TestContext.Current.SendDiagnosticMessage(
|
||||
$"Boolean round-trip: EnsureTags(Int1) + WriteLiveValues '{tag}'={written} → read back at {hit!.SourceTimestampUtc:O}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// archreview 06/U-7 retype probe — answers the assessment note's open question: what does the
|
||||
/// deployed AVEVA Historian do when <c>EnsureTags</c> (an <b>upsert</b>) is asked to change an
|
||||
/// existing tag's type? Against the <b>dedicated Boolean sandbox tag only</b> (never the shared
|
||||
/// Float write sandbox), EnsureTags first as <c>Float</c> then as <c>Int1</c> and assert the second
|
||||
/// call's per-tag outcome is <em>deterministic and observable</em> — either <c>Success=true</c>
|
||||
/// (retype / tag-versioning accepted) or <c>Success=false</c> with a non-empty <c>Error</c> (mapped
|
||||
/// to the provisioner's <c>failed</c> tally). The assertion accepts both because the contract
|
||||
/// guarantees only the observability, not the policy; the observed behavior is emitted as a
|
||||
/// diagnostic so the run documents which of accepted-versioned / accepted-retyped / rejected the
|
||||
/// historian exhibits (fed back into docs/Historian.md after the first live run).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", "LiveIntegration")]
|
||||
public async Task EnsureTags_type_change_outcome_is_observable()
|
||||
{
|
||||
if (_fx.NotConfigured) Assert.Skip(_fx.SkipReason!);
|
||||
if (_fx.BoolSandboxTag is null)
|
||||
Assert.Skip("Skipped: set HISTGW_BOOL_SANDBOX_TAG to a dedicated Boolean sandbox tag to run the retype probe.");
|
||||
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var tag = _fx.BoolSandboxTag;
|
||||
|
||||
await using var client = _fx.CreateClient();
|
||||
|
||||
// First establish the tag as Float, then ask EnsureTags to retype it to Int1 (Boolean).
|
||||
await client.EnsureTagsAsync(
|
||||
new[]
|
||||
{
|
||||
new HistorianTagDefinition
|
||||
{
|
||||
TagName = tag,
|
||||
DataType = HistorianDataType.Float,
|
||||
EngineeringUnit = string.Empty,
|
||||
Description = "OtOpcUa live validation retype probe (stage 1: Float)",
|
||||
},
|
||||
},
|
||||
ct);
|
||||
|
||||
var retype = await client.EnsureTagsAsync(
|
||||
new[]
|
||||
{
|
||||
new HistorianTagDefinition
|
||||
{
|
||||
TagName = tag,
|
||||
DataType = HistorianDataType.Int1,
|
||||
EngineeringUnit = string.Empty,
|
||||
Description = "OtOpcUa live validation retype probe (stage 2: Int1)",
|
||||
},
|
||||
},
|
||||
ct);
|
||||
|
||||
var outcome = retype.Results.ShouldHaveSingleItem();
|
||||
var observable = outcome.Success || !string.IsNullOrEmpty(outcome.Error);
|
||||
observable.ShouldBeTrue(
|
||||
"the retype outcome must be deterministic + observable: either Success=true (retype/version accepted) " +
|
||||
"or Success=false with a non-empty Error (mapped to the provisioner's failed tally).");
|
||||
|
||||
TestContext.Current.SendDiagnosticMessage(
|
||||
$"retype probe '{tag}' Float→Int1: Success={outcome.Success}, Error='{outcome.Error}'. " +
|
||||
"(records the deployed historian's in-place analog-retype policy — feed this back into docs/Historian.md).");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user