fix(historian): map Boolean historization to Int2, not degenerate Int1 (#441)
The historian's Int1 analog-tag creation path is server-degenerate: EnsureTags(Int1) stores an unusable stub (live-probed on the deployed 2023 R2 historian; root-caused + documented gateway-side in HistorianGateway #11 / PR #16). HistorianTypeMapper mapped DriverDataType.Boolean -> HistorianDataType.Int1, so Boolean historized tags failed auto-provisioning. Map Boolean -> Int2 instead (store 0/1). Int2 is a proven-creatable analog type, and the value-write path already sends every value as a double on WriteLiveValues regardless of tag type, so Boolean 0.0/1.0 round-trips cleanly. Both Float and Int2 are supported, so a pre-existing Float Boolean tag retypes cleanly on its next deploy. - HistorianTypeMapper: Boolean => Int2 (+ remarks) - Unit pins: HistorianTypeMapperTests, GatewayTagProvisionerTests (Boolean_definition_carries_explicit_Int2_presence) - Live gate: EnsureTags_boolean_sandbox_provisions_as_Int2 (was _Int1); retype probe reframed Float->Int2 (drops the dead Int1 leg) - Docs: docs/Historian.md 'Boolean historization maps to Int2', CLAUDE.md, archreview STATUS.md (R2-06 live gate -> 6/6) Resolves #441.
This commit is contained in:
+13
-14
@@ -26,24 +26,23 @@ public sealed class GatewayTagProvisionerTests
|
||||
Assert.NotNull(fake.LastEnsureDefinitions);
|
||||
Assert.Equal(2, fake.LastEnsureDefinitions!.Count);
|
||||
Assert.Equal(HistorianDataType.Float, fake.LastEnsureDefinitions[0].DataType);
|
||||
Assert.Equal(HistorianDataType.Int1, fake.LastEnsureDefinitions[1].DataType);
|
||||
Assert.Equal(HistorianDataType.Int2, fake.LastEnsureDefinitions[1].DataType);
|
||||
Assert.Equal(2, result.Requested);
|
||||
Assert.Equal(0, result.Skipped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// archreview 06/U-7 — pins the 0.2.0 Boolean→Int1 provisioning semantics at TWO levels. The
|
||||
/// 0.1.0→0.2.0 client bump made <c>HistorianTagDefinition.DataType</c> proto3-optional (explicit
|
||||
/// presence): under 0.1.0 the provisioner's <c>Int1</c> (wire 0) was indistinguishable from unset,
|
||||
/// so the gateway defaulted Boolean tags to Float; under 0.2.0 the same call carries explicit
|
||||
/// presence and provisions Int1 (the intended behavior). Asserting BOTH the mapped type AND
|
||||
/// <c>HasDataType</c> compile-pins the package floor (the <c>HasDataType</c>/<c>ClearDataType</c>
|
||||
/// members exist only on the 0.2.0 proto3-optional contract — a downgrade breaks the build) and
|
||||
/// assert-pins that the provisioner always sends explicit presence (a future contract change that
|
||||
/// stopped sending it would break the assert). This is a pin, not a fix — it passes on current code.
|
||||
/// Pins the Boolean→Int2 provisioning semantics at TWO levels. Boolean maps to <c>Int2</c> because
|
||||
/// the historian's <c>Int1</c> analog-tag creation path is server-degenerate — <c>EnsureTags(Int1)</c>
|
||||
/// stores an unusable stub (issue #441 / HistorianGateway #11) — so Boolean historizes as a small 0/1
|
||||
/// integer. Asserting BOTH the mapped type AND <c>HasDataType</c> compile-pins the package floor (the
|
||||
/// <c>HasDataType</c>/<c>ClearDataType</c> members exist only on the 0.2.0+ proto3-optional contract —
|
||||
/// a downgrade breaks the build) and assert-pins that the provisioner always sends explicit presence
|
||||
/// (a future contract change that stopped sending it would default the tag to the SDK's Float and
|
||||
/// break this assert). This is a pin, not a fix — it passes on current code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Boolean_definition_carries_explicit_Int1_presence()
|
||||
public async Task Boolean_definition_carries_explicit_Int2_presence()
|
||||
{
|
||||
var fake = new FakeHistorianGatewayClient { EnsureTagsResult = new TagOperationResults() };
|
||||
var p = Provisioner(fake);
|
||||
@@ -54,9 +53,9 @@ public sealed class GatewayTagProvisionerTests
|
||||
|
||||
var defs = fake.LastEnsureDefinitions!;
|
||||
Assert.Single(defs);
|
||||
Assert.Equal(HistorianDataType.Int1, defs[0].DataType);
|
||||
// 0.2.0 proto3-optional presence witness — explicit presence is what flips the gateway from its
|
||||
// SDK-default Float to the requested Int1. Without this, a Boolean tag would provision as Float.
|
||||
Assert.Equal(HistorianDataType.Int2, defs[0].DataType);
|
||||
// proto3-optional presence witness — explicit presence is what flips the gateway from its
|
||||
// SDK-default Float to the requested Int2. Without this, a Boolean tag would provision as Float.
|
||||
Assert.True(defs[0].HasDataType);
|
||||
}
|
||||
|
||||
|
||||
+18
-15
@@ -255,25 +255,26 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// archreview 06/U-7 — Boolean provisioning leg on the 0.2.0 contract. Through the <b>real</b>
|
||||
/// issue #441 — Boolean historization leg. 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"/>.
|
||||
/// request for the dedicated Boolean sandbox tag (mapped to <c>Int2</c> — the historian's
|
||||
/// <c>Int1</c> analog-tag creation path is server-degenerate, see issue #441 / HistorianGateway
|
||||
/// #11, so Boolean historizes as a small 0/1 integer), assert it is ensured (not failed), then
|
||||
/// <c>WriteLiveValues</c> a <c>1.0</c> and read it back over a wide window. 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()
|
||||
public async Task EnsureTags_boolean_sandbox_provisions_as_Int2()
|
||||
{
|
||||
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.");
|
||||
Assert.Skip("Skipped: set HISTGW_BOOL_SANDBOX_TAG to a writable Boolean sandbox tag (e.g. HistGW.LiveTest.BoolSandbox) to run the Boolean historization leg.");
|
||||
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var tag = _fx.BoolSandboxTag;
|
||||
|
||||
// EnsureTags (Boolean → Int1) through the REAL provisioner seam (the same code path deploys use).
|
||||
// EnsureTags (Boolean → Int2) 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(
|
||||
@@ -281,7 +282,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
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).");
|
||||
"the Boolean sandbox tag must be ensured as Int2 (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).
|
||||
@@ -308,14 +309,16 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
|
||||
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}.");
|
||||
$"Boolean round-trip: EnsureTags(Int2) + 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
|
||||
/// Float write sandbox), EnsureTags first as <c>Float</c> then as <c>Int2</c> (both supported
|
||||
/// analog types — <c>Int2</c> is the type Boolean now historizes as; the previously-probed
|
||||
/// <c>Int1</c> is a documented server-degenerate exclusion, issue #441) 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
|
||||
@@ -336,7 +339,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
|
||||
await using var client = _fx.CreateClient();
|
||||
|
||||
// First establish the tag as Float, then ask EnsureTags to retype it to Int1 (Boolean).
|
||||
// First establish the tag as Float, then ask EnsureTags to retype it to Int2.
|
||||
await client.EnsureTagsAsync(
|
||||
new[]
|
||||
{
|
||||
@@ -357,9 +360,9 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
new HistorianTagDefinition
|
||||
{
|
||||
TagName = tag,
|
||||
DataType = HistorianDataType.Int1,
|
||||
DataType = HistorianDataType.Int2,
|
||||
EngineeringUnit = string.Empty,
|
||||
Description = "OtOpcUa live validation retype probe (stage 2: Int1)",
|
||||
Description = "OtOpcUa live validation retype probe (stage 2: Int2)",
|
||||
StorageRateMs = 1000, // 0 makes the gateway's EnsureTags throw (storageRateMs must be > 0).
|
||||
},
|
||||
},
|
||||
@@ -372,7 +375,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
"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}'. " +
|
||||
$"retype probe '{tag}' Float→Int2: Success={outcome.Success}, Error='{outcome.Error}'. " +
|
||||
"(records the deployed historian's in-place analog-retype policy — feed this back into docs/Historian.md).");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping;
|
||||
public sealed class HistorianTypeMapperTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(DriverDataType.Boolean, HistorianDataType.Int1)]
|
||||
[InlineData(DriverDataType.Boolean, HistorianDataType.Int2)] // Int1 analog-tag creation is server-degenerate (issue #441) → store 0/1
|
||||
[InlineData(DriverDataType.Int16, HistorianDataType.Int2)]
|
||||
[InlineData(DriverDataType.Int32, HistorianDataType.Int4)]
|
||||
[InlineData(DriverDataType.Int64, HistorianDataType.Int8)]
|
||||
|
||||
Reference in New Issue
Block a user