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;
}
}