178 lines
7.5 KiB
C#
178 lines
7.5 KiB
C#
using System.Net.Http;
|
|
using VideoDownloader.Core.Platform;
|
|
using VideoDownloader.Core.Config;
|
|
using VideoDownloader.Core.Social;
|
|
|
|
namespace VideoDownloader.Tests.Platform;
|
|
|
|
// ── Fake ─────────────────────────────────────────────────────────────
|
|
public sealed class FakeProcessRunner : IProcessRunner
|
|
{
|
|
public ProcessSpec? LastSpec;
|
|
public Func<ProcessSpec, Action<string, string>?, CancellationToken, Task<ProcessResult>> Behavior =
|
|
(_, _, _) => Task.FromResult(new ProcessResult(0, "", ""));
|
|
|
|
public Task<ProcessResult> RunAsync(ProcessSpec spec, Action<string, string>? onLine, CancellationToken ct)
|
|
{
|
|
LastSpec = spec;
|
|
return Behavior(spec, onLine, ct);
|
|
}
|
|
}
|
|
|
|
// 재구성 ②: Core Platform 추상화 — 모바일(M0) 전제가 되는 프로세스/먹서 주입 검증.
|
|
public class ProcessRunnerAbstractionTests
|
|
{
|
|
[Fact]
|
|
public async Task DesktopProcessRunner_명령_실행과_캡처_Windows()
|
|
{
|
|
if (!OperatingSystem.IsWindows()) return;
|
|
var runner = new DesktopProcessRunner();
|
|
var res = await runner.RunAsync(
|
|
new ProcessSpec("cmd.exe", new[] { "/c", "echo hello-vd" }), null, CancellationToken.None);
|
|
Assert.Equal(0, res.ExitCode);
|
|
Assert.Contains("hello-vd", res.Stdout);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DesktopProcessRunner_종료코드_전파()
|
|
{
|
|
if (!OperatingSystem.IsWindows()) return;
|
|
var runner = new DesktopProcessRunner();
|
|
var res = await runner.RunAsync(
|
|
new ProcessSpec("cmd.exe", new[] { "/c", "exit 3" }), null, CancellationToken.None);
|
|
Assert.Equal(3, res.ExitCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DesktopProcessRunner_라인_탭_스트림_구분()
|
|
{
|
|
if (!OperatingSystem.IsWindows()) return;
|
|
// 인라인 리다이렉션 인용 문제를 피해 스크립트 파일로 stdout/stderr 분리 생성
|
|
var script = Path.Combine(Path.GetTempPath(), $"vd_tap_{Guid.NewGuid():N}.cmd");
|
|
File.WriteAllText(script, "@echo off\r\necho out-line\r\necho err-line 1>&2\r\n");
|
|
try
|
|
{
|
|
var runner = new DesktopProcessRunner();
|
|
var taps = new System.Collections.Concurrent.ConcurrentQueue<(string, string)>();
|
|
var res = await runner.RunAsync(
|
|
new ProcessSpec("cmd.exe", new[] { "/c", script }),
|
|
(s, l) => taps.Enqueue((s, l)), CancellationToken.None);
|
|
Assert.Equal(0, res.ExitCode);
|
|
Assert.Contains(taps.ToArray(), t => t.Item1 == "out" && t.Item2.Contains("out-line"));
|
|
Assert.Contains(taps.ToArray(), t => t.Item1 == "err" && t.Item2.Contains("err-line"));
|
|
Assert.Contains("err-line", res.Stderr);
|
|
}
|
|
finally { try { File.Delete(script); } catch { } }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DesktopProcessRunner_EnvPrefix로_PATH_선두_주입()
|
|
{
|
|
if (!OperatingSystem.IsWindows()) return;
|
|
var runner = new DesktopProcessRunner();
|
|
var marker = Path.GetTempPath(); // PATH 앞에 아무 값 — cmd 는 무시하지만 실행 자체는 검증
|
|
var res = await runner.RunAsync(
|
|
new ProcessSpec("cmd.exe", new[] { "/c", "echo ok" },
|
|
EnvPrefix: new Dictionary<string, string> { ["PATH"] = marker }),
|
|
null, CancellationToken.None);
|
|
Assert.Equal(0, res.ExitCode);
|
|
}
|
|
}
|
|
|
|
public class MediaMuxerAbstractionTests
|
|
{
|
|
private static string Ffmpeg => OperatingSystem.IsWindows() ? "ffmpeg.exe" : "ffmpeg";
|
|
|
|
[Fact]
|
|
public async Task RemuxAsync는_copy_리먹스_인자를_만든다()
|
|
{
|
|
var fake = new FakeProcessRunner();
|
|
var muxer = new DesktopMediaMuxer(fake);
|
|
await muxer.RemuxAsync(Ffmpeg, "in.ts", "out.mp4", CancellationToken.None);
|
|
|
|
var spec = fake.LastSpec!;
|
|
Assert.Equal(Ffmpeg, spec.ExePath);
|
|
Assert.Equal(new[] { "-y", "-hide_banner", "-loglevel", "error", "-i", "in.ts", "-c", "copy", "out.mp4" },
|
|
spec.Args);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemuxAsync_실패시_stderr_포함_예외()
|
|
{
|
|
var fake = new FakeProcessRunner
|
|
{ Behavior = (_, _, _) => Task.FromResult(new ProcessResult(1, "", "boom-err")) };
|
|
var muxer = new DesktopMediaMuxer(fake);
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => muxer.RemuxAsync(Ffmpeg, "in.ts", "out.mp4", CancellationToken.None));
|
|
Assert.Contains("boom-err", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DownloadDashAsync는_진행률을_파싱해_리포트한다()
|
|
{
|
|
var fake = new FakeProcessRunner
|
|
{
|
|
Behavior = (_, onLine, _) =>
|
|
{
|
|
onLine?.Invoke("out", "out_time_ms=2500000"); // 2.5s / 10s = 25%
|
|
onLine?.Invoke("out", "progress=end");
|
|
return Task.FromResult(new ProcessResult(0, "", ""));
|
|
}
|
|
};
|
|
var muxer = new DesktopMediaMuxer(fake);
|
|
var progress = new SyncProgress<VideoDownloader.Core.Models.DownloadProgress>();
|
|
await muxer.DownloadDashAsync(Ffmpeg, new Uri("https://x.test/a.mpd"), "out.mp4",
|
|
durationSec: 10, http: new HttpClient(), progress: progress, CancellationToken.None);
|
|
|
|
var spec = fake.LastSpec!;
|
|
Assert.Contains("-progress", spec.Args);
|
|
Assert.Contains("pipe:1", spec.Args);
|
|
Assert.Contains("https://x.test/a.mpd", spec.Args);
|
|
Assert.Contains("+faststart", spec.Args);
|
|
Assert.Contains(progress.Items, r => Math.Abs(r.Percent - 25.0) < 0.01);
|
|
}
|
|
|
|
/// <summary>Progress<T> 는 SyncContext 에 큐잉되어 레이스가 나므로 동기 기록용 구현.</summary>
|
|
private sealed class SyncProgress<T> : IProgress<T>
|
|
{
|
|
public readonly List<T> Items = new();
|
|
public void Report(T value) => Items.Add(value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DownloadDashAsync_실패시_예외()
|
|
{
|
|
var fake = new FakeProcessRunner
|
|
{ Behavior = (_, _, _) => Task.FromResult(new ProcessResult(2, "", "dash-fail")) };
|
|
var muxer = new DesktopMediaMuxer(fake);
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => muxer.DownloadDashAsync(Ffmpeg, new Uri("https://x.test/a.mpd"), "o.mp4", 10,
|
|
new HttpClient(), null, CancellationToken.None));
|
|
Assert.Contains("dash-fail", ex.Message);
|
|
}
|
|
}
|
|
|
|
public class YtDlpRunnerAbstractionTests
|
|
{
|
|
[Fact]
|
|
public async Task GetVersionAsync은_주입된_러너로_version_인자와_출력을_해석한다()
|
|
{
|
|
var fake = new FakeProcessRunner
|
|
{ Behavior = (spec, _, _) => Task.FromResult(new ProcessResult(0, "2026.07.04\n", "")) };
|
|
var version = await YtDlpRunner.GetVersionAsync("yt-dlp.exe", CancellationToken.None, fake);
|
|
Assert.Equal("2026.07.04", version);
|
|
Assert.Contains("--version", fake.LastSpec!.Args);
|
|
Assert.Equal("yt-dlp.exe", fake.LastSpec.ExePath);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunCaptureAsync도_주입된_러너를_사용한다()
|
|
{
|
|
var fake = new FakeProcessRunner
|
|
{ Behavior = (_, _, _) => Task.FromResult(new ProcessResult(0, "{\"id\":\"x\"}", "")) };
|
|
var (exit, stdout, _) = await YtDlpRunner.RunCaptureAsync(
|
|
"yt-dlp.exe", new[] { "-J", "https://youtu.be/x" }, CancellationToken.None, fake);
|
|
Assert.Equal(0, exit);
|
|
Assert.Contains("{\"id\"", stdout);
|
|
}
|
|
}
|