From 886e20e501db9b6627c62210e6b8b357bfbbdfaf Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sat, 13 Jun 2026 00:23:33 -0400 Subject: [PATCH] test(engine): teach FakeNdiInterop to model finder failure + use-after-dispose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additive, backwards-compatible test-double extensions so the discovery rebuild failure path is observable: - CreateFinderHook: optional callback invoked on every CreateFinder, letting a test throw on the Nth call to simulate a runtime that refuses to build a replacement finder. - FinderCreatedCount: how many finders were built. - The fake find handle now flips a Disposed flag, and GetCurrentSources throws ObjectDisposedException if asked to read a disposed finder — so a regression that polls a retired finder fails loudly instead of silently passing (the fake previously ignored the handle entirely). Default behavior is unchanged when CreateFinderHook is null. --- .../Fakes/FakeNdiInterop.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/tests/Dragon-ISO.Engine.Tests/Fakes/FakeNdiInterop.cs b/src/tests/Dragon-ISO.Engine.Tests/Fakes/FakeNdiInterop.cs index f5e35bb..e8cac1f 100644 --- a/src/tests/Dragon-ISO.Engine.Tests/Fakes/FakeNdiInterop.cs +++ b/src/tests/Dragon-ISO.Engine.Tests/Fakes/FakeNdiInterop.cs @@ -24,12 +24,35 @@ public sealed class FakeNdiInterop : INdiInterop /// Per-output groups string seen by ; null = default Public. public Dictionary SenderGroups { get; } = new(); + /// + /// Number of finders created so far (initial construction + every rebuild). + /// Lets tests assert how many times the discovery service tried to rebuild. + /// + public int FinderCreatedCount { get; private set; } + + /// + /// Optional hook invoked at the top of every call, + /// receiving the 1-based creation ordinal. Throw from here to simulate a runtime + /// that refuses to build a finder (e.g. the rebuild-failure path); return normally + /// to allow creation. Null (default) = always succeed. + /// + public Action? CreateFinderHook { get; set; } + public NdiFindHandle CreateFinder(string? groups = null) { + FinderCreatedCount++; + CreateFinderHook?.Invoke(FinderCreatedCount); LastFinderGroups = groups; return new FakeFindHandle(); } - public IReadOnlyList GetCurrentSources(NdiFindHandle finder) => Sources.ToArray(); + + public IReadOnlyList GetCurrentSources(NdiFindHandle finder) + { + if (finder is FakeFindHandle { Disposed: true }) + throw new ObjectDisposedException(nameof(FakeFindHandle), + "GetCurrentSources called on a finder that was already disposed."); + return Sources.ToArray(); + } public NdiReceiverHandle CreateReceiver(string sourceFullName) { @@ -72,7 +95,8 @@ public sealed class FakeNdiInterop : INdiInterop private sealed class FakeFindHandle : NdiFindHandle { - public override void Dispose() { } + public bool Disposed { get; private set; } + public override void Dispose() => Disposed = true; } private sealed class FakeReceiverHandle : NdiReceiverHandle