2026-05-31 11:18:27 -04:00
using System.Collections.Concurrent ;
using DragonISO.Engine.Interop ;
using DragonISO.Engine.Pipeline ;
2026-05-07 11:13:00 -04:00
2026-05-31 11:18:27 -04:00
namespace DragonISO.Engine.Tests.Fakes ;
2026-05-07 11:13:00 -04:00
/// <summary>
/// In-memory test double for <see cref="INdiInterop"/>. Tests configure source lists and frame
/// queues; the fake feeds those into engine code as if a real NDI runtime were present.
/// </summary>
public sealed class FakeNdiInterop : INdiInterop
{
public List < string > Sources { get ; } = new ( ) ;
public ConcurrentDictionary < string , ConcurrentQueue < RawFrame > > ReceiverFrames { get ; } = new ( ) ;
public ConcurrentDictionary < string , List < ProcessedFrame > > SentFrames { get ; } = new ( ) ;
2026-05-10 14:04:04 -04:00
/// <summary>Optional per-source audio peak queue. Each <see cref="CaptureAudioPeak"/> dequeues one entry; null if empty (matches the production "timeout" behavior).</summary>
public ConcurrentDictionary < string , ConcurrentQueue < double > > ReceiverAudioPeaks { get ; } = new ( ) ;
2026-05-07 11:13:00 -04:00
public string RuntimeVersion { get ; set ; } = "6.0.0" ;
public Dictionary < string , int > ReceiverCreatedCount { get ; } = new ( ) ;
public Dictionary < string , int > SenderCreatedCount { get ; } = new ( ) ;
2026-05-07 23:48:49 -04:00
/// <summary>Last <c>groups</c> string seen by <see cref="CreateFinder"/>; null = default Public.</summary>
public string? LastFinderGroups { get ; private set ; }
/// <summary>Per-output <c>groups</c> string seen by <see cref="CreateSender"/>; null = default Public.</summary>
public Dictionary < string , string? > SenderGroups { get ; } = new ( ) ;
2026-06-13 00:23:33 -04:00
/// <summary>
/// Number of finders created so far (initial construction + every rebuild).
/// Lets tests assert how many times the discovery service tried to rebuild.
/// </summary>
public int FinderCreatedCount { get ; private set ; }
/// <summary>
/// Optional hook invoked at the top of every <see cref="CreateFinder"/> 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.
/// </summary>
public Action < int > ? CreateFinderHook { get ; set ; }
2026-05-07 23:48:49 -04:00
public NdiFindHandle CreateFinder ( string? groups = null )
{
2026-06-13 00:23:33 -04:00
FinderCreatedCount + + ;
CreateFinderHook ? . Invoke ( FinderCreatedCount ) ;
2026-05-07 23:48:49 -04:00
LastFinderGroups = groups ;
return new FakeFindHandle ( ) ;
}
2026-06-13 00:23:33 -04:00
public IReadOnlyList < string > 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 ( ) ;
}
2026-05-07 11:13:00 -04:00
public NdiReceiverHandle CreateReceiver ( string sourceFullName )
{
ReceiverCreatedCount [ sourceFullName ] = ReceiverCreatedCount . GetValueOrDefault ( sourceFullName ) + 1 ;
ReceiverFrames . GetOrAdd ( sourceFullName , _ = > new ConcurrentQueue < RawFrame > ( ) ) ;
return new FakeReceiverHandle ( sourceFullName ) ;
}
public RawFrame ? CaptureFrame ( NdiReceiverHandle receiver , int timeoutMs )
{
var key = ( ( FakeReceiverHandle ) receiver ) . Source ;
if ( ReceiverFrames . TryGetValue ( key , out var q ) & & q . TryDequeue ( out var frame ) )
return frame ;
return null ; // simulate timeout
}
2026-05-10 14:04:04 -04:00
public double? CaptureAudioPeak ( NdiReceiverHandle receiver , int timeoutMs )
{
var key = ( ( FakeReceiverHandle ) receiver ) . Source ;
if ( ReceiverAudioPeaks . TryGetValue ( key , out var q ) & & q . TryDequeue ( out var peak ) )
return peak ;
2026-05-31 11:18:27 -04:00
return null ; // no audio queued — simulate timeout
2026-05-10 14:04:04 -04:00
}
2026-05-07 23:48:49 -04:00
public NdiSenderHandle CreateSender ( string outputName , string? groups = null )
2026-05-07 11:13:00 -04:00
{
SenderCreatedCount [ outputName ] = SenderCreatedCount . GetValueOrDefault ( outputName ) + 1 ;
2026-05-07 23:48:49 -04:00
SenderGroups [ outputName ] = groups ;
2026-05-07 11:13:00 -04:00
SentFrames . GetOrAdd ( outputName , _ = > new List < ProcessedFrame > ( ) ) ;
return new FakeSenderHandle ( outputName ) ;
}
public void SendFrame ( NdiSenderHandle sender , ProcessedFrame frame )
{
var key = ( ( FakeSenderHandle ) sender ) . Output ;
SentFrames [ key ] . Add ( frame ) ;
}
public string GetRuntimeVersion ( ) = > RuntimeVersion ;
private sealed class FakeFindHandle : NdiFindHandle
{
2026-06-13 00:23:33 -04:00
public bool Disposed { get ; private set ; }
public override void Dispose ( ) = > Disposed = true ;
2026-05-07 11:13:00 -04:00
}
private sealed class FakeReceiverHandle : NdiReceiverHandle
{
public string Source { get ; }
public FakeReceiverHandle ( string source ) = > Source = source ;
public override void Dispose ( ) { }
}
private sealed class FakeSenderHandle : NdiSenderHandle
{
public string Output { get ; }
public FakeSenderHandle ( string output ) = > Output = output ;
public override void Dispose ( ) { }
}
}