using System.Runtime.CompilerServices; using System.Text.RegularExpressions; namespace ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests; /// /// Deferred-work register row 32 — isolation for the process-global StoreAndForward /// queue-depth gauge. /// /// /// scadabridge.store_and_forward.queue.depth is an /// created once on ScadaBridgeTelemetry's static meter, reading a single /// process-global provider slot (SetQueueDepthProvider / /// ClearQueueDepthProvider). Every StoreAndForwardService.StartAsync /// writes that one slot — last registrant wins. That is the PRODUCT design (one S&F /// engine per node), and three of ' facts assert /// exactly the slot-ownership semantics (compare-and-clear identity, stop/restart /// re-seed), so they cannot be rewritten against a per-test meter without changing /// what they prove — the global slot IS their subject. /// /// /// /// The resource therefore needs mutual exclusion, not duplication: every test class in /// this assembly that starts a StoreAndForwardService (and so writes the slot) /// joins this collection, which xUnit runs serially with respect to itself. Classes /// that never start a service stay free to run in parallel. Before this, a concurrent /// StartAsync in a sibling class clobbered the gauge mid-assertion and /// Gauge_TracksBufferedDepth_AcrossEnqueueDrainAndPark read the sibling's depth /// (observed: expected 0, actual 6 — roughly 2 runs in 5 with /// parallelizeTestCollections: true). /// /// /// /// keeps the membership honest: a new /// test class that starts a service without joining this collection fails the guard /// rather than silently reintroducing the flake. /// /// [CollectionDefinition(Name)] public sealed class QueueDepthGaugeCollection { /// Collection name; referenced from [Collection(...)] on member classes. public const string Name = "StoreAndForward queue-depth gauge (process-global provider slot)"; } /// /// Staleness self-test for : asserts that every /// source file in this test project which both constructs a StoreAndForwardService /// and calls StartAsync — i.e. writes the process-global queue-depth provider /// slot — declares [Collection(QueueDepthGaugeCollection.Name)]. /// /// /// The project directory is resolved from (baked in /// at compile time by the compiler that built these sources), not from the test host's /// working directory or the output folder, so the scan finds the sources that produced /// this assembly. /// /// public class QueueDepthGaugeCollectionGuardTests { /// Absolute path of this source file, captured at compile time. private static string ThisFilePath([CallerFilePath] string path = "") => path; /// Matches a construction of the service under a global-slot-writing test. private static readonly Regex ConstructsService = new(@"new\s+StoreAndForwardService\s*\(", RegexOptions.Compiled); /// Matches a start call, which is what actually writes the global provider slot. private static readonly Regex StartsService = new(@"\.StartAsync\s*\(", RegexOptions.Compiled); /// Matches the collection attribute that puts a class in the serialized group. private static readonly Regex DeclaresCollection = new(@"\[\s*Collection\s*\(\s*QueueDepthGaugeCollection\.Name\s*\)\s*\]", RegexOptions.Compiled); [Fact] public void EverySourceFileThatStartsAService_JoinsTheGaugeCollection() { var projectDir = Path.GetDirectoryName(ThisFilePath())!; Assert.True(Directory.Exists(projectDir), $"Guard could not locate the test project sources at '{projectDir}'. " + "The [CallerFilePath] anchor must point at this project's directory."); var offenders = new List(); var members = new List(); foreach (var file in Directory.EnumerateFiles(projectDir, "*.cs", SearchOption.AllDirectories)) { // Skip generated output (bin/obj) and this guard's own definition file. var relative = Path.GetRelativePath(projectDir, file); if (relative.StartsWith("bin", StringComparison.Ordinal) || relative.StartsWith("obj", StringComparison.Ordinal)) { continue; } var text = File.ReadAllText(file); if (!ConstructsService.IsMatch(text) || !StartsService.IsMatch(text)) { continue; } if (DeclaresCollection.IsMatch(text)) { members.Add(relative); } else { offenders.Add(relative); } } // Sanity: the scan must actually find files, or a rename has silently defanged it. Assert.True(members.Count + offenders.Count > 0, "Guard found no source file constructing and starting a StoreAndForwardService — " + "the scan patterns are stale."); Assert.True(offenders.Count == 0, "These test sources start a StoreAndForwardService (writing the process-global " + $"queue-depth provider slot) without joining [Collection(QueueDepthGaugeCollection.Name)]: " + string.Join(", ", offenders) + ". Add the attribute, or the queue-depth gauge assertions will flake under parallel collections."); } }