diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Configuration/MeshRegistrationRoleScopingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Configuration/MeshRegistrationRoleScopingTests.cs
index e9e16a4f..58d5d93f 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Configuration/MeshRegistrationRoleScopingTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Configuration/MeshRegistrationRoleScopingTests.cs
@@ -16,7 +16,13 @@ namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
/// Program.cs gates several unrelated concerns behind repeated if (hasAdmin) checks
/// (auth/AdminUI mounting, health probes, gRPC endpoint mapping, ...), so this pin cannot
/// assume there is only one such block in the file — it must find the SPECIFIC block that
-/// contains each registration and prove that containment is unambiguous.
+/// contains each registration and prove that containment is unambiguous. One of the six
+/// markers (the ConfigDb gate) is a braceless single-statement if; the scanner handles
+/// that shape explicitly rather than scanning forward for a brace that belongs to unrelated
+/// code (see ). The scanner is also NOT string/comment
+/// aware — a literal {/}/; inside a string or comment inside a scanned
+/// span would misscan; it survives today only because no such span contains one, and the
+/// implausible-length tripwire below is the cheap backstop for exactly this class of mistake.
///
[Trait("Category", "Unit")]
public sealed class MeshRegistrationRoleScopingTests
@@ -29,12 +35,18 @@ public sealed class MeshRegistrationRoleScopingTests
private const string HasAdminMarker = "if (hasAdmin)";
+ // The real hasAdmin blocks in Program.cs top out around 40 lines (the mesh-registration block
+ // itself). 150 leaves ample margin for legitimate growth while still catching a mis-scan: the
+ // phantom-block defect this tripwire was added for fabricated a ~290-line range by adopting an
+ // unrelated if-block's brace.
+ private const int MaxPlausibleBlockLines = 150;
+
private static string ReadProgramSource()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx")))
dir = dir.Parent;
- dir.ShouldNotBeNull();
+ dir.ShouldNotBeNull("could not locate the repository root (no ZB.MOM.WW.OtOpcUa.slnx above the test binary)");
return File.ReadAllText(Path.Combine(
dir!.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.Host", "Program.cs"));
@@ -54,9 +66,11 @@ public sealed class MeshRegistrationRoleScopingTests
}
///
- /// Brace-matches every if (hasAdmin) block in the file (not a regex over the block
+ /// Scans every if (hasAdmin) occurrence in the file (not a regex over the block
/// body — the body is free-form C# and a regex would be fragile to reformatting) and
- /// returns each block's (open-brace, close-brace) character range.
+ /// returns each one's body as a (start, end) character range. C# permits an if with
+ /// no braces at all when the body is a single statement (Program.cs's ConfigDb gate is
+ /// exactly this shape), so each occurrence is classified by what actually follows it:
///
private static List<(int Start, int End)> FindHasAdminBlockRanges(string source)
{
@@ -68,37 +82,91 @@ public sealed class MeshRegistrationRoleScopingTests
if (markerIndex < 0)
break;
- var openBraceIndex = source.IndexOf('{', markerIndex + HasAdminMarker.Length);
- openBraceIndex.ShouldBeGreaterThan(-1,
- $"'{HasAdminMarker}' at offset {markerIndex} is not followed by an opening brace.");
+ var bodyStart = markerIndex + HasAdminMarker.Length;
+ while (bodyStart < source.Length && char.IsWhiteSpace(source[bodyStart]))
+ bodyStart++;
+ bodyStart.ShouldBeLessThan(source.Length,
+ $"'{HasAdminMarker}' at offset {markerIndex} has no body — reached end of file.");
- var depth = 0;
- var closeBraceIndex = -1;
- for (var i = openBraceIndex; i < source.Length; i++)
- {
- if (source[i] == '{')
- depth++;
- else if (source[i] == '}')
- {
- depth--;
- if (depth == 0)
- {
- closeBraceIndex = i;
- break;
- }
- }
- }
+ (int Start, int End) range;
+ if (source[bodyStart] == '{')
+ range = ScanBracedBlock(source, bodyStart, markerIndex);
+ else
+ // NOT a fallback IndexOf('{', ...) scan: the next brace in the file belongs to
+ // whatever code comes after this statement, not to this if — adopting it fabricates
+ // a phantom block spanning unrelated code (the exact defect this scanner exists to
+ // catch when it happens to a REAL registration).
+ range = ScanBracelessStatement(source, bodyStart, markerIndex);
- closeBraceIndex.ShouldBeGreaterThan(-1,
- $"brace-matching for the '{HasAdminMarker}' block starting at offset {markerIndex} never closed.");
+ var lineCount = 1 + CountOccurrences(source.Substring(range.Start, range.End - range.Start + 1), "\n");
+ lineCount.ShouldBeLessThanOrEqualTo(MaxPlausibleBlockLines,
+ $"the scanned '{HasAdminMarker}' body starting at offset {range.Start} spans {lineCount} " +
+ "lines, which is implausibly long for one of this file's admin-role gates — almost " +
+ "certainly a mis-scan (e.g. a braceless if adopting an unrelated block's brace) rather " +
+ "than a real block. Do not trust this range; fix the scanner instead.");
- ranges.Add((openBraceIndex, closeBraceIndex));
- searchIndex = closeBraceIndex + 1;
+ ranges.Add(range);
+ searchIndex = range.End + 1;
}
return ranges;
}
+ private static (int Start, int End) ScanBracedBlock(string source, int openBraceIndex, int markerIndex)
+ {
+ var depth = 0;
+ var closeBraceIndex = -1;
+ for (var i = openBraceIndex; i < source.Length; i++)
+ {
+ if (source[i] == '{')
+ depth++;
+ else if (source[i] == '}')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ closeBraceIndex = i;
+ break;
+ }
+ }
+ }
+
+ closeBraceIndex.ShouldBeGreaterThan(-1,
+ $"brace-matching for the '{HasAdminMarker}' block starting at offset {markerIndex} never closed.");
+
+ return (openBraceIndex, closeBraceIndex);
+ }
+
+ ///
+ /// Bounds a braceless single-statement if body to its terminating ;, tracking
+ /// paren/brace depth so a semicolon nested inside the statement itself (e.g. a lambda or
+ /// object initializer in the call's arguments) does not end the scan early.
+ ///
+ private static (int Start, int End) ScanBracelessStatement(string source, int statementStart, int markerIndex)
+ {
+ var depth = 0;
+ var semicolonIndex = -1;
+ for (var i = statementStart; i < source.Length; i++)
+ {
+ var c = source[i];
+ if (c is '(' or '{')
+ depth++;
+ else if (c is ')' or '}')
+ depth--;
+ else if (c == ';' && depth == 0)
+ {
+ semicolonIndex = i;
+ break;
+ }
+ }
+
+ semicolonIndex.ShouldBeGreaterThan(-1,
+ $"the braceless '{HasAdminMarker}' single-statement body starting at offset {markerIndex} " +
+ "never terminated with ';'.");
+
+ return (statementStart, semicolonIndex);
+ }
+
///
/// Locates the single if (hasAdmin) block that contains .
/// Fails loudly — rather than silently picking the first match — if the registration is
@@ -166,4 +234,29 @@ public sealed class MeshRegistrationRoleScopingTests
block.ShouldContain(DialerRegistration, Case.Sensitive,
$"IMeshCommandDialer must be registered inside 'if (hasAdmin)'. {why}");
}
+
+ ///
+ /// Pins the scanner's expected result shape against today's Program.cs — 6 markers,
+ /// 5 braced blocks + the 1 braceless ConfigDb gate, so 6 scanned ranges. Without this, a
+ /// scanner bug that silently drops or merges a marker (as the pre-fix version did to the
+ /// braceless marker — it fabricated a phantom range spanning an unrelated if-block and swallowed
+ /// a real nested 'if (hasAdmin)' block inside it, yielding 5 ranges instead of 6) would not
+ /// surface unless it happened to also break one of the two registration tests above.
+ ///
+ [Fact]
+ public void HasAdminMarkerScan_MatchesTheKnownShapeOfProgramCs()
+ {
+ var source = ReadProgramSource();
+
+ CountOccurrences(source, HasAdminMarker).ShouldBe(6,
+ "Program.cs's 'if (hasAdmin)' marker count changed. If a marker was added or removed, " +
+ "update this pin — and check whether the new/removed occurrence is braced or braceless — " +
+ "rather than letting the scanner silently mis-shape its result.");
+
+ var ranges = FindHasAdminBlockRanges(source);
+ ranges.Count.ShouldBe(6,
+ "expected one scanned range per 'if (hasAdmin)' marker, including the braceless ConfigDb " +
+ "gate. A mismatch means the scanner adopted an unrelated brace or otherwise merged/dropped " +
+ "a marker instead of handling each one independently.");
+ }
}