using System.Text.RegularExpressions; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests; /// /// Driver.MTConnect-005 — MTConnectDriver._options must be published and read the way its /// three siblings at the same commit point are (_session, _binding, /// _announcedInstanceId): Volatile.Write once, Volatile.Read everywhere else. /// /// /// /// Why a source scan and not a behavioural test. The defect is a missing memory fence: /// a plain field write publishes the new options document with no release, and a reader that /// reads _options BEFORE it reads _binding — which both ReadAsync and /// WriteAsync do — has no acquire ordering between the two, so it can in principle /// judge a batch against the previous Write.Enabled / RequestTimeoutMs paired /// with the new tag table. No test can force that interleaving on x86/arm64 in bounded time; /// what IS checkable, and what actually regresses, is whether a new read site was written /// with the fence. This is the same shape as /// LibplctagGatewayConsumptionGuardTests: the defect recurs by being reintroduced at a /// call site no per-site test covers. /// /// /// The floor below exists so the scan cannot go blind. If the count drops, the field was /// renamed or the sites were consolidated — re-derive the floor from the code; never lower it /// to make this pass. /// /// [Trait("Category", "Unit")] public sealed class MTConnectOptionsPublicationTests { /// Any mention of the field, so an unguarded read is caught as well as an unguarded write. private static readonly Regex Mention = new(@"_options\b", RegexOptions.Compiled); /// The two guarded forms. Nothing else may touch the field. private static readonly Regex Guarded = new(@"Volatile\.(Read|Write)\(ref _options[,)]", RegexOptions.Compiled); /// The declaration itself, which is neither a read nor a write. private static readonly Regex Declaration = new(@"^\s*private\s+MTConnectDriverOptions\s+_options\s*;\s*$", RegexOptions.Compiled); [Fact] public void Every_touch_of_the_shared_options_field_goes_through_Volatile() { var source = File.ReadAllLines(Path.Combine( LocateRepoRoot(), "src", "Drivers", "ZB.MOM.WW.OtOpcUa.Driver.MTConnect", "MTConnectDriver.cs")); var offending = new List(); var guarded = 0; for (var i = 0; i < source.Length; i++) { var line = source[i]; var code = StripComment(line); if (!Mention.IsMatch(code) || Declaration.IsMatch(code)) { continue; } var mentions = Mention.Matches(code).Count; var fenced = Guarded.Matches(code).Count; guarded += fenced; if (fenced < mentions) { offending.Add($"line {i + 1}: {line.Trim()}"); } } offending.ShouldBeEmpty( "MTConnectDriver._options is shared state read lock-free from the read, write, discovery " + "and health paths while a re-initialize replaces it. Publish it with Volatile.Write and " + "read it with Volatile.Read, exactly as _session / _binding / _announcedInstanceId are " + $"(Driver.MTConnect-005). Unfenced: {string.Join(" | ", offending)}"); guarded.ShouldBeGreaterThanOrEqualTo(12); } /// Removes a trailing line comment and skips XML-doc lines, which mention the field by name. private static string StripComment(string line) { var trimmed = line.TrimStart(); if (trimmed.StartsWith("//", StringComparison.Ordinal) || trimmed.StartsWith("*", StringComparison.Ordinal)) { return string.Empty; } var comment = line.IndexOf("//", StringComparison.Ordinal); return comment >= 0 ? line[..comment] : line; } private static string LocateRepoRoot() { 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("Could not locate the repo root (ZB.MOM.WW.OtOpcUa.slnx) above the test assembly."); return dir.FullName; } }