diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs index 9356abc2..61a89979 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs @@ -80,5 +80,6 @@ public enum ValidationCategory OnTriggerScriptNotFound, CrossCallViolation, MissingMetadata, - ConnectionConfig + ConnectionConfig, + NativeAlarmSourceInvalid } diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs index cdb6db45..c65bff3e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs @@ -27,7 +27,8 @@ public class SemanticValidator /// Shared scripts available for CallShared references. public ValidationResult Validate( FlattenedConfiguration configuration, - IReadOnlyList? sharedScripts = null) + IReadOnlyList? sharedScripts = null, + IReadOnlySet? alarmCapableConnectionNames = null) { var errors = new List(); var warnings = new List(); @@ -215,6 +216,33 @@ public class SemanticValidator } } + // Native alarm source bindings: connection + source reference must be + // present, and (when the alarm-capable connection set is supplied) the + // connection must resolve to an alarm-capable site data connection. + foreach (var nativeSource in configuration.NativeAlarmSources) + { + if (string.IsNullOrWhiteSpace(nativeSource.SourceReference)) + { + errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid, + $"Native alarm source '{nativeSource.CanonicalName}' has an empty source reference.", + nativeSource.CanonicalName)); + } + + if (string.IsNullOrWhiteSpace(nativeSource.ConnectionName)) + { + errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid, + $"Native alarm source '{nativeSource.CanonicalName}' has no data connection.", + nativeSource.CanonicalName)); + } + else if (alarmCapableConnectionNames is not null && + !alarmCapableConnectionNames.Contains(nativeSource.ConnectionName)) + { + errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid, + $"Native alarm source '{nativeSource.CanonicalName}' references connection '{nativeSource.ConnectionName}' which is not an alarm-capable data connection on this site.", + nativeSource.CanonicalName)); + } + } + return new ValidationResult { Errors = errors, Warnings = warnings }; } diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs index 14c9f4f0..0c82b9c3 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs @@ -7,6 +7,42 @@ public class SemanticValidatorTests { private readonly SemanticValidator _sut = new(); + [Fact] + public void Validate_NativeAlarmSource_UnknownConnection_ReturnsError() + { + var cfg = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = [new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "Ghost", SourceReference = "x" }] + }; + var result = _sut.Validate(cfg, alarmCapableConnectionNames: new HashSet { "RealConn" }); + Assert.Contains(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid); + } + + [Fact] + public void Validate_NativeAlarmSource_EmptySourceRef_ReturnsError() + { + var cfg = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = [new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "RealConn", SourceReference = "" }] + }; + var result = _sut.Validate(cfg, alarmCapableConnectionNames: new HashSet { "RealConn" }); + Assert.Contains(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid); + } + + [Fact] + public void Validate_NativeAlarmSource_ValidBinding_NoError() + { + var cfg = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = [new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "RealConn", SourceReference = "ns=2;s=T1" }] + }; + var result = _sut.Validate(cfg, alarmCapableConnectionNames: new HashSet { "RealConn" }); + Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid); + } + [Fact] public void Validate_CallScriptTargetNotFound_ReturnsError() {