79 lines
3.6 KiB
Swift
79 lines
3.6 KiB
Swift
|
|
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)
|
||
|
|
}
|
||
|
|
}
|