teamsiso/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs
Zac Gaetano 0b24fbb529 test(ndi): seed requires=ndi integration tests against real NDI runtime
Replaces the previously-skipped placeholder with 8 integration tests that exercise the production P/Invoke shim against the installed NDI 6 runtime: runtime version probe + prefix assertion (catches future SDK rebrandings), finder lifecycle on default + custom groups (incl. whitespace tolerance + multi-group), sender lifecycle on default + custom groups, and a loopback-discovery test that creates a uniquely-named sender and asserts a same-process finder sees it within 5 s.

All marked [Trait('requires', 'ndi')] so the existing CI filter (Category!=ndi&requires!=ndi) excludes them. Run locally with: dotnet test --filter requires=ndi. Today: 8/8 pass against NDI 6.2 on Windows 11.
2026-05-08 00:11:01 -04:00

124 lines
4 KiB
C#

using System.Runtime.Versioning;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using TeamsISO.Engine.Interop;
using TeamsISO.Engine.NdiInterop;
namespace TeamsISO.Engine.IntegrationTests;
/// <summary>
/// Integration tests that talk to a real NDI runtime — gated behind
/// <c>requires=ndi</c> 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
/// </summary>
[SupportedOSPlatform("windows")]
public class NdiInteropIntegrationTests
{
private static NdiInteropPInvoke NewInterop() =>
new(NullLogger<NdiInteropPInvoke>.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");
}
}