Merge pull request 'fix(historian): map Boolean historization to Int2, not degenerate Int1 (#441)' (#442)
This commit was merged in pull request #442.
This commit is contained in:
@@ -319,11 +319,12 @@ When the section is disabled the applier binds the no-op `NullHistorianProvision
|
||||
run. Every dispatch logs a tally (`dispatched/requested/ensured/skipped/failed`); `dispatched=N` with
|
||||
`requested=0` flags the dormant no-op. The API key must carry `historian:tags:write` for `EnsureTags`. (PR
|
||||
#423 shipped `GatewayTagProvisioner` but left this wiring out, so provisioning was dormant; restored 2026-06-27.)
|
||||
**0.2.0 DataType delta (archreview 06/U-7):** `EnsureTags` is an **upsert** (create-or-update, not
|
||||
skip-if-exists), and the 0.2.0 `HistorianTagDefinition.DataType` proto3-optional change means Boolean tags now
|
||||
provision as **Int1** (0.1.0 silently defaulted them to Float). A pre-bump Float Boolean is asked to retype
|
||||
on its next deploy; the AVEVA-side retype policy is pinned by the live retype probe — see the "0.2.0 DataType
|
||||
provisioning delta" section in `docs/Historian.md`.
|
||||
**DataType mapping (issue #441):** `EnsureTags` is an **upsert** (create-or-update, not skip-if-exists).
|
||||
`HistorianTypeMapper` maps **`Boolean → Int2`** (Boolean historizes as a small 0/1 integer) because the
|
||||
historian's `Int1` analog-tag creation path is **server-degenerate** — `EnsureTags(Int1)` stores an unusable
|
||||
stub (live-probed; gateway-side resolution in HistorianGateway #11 / PR #16). Both Float and Int2 are
|
||||
supported analog types, so a pre-existing Float Boolean tag retypes cleanly to Int2 on its next deploy — see
|
||||
the "Boolean historization maps to Int2" section in `docs/Historian.md`.
|
||||
|
||||
### Gateway-side prerequisites
|
||||
|
||||
@@ -358,7 +359,7 @@ export HISTGW_GATEWAY_ENDPOINT=https://wonder-sql-vd03:5222 # absolute gateway
|
||||
export HISTGW_GATEWAY_APIKEY=histgw_<id>_<secret> # must carry historian:read + historian:write (+ tags:write) scopes
|
||||
export HISTGW_TEST_TAG=<existing-tag> # read round-trip
|
||||
export HISTGW_WRITE_SANDBOX_TAG=<writable-float-tag> # e.g. HistGW.LiveTest.Sandbox — write round-trip (EnsureTags + write)
|
||||
export HISTGW_BOOL_SANDBOX_TAG=<writable-bool-tag> # e.g. HistGW.LiveTest.BoolSandbox — 0.2.0 Boolean EnsureTags leg + retype probe (06/U-7)
|
||||
export HISTGW_BOOL_SANDBOX_TAG=<writable-bool-tag> # e.g. HistGW.LiveTest.BoolSandbox — Boolean→Int2 EnsureTags leg + retype probe (issue #441)
|
||||
export HISTGW_ALARM_SOURCE=<source-name> # alarm SendEvent → ReadEvents round-trip
|
||||
dotnet test --filter "Category=LiveIntegration"
|
||||
```
|
||||
|
||||
+21
-14
@@ -327,24 +327,31 @@ convention) + a regression unit test + `StorageRateMs` on the live-suite inline
|
||||
- ✅ `Write_then_read_on_sandbox_tag` — `EnsureTags(Float)` + `WriteLiveValues` (SQL) + read-back.
|
||||
- ✅ `Alarm_SendEvent_is_acked` + ✅ `Alarm_SendEvent_then_ReadEvents` — alarm `SendEvent` + SQL `ReadEvents`.
|
||||
- ✅ `EnsureTags_type_change_outcome_is_observable` — now observes a **real** retype outcome (not a storage-rate error).
|
||||
- ❌ `EnsureTags_boolean_sandbox_provisions_as_Int1` — **Int1 (Boolean) tag creation is not supported by the
|
||||
gateway's histsdk on this historian**: `EnsureTags(Int1)` returns `ProtocolEvidenceMissingException` (the vendored
|
||||
client's "no capture evidence for this type" guard → gRPC `Unimplemented`), both fresh and as a retype target.
|
||||
A **type sweep** confirmed it is Int1-specific: **Int2/UInt2/Int4/UInt4/Float/Double all provision `success=True`;
|
||||
only Int1 fails.** Root cause: `histsdk` `HistorianTagWriteProtocol.GetAnalogDataTypeCode` has no captured
|
||||
CTagMetadata wire code for Int1. This is a **gateway/histsdk type-support gap**, not an OtOpcUa defect — **filed as
|
||||
[historiangw#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11)**.
|
||||
- ⚠️ `EnsureTags_boolean_sandbox_provisions_as_Int1` (as originally written) — **Int1 (Boolean) tag creation is not
|
||||
supported by the gateway's histsdk on this historian**: `EnsureTags(Int1)` returns `ProtocolEvidenceMissingException`
|
||||
(the vendored client's "no capture evidence for this type" guard → gRPC `Unimplemented`), both fresh and as a retype
|
||||
target. A **type sweep** confirmed it is Int1-specific: **Int2/UInt2/Int4/UInt4/Float/Double all provision
|
||||
`success=True`; only Int1 fails.** Root cause: `histsdk` `HistorianTagWriteProtocol.GetAnalogDataTypeCode` has no
|
||||
captured CTagMetadata wire code for Int1 — a **gateway/histsdk type-support gap**, not an OtOpcUa defect
|
||||
(**filed as [historiangw#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11)**, which routed the
|
||||
decision back as **[issue #441](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/441)**).
|
||||
|
||||
**✅ RESOLVED (issue #441, 2026-07-14):** `HistorianTypeMapper` now maps **`Boolean → Int2`** (0/1 integer) instead
|
||||
of the degenerate `Int1`. Int2 is a proven-creatable analog type on this historian, 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. The live gate test is renamed `EnsureTags_boolean_sandbox_provisions_as_Int2` and now passes → **6/6**. Unit
|
||||
pins updated (`HistorianTypeMapperTests`, `GatewayTagProvisionerTests.Boolean_definition_carries_explicit_Int2_presence`);
|
||||
gateway driver unit `103/103`.
|
||||
|
||||
**✅ U-7 retype policy — ANSWERED (via `grpcurl` against the real historian):**
|
||||
- `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type → `success`.
|
||||
- An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns `success`
|
||||
(of the three 06/U-7 hypotheses: **accepted-retyped**, not rejected).
|
||||
- **But the specific 0.2.0 Boolean case (Float→Int1) can't retype** — Int1 isn't a creatable type here, so a
|
||||
pre-0.2.0 **Float Boolean tag asked to become Int1 fails the provision** with `ProtocolEvidenceMissingException`
|
||||
— it is **neither silently retyped nor data-lost**; the provision just fails (and is metered `failed=N`). That is
|
||||
the practically-relevant U-7 answer for Boolean tags on this deployment.
|
||||
- **The specific `Int1` case can't retype** — Int1 isn't a creatable type here. Rather than accept a broken Boolean
|
||||
path, OtOpcUa **remapped `Boolean → Int2`** (issue #441, above), which retypes cleanly from a pre-existing Float
|
||||
Boolean tag (both are supported analog types) and never hits the Int1 gap.
|
||||
|
||||
The R2-06 **code** deliverables (S-11 fail-fast + `ServerHistorianOptionsValidator`; U-7 Boolean→Int1 mapping +
|
||||
The R2-06 **code** deliverables (S-11 fail-fast + `ServerHistorianOptionsValidator`; Boolean provisioning mapping +
|
||||
upsert; C-7 doc) were already unit-covered + merged; this live gate additionally **found + fixed the StorageRateMs
|
||||
provisioning bug** and **answered the U-7 retype policy**. The one remaining red test is a documented gateway
|
||||
histsdk Int1-support gap.
|
||||
provisioning bug**, **answered the U-7 retype policy**, and surfaced the Int1 histsdk gap that drove the
|
||||
**Boolean→Int2 remap (issue #441)** — closing the last red test to reach **6/6**.
|
||||
|
||||
+36
-40
@@ -141,48 +141,44 @@ for this node), which is the signal to check that `ServerHistorian:Enabled=true`
|
||||
> so a HistoryRead on it runs to the full `CallTimeout` (~30 s) before erroring. Auto-provisioning on deploy
|
||||
> keeps freshly-historized tags from hitting that slow path. See the `CallTimeout` row above.
|
||||
|
||||
### 0.2.0 DataType provisioning delta (archreview 06/U-7)
|
||||
### Boolean historization maps to Int2 (issue #441)
|
||||
|
||||
`EnsureTags` is contract-documented (HistorianGateway 0.2.0) as an **upsert** — *"creates or updates
|
||||
`EnsureTags` is contract-documented (HistorianGateway 0.2.0+) as an **upsert** — *"creates or updates
|
||||
historian tags according to the given definitions"* — that maps per-tag onto AVEVA's
|
||||
`HistorianClient.EnsureTagAsync(definition) -> bool`. It is **not idempotent** (not routed through the
|
||||
idempotent retry pipeline) and **not skip-if-exists**.
|
||||
|
||||
The 0.1.0→0.2.0 client bump made `HistorianTagDefinition.DataType` **proto3-optional** (explicit presence).
|
||||
Under 0.1.0, the provisioner's `Boolean → Int1` mapping sent wire value `0`, indistinguishable from unset, so
|
||||
the gateway defaulted such tags to the SDK-default **Float**. Under 0.2.0 the same call carries explicit
|
||||
presence, so Boolean tags now provision as **Int1** (the pre-existing intent of `HistorianTypeMapper` — no
|
||||
mapper code change is needed or wanted). Consequences:
|
||||
`HistorianTypeMapper` maps **`DriverDataType.Boolean → HistorianDataType.Int2`** and Boolean values
|
||||
historize as a small **0/1 integer**. Int2 is used rather than the semantically-exact `Int1` because the
|
||||
historian's **`Int1` analog-tag creation path is server-degenerate** — `EnsureTags(Int1)` stores an
|
||||
unusable stub, so a Boolean tag mapped to `Int1` never becomes historizable. This is an evidence-based
|
||||
decision from a live probe against the deployed 2023 R2 historian; the root cause and gateway-side
|
||||
resolution are tracked in [issue #441](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/441) and
|
||||
HistorianGateway [#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11) / PR #16 (which
|
||||
documents `Int1` as an explicit, evidence-based exclusion). The value-write path is unaffected: the
|
||||
recorder sends every value as a `double` on `WriteLiveValues` regardless of tag type, so a Boolean
|
||||
`0.0/1.0` round-trips cleanly into the Int2 tag.
|
||||
|
||||
1. **New Boolean tags provision correctly as Int1.** This is the fix arriving as a silent, zero-diff
|
||||
semantic change from the bump.
|
||||
2. **Pre-bump Boolean tags exist historian-side as Float.** On the next deploy that dispatches provisioning
|
||||
for such a tag, the gateway is asked (via the upsert) to update it to Int1.
|
||||
**Observed live (2026-07-13, real 0.2.0 gateway → real AVEVA historian on `wonder-sql-vd03`, probed via
|
||||
`grpcurl` — archreview R2-06 T12):**
|
||||
- `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type
|
||||
returns `Success=true`.
|
||||
- An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns
|
||||
`Success=true`.
|
||||
- A type sweep confirmed **`Int2/UInt2/Int4/UInt4/Float/Double` all provision `Success=true`; only `Int1`
|
||||
fails** (`Success=false, Error="ProtocolEvidenceMissingException"` — the vendored client's "no capture
|
||||
evidence for this type" guard → gRPC `Unimplemented`), both for a fresh tag and as a retype target. Mapping
|
||||
Boolean to `Int2` sidesteps that gap entirely.
|
||||
|
||||
**Observed live (2026-07-13, real 0.2.0 gateway → real AVEVA historian on `wonder-sql-vd03`, probed via
|
||||
`grpcurl` — archreview R2-06 T12):**
|
||||
- `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type
|
||||
returns `Success=true`.
|
||||
- An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns
|
||||
`Success=true` (hypothesis **(ii) accepted as a plain retype**, not rejected).
|
||||
- **BUT `Int1` — the 0.2.0 Boolean target — is *not a creatable type* on this historian's histsdk:**
|
||||
`EnsureTags(Int1)` returns `Success=false, Error="ProtocolEvidenceMissingException"` (the vendored client's
|
||||
"no capture evidence for this type" guard → gRPC `Unimplemented`), both for a fresh tag and as a retype
|
||||
target. A type sweep confirmed it is **Int1-specific**: `Int2/UInt2/Int4/UInt4/Float/Double` all provision
|
||||
`Success=true`; only `Int1` fails.
|
||||
- **Net for a pre-bump Float Boolean tag asked to become Int1:** the provision **fails** (per-tag
|
||||
`Success=false` → `failed=N` tally). It is **neither silently retyped nor data-lost** — the tag stays Float
|
||||
and remains readable; the Int1 provision simply doesn't land until the gateway's histsdk gains `Int1`
|
||||
create-evidence — filed as
|
||||
[historiangw#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11). (Prerequisite for reaching this
|
||||
outcome at all: the provisioner must stamp a **positive
|
||||
`StorageRateMs`** — a `0` made `EnsureTags` throw `ArgumentOutOfRangeException` and fail *every* provision;
|
||||
fixed in `GatewayTagProvisioner`, PR #439.)
|
||||
3. **The failure mode is bounded and observable either way.** Provisioning is fire-and-forget and never
|
||||
blocks/fails a deploy; a per-tag rejection surfaces only as the tally's `failed` count. Values written to
|
||||
a still-Float tag by the recorder remain readable (Float supersets Int1's 0/1 range); nothing is lost
|
||||
pending a reconcile. A bulk retype sweep is **not** done in code — assess via the probe first, then decide
|
||||
whether reconciliation is a one-time operator action.
|
||||
> **Prerequisite for provisioning to land at all:** the provisioner must stamp a **positive `StorageRateMs`**
|
||||
> — a `0` made `EnsureTags` throw `ArgumentOutOfRangeException` and fail *every* provision (fixed in
|
||||
> `GatewayTagProvisioner`, PR #439).
|
||||
|
||||
**Migration note for pre-existing Boolean tags.** A Boolean tag historian-side as Float (e.g. from the
|
||||
0.1.0-era default) is asked, on its next deploy, to upsert to Int2. Both are supported analog types, so the
|
||||
retype is accepted. Provisioning is fire-and-forget and never blocks/fails a deploy; a per-tag rejection (if
|
||||
any) surfaces only as the tally's `failed` count, and values written to a still-Float tag remain readable
|
||||
(Float supersets the 0/1 range) pending reconcile. A bulk retype sweep is **not** done in code.
|
||||
|
||||
---
|
||||
|
||||
@@ -407,11 +403,11 @@ The gateway-backed driver also ships an env-gated live suite (`Category=LiveInte
|
||||
`HISTGW_GATEWAY_ENDPOINT` / `HISTGW_GATEWAY_APIKEY` / `HISTGW_TEST_TAG` / `HISTGW_WRITE_SANDBOX_TAG` /
|
||||
`HISTGW_BOOL_SANDBOX_TAG` / `HISTGW_ALARM_SOURCE` env vars (it skips cleanly when they are absent).
|
||||
`HISTGW_BOOL_SANDBOX_TAG` (e.g. `HistGW.LiveTest.BoolSandbox`, a **dedicated** writable Boolean tag —
|
||||
never the shared Float write sandbox) drives the 0.2.0 Boolean `EnsureTags` leg
|
||||
(`EnsureTags_boolean_sandbox_provisions_as_Int1`) and the retype probe
|
||||
(`EnsureTags_type_change_outcome_is_observable`) that resolve the
|
||||
[0.2.0 DataType provisioning delta](#020-datatype-provisioning-delta-archreview-06u-7) open question. Running
|
||||
the full suite against a **0.2.0** gateway (all six tests, none skipped) also discharges 06/U-2's
|
||||
never the shared Float write sandbox) drives the Boolean historization leg
|
||||
(`EnsureTags_boolean_sandbox_provisions_as_Int2`) and the retype probe
|
||||
(`EnsureTags_type_change_outcome_is_observable`) that back the
|
||||
[Boolean historization maps to Int2](#boolean-historization-maps-to-int2-issue-441) decision. Running
|
||||
the full suite against the gateway (all six tests, none skipped) also discharges 06/U-2's
|
||||
"full documented run" for the historian suite.
|
||||
|
||||
See [AlarmHistorian.md](AlarmHistorian.md) for the alarm write path and
|
||||
|
||||
+10
-6
@@ -9,11 +9,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the nine numeric types are historizable on the gateway's analog write path. Two of them
|
||||
/// fall back to a wider historian type because the narrower one's write path is deferred upstream:
|
||||
/// <see cref="DriverDataType.UInt16"/> maps to <see cref="HistorianDataType.Uint4"/> (the historian's
|
||||
/// <c>UInt2</c> write path is not proven). String / DateTime / Reference are not historized in v1
|
||||
/// and throw <see cref="NotSupportedException"/>; callers that want to skip them without catching an
|
||||
/// exception should consult <see cref="IsHistorizable(DriverDataType)"/> first.
|
||||
/// fall back to a wider/adjacent historian type because the exact one's write path is not usable
|
||||
/// upstream: <see cref="DriverDataType.UInt16"/> maps to <see cref="HistorianDataType.Uint4"/> (the
|
||||
/// historian's <c>UInt2</c> write path is not proven), and <see cref="DriverDataType.Boolean"/> maps
|
||||
/// to <see cref="HistorianDataType.Int2"/> — the historian's <c>Int1</c> analog-tag creation path is
|
||||
/// server-degenerate (issue #441 / HistorianGateway #11: <c>EnsureTags(Int1)</c> stores an unusable
|
||||
/// stub), so Boolean historizes as a small 0/1 integer instead. String / DateTime / Reference are not
|
||||
/// historized in v1 and throw <see cref="NotSupportedException"/>; callers that want to skip them
|
||||
/// without catching an exception should consult <see cref="IsHistorizable(DriverDataType)"/> first.
|
||||
/// </remarks>
|
||||
internal static class HistorianTypeMapper
|
||||
{
|
||||
@@ -25,7 +28,8 @@ internal static class HistorianTypeMapper
|
||||
/// </exception>
|
||||
public static HistorianDataType ToHistorianDataType(DriverDataType dataType) => dataType switch
|
||||
{
|
||||
DriverDataType.Boolean => HistorianDataType.Int1,
|
||||
DriverDataType.Boolean => HistorianDataType.Int2, // Int1 analog-tag creation is server-degenerate (issue #441) → store 0/1
|
||||
|
||||
DriverDataType.Int16 => HistorianDataType.Int2,
|
||||
DriverDataType.Int32 => HistorianDataType.Int4,
|
||||
DriverDataType.Int64 => HistorianDataType.Int8,
|
||||
|
||||
+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