187 lines
6.5 KiB
C#
187 lines
6.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Xunit;
|
|
using ytLive.Services;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
public class CameraManagerTests
|
|
{
|
|
private sealed class FakeFrameSource : ICameraFrameSource
|
|
{
|
|
private readonly List<string> _started;
|
|
private readonly List<string> _stopped;
|
|
private readonly VideoFrame? _pumpOnStart;
|
|
private readonly string? _failOnStart;
|
|
|
|
public string DeviceId { get; }
|
|
public event Action<VideoFrame>? FrameAvailable;
|
|
public event Action<string>? SourceFailed;
|
|
|
|
public FakeFrameSource(string deviceId, List<string> started, List<string> stopped,
|
|
VideoFrame? pumpOnStart = null, string? failOnStart = null)
|
|
{
|
|
DeviceId = deviceId;
|
|
_started = started;
|
|
_stopped = stopped;
|
|
_pumpOnStart = pumpOnStart;
|
|
_failOnStart = failOnStart;
|
|
}
|
|
|
|
public Task StartAsync()
|
|
{
|
|
if (_failOnStart != null) throw new InvalidOperationException(_failOnStart);
|
|
_started.Add(DeviceId);
|
|
if (_pumpOnStart != null) FrameAvailable?.Invoke(_pumpOnStart);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StopAsync()
|
|
{
|
|
_stopped.Add(DeviceId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame);
|
|
public void Fail(string message) => SourceFailed?.Invoke(message);
|
|
}
|
|
|
|
// The refcount/coalescing tests model a camera whose frame pump is driven by
|
|
// the test after acquire, so they skip the first-frame proof (TimeSpan.Zero);
|
|
// the proof itself is exercised by the dedicated tests below.
|
|
private static CameraManager CreateManager(List<string> started, List<string> stopped)
|
|
=> new(new FakeEnumerator(), id => new FakeFrameSource(id, started, stopped),
|
|
null, TimeSpan.Zero);
|
|
|
|
private sealed class FakeEnumerator : ICameraEnumerator
|
|
{
|
|
public Task<IReadOnlyList<CameraDeviceInfo>> GetCamerasAsync()
|
|
=> Task.FromResult<IReadOnlyList<CameraDeviceInfo>>(new[]
|
|
{
|
|
new CameraDeviceInfo("dev1", "Logitech C920"),
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Refcount_TwoAcquires_OneCaptureUntilLastRelease()
|
|
{
|
|
var started = new List<string>();
|
|
var stopped = new List<string>();
|
|
var manager = CreateManager(started, stopped);
|
|
|
|
Assert.True(await manager.AcquireAsync("dev1"));
|
|
Assert.True(await manager.AcquireAsync("dev1"));
|
|
Assert.Single(started);
|
|
|
|
await manager.ReleaseAsync("dev1");
|
|
Assert.Empty(stopped);
|
|
|
|
await manager.ReleaseAsync("dev1");
|
|
Assert.Single(stopped);
|
|
Assert.Null(manager.GetLatestFrame("dev1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FramePump_UpdatesLatestFrame()
|
|
{
|
|
var started = new List<string>();
|
|
var stopped = new List<string>();
|
|
FakeFrameSource? captured = null;
|
|
var manager = new CameraManager(
|
|
new FakeEnumerator(),
|
|
id => captured = new FakeFrameSource(id, started, stopped),
|
|
null, TimeSpan.Zero);
|
|
|
|
await manager.AcquireAsync("dev1");
|
|
var frame = new VideoFrame(2, 2, new byte[16]);
|
|
captured!.Pump(frame);
|
|
|
|
Assert.Same(frame, manager.GetLatestFrame("dev1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Acquire_FailingCamera_ReturnsFalseAndStops()
|
|
{
|
|
var manager = new CameraManager(
|
|
new FakeEnumerator(),
|
|
id => new FakeFrameSource(id, new List<string>(), new List<string>(), failOnStart: "camera in use"),
|
|
null, TimeSpan.Zero);
|
|
string? failedDevice = null;
|
|
manager.CameraFailed += (device, _) => failedDevice = device;
|
|
|
|
Assert.False(await manager.AcquireAsync("dev1"));
|
|
Assert.Equal("dev1", failedDevice);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Acquire_EmptyDeviceId_ReturnsFalse()
|
|
{
|
|
var manager = CreateManager(new List<string>(), new List<string>());
|
|
Assert.False(await manager.AcquireAsync(" "));
|
|
}
|
|
|
|
// ─── First-frame proof (the integration test for this change) ───
|
|
|
|
[Fact]
|
|
public async Task Acquire_SilentCamera_NoFirstFrame_FailsAndSurfacesCameraFailed()
|
|
{
|
|
var stopped = new List<string>();
|
|
var manager = new CameraManager(
|
|
new FakeEnumerator(),
|
|
id => new FakeFrameSource(id, new List<string>(), stopped),
|
|
null, TimeSpan.FromMilliseconds(150));
|
|
string? failedDevice = null;
|
|
string? failedMessage = null;
|
|
manager.CameraFailed += (device, message) =>
|
|
{
|
|
failedDevice = device;
|
|
failedMessage = message;
|
|
};
|
|
|
|
// The reader "starts" fine but never delivers a frame — the exact
|
|
// silent-empty-box scenario. Must fail, be reported, and be rolled back.
|
|
Assert.False(await manager.AcquireAsync("dev1"));
|
|
Assert.Equal("dev1", failedDevice);
|
|
Assert.Contains("no frames", failedMessage);
|
|
Assert.Null(manager.GetLatestFrame("dev1"));
|
|
Assert.Contains("dev1", stopped);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Acquire_FirstFrameProvesAlive_ReturnsTrueWithoutWaitingTimeout()
|
|
{
|
|
var manager = new CameraManager(
|
|
new FakeEnumerator(),
|
|
id => new FakeFrameSource(id, new List<string>(), new List<string>(),
|
|
pumpOnStart: new VideoFrame(2, 2, new byte[16])),
|
|
null, TimeSpan.FromSeconds(10));
|
|
|
|
var started = await manager.AcquireAsync("dev1");
|
|
|
|
Assert.True(started);
|
|
Assert.NotNull(manager.GetLatestFrame("dev1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Acquire_SourceFailureAfterStart_SurfacesCameraFailedAndRollsBack()
|
|
{
|
|
var stopped = new List<string>();
|
|
FakeFrameSource? source = null;
|
|
var manager = new CameraManager(
|
|
new FakeEnumerator(),
|
|
id => source = new FakeFrameSource(id, new List<string>(), stopped,
|
|
pumpOnStart: new VideoFrame(2, 2, new byte[16])),
|
|
null, TimeSpan.Zero);
|
|
string? failedMessage = null;
|
|
manager.CameraFailed += (_, message) => failedMessage = message;
|
|
|
|
Assert.True(await manager.AcquireAsync("dev1"));
|
|
source!.Fail("capture failed (0x8007001F): device not available");
|
|
|
|
Assert.Contains("device not available", failedMessage);
|
|
Assert.Null(manager.GetLatestFrame("dev1"));
|
|
Assert.Contains("dev1", stopped);
|
|
}
|
|
}
|