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")) } }