Auto: twincat-2.3 — symbol-version invalidation listener

Closes #312
This commit is contained in:
Joseph Doherty
2026-04-25 22:16:05 -04:00
parent 569001364f
commit 4098d72bbb
6 changed files with 518 additions and 14 deletions

View File

@@ -71,15 +71,32 @@ internal class FakeTwinCATClient : ITwinCATClient
// wire-side delete needed.
if (IsConnected) _handleCache.Clear();
IsConnected = true;
// PR 2.3 — production arms the Symbol-Version listener after the AMS session is up.
// Mirror so unit tests can assert "ConnectAsync registered the version listener".
// Default cache-wipe handler matches AdsTwinCATClient.OnAdsSymbolVersionChanged.
if (!SymbolVersionRegistered)
RegisterSymbolVersionListener(WipeHandleCacheOnVersionBump);
return Task.CompletedTask;
}
private void WipeHandleCacheOnVersionBump()
{
// Note: kv-snapshot delete already happened in FireSymbolVersionChange before
// this callback fires, so the cache is already empty. Kept as the default
// listener so the wiring contract is still observable through tests that pass
// a custom action via RegisterSymbolVersionListener.
}
/// <summary>Test helper — simulate a reconnect (ConnectAsync after the connection drops).</summary>
public void SimulateReconnect()
{
IsConnected = false;
_handleCache.Clear();
// Production marks the listener as needing re-registration on reconnect.
UnregisterSymbolVersionListener();
IsConnected = true;
if (!SymbolVersionRegistered)
RegisterSymbolVersionListener(WipeHandleCacheOnVersionBump);
}
public virtual Task<(object? value, uint status)> ReadValueAsync(
@@ -164,6 +181,57 @@ internal class FakeTwinCATClient : ITwinCATClient
return Task.CompletedTask;
}
// ---- PR 2.3: Symbol-Version invalidation listener ----
//
// Mirror of the Beckhoff AdsClient.AdsSymbolVersionChanged surface. ConnectAsync
// arms the listener so test asserts can verify the production driver registered
// it on connect; FireSymbolVersionChange() drives the same handle-cache-wipe path
// AdsTwinCATClient runs on a real PLC online change.
public bool SymbolVersionRegistered { get; private set; }
public int SymbolVersionRegistrationCount { get; private set; }
public int SymbolVersionUnregistrationCount { get; private set; }
public long SymbolVersionBumps { get; private set; }
/// <summary>Externally-supplied callback (production wires this to the cache wipe).</summary>
private Action? _onSymbolVersionChanged;
/// <summary>
/// Test helper exposed in lieu of the Beckhoff event surface — the production
/// <c>AdsTwinCATClient</c> registers via <c>RegisterSymbolVersionChangedAsync</c>
/// after connect; the fake records the registration here so tests can assert
/// "subscribed on connect". The callback (an <see cref="Action"/> rather than the
/// full <see cref="EventHandler{T}"/> shape) is the cache-wipe entry point.
/// </summary>
public void RegisterSymbolVersionListener(Action onChange)
{
_onSymbolVersionChanged = onChange;
SymbolVersionRegistered = true;
SymbolVersionRegistrationCount++;
}
public void UnregisterSymbolVersionListener()
{
if (!SymbolVersionRegistered) return;
_onSymbolVersionChanged = null;
SymbolVersionRegistered = false;
SymbolVersionUnregistrationCount++;
}
/// <summary>
/// Drive the Symbol-Version-changed callback path. Production wipes the handle
/// cache + bumps the diagnostic counter; mirror so unit tests can assert
/// post-bump state without standing up a real ADS device. Safe to call when no
/// listener is registered (no-op + still bumps the counter so test code can
/// assert "we tried but no-one was listening").
/// </summary>
public void FireSymbolVersionChange()
{
SymbolVersionBumps++;
// Mirror production cache-wipe semantics: snapshot, clear, emit per-entry deletes.
foreach (var kv in _handleCache) HandleDeleteInvocations.Add(kv.Value);
_handleCache.Clear();
_onSymbolVersionChanged?.Invoke();
}
public virtual Task<bool> ProbeAsync(CancellationToken ct)
{
if (ThrowOnProbe) return Task.FromResult(false);
@@ -236,6 +304,8 @@ internal class FakeTwinCATClient : ITwinCATClient
// the fan-out delete count matches.
foreach (var kv in _handleCache) HandleDeleteInvocations.Add(kv.Value);
_handleCache.Clear();
// PR 2.3 — production unregisters the Symbol-Version listener on Dispose.
UnregisterSymbolVersionListener();
}
// ---- notification fake ----