146 lines
5.5 KiB
C#
146 lines
5.5 KiB
C#
|
|
using System.Reactive.Linq;
|
||
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
||
|
|
using TeamsISO.Engine.Controller;
|
||
|
|
using TeamsISO.Engine.Domain;
|
||
|
|
using TeamsISO.Engine.Interop;
|
||
|
|
using TeamsISO.Engine.Persistence;
|
||
|
|
using TeamsISO.Engine.Pipeline;
|
||
|
|
using TeamsISO.Engine.Tests.Fakes;
|
||
|
|
|
||
|
|
namespace TeamsISO.Engine.Tests.Controller;
|
||
|
|
|
||
|
|
public class IsoControllerTests : IDisposable
|
||
|
|
{
|
||
|
|
private readonly string _dir;
|
||
|
|
private readonly ConfigStore _store;
|
||
|
|
private readonly FakeNdiInterop _interop;
|
||
|
|
private readonly NdiRuntimeProbe _probeMatch;
|
||
|
|
private readonly NdiRuntimeProbe _probeMismatch;
|
||
|
|
private readonly List<IsoPipelineConfig> _factoryCalls = new();
|
||
|
|
private readonly Dictionary<Guid, TaskCompletionSource> _pipelineBlockers = new();
|
||
|
|
|
||
|
|
public IsoControllerTests()
|
||
|
|
{
|
||
|
|
_dir = Path.Combine(Path.GetTempPath(), $"teamsiso-controller-{Guid.NewGuid():N}");
|
||
|
|
Directory.CreateDirectory(_dir);
|
||
|
|
_store = new ConfigStore(Path.Combine(_dir, "config.json"), NullLogger<ConfigStore>.Instance);
|
||
|
|
_interop = new FakeNdiInterop { RuntimeVersion = "6.0.0" };
|
||
|
|
_probeMatch = new NdiRuntimeProbe(_interop, expectedVersion: "6.0.0");
|
||
|
|
_probeMismatch = new NdiRuntimeProbe(_interop, expectedVersion: "9.9.9");
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Dispose() => Directory.Delete(_dir, recursive: true);
|
||
|
|
|
||
|
|
private IsoPipeline TestPipelineFactory(IsoPipelineConfig config)
|
||
|
|
{
|
||
|
|
_factoryCalls.Add(config);
|
||
|
|
var blocker = new TaskCompletionSource();
|
||
|
|
_pipelineBlockers[config.ParticipantId] = blocker;
|
||
|
|
var backoff = new ExponentialBackoff(maxAttempts: 3, initial: TimeSpan.FromMilliseconds(1), cap: TimeSpan.FromMilliseconds(5));
|
||
|
|
return new IsoPipeline(
|
||
|
|
config.ParticipantId,
|
||
|
|
ct => blocker.Task.WaitAsync(ct),
|
||
|
|
backoff,
|
||
|
|
(_, _) => Task.CompletedTask,
|
||
|
|
NullLoggerFactory.Instance);
|
||
|
|
}
|
||
|
|
|
||
|
|
private IsoController NewController(NdiRuntimeProbe? probe = null) =>
|
||
|
|
new(
|
||
|
|
_interop,
|
||
|
|
TestPipelineFactory,
|
||
|
|
_store,
|
||
|
|
probe ?? _probeMatch,
|
||
|
|
NullLoggerFactory.Instance,
|
||
|
|
renameWindow: TimeSpan.FromSeconds(5),
|
||
|
|
discoveryInterval: TimeSpan.FromMilliseconds(20));
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task DiscoveredParticipant_AppearsInParticipantsObservable()
|
||
|
|
{
|
||
|
|
await using var controller = NewController();
|
||
|
|
var seenLists = new List<IReadOnlyList<Participant>>();
|
||
|
|
using var sub = controller.Participants.Subscribe(p => seenLists.Add(p));
|
||
|
|
|
||
|
|
await controller.StartAsync(CancellationToken.None);
|
||
|
|
_interop.Sources.Add("PC1 (Teams - Jane)");
|
||
|
|
|
||
|
|
// Wait for discovery to pick it up
|
||
|
|
var deadline = DateTime.UtcNow.AddSeconds(2);
|
||
|
|
while (seenLists.LastOrDefault()?.Any() != true && DateTime.UtcNow < deadline)
|
||
|
|
await Task.Delay(20);
|
||
|
|
|
||
|
|
seenLists.Last().Should().HaveCount(1);
|
||
|
|
seenLists.Last()[0].DisplayName.Should().Be("Jane");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task EnableIsoAsync_CreatesAndStartsPipeline()
|
||
|
|
{
|
||
|
|
await using var controller = NewController();
|
||
|
|
await controller.StartAsync(CancellationToken.None);
|
||
|
|
|
||
|
|
_interop.Sources.Add("PC1 (Teams - Jane)");
|
||
|
|
var pid = await WaitForFirstParticipantAsync(controller);
|
||
|
|
|
||
|
|
await controller.EnableIsoAsync(pid, customName: "TEAMSISO_JANE", CancellationToken.None);
|
||
|
|
|
||
|
|
_factoryCalls.Should().HaveCount(1);
|
||
|
|
_factoryCalls[0].OutputName.Should().Be("TEAMSISO_JANE");
|
||
|
|
_factoryCalls[0].SourceName.Should().Be("PC1 (Teams - Jane)");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task DisableIsoAsync_StopsPipeline_AndRemovesIt()
|
||
|
|
{
|
||
|
|
await using var controller = NewController();
|
||
|
|
await controller.StartAsync(CancellationToken.None);
|
||
|
|
|
||
|
|
_interop.Sources.Add("PC1 (Teams - Jane)");
|
||
|
|
var pid = await WaitForFirstParticipantAsync(controller);
|
||
|
|
await controller.EnableIsoAsync(pid, customName: null, CancellationToken.None);
|
||
|
|
|
||
|
|
// Unblock the pipeline so StopAsync can complete
|
||
|
|
_pipelineBlockers[pid].SetResult();
|
||
|
|
await controller.DisableIsoAsync(pid, CancellationToken.None);
|
||
|
|
|
||
|
|
// Re-enable should now create a new pipeline
|
||
|
|
await controller.EnableIsoAsync(pid, customName: null, CancellationToken.None);
|
||
|
|
_factoryCalls.Should().HaveCount(2);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task SetGlobalSettingsAsync_PersistsToConfigStore()
|
||
|
|
{
|
||
|
|
await using var controller = NewController();
|
||
|
|
var newSettings = new FrameProcessingSettings(
|
||
|
|
TargetFramerate.Fps59_94, TargetResolution.R1080p, AspectMode.Pillarbox, AudioMode.Auto);
|
||
|
|
|
||
|
|
await controller.SetGlobalSettingsAsync(newSettings, CancellationToken.None);
|
||
|
|
|
||
|
|
controller.GlobalSettings.Should().Be(newSettings);
|
||
|
|
_store.Load().Global.Should().Be(newSettings);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task StartAsync_RuntimeProbeMismatch_RaisesAlert()
|
||
|
|
{
|
||
|
|
await using var controller = NewController(probe: _probeMismatch);
|
||
|
|
var alerts = new List<EngineAlert>();
|
||
|
|
using var sub = controller.Alerts.Subscribe(alerts.Add);
|
||
|
|
|
||
|
|
await controller.StartAsync(CancellationToken.None);
|
||
|
|
|
||
|
|
alerts.Should().Contain(a => a is EngineAlert.NdiRuntimeMismatch);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async Task<Guid> WaitForFirstParticipantAsync(IsoController controller)
|
||
|
|
{
|
||
|
|
var tcs = new TaskCompletionSource<Guid>();
|
||
|
|
using var sub = controller.Participants
|
||
|
|
.Where(p => p.Count > 0)
|
||
|
|
.Subscribe(p => tcs.TrySetResult(p[0].Id));
|
||
|
|
return await tcs.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||
|
|
}
|
||
|
|
}
|