Files
Marc Schier a3ae6db71a [test] Accommodate non-blocking lease Dispose with thread-safe listeners and polling helpers
After commit 2a4f4f601 made ManagedTransportChannelLease.Dispose()
fire-and-forget on the thread pool (to fix a sync-context deadlock),
several test helpers exposed pre-existing races that the previous
synchronous Dispose had been masking:

  - ChannelMetricListener.Measurements was a plain List<T>; the
    metric callbacks (OnLong/Double/MeasurementRecorded) fire from
    arbitrary threadpool threads, so concurrent Add on one thread and
    enumerate on the test thread threw "Collection was modified" in
    FormatMeasurements. Change to ConcurrentQueue<MeasurementRecord>
    and use snapshot-stable enumeration in HasMeasurement +
    FormatMeasurements.
  - ChannelEventListener.Events had the same race against
    EventWritten callbacks; same fix (ConcurrentQueue).

Four tests asserted state that is now produced asynchronously by the
lease teardown task, so they need to poll before the hard assertions:
  - MetricsAreEmittedForChannelLifetimeAsync (wait for the
    opcua.channel.close metric)
  - DiscoveryClientCreateAsyncSharesSessionChannelAndReleasesLeaseAsync
    (wait for Moq.Verify on CloseAsync)
  - EventSourceFiresStateTransitionsAsync (wait for
    ParticipantDetached + ChannelClosed EventSource events)
  - FailoverWithDifferentEndpointSwapsLeaseAsync (wait for the lease
    State transition AND the underlying CloseCount increment)

Three new helpers consolidate the polling pattern:
  - WaitForMeasurementAsync (existing — used for the metric path)
  - WaitForMockInvocationAsync (new — catches Moq.MockException and
    re-runs verify until budget exhausted; final verify is allowed
    to throw)
  - WaitForConditionAsync (new — generic state poll with description)

All helpers use a 2 s budget with 25 ms polling intervals, matching
the existing pattern in MetricsAreEmittedForReconnectAndGateWaitAsync
(53ff029df).

Verification:
  - Source-tree builds clean on net10.0 (Stack/Opc.Ua.Core,
    Libraries/Opc.Ua.Client, Applications/McpServer all 0 W, 0 E)
  - Local .NET 10 SDK install became corrupted mid-session
    (C:\Program Files\dotnet\sdk\10.0.300 emptied of all but the
    Roslyn subfolder), blocking a final test rebuild for the LAST
    edit to FailoverWithDifferentEndpointSwapsLease (the
    state-and-close combined poll). The two previous test edits in
    this commit were rebuilt and verified at 4/4 pass + 10x stability
    (the FailoverWithDifferentEndpoint flake reproduced before the
    final edit and motivates the combined-condition poll). CI is the
    canonical re-verifier for the final tweak.
2026-06-11 12:26:55 +02:00
..