20a816dbf4
The mxaccessgw OnWriteComplete correlation is live (server b948e69, worker 53f69cd — all four unary write kinds), so every write reply's statuses[0] now carries the real COM commit outcome. Live-gating the cutover on windev found and fixed three defects: - StatusCodeMap.FromMxStatus truncated the correlated failure row's MxStatus detail code to a byte (1007 & 0xFF = 239 -> OPC DA Good band), mapping a REFUSED write to Good. New invariant: a row that is not IsSuccess() never maps Good — details > 0xFF (and contradictory Good-band substatuses) route by category (SECURITY_ERROR -> BadUserAccessDenied, named const covered by StatusCodeParityTests; COMMUNICATION/CONFIGURATION keep their codes). - The borrow-from-subscription-registry write optimization could never commit: a subscription handle is plain-advised, and MXAccess refuses both the no-login write through it (SECURITY_ERROR detail 1008, live-captured) and the supervisory advise upgrade (E_INVALIDARG) — one advise per HANDLE, not per attribute. Removed: writer subscribedHandleSource ctor param + borrow branch, GalaxyDriver wiring, SubscriptionRegistry.TryResolveItemHandle + its forward index and test file. The writer always AddItems its own supervisory-advised handle, which commits fine while the same attribute is subscribed (live-proven; pinned by the new write-while-subscribed smoke). - (gateway-side, fixed there) the correlation initially covered WriteSecured only, while the dominant no-login path emits plain Write. Live gate, all legs PASSED against windev: happy write Good off a confirmed status row with galaxy.writes.unconfirmed + advise_failed both FLAT; read-only refusal surfaces Bad-family (the #5 node-revert input Galaxy could never produce before); WriteCompletionWaitMilliseconds=0 control -> provisional Good + metered unconfirmed (override removed after, restoration re-verified). New env-gated smokes: Live_correlated_write_statuses_confirm_commit_and_surface_refusal, Live_write_to_subscribed_tag_commits_via_own_handle (replaces the borrow smoke), GatewayGalaxyWriteMeterLiveTests (run filtered — meters are process-wide).
99 lines
4.5 KiB
C#
99 lines
4.5 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/>.
|
|
/// The SDK session types are sealed with internal ctors and cannot be faked, so we
|
|
/// drive the cache-seeding path through
|
|
/// <see cref="GatewayGalaxyDataWriter.SeedHandleCachesForTest"/> and verify the
|
|
/// handle-count seams — the contract under test is purely that
|
|
/// <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/> zeroes both dictionaries
|
|
/// so the next write is forced to re-AddItem + re-AdviseSupervisory.
|
|
/// </summary>
|
|
public sealed class GatewayGalaxyDataWriterTests
|
|
{
|
|
private static GalaxyMxSession MinimalSession()
|
|
=> new(new GalaxyMxAccessOptions(ClientName: "OtOpcUa-Test"));
|
|
|
|
/// <summary>
|
|
/// Approach (b): seed the item-handle cache directly via the internal test seam,
|
|
/// confirm the count is positive, call <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/>,
|
|
/// and confirm both caches are cleared.
|
|
/// The next write (not simulated here — needs a live gw) would therefore be forced
|
|
/// to re-AddItem because the cache is empty.
|
|
/// </summary>
|
|
[Fact]
|
|
public void InvalidateHandleCaches_clears_item_and_supervised_handle_caches()
|
|
{
|
|
var session = MinimalSession();
|
|
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
|
|
|
|
// Pre-seed both caches via the internal test seam so we can assert the
|
|
// "after a write" state without spinning up a real gRPC gateway session.
|
|
writer.SeedHandleCachesForTest("TestMachine_001.TestAttr", itemHandle: 42, supervised: true);
|
|
|
|
writer.CachedItemHandleCount.ShouldBe(1);
|
|
writer.CachedSupervisedHandleCount.ShouldBe(1);
|
|
|
|
writer.InvalidateHandleCaches();
|
|
|
|
writer.CachedItemHandleCount.ShouldBe(0);
|
|
writer.CachedSupervisedHandleCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A second seed + invalidate cycle proves the method isn't one-shot — a reconnect
|
|
/// followed by writes followed by another reconnect must also start fresh.
|
|
/// </summary>
|
|
[Fact]
|
|
public void InvalidateHandleCaches_is_repeatable_across_multiple_reconnects()
|
|
{
|
|
var session = MinimalSession();
|
|
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
|
|
|
|
// First session cycle
|
|
writer.SeedHandleCachesForTest("Tag.A", itemHandle: 1, supervised: false);
|
|
writer.SeedHandleCachesForTest("Tag.B", itemHandle: 2, supervised: true);
|
|
writer.CachedItemHandleCount.ShouldBe(2);
|
|
writer.InvalidateHandleCaches();
|
|
writer.CachedItemHandleCount.ShouldBe(0);
|
|
writer.CachedSupervisedHandleCount.ShouldBe(0);
|
|
|
|
// Second session cycle — handles re-populated after the reconnect's replay
|
|
writer.SeedHandleCachesForTest("Tag.A", itemHandle: 99, supervised: true);
|
|
writer.CachedItemHandleCount.ShouldBe(1);
|
|
writer.InvalidateHandleCaches();
|
|
writer.CachedItemHandleCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/> on a fresh (never-used)
|
|
/// writer must be a no-op rather than throwing — the reconnect supervisor may call it
|
|
/// before any write has occurred.
|
|
/// </summary>
|
|
[Fact]
|
|
public void InvalidateHandleCaches_on_empty_caches_is_a_noop()
|
|
{
|
|
var session = MinimalSession();
|
|
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
|
|
|
|
// Caches are empty — must not throw.
|
|
writer.CachedItemHandleCount.ShouldBe(0);
|
|
writer.CachedSupervisedHandleCount.ShouldBe(0);
|
|
writer.InvalidateHandleCaches();
|
|
writer.CachedItemHandleCount.ShouldBe(0);
|
|
writer.CachedSupervisedHandleCount.ShouldBe(0);
|
|
}
|
|
|
|
// The former subscribedHandleSource / TryResolveCachedOrBorrowed tests are gone with the
|
|
// borrow-for-write optimization itself: a subscription's handle is plain-advised, and MXAccess
|
|
// both refuses a no-login write through it (SECURITY_ERROR detail 1008) and refuses upgrading
|
|
// its advise mode (E_INVALIDARG) — live-captured on the correlated write replies (06/S-1). The
|
|
// writer now always AddItems its own supervisory-advised handle, which MXAccess accepts even
|
|
// while the same attribute is subscribed (the advise limit is per handle, not per attribute).
|
|
}
|