using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Scripting; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests; /// /// Unit tests for — the Roslyn-free classifier that recognises /// the trivial mirror shape return ctx.GetTag("X").Value;. The pattern must be narrow: /// any near-miss (arithmetic, a different member, multi-statement, a different method) falls /// through so the Roslyn path stays authoritative. /// public sealed class PassthroughScriptTests { /// The canonical mirror shape matches and captures the referenced tag. [Fact] public void Matches_canonical_mirror_and_captures_tag() { PassthroughScript.TryMatch("return ctx.GetTag(\"a\").Value;", out var tag).ShouldBeTrue(); tag.ShouldBe("a"); } /// A dotted / hierarchical tag reference is captured verbatim. [Fact] public void Captures_dotted_tag_reference() { PassthroughScript.TryMatch("return ctx.GetTag(\"Area1.Machine.Speed\").Value;", out var tag) .ShouldBeTrue(); tag.ShouldBe("Area1.Machine.Speed"); } /// Whitespace around every token is tolerated. [Fact] public void Tolerates_surrounding_and_inner_whitespace() { PassthroughScript.TryMatch(" return ctx . GetTag( \"a\" ) . Value ; ", out var tag) .ShouldBeTrue(); tag.ShouldBe("a"); } /// Arithmetic on the mirror value is NOT a passthrough. [Fact] public void Rejects_arithmetic_on_value() { PassthroughScript.TryMatch("return (int)ctx.GetTag(\"a\").Value + 1;", out _).ShouldBeFalse(); } /// Reading a different member (StatusCode) is NOT a passthrough. [Fact] public void Rejects_other_member_access() { PassthroughScript.TryMatch("return ctx.GetTag(\"a\").StatusCode;", out _).ShouldBeFalse(); } /// A multi-statement body is NOT a passthrough. [Fact] public void Rejects_multi_statement_body() { PassthroughScript.TryMatch("var x = ctx.GetTag(\"a\").Value; return x;", out _).ShouldBeFalse(); } /// A different method name is NOT a passthrough. [Fact] public void Rejects_different_method() { PassthroughScript.TryMatch("return ctx.GetVirtualTag(\"a\").Value;", out _).ShouldBeFalse(); } /// Null / empty / whitespace input is rejected. [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Rejects_null_or_blank(string? source) { PassthroughScript.TryMatch(source, out var tag).ShouldBeFalse(); tag.ShouldBe(string.Empty); } /// /// A tag literal containing a backslash escape (C# source "a\\b" → runtime name /// a\b) does NOT match the passthrough pattern — it falls through to Roslyn, which /// interprets the escape and resolves the correct dependency key. Capturing the raw source /// text a\\b would produce a wrong-result silent miss against the key a\b. /// [Fact] public void Rejects_tag_literal_containing_backslash_escape() { // C# literal "return ctx.GetTag(\"a\\\\b\").Value;" → script source contains: a\\b (two chars: backslash + b) PassthroughScript.TryMatch("return ctx.GetTag(\"a\\\\b\").Value;", out var tag).ShouldBeFalse(); tag.ShouldBe(string.Empty); } /// A plain dotted tag name (no backslash) still matches — the fix is additive only. [Fact] public void Matches_plain_dotted_tag_after_backslash_fix() { PassthroughScript.TryMatch("return ctx.GetTag(\"Site1.Area.Tag\").Value;", out var tag).ShouldBeTrue(); tag.ShouldBe("Site1.Area.Tag"); } }