import XCTest @testable import LivePhotoCore final class LivePhotoCoreTests: XCTestCase { // MARK: - ExportParams Tests func testExportParamsDefaults() { let params = ExportParams() XCTAssertEqual(params.trimStart, 0) XCTAssertEqual(params.trimEnd, 1.0) XCTAssertEqual(params.keyFrameTime, 0.5) XCTAssertEqual(params.audioPolicy, .keep) XCTAssertEqual(params.codecPolicy, .fallbackH264) XCTAssertEqual(params.hdrPolicy, .toneMapToSDR) XCTAssertEqual(params.maxDimension, 1920) } func testExportParamsCustomValues() { let params = ExportParams( trimStart: 0.5, trimEnd: 2.0, keyFrameTime: 1.0, audioPolicy: .remove, codecPolicy: .passthrough, hdrPolicy: .keep, maxDimension: 1080 ) XCTAssertEqual(params.trimStart, 0.5) XCTAssertEqual(params.trimEnd, 2.0) XCTAssertEqual(params.keyFrameTime, 1.0) XCTAssertEqual(params.audioPolicy, .remove) XCTAssertEqual(params.codecPolicy, .passthrough) XCTAssertEqual(params.hdrPolicy, .keep) XCTAssertEqual(params.maxDimension, 1080) } func testExportParamsCodable() throws { let original = ExportParams( trimStart: 1.0, trimEnd: 3.0, keyFrameTime: 2.0, audioPolicy: .remove, codecPolicy: .passthrough, hdrPolicy: .keep, maxDimension: 720 ) let encoded = try JSONEncoder().encode(original) let decoded = try JSONDecoder().decode(ExportParams.self, from: encoded) XCTAssertEqual(decoded.trimStart, original.trimStart) XCTAssertEqual(decoded.trimEnd, original.trimEnd) XCTAssertEqual(decoded.keyFrameTime, original.keyFrameTime) XCTAssertEqual(decoded.audioPolicy, original.audioPolicy) XCTAssertEqual(decoded.codecPolicy, original.codecPolicy) XCTAssertEqual(decoded.hdrPolicy, original.hdrPolicy) XCTAssertEqual(decoded.maxDimension, original.maxDimension) } // MARK: - AppError Tests func testAppErrorInit() { let error = AppError( code: "LPB-101", stage: .normalize, message: "Test error", underlyingErrorDescription: "Underlying", suggestedActions: ["Action 1", "Action 2"] ) XCTAssertEqual(error.code, "LPB-101") XCTAssertEqual(error.stage, .normalize) XCTAssertEqual(error.message, "Test error") XCTAssertEqual(error.underlyingErrorDescription, "Underlying") XCTAssertEqual(error.suggestedActions, ["Action 1", "Action 2"]) } func testAppErrorCodable() throws { let original = AppError( code: "LPB-201", stage: .extractKeyFrame, message: "封面生成失败", underlyingErrorDescription: nil, suggestedActions: ["重试"] ) let encoded = try JSONEncoder().encode(original) let decoded = try JSONDecoder().decode(AppError.self, from: encoded) XCTAssertEqual(decoded.code, original.code) XCTAssertEqual(decoded.stage, original.stage) XCTAssertEqual(decoded.message, original.message) XCTAssertEqual(decoded.suggestedActions, original.suggestedActions) } // MARK: - SourceRef Tests func testSourceRefWithAssetIdentifier() { let ref = SourceRef(phAssetLocalIdentifier: "ABC123") XCTAssertEqual(ref.phAssetLocalIdentifier, "ABC123") XCTAssertNil(ref.fileURL) } func testSourceRefWithFileURL() { let url = URL(fileURLWithPath: "/tmp/test.mov") let ref = SourceRef(fileURL: url) XCTAssertNil(ref.phAssetLocalIdentifier) XCTAssertEqual(ref.fileURL, url) } // MARK: - WorkItem Tests func testWorkItemDefaults() { let cacheDir = URL(fileURLWithPath: "/tmp/cache") let sourceRef = SourceRef(phAssetLocalIdentifier: "test-id") let item = WorkItem( sourceVideo: sourceRef, cacheDir: cacheDir ) XCTAssertNotNil(item.id) XCTAssertNotNil(item.createdAt) XCTAssertEqual(item.status, .idle) XCTAssertNil(item.resultAssetId) XCTAssertNil(item.error) XCTAssertNil(item.coverImage) } // MARK: - LivePhotoBuildProgress Tests func testLivePhotoBuildProgress() { let progress = LivePhotoBuildProgress(stage: .normalize, fraction: 0.5) XCTAssertEqual(progress.stage, .normalize) XCTAssertEqual(progress.fraction, 0.5) } // MARK: - LivePhotoBuildStage Tests func testLivePhotoBuildStageRawValues() { XCTAssertEqual(LivePhotoBuildStage.normalize.rawValue, "normalize") XCTAssertEqual(LivePhotoBuildStage.extractKeyFrame.rawValue, "extractKeyFrame") XCTAssertEqual(LivePhotoBuildStage.writePhotoMetadata.rawValue, "writePhotoMetadata") XCTAssertEqual(LivePhotoBuildStage.writeVideoMetadata.rawValue, "writeVideoMetadata") XCTAssertEqual(LivePhotoBuildStage.saveToAlbum.rawValue, "saveToAlbum") XCTAssertEqual(LivePhotoBuildStage.validate.rawValue, "validate") } // MARK: - WorkStatus Tests func testWorkStatusRawValues() { XCTAssertEqual(WorkStatus.idle.rawValue, "idle") XCTAssertEqual(WorkStatus.editing.rawValue, "editing") XCTAssertEqual(WorkStatus.processing.rawValue, "processing") XCTAssertEqual(WorkStatus.success.rawValue, "success") XCTAssertEqual(WorkStatus.failed.rawValue, "failed") } // MARK: - CacheManager Tests func testCacheManagerInit() throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) let manager = try CacheManager(baseDirectory: tempDir) XCTAssertEqual(manager.baseDirectory, tempDir) XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.path)) // Cleanup try? FileManager.default.removeItem(at: tempDir) } func testCacheManagerMakeWorkPaths() throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) let manager = try CacheManager(baseDirectory: tempDir) let workId = UUID() let paths = try manager.makeWorkPaths(workId: workId) XCTAssertTrue(paths.workDir.path.contains(workId.uuidString)) XCTAssertEqual(paths.photoURL.pathExtension, "heic") XCTAssertEqual(paths.pairedVideoURL.pathExtension, "mov") XCTAssertEqual(paths.logURL.pathExtension, "log") XCTAssertTrue(FileManager.default.fileExists(atPath: paths.workDir.path)) // Cleanup try? FileManager.default.removeItem(at: tempDir) } func testCacheManagerClearWork() throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) let manager = try CacheManager(baseDirectory: tempDir) let workId = UUID() // Create work directory let paths = try manager.makeWorkPaths(workId: workId) XCTAssertTrue(FileManager.default.fileExists(atPath: paths.workDir.path)) // Clear it try manager.clearWork(workId: workId) XCTAssertFalse(FileManager.default.fileExists(atPath: paths.workDir.path)) // Cleanup try? FileManager.default.removeItem(at: tempDir) } // MARK: - LivePhotoWorkPaths Tests func testLivePhotoWorkPaths() { let workDir = URL(fileURLWithPath: "/tmp/work") let photoURL = URL(fileURLWithPath: "/tmp/work/photo.heic") let pairedVideoURL = URL(fileURLWithPath: "/tmp/work/paired.mov") let logURL = URL(fileURLWithPath: "/tmp/work/builder.log") let paths = LivePhotoWorkPaths( workDir: workDir, photoURL: photoURL, pairedVideoURL: pairedVideoURL, logURL: logURL ) XCTAssertEqual(paths.workDir, workDir) XCTAssertEqual(paths.photoURL, photoURL) XCTAssertEqual(paths.pairedVideoURL, pairedVideoURL) XCTAssertEqual(paths.logURL, logURL) } // MARK: - LivePhotoBuildOutput Tests func testLivePhotoBuildOutput() { let workId = UUID() let assetId = "test-asset-id" let photoURL = URL(fileURLWithPath: "/tmp/photo.heic") let videoURL = URL(fileURLWithPath: "/tmp/paired.mov") let output = LivePhotoBuildOutput( workId: workId, assetIdentifier: assetId, pairedImageURL: photoURL, pairedVideoURL: videoURL ) XCTAssertEqual(output.workId, workId) XCTAssertEqual(output.assetIdentifier, assetId) XCTAssertEqual(output.pairedImageURL, photoURL) XCTAssertEqual(output.pairedVideoURL, videoURL) } // MARK: - Policy Enums Tests func testAudioPolicyCodable() throws { let policies: [AudioPolicy] = [.keep, .remove] for policy in policies { let encoded = try JSONEncoder().encode(policy) let decoded = try JSONDecoder().decode(AudioPolicy.self, from: encoded) XCTAssertEqual(decoded, policy) } } func testCodecPolicyCodable() throws { let policies: [CodecPolicy] = [.passthrough, .fallbackH264] for policy in policies { let encoded = try JSONEncoder().encode(policy) let decoded = try JSONDecoder().decode(CodecPolicy.self, from: encoded) XCTAssertEqual(decoded, policy) } } func testHDRPolicyCodable() throws { let policies: [HDRPolicy] = [.keep, .toneMapToSDR] for policy in policies { let encoded = try JSONEncoder().encode(policy) let decoded = try JSONDecoder().decode(HDRPolicy.self, from: encoded) XCTAssertEqual(decoded, policy) } } }