2d7dffcc42
Driver.MTConnect-005/-006/-007, each TDD'd red-first. - -005: _options was the one field at the StartCoreAsync commit point published by a plain write and read lock-free from ReadAsync/WriteAsync/DiscoverAsync/HostName/health. Now Volatile.Write/Volatile.Read like _session/_binding/_announcedInstanceId, pinned by a source scan (MTConnectOptionsPublicationTests) that fails on any unfenced touch. - -006: TryFormatValue guarded null and Array, but Convert.ToString renders ANY non-IConvertible value as its type name under a Good status. Guard is now the property the remark relies on; the accepted set (string/bool/numeric/DateTime) is unchanged. - -007: MTConnectAgentClient.Dispose was a check-then-set on a non-volatile bool, so two racing disposals both passed and the loser cancelled an already-disposed CTS. Now an Interlocked.Exchange claim over an int, with the request seams reading it volatile.
110 lines
4.6 KiB
C#
110 lines
4.6 KiB
C#
using System.Text.RegularExpressions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
|
|
|
|
/// <summary>
|
|
/// Driver.MTConnect-005 — <c>MTConnectDriver._options</c> must be published and read the way its
|
|
/// three siblings at the same commit point are (<c>_session</c>, <c>_binding</c>,
|
|
/// <c>_announcedInstanceId</c>): <c>Volatile.Write</c> once, <c>Volatile.Read</c> everywhere else.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Why a source scan and not a behavioural test.</b> The defect is a missing memory fence:
|
|
/// a plain field write publishes the new options document with no release, and a reader that
|
|
/// reads <c>_options</c> BEFORE it reads <c>_binding</c> — which both <c>ReadAsync</c> and
|
|
/// <c>WriteAsync</c> do — has no acquire ordering between the two, so it can in principle
|
|
/// judge a batch against the previous <c>Write.Enabled</c> / <c>RequestTimeoutMs</c> 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
|
|
/// <c>LibplctagGatewayConsumptionGuardTests</c>: the defect recurs by being reintroduced at a
|
|
/// call site no per-site test covers.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class MTConnectOptionsPublicationTests
|
|
{
|
|
/// <summary>Any mention of the field, so an unguarded read is caught as well as an unguarded write.</summary>
|
|
private static readonly Regex Mention = new(@"_options\b", RegexOptions.Compiled);
|
|
|
|
/// <summary>The two guarded forms. Nothing else may touch the field.</summary>
|
|
private static readonly Regex Guarded =
|
|
new(@"Volatile\.(Read|Write)\(ref _options[,)]", RegexOptions.Compiled);
|
|
|
|
/// <summary>The declaration itself, which is neither a read nor a write.</summary>
|
|
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<string>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>Removes a trailing line comment and skips XML-doc lines, which mention the field by name.</summary>
|
|
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;
|
|
}
|
|
}
|