2b0eed1a8f
- ALARM: selectors (HeidenhainAlarmSelector, Contracts): ALARM:*, ALARM:OTHER, ALARM:CLASS=<r>[/GROUP=<r>][/NUMBER=<r>] (<r> = n | lo..hi; negative numbers allowed). An ALARM: address and the cross-driver alarm object only work together — the factory refuses either alone, so editor, save path, CSV and deploy inspector agree. Data type fixed Boolean; never a value (ReadAsync BadNotSupported), never in a data subscription. - HeidenhainDriver implements IAlarmSource: one condition per ALARM: tag at its RawPath, driven by the SAME allowlisted R_RI 27/28 walk on the control's single loop, on the new Polling.AlarmMs cadence (default 2000 ms, only for controls carrying an ALARM: tag). FOCAS #647 transition rules (Raise / Retrigger / Clear), identity = class+group+number+text, deterministic display order, told-record on the driver instance, recorded only while a handler listens. A failed walk (refusal, overflow, comms loss) is quality-only, never a clear. - Acknowledge is LOCAL-ONLY: AcknowledgeAsync is a no-op success (FOCAS/MTConnect posture); Lsv2ReadOnlyPolicy is unchanged — no ack or write request exists. - Opt-in per tag, no driver flag (FOCAS #647 / MTConnect #626 precedent); Alarms/* arrays unchanged. - IRediscoverable: a reconnect whose R_VR identity (model / NC / PLC / options) differs from the previous session raises OnRediscoveryNeeded once (advisory re-browse chip). No extra traffic. - AdminUI: Heidenhain joins NativeAlarmModel's alarm drivers; the typed editor fixes ALARM: tags at Boolean and seeds/drops the alarm object with the selector; driver form gains "Alarm conditions (ms)". (HeidenhainDriverFormModel also carries the #830 credential-notice change.) Tests: HeidenhainAlarmTests (grammar, pairing, rules, simulator e2e with the captured iTNC 530 alarm record, local-only ack = zero bytes sent, quality-only outage/refusal, rediscovery), Runtime.Tests HeidenhainNativeAlarmEndToEndTests (real DriverHostActor condition), a DraftValidator case, and the NativeAlarmDriverSetGuard now scans the Heidenhain driver.
115 lines
5.5 KiB
C#
115 lines
5.5 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#803: the Heidenhain read-only, credential and endpoint deploy gates.</summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DraftValidatorHeidenhainTests
|
|
{
|
|
private static DriverInstance Driver(string config = "{}", string type = "Heidenhain") => new()
|
|
{
|
|
DriverInstanceId = "hh", ClusterId = "c", Name = "hh", DriverType = type, DriverConfig = config,
|
|
};
|
|
|
|
private static Device Dev(string config = """{"Host":"10.0.0.5"}""") => new()
|
|
{
|
|
DeviceId = "d1", DriverInstanceId = "hh", Name = "dev1", DeviceConfig = config,
|
|
};
|
|
|
|
private static Tag T(TagAccessLevel access) => new()
|
|
{
|
|
TagId = "t1", DeviceId = "d1", Name = "ExecState", DataType = "Int32", AccessLevel = access,
|
|
TagConfig = """{"address":"Status/ExecutionState"}""",
|
|
};
|
|
|
|
private static IReadOnlyList<ValidationError> Validate(DriverInstance d, Device dev, params Tag[] tags) =>
|
|
DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1, ClusterId = "c", DriverInstances = [d], Devices = [dev], Tags = tags,
|
|
});
|
|
|
|
[Fact]
|
|
public void A_read_only_tag_on_a_configured_control_is_valid()
|
|
=> Validate(Driver(), Dev(), T(TagAccessLevel.Read)).ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
|
|
[Fact]
|
|
public void A_ReadWrite_tag_is_refused_so_no_node_is_ever_writable()
|
|
{
|
|
var e = Validate(Driver(), Dev(), T(TagAccessLevel.ReadWrite)).Single(x => x.Code == HeidenhainDeployChecks.WritableCode);
|
|
e.Context.ShouldBe("t1");
|
|
e.Message.ShouldContain("read-only");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("""{"Access":{"InspectPassword":"hunter2"}}""", "{\"Host\":\"10.0.0.5\"}")]
|
|
[InlineData("{}", """{"Host":"10.0.0.5","DncPassword":"hunter2"}""")]
|
|
public void A_literal_password_is_refused_without_echoing_it(string driverConfig, string deviceConfig)
|
|
{
|
|
var errors = Validate(Driver(driverConfig), Dev(deviceConfig), T(TagAccessLevel.Read));
|
|
errors.ShouldContain(e => e.Code == HeidenhainDeployChecks.CredentialCode);
|
|
errors.ShouldAllBe(e => !e.Message.Contains("hunter2"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Secret_and_env_references_are_accepted()
|
|
=> Validate(Driver("""{"Access":{"InspectPassword":"secret:heidenhain/inspect","DncPassword":"env:OTOPCUA_HEIDENHAIN_DNC"}}"""), Dev(), T(TagAccessLevel.Read))
|
|
.ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
|
|
[Fact]
|
|
public void A_device_with_tags_but_no_endpoint_is_refused_while_an_empty_device_is_not()
|
|
{
|
|
Validate(Driver(), Dev("{}"), T(TagAccessLevel.Read)).ShouldContain(e => e.Code == HeidenhainDeployChecks.ConfigCode && e.Context == "d1");
|
|
Validate(Driver(), Dev("{}")).ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_tag_data_type_that_disagrees_with_the_published_type_is_refused()
|
|
{
|
|
var tag = T(TagAccessLevel.Read);
|
|
tag.DataType = "Float";
|
|
var e = Validate(Driver(), Dev(), tag).Single(x => x.Code == HeidenhainDeployChecks.DataTypeCode);
|
|
e.Context.ShouldBe("t1");
|
|
e.Message.ShouldContain("Int32");
|
|
var binding = T(TagAccessLevel.Read);
|
|
binding.TagConfig = """{"address":"PLC:DWORD:0","dataType":"Int64"}""";
|
|
binding.DataType = "Int64";
|
|
Validate(Driver(), Dev(), binding).ShouldNotContain(x => x.Code == HeidenhainDeployChecks.DataTypeCode);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("env:ConnectionStrings__ConfigDb")]
|
|
[InlineData("secret:galaxy/inst1/apikey")]
|
|
public void An_out_of_namespace_reference_is_refused(string reference)
|
|
=> Validate(Driver("{\"Access\":{\"InspectPassword\":\"" + reference + "\"}}"), Dev(), T(TagAccessLevel.Read))
|
|
.ShouldContain(e => e.Code == HeidenhainDeployChecks.CredentialCode);
|
|
|
|
[Fact]
|
|
public void Out_of_range_settings_are_refused()
|
|
=> Validate(Driver("""{"Polling":{"MaxAlarms":999}}"""), Dev(), T(TagAccessLevel.Read))
|
|
.ShouldContain(e => e.Code == HeidenhainDeployChecks.ConfigCode);
|
|
|
|
[Fact]
|
|
public void An_alarm_condition_tag_deploys_as_Boolean_and_any_other_data_type_is_refused()
|
|
{
|
|
// lmxopcua#831: an ALARM: selector + the alarm object is a Part 9 condition; its Tag.DataType is fixed.
|
|
var tag = T(TagAccessLevel.Read);
|
|
tag.TagConfig = """{"address":"ALARM:CLASS=7","dataType":"Boolean","alarm":{"alarmType":"AlarmCondition","severity":800}}""";
|
|
tag.DataType = "Boolean";
|
|
Validate(Driver(), Dev(), tag).ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
tag.DataType = "Int32";
|
|
Validate(Driver(), Dev(), tag).ShouldContain(e => e.Code == HeidenhainDeployChecks.DataTypeCode && e.Message.Contains("Boolean"));
|
|
tag.DataType = "Boolean";
|
|
tag.AccessLevel = TagAccessLevel.ReadWrite;
|
|
Validate(Driver(), Dev(), tag).ShouldContain(e => e.Code == HeidenhainDeployChecks.WritableCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void Other_drivers_are_unaffected()
|
|
=> Validate(Driver("""{"Access":{"InspectPassword":"x"}}""", type: "Modbus"), Dev(), T(TagAccessLevel.ReadWrite))
|
|
.ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
}
|