6c4d29902e
v2-ci / unit-tests (push) Has been cancelled
#899 — EnsureTags is an upsert, so binding a node to an existing AVEVA tag via historianTagname re-typed/re-described it on every add and host start. - TagConfig `historianReadOnly: true` (TagConfigIntent -> RawTagPlan.HistorianReadOnly): HistoryRead from an existing historian tag that OtOpcUa never provisions and never records into (AddressSpaceApplier.IsOwnedHistorizedValue gates both the EnsureTags hook and the recorder feed). Node-inert: surgical, not mirrored on UNS refs, in the RawTagPlan equality/hash. A non-bool value is the deploy error HistorianReadOnlyInvalid. AdminUI: CSV column HistorianReadOnly (strict), TagHistorizeConfig reads via the runtime parse. - ServerHistorian:ProvisionTags = CreateOrUpdate (default, unchanged) | CreateOnly (Gateway only: TagExistsAsync first, existing tags left alone, failed lookup not sent) | Off. Unknown value / CreateOnly on a SQL backend fail host start. - Default for an explicit historianTagname stays OWNED: overrides pin owned history across renames, a read-only default would silently stop provisioning + recording. #898 — the hook parsed Tag.DataType (an OPC UA name) as DriverDataType, so historized Float/Double/Byte/SByte tags were never provisioned. New shared HistorianDataTypeNames.TryResolve: Float->Float32, Double->Float64, Byte/SByte->Int16 (lossless, provisioned as Int2); unmapped names logged at Information. Gateway now skips Int64/UInt64 (HistorianGateway cannot create Int8/UInt8: lossless-cast-or-reject). UPGRADE: the first host start after this provisions (upserts) every existing historized Float/Double/Byte/SByte tag. Mark foreign-tag bindings historianReadOnly, or set ServerHistorian:ProvisionTags=CreateOnly/Off, before upgrading (docs/Historian.md). Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
56 lines
2.2 KiB
C#
56 lines
2.2 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#899: <c>historianReadOnly</c> must be a JSON boolean. The runtime honours only a bool <c>true</c>, so a
|
|
/// string <c>"true"</c> would deploy as an OWNED tag whose foreign historian tag then gets provisioned (upserted)
|
|
/// and recorded into — the deploy gate refuses it instead.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DraftValidatorHistorianReadOnlyTests
|
|
{
|
|
private static DraftSnapshot Draft(params Tag[] tags) => new()
|
|
{
|
|
GenerationId = 1, ClusterId = "c",
|
|
DriverInstances =
|
|
[
|
|
new DriverInstance
|
|
{
|
|
DriverInstanceId = "di", ClusterId = "c", Name = "mb", DriverType = "Modbus", DriverConfig = "{}",
|
|
},
|
|
],
|
|
Devices = [new Device { DeviceId = "dev", DriverInstanceId = "di", Name = "plc", DeviceConfig = "{}" }],
|
|
Tags = tags,
|
|
};
|
|
|
|
private static Tag TagRow(string id, string tagConfig) => new()
|
|
{
|
|
TagId = id, DeviceId = "dev", Name = id, DataType = "Int32", AccessLevel = TagAccessLevel.Read, TagConfig = tagConfig,
|
|
};
|
|
|
|
[Theory]
|
|
[InlineData("{\"isHistorized\":true,\"historianReadOnly\":\"true\"}")]
|
|
[InlineData("{\"isHistorized\":true,\"historianReadOnly\":1}")]
|
|
[InlineData("{\"historianReadOnly\":null}")]
|
|
public void A_non_boolean_historianReadOnly_is_a_deploy_error(string tagConfig)
|
|
{
|
|
var errors = DraftValidator.Validate(Draft(TagRow("t-bad", tagConfig)));
|
|
errors.ShouldContain(e => e.Code == "HistorianReadOnlyInvalid" && e.Context == "t-bad");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("{\"isHistorized\":true,\"historianTagname\":\"SysTimeSec\",\"historianReadOnly\":true}")]
|
|
[InlineData("{\"isHistorized\":true,\"historianReadOnly\":false}")]
|
|
[InlineData("{\"isHistorized\":true}")]
|
|
public void A_boolean_or_absent_historianReadOnly_passes(string tagConfig)
|
|
{
|
|
var errors = DraftValidator.Validate(Draft(TagRow("t-ok", tagConfig)));
|
|
errors.ShouldNotContain(e => e.Code == "HistorianReadOnlyInvalid");
|
|
}
|
|
}
|