import XCTest import Foundation import ForgeGrade import ForgeColor @testable import ForgeVideo final class VideoTests: XCTestCase { // Solid-color test source produces frames of declared size. func testTestPatternSource() async throws { let source = TestPatternSource(width: 8, height: 4, color: SIMD3(0.5, 0.25, 0.75)) let frame = try await source.nextFrame() XCTAssertEqual(frame.width, 8) XCTAssertEqual(frame.height, 4) XCTAssertEqual(frame.pixels.count, 8 * 4) XCTAssertEqual(frame.pixels[0].x, 0.5, accuracy: 1e-6) } // Grading a frame applies the stack per pixel. func testGradeFrame() async throws { let source = TestPatternSource(width: 4, height: 2, color: SIMD3(0.25, 0.25, 0.25)) let stack = GradeStack(nodes: [GradeNode(kind: .cdl(CDL( slope: SIMD3(2, 2, 2), offset: .zero, power: .one, saturation: 1)))]) let frame = try await source.nextFrame() let graded = FrameProcessor.apply(stack: stack, to: frame) XCTAssertEqual(graded.pixels[0].x, 0.5, accuracy: 1e-6) XCTAssertEqual(graded.width, 4) // Timecode preserved. XCTAssertEqual(graded.timecode, frame.timecode) } // Still grab writes PPM (portable, no image lib dep) and returns path. func testStillGrabWritesPPM() async throws { let source = TestPatternSource(width: 4, height: 4, color: SIMD3(1, 0, 0)) let frame = try await source.nextFrame() let dir = NSTemporaryDirectory() + "forge-stills-\(UUID().uuidString)" let grabber = StillGrabber(directory: dir) let path = try grabber.grab(frame: frame, clipName: "A001C001") defer { try? FileManager.default.removeItem(atPath: dir) } XCTAssertTrue(FileManager.default.fileExists(atPath: path)) XCTAssertTrue(path.hasSuffix(".ppm")) XCTAssertTrue(path.contains("A001C001")) let data = try Data(contentsOf: URL(fileURLWithPath: path)) // Compare header bytes directly — string decoding of Data slices is // unreliable across Foundation implementations. let expectedHeader = [UInt8]("P6\n4 4\n255\n".utf8) XCTAssertEqual([UInt8](data.prefix(expectedHeader.count)), expectedHeader) // First pixel red: 255 0 0. let headerLen = "P6\n4 4\n255\n".utf8.count XCTAssertEqual(data[headerLen], 255) XCTAssertEqual(data[headerLen + 1], 0) XCTAssertEqual(data[headerLen + 2], 0) } // Values clamp to 0-255 on write. func testStillClampsRange() async throws { let source = TestPatternSource(width: 2, height: 2, color: SIMD3(2.0, -0.5, 0.5)) let frame = try await source.nextFrame() let dir = NSTemporaryDirectory() + "forge-stills-\(UUID().uuidString)" defer { try? FileManager.default.removeItem(atPath: dir) } let path = try StillGrabber(directory: dir).grab(frame: frame, clipName: "clamp") let data = try Data(contentsOf: URL(fileURLWithPath: path)) let headerLen = "P6\n2 2\n255\n".utf8.count XCTAssertEqual(data[headerLen], 255) // 2.0 -> 255 XCTAssertEqual(data[headerLen + 1], 0) // -0.5 -> 0 XCTAssertEqual(data[headerLen + 2], 128) // 0.5 -> 128 (rounded) } // Null sink accepts frames without error (SDI-out stand-in). func testNullSink() async throws { let sink = NullFrameSink() let source = TestPatternSource(width: 2, height: 2, color: .zero) try await sink.consume(frame: try await source.nextFrame()) let count = await sink.consumedCount XCTAssertEqual(count, 1) } }