import XCTest @testable import DropTwoCore final class JobQueueTests: XCTestCase { private var tempDirectory: URL! private var outputDirectory: URL! override func setUpWithError() throws { tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) outputDirectory = tempDirectory.appendingPathComponent("output") try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) } override func tearDownWithError() throws { try? FileManager.default.removeItem(at: tempDirectory) } func testProcessesAllJobsAndReportsEachOutcome() async throws { let validClip = try TestVideoFactory.makeNineBySixteenClip(fileName: "SOMOS-9x16-v3-DE.mov", in: tempDirectory) let skippedURL = tempDirectory.appendingPathComponent("SOMOS-4x5-v3-DE.mov") let rejectedURL = tempDirectory.appendingPathComponent("SOMOS-v3-DE.mov") let jobs = [ JobQueue.Job(sourceURL: validClip.url), JobQueue.Job(sourceURL: skippedURL), JobQueue.Job(sourceURL: rejectedURL) ] var results: [JobQueue.JobResult] = [] for await result in JobQueue.run(jobs: jobs, outputRootDirectory: outputDirectory) { results.append(result) } XCTAssertEqual(results.count, 3) func outcome(for job: JobQueue.Job) -> ConversionPipeline.Outcome? { results.first { $0.job.id == job.id }?.outcome } guard case .succeeded = outcome(for: jobs[0]) else { return XCTFail("Erwartete .succeeded für die valide Datei") } guard case .skipped(_, .alreadyTargetFormat) = outcome(for: jobs[1]) else { return XCTFail("Erwartete .skipped für die bereits-4:5-Datei") } guard case .rejected(_, .formatSegmentMissing) = outcome(for: jobs[2]) else { return XCTFail("Erwartete .rejected für die Datei ohne Format-Segment") } } func testWorksWithMaxConcurrentOne() async throws { let clipA = try TestVideoFactory.makeNineBySixteenClip(fileName: "SOMOS-9x16-v3-DE.mov", in: tempDirectory) let clipB = try TestVideoFactory.makeNineBySixteenClip(fileName: "Somos-Pendant-9x16-v3-DE.mov", in: tempDirectory) let jobs = [JobQueue.Job(sourceURL: clipA.url), JobQueue.Job(sourceURL: clipB.url)] var successCount = 0 for await result in JobQueue.run(jobs: jobs, outputRootDirectory: outputDirectory, maxConcurrent: 1) { if case .succeeded = result.outcome { successCount += 1 } } XCTAssertEqual(successCount, 2) } func testReportsProgressForJobs() async throws { let clip = try TestVideoFactory.makeNineBySixteenClip(fileName: "SOMOS-9x16-v3-DE.mov", in: tempDirectory) let job = JobQueue.Job(sourceURL: clip.url) let progressBox = ProgressBox() var results: [JobQueue.JobResult] = [] for await result in JobQueue.run( jobs: [job], outputRootDirectory: outputDirectory, progress: { _, value in progressBox.record(value) } ) { results.append(result) } XCTAssertEqual(results.count, 1) XCTAssertFalse(progressBox.values.isEmpty) } /// Sammelt Fortschrittswerte thread-sicher (synchron per Lock statt /// Actor-Hop), da `progress` von einer Hintergrund-Queue innerhalb von /// CropEngine aufgerufen wird und wir sofort nach dem Batch lesen. private final class ProgressBox: @unchecked Sendable { private let lock = NSLock() private var _values: [Double] = [] func record(_ value: Double) { lock.lock() _values.append(value) lock.unlock() } var values: [Double] { lock.lock() defer { lock.unlock() } return _values } } }