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.");
}
}