diff --git a/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs b/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs index 6d33330..05ebdf6 100644 --- a/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs +++ b/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs @@ -1,11 +1,124 @@ +using System.Runtime.Versioning; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using TeamsISO.Engine.Interop; +using TeamsISO.Engine.NdiInterop; + namespace TeamsISO.Engine.IntegrationTests; -public class IntegrationTestsScaffold +/// +/// 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 { - [Fact(Skip = "Phase A: integration tests require NDI runtime — added in Phase B.")] + private static NdiInteropPInvoke NewInterop() => + new(NullLogger.Instance); + + [Fact] [Trait("requires", "ndi")] - public void ScaffoldFactSkipsCleanly() + public void NdiRuntime_LoadsAndReportsVersion() { - Assert.True(true); + 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"); } }