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.
124 lines
4 KiB
C#
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");
|
|
}
|
|
}
|