using System.Diagnostics; using System.IO; using Xunit; using ytLive.Services; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// TASK 21 (slice 2): the ffprobe frame-rate parser is pure (synthetic text, no /// probe); the one integration test drives the real /// against a fake locator + fake probe subprocess end to end. /// public class FfmpegFrameRateParserTests { // ─── Pure parser (unit) ─── [Fact] public void Parse_AvgFrameRate_Rational() { var out0 = "codec_name=h264\navg_frame_rate=30/1\nr_frame_rate=30/1\n"; Assert.Equal(30.0, FfmpegFrameRateParser.ParseFrameRate(out0)); } [Fact] public void Parse_AvgFrameRate_29_97() { var out0 = "avg_frame_rate=30000/1001\nr_frame_rate=30/1\n"; Assert.Equal(30000.0 / 1001.0, FfmpegFrameRateParser.ParseFrameRate(out0)!.Value, 3); } [Fact] public void Parse_PrefersAvgOverRFrame() { var out0 = "avg_frame_rate=25/1\nr_frame_rate=30000/1001\n"; Assert.Equal(25.0, FfmpegFrameRateParser.ParseFrameRate(out0)); } [Fact] public void Parse_FallsBackToRFrame_WhenAvgUnknown() { var out0 = "avg_frame_rate=0/0\nr_frame_rate=50/1\n"; Assert.Equal(50.0, FfmpegFrameRateParser.ParseFrameRate(out0)); } [Fact] public void Parse_IntegerValue_HasUnitDenominator() { var out0 = "avg_frame_rate=60\n"; Assert.Equal(60.0, FfmpegFrameRateParser.ParseFrameRate(out0)); } [Theory] [InlineData("avg_frame_rate=0/0\nr_frame_rate=0/0\n")] [InlineData("")] [InlineData("codec_name=h264\n")] public void Parse_Unknown_ReturnsNull(string output) { Assert.Null(FfmpegFrameRateParser.ParseFrameRate(output)); } // ─── The one integration test: probe against fake locator + fake subprocess ─── [Fact] public async Task Probe_ReturnsParsedFps_FromFakeSubprocess() { var toolsDir = CreateTempDir(); try { // ffprobe.exe must exist next to the located ffmpeg for the sibling // derivation to return a usable path. var probeExe = Path.Combine(toolsDir, "ffprobe.exe"); File.WriteAllText(probeExe, "ffprobe"); var fakeOutput = "codec_name=h264\navg_frame_rate=24000/1001\nr_frame_rate=24000/1001\n"; var probe = new FfmpegFrameRateProbe( new FakeLocator(toolsDir), () => new FakeProbeProcess(System.Text.Encoding.UTF8.GetBytes(fakeOutput))); var fps = await probe.ProbeAsync(@"C:\videos\clip.mp4"); Assert.Equal(24000.0 / 1001.0, fps!.Value, 3); } finally { Directory.Delete(toolsDir, recursive: true); } } private static string CreateTempDir() { var dir = Path.Combine(Path.GetTempPath(), "ytllive-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); return dir; } private sealed class FakeLocator : IFfmpegLocator { private readonly string _dir; public FakeLocator(string dir) => _dir = dir; public Task LocateAsync(CancellationToken ct = default) => Task.FromResult(Path.Combine(_dir, "ffmpeg.exe")); } private sealed class FakeProbeProcess : IDecodeProcess { private readonly MemoryStream _stream; public FakeProbeProcess(byte[] bytes) => _stream = new MemoryStream(bytes); public void Start(ProcessStartInfo startInfo) { } public Stream StandardOutput => _stream; public bool HasExited => _stream.Position >= _stream.Length; public int ExitCode => 0; public void Kill() { } public Task WaitForExitAsync(CancellationToken ct = default) => Task.CompletedTask; public void Dispose() => _stream.Dispose(); } }