f5090fcd9e
An instance unique name Akka refuses as a path element (space, '#', '/', non-ASCII) made Context.ActorOf throw on the singleton's own thread; the default Restart stopped every Instance Actor on the site. DeployCompileValidator now checks the instance + script/alarm/native-source actor names with ActorPath.IsValidPathElement (the Commons ActorNameRules mirror is stricter than Akka for " ( ), pinned by a parity sweep), CreateInstanceActor catches InvalidActorNameException and every caller reports it, and the Instance Actor skips a refused child name like a compile failure.
155 lines
5.7 KiB
C#
155 lines
5.7 KiB
C#
using System.Text;
|
||
using Akka.Actor;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Validation;
|
||
|
||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Deployment;
|
||
|
||
/// <summary>
|
||
/// Pins <see cref="ActorNameRules"/> (Commons, which cannot reference Akka) to the rule it
|
||
/// mirrors, <see cref="ActorPath.IsValidPathElement"/>. The site deploy gate rejects names
|
||
/// through <see cref="ActorNameRules"/> 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
|
||
/// <c>Context.ActorOf</c>; 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.
|
||
/// </summary>
|
||
public class ActorNameRulesParityTests
|
||
{
|
||
/// <summary>Printable ASCII 0x20–0x7E.</summary>
|
||
private static IEnumerable<char> PrintableAscii()
|
||
{
|
||
for (var c = (char)0x20; c <= (char)0x7E; c++)
|
||
yield return c;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Characters Akka.NET 1.5 accepts in a path element that <see cref="ActorNameRules"/> 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 (<c>DeployCompileValidator</c>) decides with <see cref="ActorPath.IsValidPathElement"/>
|
||
/// 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.
|
||
/// </summary>
|
||
private const string KnownMirrorStricterChars = "\"()";
|
||
|
||
private static void AssertParity(string candidate, List<string> 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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// True when the disagreement is exactly the documented gap: the mirror refuses, Akka
|
||
/// accepts, and replacing every <see cref="KnownMirrorStricterChars"/> character with a
|
||
/// letter makes the two agree again.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Akka accepts every known-gap character, so a name such as <c>Pump(A)</c> is deployable —
|
||
/// which is why the site gate must not decide with the stricter mirror.
|
||
/// </summary>
|
||
[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();
|
||
}
|
||
|
||
/// <summary>Every printable ASCII character alone, and with a letter on either side.</summary>
|
||
[Fact]
|
||
public void SingleCharacters_AgreeWithAkka()
|
||
{
|
||
var disagreements = new List<string>();
|
||
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));
|
||
}
|
||
|
||
/// <summary>Every ordered pair of printable ASCII characters (95 × 95).</summary>
|
||
[Fact]
|
||
public void CharacterPairs_AgreeWithAkka()
|
||
{
|
||
var disagreements = new List<string>();
|
||
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));
|
||
}
|
||
|
||
/// <summary>Percent-escapes, leading '$', empty/null-ish and non-ASCII samples.</summary>
|
||
[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<string>();
|
||
AssertParity(candidate, disagreements);
|
||
Assert.True(disagreements.Count == 0, string.Join(Environment.NewLine, disagreements));
|
||
}
|
||
|
||
/// <summary>Null is invalid on both sides of the mirror.</summary>
|
||
[Fact]
|
||
public void Null_IsInvalid()
|
||
{
|
||
Assert.False(ActorNameRules.IsValid(null));
|
||
Assert.NotNull(ActorNameRules.Describe(null));
|
||
}
|
||
}
|