rainbow-dragon/Tests/ForgeProxyTests/ProxyTests.swift

181 lines
7.8 KiB
Swift
Raw Normal View History

import XCTest
import Foundation
import ForgeColor
import ForgeGrade
@testable import ForgeProxy
final class FFmpegCommandTests: XCTestCase {
// ProRes proxy preset argv golden.
func testProResProxyArgs() {
let cmd = FFmpegCommandBuilder.build(
input: "/media/A001C001.mov",
output: "/proxies/A001C001.mov",
preset: .proresProxy,
scale: .half,
lutPath: nil,
burnIn: nil)
XCTAssertEqual(cmd.first, "-y")
XCTAssertTrue(cmd.contains("-i"))
XCTAssertTrue(cmd.contains("/media/A001C001.mov"))
XCTAssertTrue(cmd.contains("prores_ks"))
// Profile 0 = proxy.
let profileIdx = cmd.firstIndex(of: "-profile:v")!
XCTAssertEqual(cmd[profileIdx + 1], "0")
XCTAssertTrue(cmd.contains("scale=iw/2:ih/2"))
XCTAssertEqual(cmd.last, "/proxies/A001C001.mov")
}
// H264 preset.
func testH264Args() {
let cmd = FFmpegCommandBuilder.build(
input: "in.mov", output: "out.mp4",
preset: .h264, scale: .quarter, lutPath: nil, burnIn: nil)
XCTAssertTrue(cmd.contains("libx264"))
XCTAssertTrue(cmd.contains("scale=iw/4:ih/4"))
}
// DNxHR preset.
func testDNxHRArgs() {
let cmd = FFmpegCommandBuilder.build(
input: "in.mov", output: "out.mxf",
preset: .dnxhrLB, scale: .full, lutPath: nil, burnIn: nil)
XCTAssertTrue(cmd.contains("dnxhd"))
XCTAssertTrue(cmd.contains("dnxhr_lb"))
// Full scale: no scale filter unless LUT/burn present.
XCTAssertFalse(cmd.joined(separator: " ").contains("scale="))
}
// LUT filter: lut3d with escaped path joins filter chain before scale.
func testLutFilter() {
let cmd = FFmpegCommandBuilder.build(
input: "in.mov", output: "out.mov",
preset: .proresProxy, scale: .half,
lutPath: "/tmp/my look's.cube", burnIn: nil)
let vf = cmd[cmd.firstIndex(of: "-vf")! + 1]
XCTAssertTrue(vf.contains("lut3d="))
// ffmpeg filter escaping: ' -> '\'' inside quoted value, or backslash escapes.
XCTAssertTrue(vf.contains("my look"))
XCTAssertTrue(vf.contains("lut3d="), "lut3d must precede scale")
XCTAssertLessThan(vf.range(of: "lut3d=")!.lowerBound, vf.range(of: "scale=")!.lowerBound)
}
// Burn-in: drawtext with clip name + timecode.
func testBurnIn() {
let cmd = FFmpegCommandBuilder.build(
input: "in.mov", output: "out.mov",
preset: .h264, scale: .half,
lutPath: nil,
burnIn: BurnIn(clipName: "A001C001", timecode: "10:00:00:00", fps: 24))
let vf = cmd[cmd.firstIndex(of: "-vf")! + 1]
XCTAssertTrue(vf.contains("drawtext"))
XCTAssertTrue(vf.contains("A001C001"))
XCTAssertTrue(vf.contains("timecode"))
// TC colons escaped for filter syntax.
XCTAssertTrue(vf.contains("10\\:00\\:00\\:00"))
}
// Audio: copy through.
func testAudioCopied() {
let cmd = FFmpegCommandBuilder.build(
input: "in.mov", output: "out.mov",
preset: .proresProxy, scale: .full, lutPath: nil, burnIn: nil)
let aIdx = cmd.firstIndex(of: "-c:a")!
XCTAssertEqual(cmd[aIdx + 1], "copy")
}
}
final class ProxyJobTests: XCTestCase {
// Runner invokes executable with built argv; fake ffmpeg script records args.
func testRunnerInvokesWithArgs() async throws {
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(atPath: dir) }
let fake = dir + "/fake-ffmpeg"
try #"""
#!/bin/bash
echo "$@" > "$(dirname "$0")/argv.txt"
exit 0
"""#.write(toFile: fake, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
let runner = ProxyRunner(ffmpegPath: fake)
try await runner.run(args: ["-y", "-i", "in.mov", "out.mov"])
let argv = try String(contentsOfFile: dir + "/argv.txt", encoding: .utf8)
XCTAssertEqual(argv.trimmingCharacters(in: .whitespacesAndNewlines), "-y -i in.mov out.mov")
}
// Nonzero exit throws with stderr.
func testRunnerThrowsOnFailure() async throws {
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(atPath: dir) }
let fake = dir + "/fake-ffmpeg"
try "#!/bin/bash\necho 'boom' >&2\nexit 1\n".write(toFile: fake, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
let runner = ProxyRunner(ffmpegPath: fake)
do {
try await runner.run(args: ["x"])
XCTFail("expected throw")
} catch let e as ProxyError {
guard case .ffmpegFailed(_, let stderr) = e else { return XCTFail("wrong error") }
XCTAssertTrue(stderr.contains("boom"))
}
}
// Graded job: snapshot -> temp .cube written -> lut3d arg references it -> cleaned up after.
func testGradedJobWritesCube() async throws {
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(atPath: dir) }
let fake = dir + "/fake-ffmpeg"
try #"""
#!/bin/bash
echo "$@" > "$(dirname "$0")/argv.txt"
# capture cube existence at run time
if [[ "$@" == *lut3d* ]]; then
cube=$(echo "$@" | grep -oE "[^ =']*\.cube" | head -1)
[[ -f "$cube" ]] && echo "cube-exists" > "$(dirname "$0")/cube.txt"
fi
exit 0
"""#.write(toFile: fake, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
let stack = GradeStack(nodes: [GradeNode(kind: .cdl(CDL(
slope: SIMD3(1.2, 1, 1), offset: .zero, power: .one, saturation: 1)))])
let snapshot = try GradeSnapshot.encode(stack)
let job = ProxyJob(
input: "in.mov",
output: dir + "/out.mov",
preset: .proresProxy,
scale: .half,
gradeSnapshot: snapshot,
burnIn: nil)
try await job.run(ffmpegPath: fake, latticeSize: 9)
let argv = try String(contentsOfFile: dir + "/argv.txt", encoding: .utf8)
XCTAssertTrue(argv.contains("lut3d="))
XCTAssertEqual(try String(contentsOfFile: dir + "/cube.txt", encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines), "cube-exists")
}
// Ungraded fallback: no snapshot -> no lut3d in args.
func testUngradedNoLut() async throws {
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(atPath: dir) }
let fake = dir + "/fake-ffmpeg"
try "#!/bin/bash\necho \"$@\" > \"$(dirname \"$0\")/argv.txt\"\nexit 0\n".write(toFile: fake, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
let job = ProxyJob(input: "in.mov", output: dir + "/out.mov", preset: .h264, scale: .half, gradeSnapshot: nil, burnIn: nil)
try await job.run(ffmpegPath: fake, latticeSize: 9)
let argv = try String(contentsOfFile: dir + "/argv.txt", encoding: .utf8)
XCTAssertFalse(argv.contains("lut3d"))
}
}