using ScadaLink.Commons.Types.Flattening; using ScadaLink.TemplateEngine.Validation; namespace ScadaLink.TemplateEngine.Tests.Validation; public class SemanticValidatorTests { private readonly SemanticValidator _sut = new(); [Fact] public void Validate_CallScriptTargetNotFound_ReturnsError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "Caller", Code = "CallScript(\"NonExistent\");" } ] }; var result = _sut.Validate(config); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound && e.Message.Contains("NonExistent")); } [Fact] public void Validate_CallScriptTargetExists_NoError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "Target", Code = "var x = 1;" }, new ResolvedScript { CanonicalName = "Caller", Code = "CallScript(\"Target\");" } ] }; var result = _sut.Validate(config); Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound); } [Fact] public void Validate_CallSharedTargetNotFound_ReturnsError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "Caller", Code = "CallShared(\"MissingShared\");" } ] }; var result = _sut.Validate(config, sharedScripts: []); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound && e.Message.Contains("MissingShared")); } [Fact] public void Validate_CallSharedTargetExists_NoError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "Caller", Code = "CallShared(\"Utility\");" } ] }; var shared = new List { new() { CanonicalName = "Utility", Code = "// shared" } }; var result = _sut.Validate(config, shared); Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound); } [Fact] public void Validate_ParameterCountMismatch_ReturnsError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "Target", Code = "var x = 1;", ParameterDefinitions = "[{\"name\":\"a\",\"type\":\"Int32\"},{\"name\":\"b\",\"type\":\"String\"}]" }, new ResolvedScript { CanonicalName = "Caller", Code = "CallScript(\"Target\", 42);" // 1 arg but 2 expected } ] }; var result = _sut.Validate(config); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch); } [Fact] public void Validate_RangeViolationOnNonNumeric_ReturnsError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Attributes = [ new ResolvedAttribute { CanonicalName = "Status", DataType = "String" } ], Alarms = [ new ResolvedAlarm { CanonicalName = "BadAlarm", TriggerType = "RangeViolation", TriggerConfiguration = "{\"attributeName\":\"Status\"}" } ] }; var result = _sut.Validate(config); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType); } [Fact] public void Validate_RangeViolationOnNumeric_NoError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Attributes = [ new ResolvedAttribute { CanonicalName = "Temp", DataType = "Double" } ], Alarms = [ new ResolvedAlarm { CanonicalName = "HighTemp", TriggerType = "RangeViolation", TriggerConfiguration = "{\"attributeName\":\"Temp\"}" } ] }; var result = _sut.Validate(config); Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType); } [Fact] public void Validate_OnTriggerScriptNotFound_ReturnsError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [new ResolvedScript { CanonicalName = "OtherScript", Code = "var x = 1;" }], Alarms = [ new ResolvedAlarm { CanonicalName = "Alarm1", TriggerType = "ValueMatch", OnTriggerScriptCanonicalName = "MissingScript" } ] }; var result = _sut.Validate(config); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.OnTriggerScriptNotFound); } [Fact] public void Validate_InstanceScriptCallsAlarmOnTrigger_ReturnsError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "AlarmHandler", Code = "// alarm handler" }, new ResolvedScript { CanonicalName = "RegularScript", Code = "CallScript(\"AlarmHandler\");" } ], Alarms = [ new ResolvedAlarm { CanonicalName = "Alarm1", TriggerType = "ValueMatch", OnTriggerScriptCanonicalName = "AlarmHandler" } ] }; var result = _sut.Validate(config); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.CrossCallViolation); } [Fact] public void ExtractCallTargets_MultipleCallTypes() { var code = @" var x = CallScript(""Script1"", arg1, arg2); CallShared(""Shared1""); CallScript(""Script2""); "; var targets = SemanticValidator.ExtractCallTargets(code); Assert.Equal(3, targets.Count); Assert.Contains(targets, t => t.TargetName == "Script1" && !t.IsShared && t.ArgumentCount == 2); Assert.Contains(targets, t => t.TargetName == "Shared1" && t.IsShared && t.ArgumentCount == 0); Assert.Contains(targets, t => t.TargetName == "Script2" && !t.IsShared && t.ArgumentCount == 0); } [Fact] public void ParseParameterDefinitions_ValidJson_ReturnsList() { var json = "[{\"name\":\"a\",\"type\":\"Int32\"},{\"name\":\"b\",\"type\":\"String\"}]"; var result = SemanticValidator.ParseParameterDefinitions(json); Assert.Equal(2, result.Count); Assert.Equal("Int32", result[0]); Assert.Equal("String", result[1]); } [Fact] public void ParseParameterDefinitions_NullOrEmpty_ReturnsEmpty() { Assert.Empty(SemanticValidator.ParseParameterDefinitions(null)); Assert.Empty(SemanticValidator.ParseParameterDefinitions("")); } }