using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Types; namespace ZB.MOM.WW.OtOpcUa.Commons.Tests; public class EquipmentScriptPathsTests { // ---- ExtractAlarmDependencyRefs (scripted-alarm dep graph; byte-parity seam) ---- [Fact] public void ExtractAlarmDependencyRefs_predicate_reads_first_then_template_tokens_deduped() { // A template token identical to a predicate read appears once (predicate-first); a distinct // template token is appended after. This dedup/order is the parity contract both the composer // and the artifact decode rely on. EquipmentScriptPaths .ExtractAlarmDependencyRefs( predicateSource: "return ctx.GetTag(\"Mach.Temp\").Value > 90;", messageTemplate: "Temp {Mach.Temp} over {Mach.Limit}") .ShouldBe(["Mach.Temp", "Mach.Limit"]); } [Fact] public void ExtractAlarmDependencyRefs_excludes_reserved_equip_double_brace_token() { // The reserved {{equip}} double-brace form must NOT be picked up as a single-brace {TagPath} // token; only the genuine {Line.Temp} token is extracted. EquipmentScriptPaths .ExtractAlarmDependencyRefs( predicateSource: null, messageTemplate: "{{equip}} too hot: {Line.Temp}") .ShouldBe(["Line.Temp"]); } // ---- DeriveEquipmentBase ---- [Fact] public void DeriveEquipmentBase_returns_shared_prefix() { EquipmentScriptPaths .DeriveEquipmentBase(["TestMachine_001.A", "TestMachine_001.B"]) .ShouldBe("TestMachine_001"); } [Fact] public void DeriveEquipmentBase_returns_null_when_prefixes_diverge() { EquipmentScriptPaths .DeriveEquipmentBase(["TestMachine_001.A", "DelmiaReceiver_001.B"]) .ShouldBeNull(); } [Fact] public void DeriveEquipmentBase_returns_null_for_empty_sequence() { EquipmentScriptPaths .DeriveEquipmentBase([]) .ShouldBeNull(); } [Fact] public void DeriveEquipmentBase_returns_whole_value_when_no_dot() { EquipmentScriptPaths .DeriveEquipmentBase(["NoDot"]) .ShouldBe("NoDot"); } [Fact] public void DeriveEquipmentBase_skips_null_empty_and_whitespace_entries() { EquipmentScriptPaths .DeriveEquipmentBase([null, "", " ", "TestMachine_001.A"]) .ShouldBe("TestMachine_001"); } // ---- SubstituteEquipmentToken ---- [Fact] public void SubstituteEquipmentToken_substitutes_inside_GetTag() { var result = EquipmentScriptPaths.SubstituteEquipmentToken( "ctx.GetTag(\"{{equip}}.Source\")", "TestMachine_001"); result.ShouldContain("ctx.GetTag(\"TestMachine_001.Source\")"); } [Fact] public void SubstituteEquipmentToken_substitutes_inside_SetVirtualTag() { var result = EquipmentScriptPaths.SubstituteEquipmentToken( "ctx.SetVirtualTag(\"{{equip}}.Out\", x)", "TestMachine_001"); result.ShouldContain("ctx.SetVirtualTag(\"TestMachine_001.Out\""); } [Fact] public void SubstituteEquipmentToken_leaves_comment_unchanged() { var source = "// uses {{equip}} here"; EquipmentScriptPaths.SubstituteEquipmentToken(source, "TestMachine_001") .ShouldBe(source); } [Fact] public void SubstituteEquipmentToken_leaves_logger_string_unchanged() { var source = "ctx.Logger.Information(\"{{equip}}\")"; EquipmentScriptPaths.SubstituteEquipmentToken(source, "TestMachine_001") .ShouldBe(source); } [Fact] public void SubstituteEquipmentToken_substitutes_all_occurrences() { var source = "ctx.GetTag(\"{{equip}}.A\"); ctx.GetTag(\"{{equip}}.B\");"; var result = EquipmentScriptPaths.SubstituteEquipmentToken(source, "TestMachine_001"); result.ShouldContain("ctx.GetTag(\"TestMachine_001.A\")"); result.ShouldContain("ctx.GetTag(\"TestMachine_001.B\")"); result.ShouldNotContain(EquipmentScriptPaths.EquipToken); } [Fact] public void SubstituteEquipmentToken_is_identity_when_equipBase_is_null() { var source = "ctx.GetTag(\"{{equip}}.Source\")"; EquipmentScriptPaths.SubstituteEquipmentToken(source, null) .ShouldBe(source); } [Fact] public void SubstituteEquipmentToken_is_identity_when_equipBase_is_empty() { var source = "ctx.GetTag(\"{{equip}}.Source\")"; EquipmentScriptPaths.SubstituteEquipmentToken(source, "") .ShouldBe(source); } [Fact] public void SubstituteEquipmentToken_is_identity_when_token_absent() { var source = "ctx.GetTag(\"TestMachine_001.Source\")"; EquipmentScriptPaths.SubstituteEquipmentToken(source, "TestMachine_001") .ShouldBe(source); } [Fact] public void SubstituteEquipmentToken_does_not_touch_raw_string_literal() { // Documents the regex limitation: only "double-quote" literals are handled, // matching the existing seam extractors. A raw-string literal is left as-is. var source = "ctx.GetTag(\"\"\"{{equip}}.X\"\"\")"; EquipmentScriptPaths.SubstituteEquipmentToken(source, "TestMachine_001") .ShouldBe(source); } // ---- ExtractDependencyRefs ---- [Fact] public void ExtractDependencyRefs_returns_distinct_refs_in_first_seen_order() { var source = "ctx.GetTag(\"A\"); ctx.GetTag(\"B\"); ctx.GetTag(\"A\");"; EquipmentScriptPaths.ExtractDependencyRefs(source) .ShouldBe(["A", "B"]); } [Fact] public void ExtractDependencyRefs_excludes_SetVirtualTag_writes() { var source = "ctx.GetTag(\"X\"); ctx.SetVirtualTag(\"Y\", 1);"; EquipmentScriptPaths.ExtractDependencyRefs(source) .ShouldBe(["X"]); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void ExtractDependencyRefs_returns_empty_for_null_or_whitespace(string? source) { EquipmentScriptPaths.ExtractDependencyRefs(source).ShouldBeEmpty(); } // ---- ContainsEquipToken ---- [Fact] public void ContainsEquipToken_true_when_token_present() { EquipmentScriptPaths.ContainsEquipToken("ctx.GetTag(\"{{equip}}.X\")").ShouldBeTrue(); } [Fact] public void ContainsEquipToken_false_when_token_absent() { EquipmentScriptPaths.ContainsEquipToken("ctx.GetTag(\"X\")").ShouldBeFalse(); } [Fact] public void ContainsEquipToken_false_for_null() { EquipmentScriptPaths.ContainsEquipToken(null).ShouldBeFalse(); } }