review(track0): async ContextMenu OnClick + keyboard-⋯ anchoring + focus-return; discovery-driven DriverTypeNames guard; CSV round-trip comment
Wave-0 reviewer findings (no Critical/High): - T0-1 M: ContextMenuItem.OnClick Action→Func<Task> (kills async-void at menu call sites Wave A/B/C need); keyboard-activated ⋯ now anchors below the button instead of viewport (0,0); focus returns to trigger on close; backdrop closes on native right-click too. - T0-3 M: guard test now discovers *DriverFactoryExtensions.Register by convention from the deployed driver assemblies instead of a hand-copied list, so a future driver lacking a constant is actually caught. - T0-2 L: corrected the round-trip 'sole non-round-trippable shape' comment (any table whose final row is a lone empty field). Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -16,10 +16,11 @@ public sealed class CsvRoundTripTests
|
||||
// Each case is a jagged table of fields.
|
||||
yield return new object[] { new[] { new[] { "a", "b", "c" } } };
|
||||
yield return new object[] { new[] { new[] { "a", "b" }, new[] { "c", "d" } } };
|
||||
// NOTE: a single row of a single empty field ([[""]]) is deliberately excluded — it serialises to
|
||||
// the empty string, which is indistinguishable from an empty document and parses back to zero rows.
|
||||
// That one degenerate case is the sole non-round-trippable shape; every multi-field / multi-row
|
||||
// empty case below (",," and blank interior lines) does round-trip exactly.
|
||||
// NOTE: a table whose FINAL row is a single empty field ([[""]], or e.g. [["a"],[""]]) is deliberately
|
||||
// excluded — that trailing lone-empty-field row serialises to nothing after the preceding row's
|
||||
// terminator, so the parser's no-phantom-trailing-row rule drops it on the way back. This is the only
|
||||
// non-round-trippable shape; a single-empty-field row anywhere but last (see the interior [""] case
|
||||
// below), ",,", and blank interior lines all round-trip exactly.
|
||||
yield return new object[] { new[] { new[] { "", "", "" } } };
|
||||
yield return new object[] { new[] { new[] { "a" }, new[] { "" }, new[] { "b" } } };
|
||||
yield return new object[] { new[] { new[] { "a,b", "c" } } }; // embedded delimiter
|
||||
|
||||
@@ -8,34 +8,72 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Guards <see cref="DriverTypeNames"/> against drift from the driver factories that are the
|
||||
/// authority for these strings. The test builds the <b>real</b> registered-factory set the way
|
||||
/// production does — it drives each driver assembly's <c>*DriverFactoryExtensions.Register</c>
|
||||
/// entry point into a live <see cref="DriverFactoryRegistry"/>, exactly as the Host's
|
||||
/// <c>DriverFactoryBootstrap.Register</c> does — then asserts bidirectional parity with the
|
||||
/// constants. If a factory renames its <c>DriverTypeName</c> (or a constant is added/removed
|
||||
/// without a matching factory), this fails, forcing the constant and the factory back in sync.
|
||||
/// authority for these strings. Factory discovery is <b>reflection-driven, not a hand-copied list</b>:
|
||||
/// the test scans the driver assemblies deployed to its output directory for the
|
||||
/// <c>*DriverFactoryExtensions.Register(DriverFactoryRegistry, …)</c> convention and invokes each into
|
||||
/// a live <see cref="DriverFactoryRegistry"/> — the same registration path production takes — then
|
||||
/// asserts bidirectional parity with the constants. Because discovery is by convention, a brand-new
|
||||
/// driver assembly referenced by this project (and thus copied to bin) is picked up automatically: if
|
||||
/// it registers a <c>DriverType</c> string with no matching <see cref="DriverTypeNames"/> constant (or
|
||||
/// a constant is added/removed without a matching factory), this fails, forcing the two back in sync.
|
||||
/// </summary>
|
||||
public sealed class DriverTypeNamesGuardTests
|
||||
{
|
||||
// Driver-adjacent assemblies that ship no runtime factory (contracts/addressing/CLI helpers).
|
||||
private static readonly string[] NonFactoryDriverAssemblySuffixes =
|
||||
{ ".Contracts", ".Addressing", ".Cli" };
|
||||
|
||||
/// <summary>
|
||||
/// Build the authoritative registered-factory set by invoking every driver assembly's
|
||||
/// registration extension against a fresh registry — the same set of calls, in the same
|
||||
/// spirit, as <c>DriverFactoryBootstrap.Register</c> on a driver-role node.
|
||||
/// Build the authoritative registered-factory set by discovering every
|
||||
/// <c>*DriverFactoryExtensions.Register</c> in the deployed driver assemblies (by convention) and
|
||||
/// invoking it against a fresh registry. The factory func is never invoked — we only need the
|
||||
/// type-name key each <c>Register</c> call installs — so <see langword="null"/> is passed for the
|
||||
/// optional <c>loggerFactory</c>/other parameters.
|
||||
/// </summary>
|
||||
private static IReadOnlyCollection<string> RegisteredDriverTypeNames()
|
||||
{
|
||||
var registry = new DriverFactoryRegistry();
|
||||
var invoked = 0;
|
||||
|
||||
// loggerFactory is optional on every extension; the factory func is never invoked here,
|
||||
// so null is fine — we only need the type-name key each Register call installs.
|
||||
Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry);
|
||||
Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory: null);
|
||||
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry);
|
||||
Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, loggerFactory: null);
|
||||
Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory: null);
|
||||
Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory: null);
|
||||
Driver.S7.S7DriverFactoryExtensions.Register(registry);
|
||||
Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry);
|
||||
var binDir = Path.GetDirectoryName(typeof(DriverTypeNamesGuardTests).Assembly.Location)!;
|
||||
foreach (var dll in Directory.GetFiles(binDir, "ZB.MOM.WW.OtOpcUa.Driver.*.dll"))
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(dll);
|
||||
if (NonFactoryDriverAssemblySuffixes.Any(s => name.EndsWith(s, StringComparison.Ordinal)))
|
||||
continue;
|
||||
|
||||
Assembly asm;
|
||||
try { asm = Assembly.LoadFrom(dll); }
|
||||
catch { continue; }
|
||||
|
||||
Type[] types;
|
||||
try { types = asm.GetTypes(); }
|
||||
catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t is not null).ToArray()!; }
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type is null || !type.IsClass || type.Name is not { } n
|
||||
|| !n.EndsWith("DriverFactoryExtensions", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var register = type.GetMethod("Register", BindingFlags.Public | BindingFlags.Static);
|
||||
var ps = register?.GetParameters();
|
||||
if (register is null || ps is null || ps.Length == 0
|
||||
|| ps[0].ParameterType != typeof(DriverFactoryRegistry))
|
||||
continue;
|
||||
|
||||
var args = new object?[ps.Length];
|
||||
args[0] = registry; // remaining params (optional loggerFactory, etc.) default to null
|
||||
register.Invoke(null, args);
|
||||
invoked++;
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery must find *something*; a zero here means the bin scan/convention broke, not that the
|
||||
// fleet has no drivers — fail loudly rather than pass a vacuously-empty parity check.
|
||||
invoked.ShouldBeGreaterThan(0,
|
||||
"no *DriverFactoryExtensions.Register was discovered in the output directory — the driver " +
|
||||
"assemblies did not deploy to bin, or the naming convention changed");
|
||||
|
||||
return registry.RegisteredTypes;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user