using System.Runtime.Versioning; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using TeamsISO.Engine.Interop; using TeamsISO.Engine.NdiInterop; namespace TeamsISO.Engine.IntegrationTests; /// /// Integration tests that talk to a real NDI runtime — gated behind /// requires=ndi so the default CI run skips them. Run locally with: /// dotnet test --filter requires=ndi /// /// Pre-conditions: /// - Windows host /// - NDI 6 Runtime installed (NDI_RUNTIME_DIR_V6 set) /// - Network discovery permitted on the loopback / local subnet /// [SupportedOSPlatform("windows")] public class NdiInteropIntegrationTests { private static NdiInteropPInvoke NewInterop() => new(NullLogger.Instance); [Fact] [Trait("requires", "ndi")] public void NdiRuntime_LoadsAndReportsVersion() { using var interop = NewInterop(); var version = interop.GetRuntimeVersion(); version.Should().NotBeNullOrEmpty(); version.Should().StartWith(NdiVersion.ExpectedRuntimeVersionPrefix, because: "the engine probe asserts this prefix; if it ever drifts CI must catch it"); } [Fact] [Trait("requires", "ndi")] public void Finder_DefaultGroups_CreatesAndDisposesCleanly() { using var interop = NewInterop(); using (var finder = interop.CreateFinder()) { finder.Should().NotBeNull(because: "default-group finder must construct successfully"); // Snapshot any visible sources — exercises the path; we don't assert on count // because the test environment's NDI sources are unknowable. _ = interop.GetCurrentSources(finder); } } [Theory] [Trait("requires", "ndi")] [InlineData("teamsiso-test-input")] [InlineData("teamsiso-test-input,production")] [InlineData(" teamsiso-test-input ")] public void Finder_CustomGroups_DoesNotThrow(string groups) { using var interop = NewInterop(); var act = () => { using var finder = interop.CreateFinder(groups); _ = interop.GetCurrentSources(finder); }; act.Should().NotThrow(because: $"groups='{groups}' must round-trip into NDIlib_find_create_v2 cleanly"); } [Fact] [Trait("requires", "ndi")] public void Sender_DefaultGroups_CreatesAndDisposesCleanly() { using var interop = NewInterop(); using (var sender = interop.CreateSender("TEAMSISO_TEST_DEFAULT")) { sender.Should().NotBeNull(); } } [Fact] [Trait("requires", "ndi")] public void Sender_CustomGroups_CreatesAndDisposesCleanly() { using var interop = NewInterop(); using (var sender = interop.CreateSender("TEAMSISO_TEST_GROUPED", "teamsiso-test-output")) { sender.Should().NotBeNull(); } } [Fact] [Trait("requires", "ndi")] public async Task LoopbackDiscovery_FindsOurOwnSenderWithinFiveSeconds() { // End-to-end check: a sender we create on the local machine must be visible // to a finder running in the same process, within a reasonable window. Catches // network-layer regressions (firewall, mDNS, multicast disable). using var interop = NewInterop(); var uniqueName = $"TEAMSISO_LOOP_{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}"; using var sender = interop.CreateSender(uniqueName); using var finder = interop.CreateFinder(); var deadline = DateTime.UtcNow.AddSeconds(5); bool found = false; while (DateTime.UtcNow < deadline) { var sources = interop.GetCurrentSources(finder); if (sources.Any(s => s.Contains(uniqueName, StringComparison.Ordinal))) { found = true; break; } await Task.Delay(250); } found.Should().BeTrue( because: $"local sender '{uniqueName}' must be discovered by a same-process finder within 5s"); } }