import CoreMedia import XCTest @testable import DropTwoCore final class CropEngineTests: XCTestCase { private var tempDirectory: URL! override func setUpWithError() throws { tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true) } override func tearDownWithError() throws { try? FileManager.default.removeItem(at: tempDirectory) } // MARK: - Geometrie (rein arithmetisch, kein Video nötig) func testTargetSizeMatchesReadmeExamples() { XCTAssertEqual( CropEngine.targetSize(forSourceSize: CGSize(width: 1080, height: 1920)), CGSize(width: 1080, height: 1350) ) XCTAssertEqual( CropEngine.targetSize(forSourceSize: CGSize(width: 2160, height: 3840)), CGSize(width: 2160, height: 2700) ) } func testTargetSizeRoundsHeightToEvenNumber() { let size = CropEngine.targetSize(forSourceSize: CGSize(width: 217, height: 386)) XCTAssertEqual(size.width, 217) XCTAssertEqual(size.height.truncatingRemainder(dividingBy: 2), 0) } // MARK: - End-to-End gegen einen synthetischen Clip func testCropsToTargetSizeAndKeepsAudioAndDuration() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mp4") try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) let resultInfo = try await VideoInspector.inspect(destination) let expectedSize = CropEngine.targetSize(forSourceSize: clip.size) XCTAssertEqual(resultInfo.pixelSize, expectedSize) XCTAssertEqual(resultInfo.codec, .h264) XCTAssertTrue(resultInfo.hasAudioTrack) XCTAssertEqual(CMTimeGetSeconds(resultInfo.duration), clip.seconds, accuracy: 0.3) } func testCropsWithoutAudioTrack() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(includeAudio: false, in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mp4") try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) let resultInfo = try await VideoInspector.inspect(destination) XCTAssertFalse(resultInfo.hasAudioTrack) } func testReportsProgress() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mp4") var reportedValues: [Double] = [] try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) { value in reportedValues.append(value) } XCTAssertFalse(reportedValues.isEmpty) XCTAssertEqual(reportedValues.last ?? 0, 1.0, accuracy: 0.15) } // MARK: - Ablehnungen func testRejectsNonNineBySixteenSource() async throws { let clip = try TestVideoFactory.makeClip(size: CGSize(width: 400, height: 400), in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mp4") do { try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) XCTFail("Erwartete Ablehnung für eine Nicht-9:16-Quelle") } catch CropEngineError.sourceNotNineBySixteen { // erwartet } } func testRejectsExistingDestinationWithoutOverwrite() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mp4") FileManager.default.createFile(atPath: destination.path, contents: Data()) do { try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) XCTFail("Erwartete Ablehnung wegen bestehender Zieldatei") } catch CropEngineError.destinationAlreadyExists { // erwartet } } func testOverwritesExistingDestinationWhenExplicitlyAllowed() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mp4") FileManager.default.createFile(atPath: destination.path, contents: Data("platzhalter".utf8)) try await CropEngine.crop( sourceURL: clip.url, destinationURL: destination, options: .init(overwrite: true) ) let resultInfo = try await VideoInspector.inspect(destination) XCTAssertEqual(resultInfo.pixelSize, CropEngine.targetSize(forSourceSize: clip.size)) } func testRejectsUnsupportedContainer() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mxf") do { try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) XCTFail("Erwartete Ablehnung für ein MXF-Ziel") } catch CropEngineError.unsupportedContainer { // erwartet } } func testRejectsMovAsDestinationBecauseTargetIsFixedToMp4() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory) let destination = tempDirectory.appendingPathComponent("out.mov") do { try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) XCTFail("Erwartete Ablehnung für ein MOV-Ziel — Zielformat ist fest auf MP4 gesetzt") } catch CropEngineError.unsupportedContainer { // erwartet } } }