using System.Text; using Akka.Actor; using ZB.MOM.WW.ScadaBridge.Commons.Validation; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Deployment; /// /// Pins (Commons, which cannot reference Akka) to the rule it /// mirrors, . The site deploy gate rejects names /// through on the strength of this equivalence (review 2026-10-21 /// 01/SR-C1): a name the mirror accepts but Akka refuses would slip past the gate and fail at /// Context.ActorOf; a name the mirror refuses but Akka accepts would block a deploy that /// would have worked. Run on this test project because it is the nearest one referencing both. /// public class ActorNameRulesParityTests { /// Printable ASCII 0x20–0x7E. private static IEnumerable PrintableAscii() { for (var c = (char)0x20; c <= (char)0x7E; c++) yield return c; } /// /// Characters Akka.NET 1.5 accepts in a path element that does /// not (found by this sweep, 2026-10-22). The mirror is STRICTER here — it can only refuse a /// name Akka would have created, never admit one Akka refuses — which is why the site deploy /// gate (DeployCompileValidator) decides with /// itself rather than the mirror. Reported for the Commons owner to widen the mirror; once /// it does, this set simply stops matching anything and the test keeps passing. /// private const string KnownMirrorStricterChars = "\"()"; private static void AssertParity(string candidate, List disagreements) { var mirror = ActorNameRules.IsValid(candidate); var akka = ActorPath.IsValidPathElement(candidate); if (mirror != akka && !IsKnownMirrorStricterGap(candidate, mirror, akka)) { disagreements.Add( $"\"{Escape(candidate)}\": ActorNameRules={mirror}, Akka={akka}"); } } /// /// True when the disagreement is exactly the documented gap: the mirror refuses, Akka /// accepts, and replacing every character with a /// letter makes the two agree again. /// private static bool IsKnownMirrorStricterGap(string candidate, bool mirror, bool akka) { if (mirror || !akka || candidate.IndexOfAny(KnownMirrorStricterChars.ToCharArray()) < 0) return false; var patched = new string(candidate .Select(c => KnownMirrorStricterChars.Contains(c) ? 'a' : c).ToArray()); return ActorNameRules.IsValid(patched) == ActorPath.IsValidPathElement(patched); } /// /// Akka accepts every known-gap character, so a name such as Pump(A) is deployable — /// which is why the site gate must not decide with the stricter mirror. /// [Theory] [InlineData("\"")] [InlineData("(")] [InlineData(")")] [InlineData("Pump(A)")] public void KnownGapCharacters_AreAcceptedByAkka(string candidate) { Assert.True(ActorPath.IsValidPathElement(candidate)); } private static string Escape(string s) { var sb = new StringBuilder(); foreach (var c in s) sb.Append(c is < (char)0x20 or > (char)0x7E ? $"\\u{(int)c:X4}" : c.ToString()); return sb.ToString(); } /// Every printable ASCII character alone, and with a letter on either side. [Fact] public void SingleCharacters_AgreeWithAkka() { var disagreements = new List(); foreach (var c in PrintableAscii()) { AssertParity(c.ToString(), disagreements); AssertParity("a" + c, disagreements); AssertParity(c + "a", disagreements); } Assert.True(disagreements.Count == 0, string.Join(Environment.NewLine, disagreements)); } /// Every ordered pair of printable ASCII characters (95 × 95). [Fact] public void CharacterPairs_AgreeWithAkka() { var disagreements = new List(); foreach (var a in PrintableAscii()) foreach (var b in PrintableAscii()) AssertParity(new string([a, b]), disagreements); Assert.True(disagreements.Count == 0, string.Join(Environment.NewLine, disagreements)); } /// Percent-escapes, leading '$', empty/null-ish and non-ASCII samples. [Theory] [InlineData("")] [InlineData("%41")] [InlineData("a%41b")] [InlineData("%4")] [InlineData("%")] [InlineData("a%")] [InlineData("%%41")] [InlineData("%G1")] [InlineData("%1G")] [InlineData("%aF")] [InlineData("%41%42")] [InlineData("$")] [InlineData("$a")] [InlineData("a$")] [InlineData("a$b")] [InlineData("-a")] [InlineData("Furnace #3")] [InlineData("Line 1")] [InlineData("a b")] [InlineData("Ofen Süd")] [InlineData("Ünit")] [InlineData("名前")] [InlineData("é")] [InlineData("a\tb")] [InlineData("a\nb")] [InlineData("script-Module.Member")] [InlineData("alarm-HiTemp")] [InlineData("native-alarm-Conn.Source")] [InlineData("Pump_01;x'y~z!")] public void Samples_AgreeWithAkka(string candidate) { var disagreements = new List(); AssertParity(candidate, disagreements); Assert.True(disagreements.Count == 0, string.Join(Environment.NewLine, disagreements)); } /// Null is invalid on both sides of the mirror. [Fact] public void Null_IsInvalid() { Assert.False(ActorNameRules.IsValid(null)); Assert.NotNull(ActorNameRules.Describe(null)); } }