From 70f777558622e699abee1fc3e947ba98dfaaa3bb Mon Sep 17 00:00:00 2001 From: enebin Date: Thu, 15 Jun 2023 18:54:42 +0900 Subject: [PATCH 01/11] Improve SwiftUI preview handling --- .../Util/Extension/SwiftUI+Extension.swift | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift b/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift index accb14c..8c5192b 100644 --- a/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift +++ b/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift @@ -15,47 +15,38 @@ public extension AespaSession { /// - Parameter gravity: Define `AVLayerVideoGravity` for preview's orientation. `.resizeAspectFill` by default. /// /// - Returns: `some UIViewRepresentable` which can coordinate other `View` components - func preview(gravity: AVLayerVideoGravity = .resizeAspectFill) -> some UIViewRepresentable { - AespaSwiftUIPreview(with: previewLayerPublisher, gravity: gravity) + func preview(gravity: AVLayerVideoGravity = .resizeAspectFill) -> some UIViewControllerRepresentable { + Preview(of: previewLayer, gravity: gravity) } } -fileprivate struct AespaSwiftUIPreview: UIViewRepresentable { - @StateObject var viewModel: AespaSwiftUIPreviewViewModel +fileprivate struct Preview: UIViewControllerRepresentable { + let previewLayer: AVCaptureVideoPreviewLayer let gravity: AVLayerVideoGravity init( - with publisher: AnyPublisher, + of previewLayer: AVCaptureVideoPreviewLayer, gravity: AVLayerVideoGravity ) { - _viewModel = StateObject( - wrappedValue: AespaSwiftUIPreviewViewModel(previewLayerPublisher: publisher)) self.gravity = gravity + self.previewLayer = previewLayer } - func makeUIView(context: Context) -> UIView { - let view = UIView() - return view + func makeUIViewController(context: Context) -> UIViewController { + let viewController = UIViewController() + viewController.view.backgroundColor = .clear + + return viewController } - func updateUIView(_ uiView: UIView, context: Context) { - guard let previewLayer = viewModel.previewLayer else { return } - + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { previewLayer.videoGravity = gravity - uiView.layer.addSublayer(previewLayer) + uiViewController.view.layer.addSublayer(previewLayer) - previewLayer.frame = uiView.bounds + previewLayer.frame = uiViewController.view.bounds } -} -fileprivate class AespaSwiftUIPreviewViewModel: ObservableObject { - var subscription: Cancellable? - @Published var previewLayer: AVCaptureVideoPreviewLayer? - - init(previewLayerPublisher: AnyPublisher) { - subscription = previewLayerPublisher - .subscribe(on: DispatchQueue.global()) - .receive(on: DispatchQueue.main) - .sink { self.previewLayer = $0 } + func dismantleUIViewController(_ uiViewController: UIViewController, coordinator: ()) { + previewLayer.removeFromSuperlayer() } } From d1a796abef3192b9773e39fd9d60d137998e7cdc Mon Sep 17 00:00:00 2001 From: enebin Date: Sun, 18 Jun 2023 11:36:28 +0900 Subject: [PATCH 02/11] Add tests Add device tests Add connection tests Add test workflow Fix typo Delete reporting Add fileoutput test Add photos related tests Fix workflow Change file name Add album importer tests Add file util test --- .github/workflows/unit-test.yml | 57 + .gitignore | 6 + Scripts/gen-mocks.sh | 32 + Sources/Aespa/AespaSession.swift | 3 +- ...CaptureConnection+AespaRepresentable.swift | 28 + .../AVCaptureDevice+AespaRepresentable.swift | 41 + ...CaptureFileOutput+AespaRepresentable.swift | 21 + ...AespaCoreSession+AespaRepresentable.swift} | 0 .../Photos+AespaRepresentable.swift | 50 + Sources/Aespa/Processor/AespaProcessing.swift | 7 +- .../Asset/AssetAdditionProcessor.swift | 30 +- .../Record/FinishRecordProcessor.swift | 4 +- .../Record/StartRecordProcessor.swift | 6 +- Sources/Aespa/Tuner/AespaTuning.swift | 6 +- .../Connection/VideoOrientationTuner.swift | 4 +- .../Connection/VideoStabilizationTuner.swift | 2 +- .../Aespa/Tuner/Device/AutoFocusTuner.swift | 4 +- Sources/Aespa/Tuner/Device/ZoomTuner.swift | 4 +- .../Extension/AVFoundation+Extension.swift | 39 - .../Util/Video/Album/AlbumImporter.swift | 27 +- .../Util/Video/File/VideoFileGenerator.swift | 2 +- .../Video/File/VideoFilePathProvider.swift | 9 +- .../Aespa-iOS-test.xcodeproj/project.pbxproj | 189 +- .../xcschemes/Aespa-iOS-test-scheme.xcscheme | 32 +- .../xcshareddata/xcschemes/Aespa.xcscheme | 66 + Tests/Aespa-iOS-test.xctestplan | 10 +- Tests/Aespa-iOS-testTests.xctestplan | 33 + Tests/Aespa-iOS-testTests/Mock/.gitkeep | 0 .../Mock/GeneratedMocks.swift | 4207 ----------------- .../Mock/MockFileManager.swift | 34 + .../Mock/Video/MockVideo.swift | 24 + .../Aespa-iOS-testTests/Mock/Video/video.mp4 | Bin 0 -> 12857 bytes .../Processor/AssetProcessorTests.swift | 54 + .../Processor/FileOutputProcessorTests.swift | 63 + .../Tuner/ConnectionTunerTests.swift | 61 + .../Tuner/DeviceTunerTests.swift | 65 + .../Tuner/SessionTunerTests.swift | 5 +- .../Util/AlbumUtilTests.swift | 79 + .../Util/FileUtilTests.swift | 66 + Tests/Cuckoo/gen-mocks.sh | 17 - 40 files changed, 933 insertions(+), 4454 deletions(-) create mode 100644 .github/workflows/unit-test.yml create mode 100755 Scripts/gen-mocks.sh create mode 100644 Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift create mode 100644 Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift create mode 100644 Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift rename Sources/Aespa/Core/{AespaCoreSession+AespaCoreSessionRepresentable.swift => Representable/AespaCoreSession+AespaRepresentable.swift} (100%) create mode 100644 Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift create mode 100644 Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa.xcscheme create mode 100644 Tests/Aespa-iOS-testTests.xctestplan create mode 100644 Tests/Aespa-iOS-testTests/Mock/.gitkeep delete mode 100644 Tests/Aespa-iOS-testTests/Mock/GeneratedMocks.swift create mode 100644 Tests/Aespa-iOS-testTests/Mock/MockFileManager.swift create mode 100644 Tests/Aespa-iOS-testTests/Mock/Video/MockVideo.swift create mode 100644 Tests/Aespa-iOS-testTests/Mock/Video/video.mp4 create mode 100644 Tests/Aespa-iOS-testTests/Processor/AssetProcessorTests.swift create mode 100644 Tests/Aespa-iOS-testTests/Processor/FileOutputProcessorTests.swift create mode 100644 Tests/Aespa-iOS-testTests/Tuner/ConnectionTunerTests.swift create mode 100644 Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift create mode 100644 Tests/Aespa-iOS-testTests/Util/AlbumUtilTests.swift create mode 100644 Tests/Aespa-iOS-testTests/Util/FileUtilTests.swift delete mode 100755 Tests/Cuckoo/gen-mocks.sh diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..2e9d2d0 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,57 @@ +name: Unit testing and collect result infomations + +on: + pull_request: + types: [synchronize] + +concurrency: + group: unit-test-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v3 + + - name: Select Xcode + run: sudo xcode-select -s '/Applications/Xcode.app/Contents/Developer' + + - name: Prepare test + run: | + cd ./Scripts + ROOT_PATH="../" + + ROOT_DIR_NAME=$(basename "$(cd "$ROOT_PATH" && pwd)") + if [ "$ROOT_DIR_NAME" != "Aespa" ]; then + echo "❌ Error: Script's not called in proper path." + exit 1 + fi + + # Generate mocks + chmod +x ./gen-mocks.sh + ./gen-mocks.sh + + - name: Test + run: | + ROOT_PATH="./" + + # Now do test + DERIVED_DATA_PATH="./DerivedData" + TEST_SCHEME="Aespa-iOS-test-scheme" + + xcodebuild test \ + -verbose \ + -project ${ROOT_PATH}/Tests/Aespa-iOS-test.xcodeproj \ + -scheme ${TEST_SCHEME} \ + -destination 'platform=iOS Simulator,name=iPhone 14 Pro,OS=latest' \ + -derivedDataPath ${DERIVED_DATA_PATH} \ + -enableCodeCoverage YES \ + | xcpretty --color + + # Check if the tests failed + if [ $? -ne 0 ]; then + echo "❌ Error: Tests failed." + exit 1 + fi diff --git a/.gitignore b/.gitignore index 5add839..4d62266 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ Aespa-test-env/run Tests/Cuckoo/cuckoo_generator Tests/Cuckoo/run + +Tests/Aespa-iOS-testTests/Mock/GeneratedMocks.swift + +Scripts/cuckoo_generator + +Scripts/run diff --git a/Scripts/gen-mocks.sh b/Scripts/gen-mocks.sh new file mode 100755 index 0000000..f917447 --- /dev/null +++ b/Scripts/gen-mocks.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +ROOT_PATH="../" +ROOT_DIR_NAME=$(basename "$(cd "$ROOT_PATH" && pwd)") +if [ "$ROOT_DIR_NAME" != "Aespa" ]; then + echo "❌ Error: Script's not called in proper path." + exit 1 +fi + +if [ ! -f run ]; then + curl -Lo run https://raw.githubusercontent.com/Brightify/Cuckoo/master/run && chmod +x run +fi + +PROJECT_NAME="Aespa" +TESTER_NAME="Aespa-iOS-test" +PACKAGE_SOURCE_PATH="${ROOT_PATH}/Sources/Aespa" +OUTPUT_FILE="${ROOT_PATH}/Tests/${TESTER_NAME}Tests/Mock/GeneratedMocks.swift" +SWIFT_FILES=$(find "$PACKAGE_SOURCE_PATH" -type f -name "*.swift" -print0 | xargs -0) + +echo "✅ Generated Mocks File = ${OUTPUT_FILE}" +echo "✅ Mocks Input Directory = ${PACKAGE_SOURCE_PATH}" + + +./run --download generate --testable "${PROJECT_NAME}" --output "${OUTPUT_FILE}" ${SWIFT_FILES} + +# Check the exit status of the last command +if [ $? -ne 0 ]; then + echo "❌ Error: Failed to generate mocks." + exit 1 +fi + +echo "✅ Generating mock was successful" \ No newline at end of file diff --git a/Sources/Aespa/AespaSession.swift b/Sources/Aespa/AespaSession.swift index 0ed9dc2..e7fc13b 100644 --- a/Sources/Aespa/AespaSession.swift +++ b/Sources/Aespa/AespaSession.swift @@ -319,7 +319,8 @@ open class AespaSession { let filePath = try VideoFilePathProvider.requestFilePath( from: fileManager.systemFileManager, directoryName: option.asset.albumName, - fileName: fileName) + fileName: fileName, + extension: "mp4") if option.session.autoVideoOrientationEnabled { try setOrientationWithError(to: UIDevice.current.orientation.toVideoOrientation) diff --git a/Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift new file mode 100644 index 0000000..22b6709 --- /dev/null +++ b/Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift @@ -0,0 +1,28 @@ +// +// File.swift +// +// +// Created by 이영빈 on 2023/06/16. +// + +import Foundation +import AVFoundation + +protocol AespaCaptureConnectionRepresentable { + var videoOrientation: AVCaptureVideoOrientation { get set } + var preferredVideoStabilizationMode: AVCaptureVideoStabilizationMode { get set } + + func setOrientation(to orientation: AVCaptureVideoOrientation) + func setStabilizationMode(to mode: AVCaptureVideoStabilizationMode) +} + +extension AVCaptureConnection: AespaCaptureConnectionRepresentable { + func setOrientation(to orientation: AVCaptureVideoOrientation) { + self.videoOrientation = orientation + } + + func setStabilizationMode(to mode: AVCaptureVideoStabilizationMode) { + self.preferredVideoStabilizationMode = mode + } +} + diff --git a/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift new file mode 100644 index 0000000..104899d --- /dev/null +++ b/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift @@ -0,0 +1,41 @@ +// +// AespaCaptureDeviceRepresentable.swift +// +// +// Created by 이영빈 on 2023/06/16. +// + +import Foundation +import AVFoundation + +protocol AespaCaptureDeviceRepresentable { + var focusMode: AVCaptureDevice.FocusMode { get set } + var videoZoomFactor: CGFloat { get set } + + var maxResolution: Double? { get } + + func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool + + func setFocusMode(_ focusMode: AVCaptureDevice.FocusMode) throws + func setZoomFactor(_ factor: CGFloat) +} + +extension AVCaptureDevice: AespaCaptureDeviceRepresentable { + func setFocusMode(_ focusMode: FocusMode) throws { + self.focusMode = focusMode + } + + func setZoomFactor(_ factor: CGFloat) { + self.videoZoomFactor = factor + } + + var maxResolution: Double? { + var maxResolution: Double = 0 + for format in self.formats { + let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription) + let resolution = Double(dimensions.width * dimensions.height) + maxResolution = max(resolution, maxResolution) + } + return maxResolution + } +} diff --git a/Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift new file mode 100644 index 0000000..c8f2d77 --- /dev/null +++ b/Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift @@ -0,0 +1,21 @@ +// +// AVCaptureFileOutput+AespaFileOutputRepresentable.swift +// +// +// Created by 이영빈 on 2023/06/16. +// + +import Foundation +import AVFoundation + +protocol AespaFileOutputRepresentable { + func stopRecording() + func startRecording(to outputFileURL: URL, recordingDelegate delegate: AVCaptureFileOutputRecordingDelegate) + func getConnection(with mediaType: AVMediaType) -> AespaCaptureConnectionRepresentable? +} + +extension AVCaptureFileOutput: AespaFileOutputRepresentable { + func getConnection(with mediaType: AVMediaType) -> AespaCaptureConnectionRepresentable? { + return connection(with: mediaType) + } +} diff --git a/Sources/Aespa/Core/AespaCoreSession+AespaCoreSessionRepresentable.swift b/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift similarity index 100% rename from Sources/Aespa/Core/AespaCoreSession+AespaCoreSessionRepresentable.swift rename to Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift diff --git a/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift new file mode 100644 index 0000000..0177a26 --- /dev/null +++ b/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift @@ -0,0 +1,50 @@ +// +// File.swift +// +// +// Created by 이영빈 on 2023/06/16. +// + +import Photos +import Foundation + +protocol AespaAssetLibraryRepresentable { + func performChanges(_ changes: @escaping () -> Void) async throws + func performChangesAndWait(_ changeBlock: @escaping () -> Void) throws + func requestAuthorization(for accessLevel: PHAccessLevel) async -> PHAuthorizationStatus + func fetchAlbum(title: String, fetchOptions: PHFetchOptions) -> Collection? +} + +protocol AespaAssetCollectionRepresentable { + var underlyingAssetCollection: PHAssetCollection { get } + var localizedTitle: String? { get } + + func canAdd(_ filePath: URL) -> Bool +} + +extension PHPhotoLibrary: AespaAssetLibraryRepresentable { + func fetchAlbum(title: String, fetchOptions: PHFetchOptions) -> Collection? { + fetchOptions.predicate = NSPredicate(format: "title = %@", title) + + let collections = PHAssetCollection.fetchAssetCollections( + with: .album, subtype: .any, options: fetchOptions + ) + + return collections.firstObject as? Collection + } + + func requestAuthorization(for accessLevel: PHAccessLevel) async -> PHAuthorizationStatus { + await PHPhotoLibrary.requestAuthorization(for: accessLevel) + } +} + +extension PHAssetCollection: AespaAssetCollectionRepresentable { + var underlyingAssetCollection: PHAssetCollection { self } + + func canAdd(_ filePath: URL) -> Bool { + let asset = AVAsset(url: filePath) + let tracks = asset.tracks(withMediaType: AVMediaType.video) + + return !tracks.isEmpty + } +} diff --git a/Sources/Aespa/Processor/AespaProcessing.swift b/Sources/Aespa/Processor/AespaProcessing.swift index 2b1041f..03db488 100644 --- a/Sources/Aespa/Processor/AespaProcessing.swift +++ b/Sources/Aespa/Processor/AespaProcessing.swift @@ -10,9 +10,12 @@ import Foundation import AVFoundation protocol AespaFileOutputProcessing { - func process(_ output: AVCaptureFileOutput) throws + func process(_ output: T) throws } protocol AespaAssetProcessing { - func process(_ photoLibrary: PHPhotoLibrary, _ assetCollection: PHAssetCollection) async throws + func process( + _ photoLibrary: T, _ assetCollection: U + ) async throws + where T: AespaAssetLibraryRepresentable, U: AespaAssetCollectionRepresentable } diff --git a/Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift b/Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift index b81555e..91d83e4 100644 --- a/Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift +++ b/Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift @@ -10,48 +10,38 @@ import Foundation struct AssetAdditionProcessor: AespaAssetProcessing { let filePath: URL - - func process(_ photoLibrary: PHPhotoLibrary, _ assetCollection: PHAssetCollection) async throws { + + func process(_ photoLibrary: T, _ assetCollection: U) async throws { guard - case .authorized = await PHPhotoLibrary.requestAuthorization(for: .addOnly) + case .authorized = await photoLibrary.requestAuthorization(for: .addOnly) else { let error = AespaError.album(reason: .unabledToAccess) Logger.log(error: error) throw error } - - let album = assetCollection - try await add(video: filePath, to: album, photoLibrary) + + try await add(video: filePath, to: assetCollection, photoLibrary) Logger.log(message: "File is added to album") } /// Add the video to the app's album roll - func add(video path: URL, to album: PHAssetCollection, _ photoLibrary: PHPhotoLibrary) async throws -> Void { - guard isVideo(fileUrl: path) else { + func add(video path: URL, to album: U, _ photoLibrary: T) async throws -> Void { + guard album.canAdd(path) else { throw AespaError.album(reason: .notVideoURL) } - + return try await photoLibrary.performChanges { guard let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path), let placeholder = assetChangeRequest.placeholderForCreatedAsset, - let albumChangeRequest = PHAssetCollectionChangeRequest(for: album) + let albumChangeRequest = PHAssetCollectionChangeRequest(for: album.underlyingAssetCollection) else { Logger.log(error: AespaError.album(reason: .unabledToAccess)) return } - + let enumeration = NSArray(object: placeholder) albumChangeRequest.addAssets(enumeration) } } } - -private extension AssetAdditionProcessor { - func isVideo(fileUrl: URL) -> Bool { - let asset = AVAsset(url: fileUrl) - let tracks = asset.tracks(withMediaType: AVMediaType.video) - - return !tracks.isEmpty - } -} diff --git a/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift b/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift index b774c84..3e17b39 100644 --- a/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift +++ b/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift @@ -5,11 +5,11 @@ // Created by 이영빈 on 2023/06/02. // -import Combine + import AVFoundation struct FinishRecordProcessor: AespaFileOutputProcessing { - func process(_ output: AVCaptureFileOutput) throws { + func process(_ output: T) throws { output.stopRecording() } } diff --git a/Sources/Aespa/Processor/Record/StartRecordProcessor.swift b/Sources/Aespa/Processor/Record/StartRecordProcessor.swift index 9658f7a..1ad9657 100644 --- a/Sources/Aespa/Processor/Record/StartRecordProcessor.swift +++ b/Sources/Aespa/Processor/Record/StartRecordProcessor.swift @@ -12,10 +12,8 @@ struct StartRecordProcessor: AespaFileOutputProcessing { let filePath: URL let delegate: AVCaptureFileOutputRecordingDelegate - func process(_ output: AVCaptureFileOutput) throws { - guard - output.connection(with: .video) != nil - else { + func process(_ output: T) throws { + guard output.getConnection(with: .video) != nil else { throw AespaError.session(reason: .cannotFindConnection) } diff --git a/Sources/Aespa/Tuner/AespaTuning.swift b/Sources/Aespa/Tuner/AespaTuning.swift index be083ac..5da761e 100644 --- a/Sources/Aespa/Tuner/AespaTuning.swift +++ b/Sources/Aespa/Tuner/AespaTuning.swift @@ -20,9 +20,9 @@ public extension AespaSessionTuning { var needTransaction: Bool { true } } - +/// AespaConnectionTuning protocol AespaConnectionTuning { - func tune(_ connection: AVCaptureConnection) throws + func tune(_ connection: T) throws } @@ -30,5 +30,5 @@ protocol AespaConnectionTuning { /// Instead, use `needLock` flag protocol AespaDeviceTuning { var needLock: Bool { get } - func tune(_ device: AVCaptureDevice) throws + func tune(_ device: T) throws } diff --git a/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift b/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift index 9ac3a90..957fc8f 100644 --- a/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift +++ b/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift @@ -10,7 +10,7 @@ import AVFoundation struct VideoOrientationTuner: AespaConnectionTuning { var orientation: AVCaptureVideoOrientation - func tune(_ connection: AVCaptureConnection) { - connection.videoOrientation = orientation + func tune(_ connection: T) throws { + connection.setOrientation(to: orientation) } } diff --git a/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift b/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift index 2d464d3..c1b0ad4 100644 --- a/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift +++ b/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift @@ -10,7 +10,7 @@ import AVFoundation struct VideoStabilizationTuner: AespaConnectionTuning { var stabilzationMode: AVCaptureVideoStabilizationMode - func tune(_ connection: AVCaptureConnection) { + func tune(_ connection: T) { connection.setStabilizationMode(to: stabilzationMode) } } diff --git a/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift b/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift index a1333c5..91bbf9a 100644 --- a/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift +++ b/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift @@ -12,11 +12,11 @@ struct AutoFocusTuner: AespaDeviceTuning { let needLock = true let mode: AVCaptureDevice.FocusMode - func tune(_ device: AVCaptureDevice) throws { + func tune(_ device: T) throws { guard device.isFocusModeSupported(mode) else { throw AespaError.device(reason: .unsupported) } - device.focusMode = mode + try device.setFocusMode(mode) } } diff --git a/Sources/Aespa/Tuner/Device/ZoomTuner.swift b/Sources/Aespa/Tuner/Device/ZoomTuner.swift index 5b20077..806c898 100644 --- a/Sources/Aespa/Tuner/Device/ZoomTuner.swift +++ b/Sources/Aespa/Tuner/Device/ZoomTuner.swift @@ -11,7 +11,7 @@ struct ZoomTuner: AespaDeviceTuning { var needLock = true var zoomFactor: CGFloat - func tune(_ device: AVCaptureDevice) { - device.setZoomFactor(factor: zoomFactor) + func tune(_ device: T) { + device.setZoomFactor(zoomFactor) } } diff --git a/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift b/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift index f45aade..fa8a103 100644 --- a/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift +++ b/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift @@ -7,16 +7,6 @@ import AVFoundation -extension AVCaptureConnection { - func setOrientation(to orientation: AVCaptureVideoOrientation) { - self.videoOrientation = orientation - } - - func setStabilizationMode(to mode: AVCaptureVideoStabilizationMode) { - self.preferredVideoStabilizationMode = mode - } -} - extension AVCaptureDevice.Position { var chooseBestCamera: AVCaptureDevice? { let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInTripleCamera, .builtInWideAngleCamera], @@ -36,32 +26,3 @@ extension AVCaptureDevice.Position { return sortedDevices.first } } - -fileprivate extension AVCaptureDevice { - var maxResolution: Double? { - var maxResolution: Double = 0 - for format in self.formats { - let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription) - let resolution = Double(dimensions.width * dimensions.height) - maxResolution = max(resolution, maxResolution) - } - return maxResolution - } -} - - -extension AVCaptureDevice { - func setZoomFactor(factor: CGFloat) { - let device = self - - do { - try device.lockForConfiguration() - - device.videoZoomFactor = factor - - device.unlockForConfiguration() - } catch { - device.unlockForConfiguration() - } - } -} diff --git a/Sources/Aespa/Util/Video/Album/AlbumImporter.swift b/Sources/Aespa/Util/Video/Album/AlbumImporter.swift index 94c4c20..8976ffd 100644 --- a/Sources/Aespa/Util/Video/Album/AlbumImporter.swift +++ b/Sources/Aespa/Util/Video/Album/AlbumImporter.swift @@ -11,23 +11,28 @@ import Photos import UIKit struct AlbumImporter { - static func getAlbum(name: String, in photoLibrary: PHPhotoLibrary) throws -> PHAssetCollection { - let fetchOptions = PHFetchOptions() - fetchOptions.predicate = NSPredicate(format: "title = %@", name) - - let collection = PHAssetCollection.fetchAssetCollections( - with: .album, subtype: .any, options: fetchOptions - ) + static func getAlbum( + name: String, + in photoLibrary: Library, + retry: Bool = true, + _ fetchOptions: PHFetchOptions = .init() + ) throws -> Collection { + let album: Collection? = photoLibrary.fetchAlbum(title: name, fetchOptions: fetchOptions) - if let album = collection.firstObject { + if let album { return album - } else { + } else if retry { try createAlbum(name: name, in: photoLibrary) - return try getAlbum(name: name, in: photoLibrary) + return try getAlbum(name: name, in: photoLibrary, retry: false, fetchOptions) + } else { + throw AespaError.album(reason: .unabledToAccess) } } - static private func createAlbum(name: String, in photoLibrary: PHPhotoLibrary) throws { + static private func createAlbum( + name: String, + in photoLibrary: Library + ) throws { try photoLibrary.performChangesAndWait { PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name) } diff --git a/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift b/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift index d45b0b5..b7f7929 100644 --- a/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift +++ b/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift @@ -9,7 +9,6 @@ import UIKit import AVFoundation struct VideoFileGenerator { - static func generate(with path: URL, date: Date) -> VideoFile { return VideoFile( generatedDate: date, @@ -19,6 +18,7 @@ struct VideoFileGenerator { static func generateThumbnail(for path: URL) -> UIImage? { let asset = AVURLAsset(url: path, options: nil) + let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true imageGenerator.maximumSize = .init(width: 250, height: 250) diff --git a/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift b/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift index decf3f4..5e45988 100644 --- a/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift +++ b/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift @@ -8,11 +8,16 @@ import UIKit struct VideoFilePathProvider { - static func requestFilePath(from fileManager: FileManager, directoryName: String, fileName: String) throws -> URL { + static func requestFilePath( + from fileManager: FileManager, + directoryName: String, + fileName: String, + extension: String + ) throws -> URL { let directoryPath = try requestDirectoryPath(from: fileManager, name: directoryName) let filePath = directoryPath .appendingPathComponent(fileName) - .appendingPathExtension("mp4") + .appendingPathExtension(`extension`) return filePath } diff --git a/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj b/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj index ba7a1dd..5a54d85 100644 --- a/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj +++ b/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj @@ -14,7 +14,16 @@ 07F359B82A347CA400F4EF16 /* Cuckoo in Frameworks */ = {isa = PBXBuildFile; productRef = 07F359B72A347CA400F4EF16 /* Cuckoo */; }; 07F359BA2A347DF600F4EF16 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F359B92A347DF600F4EF16 /* GeneratedMocks.swift */; }; 07F359BE2A3489C000F4EF16 /* SessionTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F359BD2A3489C000F4EF16 /* SessionTunerTests.swift */; }; - 9CA7CFF82A380673000B11B3 /* Aespa in Frameworks */ = {isa = PBXBuildFile; productRef = 9CA7CFF72A380673000B11B3 /* Aespa */; }; + 07FBEE722A3DA67E003CC5FD /* Aespa in Frameworks */ = {isa = PBXBuildFile; productRef = 07FBEE712A3DA67E003CC5FD /* Aespa */; }; + 07FBEE762A3DD55C003CC5FD /* AlbumUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE752A3DD55C003CC5FD /* AlbumUtilTests.swift */; }; + 07FBEE782A3E7500003CC5FD /* FileUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE772A3E7500003CC5FD /* FileUtilTests.swift */; }; + 07FBEE7A2A3E77B5003CC5FD /* video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 07FBEE792A3E77B5003CC5FD /* video.mp4 */; }; + 07FBEE7D2A3E77D8003CC5FD /* MockVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE7C2A3E77D8003CC5FD /* MockVideo.swift */; }; + 07FBEE822A3E7F95003CC5FD /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE812A3E7F95003CC5FD /* MockFileManager.swift */; }; + 9B24721A2A3C212500D82A2E /* DeviceTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2472192A3C212500D82A2E /* DeviceTunerTests.swift */; }; + 9B24721C2A3C310D00D82A2E /* ConnectionTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B24721B2A3C310D00D82A2E /* ConnectionTunerTests.swift */; }; + 9B4975BD2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4975BC2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift */; }; + 9B4975BF2A3CA0BF0068FA35 /* AssetProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4975BE2A3CA0BF0068FA35 /* AssetProcessorTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -25,13 +34,6 @@ remoteGlobalIDString = 07939F8C2A343B8D00DFA8BB; remoteInfo = "Aespa-iOS-test"; }; - 07939FA82A343B8E00DFA8BB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 07939F852A343B8D00DFA8BB /* Project object */; - proxyType = 1; - remoteGlobalIDString = 07939F8C2A343B8D00DFA8BB; - remoteInfo = "Aespa-iOS-test"; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -41,10 +43,19 @@ 07939F942A343B8E00DFA8BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 07939F972A343B8E00DFA8BB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 07939F9D2A343B8E00DFA8BB /* Aespa-iOS-testTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Aespa-iOS-testTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 07939FA72A343B8E00DFA8BB /* Aespa-iOS-testUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Aespa-iOS-testUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 07F359B92A347DF600F4EF16 /* GeneratedMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; 07F359BD2A3489C000F4EF16 /* SessionTunerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTunerTests.swift; sourceTree = ""; }; - 9CA7CFFA2A38069C000B11B3 /* Aespa */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Aespa; path = ..; sourceTree = ""; }; + 07FBEE742A3DA69A003CC5FD /* Aespa */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Aespa; path = ..; sourceTree = ""; }; + 07FBEE752A3DD55C003CC5FD /* AlbumUtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumUtilTests.swift; sourceTree = ""; }; + 07FBEE772A3E7500003CC5FD /* FileUtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtilTests.swift; sourceTree = ""; }; + 07FBEE792A3E77B5003CC5FD /* video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = video.mp4; sourceTree = ""; }; + 07FBEE7C2A3E77D8003CC5FD /* MockVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVideo.swift; sourceTree = ""; }; + 07FBEE812A3E7F95003CC5FD /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; + 9B2472192A3C212500D82A2E /* DeviceTunerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTunerTests.swift; sourceTree = ""; }; + 9B24721B2A3C310D00D82A2E /* ConnectionTunerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTunerTests.swift; sourceTree = ""; }; + 9B24721E2A3C366100D82A2E /* Aespa-iOS-testTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Aespa-iOS-testTests.xctestplan"; sourceTree = ""; }; + 9B4975BC2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOutputProcessorTests.swift; sourceTree = ""; }; + 9B4975BE2A3CA0BF0068FA35 /* AssetProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetProcessorTests.swift; sourceTree = ""; }; 9CA7CFFB2A380754000B11B3 /* Aespa-iOS-test.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Aespa-iOS-test.xctestplan"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -54,7 +65,7 @@ buildActionMask = 2147483647; files = ( 07F359B82A347CA400F4EF16 /* Cuckoo in Frameworks */, - 9CA7CFF82A380673000B11B3 /* Aespa in Frameworks */, + 07FBEE722A3DA67E003CC5FD /* Aespa in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,21 +76,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 07939FA42A343B8E00DFA8BB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 07939F842A343B8D00DFA8BB = { isa = PBXGroup; children = ( + 07FBEE732A3DA69A003CC5FD /* Packages */, + 9B24721E2A3C366100D82A2E /* Aespa-iOS-testTests.xctestplan */, 9CA7CFFB2A380754000B11B3 /* Aespa-iOS-test.xctestplan */, - 9CA7CFF92A38069C000B11B3 /* Packages */, 07939F8F2A343B8D00DFA8BB /* Aespa-iOS-test */, 07939FA02A343B8E00DFA8BB /* Aespa-iOS-testTests */, 07939F8E2A343B8D00DFA8BB /* Products */, @@ -92,7 +97,6 @@ children = ( 07939F8D2A343B8D00DFA8BB /* Aespa-iOS-test.app */, 07939F9D2A343B8E00DFA8BB /* Aespa-iOS-testTests.xctest */, - 07939FA72A343B8E00DFA8BB /* Aespa-iOS-testUITests.xctest */, ); name = Products; sourceTree = ""; @@ -119,6 +123,8 @@ 07939FA02A343B8E00DFA8BB /* Aespa-iOS-testTests */ = { isa = PBXGroup; children = ( + 07FBEE6E2A3D9C02003CC5FD /* Util */, + 9B4975BB2A3C8BA50068FA35 /* Processor */, 0795E11F2A35A2FC001AD4DC /* Tuner */, 07F359AA2A3443EC00F4EF16 /* Mock */, ); @@ -129,6 +135,8 @@ isa = PBXGroup; children = ( 07F359BD2A3489C000F4EF16 /* SessionTunerTests.swift */, + 9B2472192A3C212500D82A2E /* DeviceTunerTests.swift */, + 9B24721B2A3C310D00D82A2E /* ConnectionTunerTests.swift */, ); path = Tuner; sourceTree = ""; @@ -136,7 +144,9 @@ 07F359AA2A3443EC00F4EF16 /* Mock */ = { isa = PBXGroup; children = ( + 07FBEE7B2A3E77CF003CC5FD /* Video */, 07F359B92A347DF600F4EF16 /* GeneratedMocks.swift */, + 07FBEE812A3E7F95003CC5FD /* MockFileManager.swift */, ); path = Mock; sourceTree = ""; @@ -148,14 +158,41 @@ name = Frameworks; sourceTree = ""; }; - 9CA7CFF92A38069C000B11B3 /* Packages */ = { + 07FBEE6E2A3D9C02003CC5FD /* Util */ = { isa = PBXGroup; children = ( - 9CA7CFFA2A38069C000B11B3 /* Aespa */, + 07FBEE752A3DD55C003CC5FD /* AlbumUtilTests.swift */, + 07FBEE772A3E7500003CC5FD /* FileUtilTests.swift */, + ); + path = Util; + sourceTree = ""; + }; + 07FBEE732A3DA69A003CC5FD /* Packages */ = { + isa = PBXGroup; + children = ( + 07FBEE742A3DA69A003CC5FD /* Aespa */, ); name = Packages; sourceTree = ""; }; + 07FBEE7B2A3E77CF003CC5FD /* Video */ = { + isa = PBXGroup; + children = ( + 07FBEE792A3E77B5003CC5FD /* video.mp4 */, + 07FBEE7C2A3E77D8003CC5FD /* MockVideo.swift */, + ); + path = Video; + sourceTree = ""; + }; + 9B4975BB2A3C8BA50068FA35 /* Processor */ = { + isa = PBXGroup; + children = ( + 9B4975BC2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift */, + 9B4975BE2A3CA0BF0068FA35 /* AssetProcessorTests.swift */, + ); + path = Processor; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -174,7 +211,7 @@ name = "Aespa-iOS-test"; packageProductDependencies = ( 07F359B72A347CA400F4EF16 /* Cuckoo */, - 9CA7CFF72A380673000B11B3 /* Aespa */, + 07FBEE712A3DA67E003CC5FD /* Aespa */, ); productName = "Aespa-iOS-test"; productReference = 07939F8D2A343B8D00DFA8BB /* Aespa-iOS-test.app */; @@ -198,24 +235,6 @@ productReference = 07939F9D2A343B8E00DFA8BB /* Aespa-iOS-testTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 07939FA62A343B8E00DFA8BB /* Aespa-iOS-testUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 07939FB72A343B8E00DFA8BB /* Build configuration list for PBXNativeTarget "Aespa-iOS-testUITests" */; - buildPhases = ( - 07939FA32A343B8E00DFA8BB /* Sources */, - 07939FA42A343B8E00DFA8BB /* Frameworks */, - 07939FA52A343B8E00DFA8BB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 07939FA92A343B8E00DFA8BB /* PBXTargetDependency */, - ); - name = "Aespa-iOS-testUITests"; - productName = "Aespa-iOS-testUITests"; - productReference = 07939FA72A343B8E00DFA8BB /* Aespa-iOS-testUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -233,10 +252,6 @@ CreatedOnToolsVersion = 14.3; TestTargetID = 07939F8C2A343B8D00DFA8BB; }; - 07939FA62A343B8E00DFA8BB = { - CreatedOnToolsVersion = 14.3; - TestTargetID = 07939F8C2A343B8D00DFA8BB; - }; }; }; buildConfigurationList = 07939F882A343B8D00DFA8BB /* Build configuration list for PBXProject "Aespa-iOS-test" */; @@ -257,7 +272,6 @@ targets = ( 07939F8C2A343B8D00DFA8BB /* Aespa-iOS-test */, 07939F9C2A343B8E00DFA8BB /* Aespa-iOS-testTests */, - 07939FA62A343B8E00DFA8BB /* Aespa-iOS-testUITests */, ); }; /* End PBXProject section */ @@ -276,13 +290,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 07939FA52A343B8E00DFA8BB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 07FBEE7A2A3E77B5003CC5FD /* video.mp4 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -302,15 +310,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9B24721A2A3C212500D82A2E /* DeviceTunerTests.swift in Sources */, + 9B4975BF2A3CA0BF0068FA35 /* AssetProcessorTests.swift in Sources */, 07F359BE2A3489C000F4EF16 /* SessionTunerTests.swift in Sources */, + 9B4975BD2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift in Sources */, + 07FBEE762A3DD55C003CC5FD /* AlbumUtilTests.swift in Sources */, + 07FBEE782A3E7500003CC5FD /* FileUtilTests.swift in Sources */, + 9B24721C2A3C310D00D82A2E /* ConnectionTunerTests.swift in Sources */, + 07FBEE7D2A3E77D8003CC5FD /* MockVideo.swift in Sources */, 07F359BA2A347DF600F4EF16 /* GeneratedMocks.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 07939FA32A343B8E00DFA8BB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 07FBEE822A3E7F95003CC5FD /* MockFileManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -322,11 +331,6 @@ target = 07939F8C2A343B8D00DFA8BB /* Aespa-iOS-test */; targetProxy = 07939F9E2A343B8E00DFA8BB /* PBXContainerItemProxy */; }; - 07939FA92A343B8E00DFA8BB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 07939F8C2A343B8D00DFA8BB /* Aespa-iOS-test */; - targetProxy = 07939FA82A343B8E00DFA8BB /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -380,7 +384,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -434,7 +438,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -519,7 +523,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = W6QHM4Y43Z; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-testTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -539,7 +543,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = W6QHM4Y43Z; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-testTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -550,42 +554,6 @@ }; name = Release; }; - 07939FB82A343B8E00DFA8BB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = W6QHM4Y43Z; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-testUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "Aespa-iOS-test"; - }; - name = Debug; - }; - 07939FB92A343B8E00DFA8BB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = W6QHM4Y43Z; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-testUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "Aespa-iOS-test"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -616,15 +584,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 07939FB72A343B8E00DFA8BB /* Build configuration list for PBXNativeTarget "Aespa-iOS-testUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 07939FB82A343B8E00DFA8BB /* Debug */, - 07939FB92A343B8E00DFA8BB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -644,7 +603,7 @@ package = 07F359B62A347CA400F4EF16 /* XCRemoteSwiftPackageReference "Cuckoo" */; productName = Cuckoo; }; - 9CA7CFF72A380673000B11B3 /* Aespa */ = { + 07FBEE712A3DA67E003CC5FD /* Aespa */ = { isa = XCSwiftPackageProductDependency; productName = Aespa; }; diff --git a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme b/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme index 11f5666..c13cc3a 100644 --- a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme +++ b/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme @@ -1,32 +1,23 @@ + version = "1.7"> - - - - + buildForAnalyzing = "YES"> + BlueprintIdentifier = "Aespa" + BuildableName = "Aespa" + BlueprintName = "Aespa" + ReferencedContainer = "container:.."> @@ -35,8 +26,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Aespa-iOS-test.xctestplan b/Tests/Aespa-iOS-test.xctestplan index d0e01fa..421fb11 100644 --- a/Tests/Aespa-iOS-test.xctestplan +++ b/Tests/Aespa-iOS-test.xctestplan @@ -9,7 +9,15 @@ } ], "defaultOptions" : { - + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:..", + "identifier" : "Aespa", + "name" : "Aespa" + } + ] + } }, "testTargets" : [ { diff --git a/Tests/Aespa-iOS-testTests.xctestplan b/Tests/Aespa-iOS-testTests.xctestplan new file mode 100644 index 0000000..4ffc41e --- /dev/null +++ b/Tests/Aespa-iOS-testTests.xctestplan @@ -0,0 +1,33 @@ +{ + "configurations" : [ + { + "id" : "F430D1B4-52B5-4C85-8BEB-263A506AFE77", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:..", + "identifier" : "Aespa", + "name" : "Aespa" + } + ] + } + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:Aespa-iOS-test.xcodeproj", + "identifier" : "07939F9C2A343B8E00DFA8BB", + "name" : "Aespa-iOS-testTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/Aespa-iOS-testTests/Mock/.gitkeep b/Tests/Aespa-iOS-testTests/Mock/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Aespa-iOS-testTests/Mock/GeneratedMocks.swift b/Tests/Aespa-iOS-testTests/Mock/GeneratedMocks.swift deleted file mode 100644 index 39cdd99..0000000 --- a/Tests/Aespa-iOS-testTests/Mock/GeneratedMocks.swift +++ /dev/null @@ -1,4207 +0,0 @@ -// MARK: - Mocks generated from file: ../../Sources/Aespa/Aespa.swift at 2023-06-13 08:25:34 +0000 - -// -// Aespa.swift -// -// -// Created by 이영빈 on 2023/06/02. -// - -/// Top-level class that serves as the main access point for video recording sessions. - -import Cuckoo -@testable import Aespa - - - - - - -public class MockAespa: Aespa, Cuckoo.ClassMock { - - public typealias MocksType = Aespa - - public typealias Stubbing = __StubbingProxy_Aespa - public typealias Verification = __VerificationProxy_Aespa - - public let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true) - - - private var __defaultImplStub: Aespa? - - public func enableDefaultImplementation(_ stub: Aespa) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - public struct __StubbingProxy_Aespa: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - public init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - } - - public struct __VerificationProxy_Aespa: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - public init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - } -} - - -public class AespaStub: Aespa { - - - - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/AespaError.swift at 2023-06-13 08:25:34 +0000 - -// -// VideoRecorderError.swift -// YellowIsTheNewBlack -// -// Created by 이영빈 on 2022/06/07 -import Cuckoo -@testable import Aespa - -import Foundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/AespaOption.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaOption.swift -// -// -// Created by 이영빈 on 2023/05/26 -import Cuckoo -@testable import Aespa - -import AVFoundation -import Foundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/AespaSession.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaSession.swift -// -// -// Created by Young Bin on 2023/06/03. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import Combine -import Foundation -import UIKit - - - - - - -public class MockAespaSession: AespaSession, Cuckoo.ClassMock { - - public typealias MocksType = AespaSession - - public typealias Stubbing = __StubbingProxy_AespaSession - public typealias Verification = __VerificationProxy_AespaSession - - public let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true) - - - private var __defaultImplStub: AespaSession? - - public func enableDefaultImplementation(_ stub: AespaSession) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - public override var captureSession: AVCaptureSession { - get { - return cuckoo_manager.getter("captureSession", - superclassCall: - - super.captureSession - , - defaultCall: __defaultImplStub!.captureSession) - } - - } - - - - - - public override var isMuted: Bool { - get { - return cuckoo_manager.getter("isMuted", - superclassCall: - - super.isMuted - , - defaultCall: __defaultImplStub!.isMuted) - } - - } - - - - - - public override var maxZoomFactor: CGFloat? { - get { - return cuckoo_manager.getter("maxZoomFactor", - superclassCall: - - super.maxZoomFactor - , - defaultCall: __defaultImplStub!.maxZoomFactor) - } - - } - - - - - - public override var currentZoomFactor: CGFloat? { - get { - return cuckoo_manager.getter("currentZoomFactor", - superclassCall: - - super.currentZoomFactor - , - defaultCall: __defaultImplStub!.currentZoomFactor) - } - - } - - - - - - public override var videoFilePublisher: AnyPublisher, Never> { - get { - return cuckoo_manager.getter("videoFilePublisher", - superclassCall: - - super.videoFilePublisher - , - defaultCall: __defaultImplStub!.videoFilePublisher) - } - - } - - - - - - public override var previewLayerPublisher: AnyPublisher { - get { - return cuckoo_manager.getter("previewLayerPublisher", - superclassCall: - - super.previewLayerPublisher - , - defaultCall: __defaultImplStub!.previewLayerPublisher) - } - - } - - - - - - - - - - public override func startRecording() { - - return cuckoo_manager.call( - """ - startRecording() - """, - parameters: (), - escapingParameters: (), - superclassCall: - - super.startRecording() - , - defaultCall: __defaultImplStub!.startRecording()) - - } - - - - - - public override func stopRecording(_ completionHandler: @escaping (Result) -> Void) { - - return cuckoo_manager.call( - """ - stopRecording(_: @escaping (Result) -> Void) - """, - parameters: (completionHandler), - escapingParameters: (completionHandler), - superclassCall: - - super.stopRecording(completionHandler) - , - defaultCall: __defaultImplStub!.stopRecording(completionHandler)) - - } - - - - - - public override func mute() -> AespaSession { - - return cuckoo_manager.call( - """ - mute() -> AespaSession - """, - parameters: (), - escapingParameters: (), - superclassCall: - - super.mute() - , - defaultCall: __defaultImplStub!.mute()) - - } - - - - - - public override func unmute() -> AespaSession { - - return cuckoo_manager.call( - """ - unmute() -> AespaSession - """, - parameters: (), - escapingParameters: (), - superclassCall: - - super.unmute() - , - defaultCall: __defaultImplStub!.unmute()) - - } - - - - - - public override func setQuality(to preset: AVCaptureSession.Preset) -> AespaSession { - - return cuckoo_manager.call( - """ - setQuality(to: AVCaptureSession.Preset) -> AespaSession - """, - parameters: (preset), - escapingParameters: (preset), - superclassCall: - - super.setQuality(to: preset) - , - defaultCall: __defaultImplStub!.setQuality(to: preset)) - - } - - - - - - public override func setPosition(to position: AVCaptureDevice.Position) -> AespaSession { - - return cuckoo_manager.call( - """ - setPosition(to: AVCaptureDevice.Position) -> AespaSession - """, - parameters: (position), - escapingParameters: (position), - superclassCall: - - super.setPosition(to: position) - , - defaultCall: __defaultImplStub!.setPosition(to: position)) - - } - - - - - - public override func setOrientation(to orientation: AVCaptureVideoOrientation) -> AespaSession { - - return cuckoo_manager.call( - """ - setOrientation(to: AVCaptureVideoOrientation) -> AespaSession - """, - parameters: (orientation), - escapingParameters: (orientation), - superclassCall: - - super.setOrientation(to: orientation) - , - defaultCall: __defaultImplStub!.setOrientation(to: orientation)) - - } - - - - - - public override func setStabilization(mode: AVCaptureVideoStabilizationMode) -> AespaSession { - - return cuckoo_manager.call( - """ - setStabilization(mode: AVCaptureVideoStabilizationMode) -> AespaSession - """, - parameters: (mode), - escapingParameters: (mode), - superclassCall: - - super.setStabilization(mode: mode) - , - defaultCall: __defaultImplStub!.setStabilization(mode: mode)) - - } - - - - - - public override func setAutofocusing(mode: AVCaptureDevice.FocusMode) -> AespaSession { - - return cuckoo_manager.call( - """ - setAutofocusing(mode: AVCaptureDevice.FocusMode) -> AespaSession - """, - parameters: (mode), - escapingParameters: (mode), - superclassCall: - - super.setAutofocusing(mode: mode) - , - defaultCall: __defaultImplStub!.setAutofocusing(mode: mode)) - - } - - - - - - public override func zoom(factor: CGFloat) -> AespaSession { - - return cuckoo_manager.call( - """ - zoom(factor: CGFloat) -> AespaSession - """, - parameters: (factor), - escapingParameters: (factor), - superclassCall: - - super.zoom(factor: factor) - , - defaultCall: __defaultImplStub!.zoom(factor: factor)) - - } - - - - - - public override func startRecordingWithError() throws { - - return try cuckoo_manager.callThrows( - """ - startRecordingWithError() throws - """, - parameters: (), - escapingParameters: (), - superclassCall: - - super.startRecordingWithError() - , - defaultCall: __defaultImplStub!.startRecordingWithError()) - - } - - - - - - public override func stopRecording() async throws -> VideoFile { - - return try await cuckoo_manager.callThrows( - """ - stopRecording() async throws -> VideoFile - """, - parameters: (), - escapingParameters: (), - superclassCall: - - await super.stopRecording() - , - defaultCall: await __defaultImplStub!.stopRecording()) - - } - - - - - - public override func muteWithError() throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - muteWithError() throws -> AespaSession - """, - parameters: (), - escapingParameters: (), - superclassCall: - - super.muteWithError() - , - defaultCall: __defaultImplStub!.muteWithError()) - - } - - - - - - public override func unmuteWithError() throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - unmuteWithError() throws -> AespaSession - """, - parameters: (), - escapingParameters: (), - superclassCall: - - super.unmuteWithError() - , - defaultCall: __defaultImplStub!.unmuteWithError()) - - } - - - - - - public override func setQualityWithError(to preset: AVCaptureSession.Preset) throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - setQualityWithError(to: AVCaptureSession.Preset) throws -> AespaSession - """, - parameters: (preset), - escapingParameters: (preset), - superclassCall: - - super.setQualityWithError(to: preset) - , - defaultCall: __defaultImplStub!.setQualityWithError(to: preset)) - - } - - - - - - public override func setPositionWithError(to position: AVCaptureDevice.Position) throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - setPositionWithError(to: AVCaptureDevice.Position) throws -> AespaSession - """, - parameters: (position), - escapingParameters: (position), - superclassCall: - - super.setPositionWithError(to: position) - , - defaultCall: __defaultImplStub!.setPositionWithError(to: position)) - - } - - - - - - public override func setOrientationWithError(to orientation: AVCaptureVideoOrientation) throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - setOrientationWithError(to: AVCaptureVideoOrientation) throws -> AespaSession - """, - parameters: (orientation), - escapingParameters: (orientation), - superclassCall: - - super.setOrientationWithError(to: orientation) - , - defaultCall: __defaultImplStub!.setOrientationWithError(to: orientation)) - - } - - - - - - public override func setStabilizationWithError(mode: AVCaptureVideoStabilizationMode) throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - setStabilizationWithError(mode: AVCaptureVideoStabilizationMode) throws -> AespaSession - """, - parameters: (mode), - escapingParameters: (mode), - superclassCall: - - super.setStabilizationWithError(mode: mode) - , - defaultCall: __defaultImplStub!.setStabilizationWithError(mode: mode)) - - } - - - - - - public override func setAutofocusingWithError(mode: AVCaptureDevice.FocusMode) throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - setAutofocusingWithError(mode: AVCaptureDevice.FocusMode) throws -> AespaSession - """, - parameters: (mode), - escapingParameters: (mode), - superclassCall: - - super.setAutofocusingWithError(mode: mode) - , - defaultCall: __defaultImplStub!.setAutofocusingWithError(mode: mode)) - - } - - - - - - public override func zoomWithError(factor: CGFloat) throws -> AespaSession { - - return try cuckoo_manager.callThrows( - """ - zoomWithError(factor: CGFloat) throws -> AespaSession - """, - parameters: (factor), - escapingParameters: (factor), - superclassCall: - - super.zoomWithError(factor: factor) - , - defaultCall: __defaultImplStub!.zoomWithError(factor: factor)) - - } - - - - - - public override func customize(_ tuner: T) throws { - - return try cuckoo_manager.callThrows( - """ - customize(_: T) throws - """, - parameters: (tuner), - escapingParameters: (tuner), - superclassCall: - - super.customize(tuner) - , - defaultCall: __defaultImplStub!.customize(tuner)) - - } - - - - - - public override func fetchVideoFiles(limit: Int) -> [VideoFile] { - - return cuckoo_manager.call( - """ - fetchVideoFiles(limit: Int) -> [VideoFile] - """, - parameters: (limit), - escapingParameters: (limit), - superclassCall: - - super.fetchVideoFiles(limit: limit) - , - defaultCall: __defaultImplStub!.fetchVideoFiles(limit: limit)) - - } - - - - - - public override func doctor() async throws { - - return try await cuckoo_manager.callThrows( - """ - doctor() async throws - """, - parameters: (), - escapingParameters: (), - superclassCall: - - await super.doctor() - , - defaultCall: await __defaultImplStub!.doctor()) - - } - - - - public struct __StubbingProxy_AespaSession: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - public init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - var captureSession: Cuckoo.ClassToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "captureSession") - } - - - - - var isMuted: Cuckoo.ClassToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "isMuted") - } - - - - - var maxZoomFactor: Cuckoo.ClassToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "maxZoomFactor") - } - - - - - var currentZoomFactor: Cuckoo.ClassToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "currentZoomFactor") - } - - - - - var videoFilePublisher: Cuckoo.ClassToBeStubbedReadOnlyProperty, Never>> { - return .init(manager: cuckoo_manager, name: "videoFilePublisher") - } - - - - - var previewLayerPublisher: Cuckoo.ClassToBeStubbedReadOnlyProperty> { - return .init(manager: cuckoo_manager, name: "previewLayerPublisher") - } - - - - - - func startRecording() -> Cuckoo.ClassStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - startRecording() - """, parameterMatchers: matchers)) - } - - - - - func stopRecording(_ completionHandler: M1) -> Cuckoo.ClassStubNoReturnFunction<((Result) -> Void)> where M1.MatchedType == (Result) -> Void { - let matchers: [Cuckoo.ParameterMatcher<((Result) -> Void)>] = [wrap(matchable: completionHandler) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - stopRecording(_: @escaping (Result) -> Void) - """, parameterMatchers: matchers)) - } - - - - - func mute() -> Cuckoo.ClassStubFunction<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - mute() -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func unmute() -> Cuckoo.ClassStubFunction<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - unmute() -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setQuality(to preset: M1) -> Cuckoo.ClassStubFunction<(AVCaptureSession.Preset), AespaSession> where M1.MatchedType == AVCaptureSession.Preset { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureSession.Preset)>] = [wrap(matchable: preset) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setQuality(to: AVCaptureSession.Preset) -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setPosition(to position: M1) -> Cuckoo.ClassStubFunction<(AVCaptureDevice.Position), AespaSession> where M1.MatchedType == AVCaptureDevice.Position { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.Position)>] = [wrap(matchable: position) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setPosition(to: AVCaptureDevice.Position) -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setOrientation(to orientation: M1) -> Cuckoo.ClassStubFunction<(AVCaptureVideoOrientation), AespaSession> where M1.MatchedType == AVCaptureVideoOrientation { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoOrientation)>] = [wrap(matchable: orientation) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setOrientation(to: AVCaptureVideoOrientation) -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setStabilization(mode: M1) -> Cuckoo.ClassStubFunction<(AVCaptureVideoStabilizationMode), AespaSession> where M1.MatchedType == AVCaptureVideoStabilizationMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoStabilizationMode)>] = [wrap(matchable: mode) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setStabilization(mode: AVCaptureVideoStabilizationMode) -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setAutofocusing(mode: M1) -> Cuckoo.ClassStubFunction<(AVCaptureDevice.FocusMode), AespaSession> where M1.MatchedType == AVCaptureDevice.FocusMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.FocusMode)>] = [wrap(matchable: mode) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setAutofocusing(mode: AVCaptureDevice.FocusMode) -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func zoom(factor: M1) -> Cuckoo.ClassStubFunction<(CGFloat), AespaSession> where M1.MatchedType == CGFloat { - let matchers: [Cuckoo.ParameterMatcher<(CGFloat)>] = [wrap(matchable: factor) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - zoom(factor: CGFloat) -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func startRecordingWithError() -> Cuckoo.ClassStubNoReturnThrowingFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - startRecordingWithError() throws - """, parameterMatchers: matchers)) - } - - - - - func stopRecording() -> Cuckoo.ClassStubThrowingFunction<(), VideoFile> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - stopRecording() async throws -> VideoFile - """, parameterMatchers: matchers)) - } - - - - - func muteWithError() -> Cuckoo.ClassStubThrowingFunction<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - muteWithError() throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func unmuteWithError() -> Cuckoo.ClassStubThrowingFunction<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - unmuteWithError() throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setQualityWithError(to preset: M1) -> Cuckoo.ClassStubThrowingFunction<(AVCaptureSession.Preset), AespaSession> where M1.MatchedType == AVCaptureSession.Preset { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureSession.Preset)>] = [wrap(matchable: preset) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setQualityWithError(to: AVCaptureSession.Preset) throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setPositionWithError(to position: M1) -> Cuckoo.ClassStubThrowingFunction<(AVCaptureDevice.Position), AespaSession> where M1.MatchedType == AVCaptureDevice.Position { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.Position)>] = [wrap(matchable: position) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setPositionWithError(to: AVCaptureDevice.Position) throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setOrientationWithError(to orientation: M1) -> Cuckoo.ClassStubThrowingFunction<(AVCaptureVideoOrientation), AespaSession> where M1.MatchedType == AVCaptureVideoOrientation { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoOrientation)>] = [wrap(matchable: orientation) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setOrientationWithError(to: AVCaptureVideoOrientation) throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setStabilizationWithError(mode: M1) -> Cuckoo.ClassStubThrowingFunction<(AVCaptureVideoStabilizationMode), AespaSession> where M1.MatchedType == AVCaptureVideoStabilizationMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoStabilizationMode)>] = [wrap(matchable: mode) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setStabilizationWithError(mode: AVCaptureVideoStabilizationMode) throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func setAutofocusingWithError(mode: M1) -> Cuckoo.ClassStubThrowingFunction<(AVCaptureDevice.FocusMode), AespaSession> where M1.MatchedType == AVCaptureDevice.FocusMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.FocusMode)>] = [wrap(matchable: mode) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - setAutofocusingWithError(mode: AVCaptureDevice.FocusMode) throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func zoomWithError(factor: M1) -> Cuckoo.ClassStubThrowingFunction<(CGFloat), AespaSession> where M1.MatchedType == CGFloat { - let matchers: [Cuckoo.ParameterMatcher<(CGFloat)>] = [wrap(matchable: factor) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - zoomWithError(factor: CGFloat) throws -> AespaSession - """, parameterMatchers: matchers)) - } - - - - - func customize(_ tuner: M1) -> Cuckoo.ClassStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - customize(_: T) throws - """, parameterMatchers: matchers)) - } - - - - - func fetchVideoFiles(limit: M1) -> Cuckoo.ClassStubFunction<(Int), [VideoFile]> where M1.MatchedType == Int { - let matchers: [Cuckoo.ParameterMatcher<(Int)>] = [wrap(matchable: limit) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - fetchVideoFiles(limit: Int) -> [VideoFile] - """, parameterMatchers: matchers)) - } - - - - - func doctor() -> Cuckoo.ClassStubNoReturnThrowingFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSession.self, method: - """ - doctor() async throws - """, parameterMatchers: matchers)) - } - - - } - - public struct __VerificationProxy_AespaSession: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - public init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - var captureSession: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "captureSession", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var isMuted: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "isMuted", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var maxZoomFactor: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "maxZoomFactor", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var currentZoomFactor: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "currentZoomFactor", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var videoFilePublisher: Cuckoo.VerifyReadOnlyProperty, Never>> { - return .init(manager: cuckoo_manager, name: "videoFilePublisher", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var previewLayerPublisher: Cuckoo.VerifyReadOnlyProperty> { - return .init(manager: cuckoo_manager, name: "previewLayerPublisher", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - - - @discardableResult - func startRecording() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - startRecording() - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func stopRecording(_ completionHandler: M1) -> Cuckoo.__DoNotUse<((Result) -> Void), Void> where M1.MatchedType == (Result) -> Void { - let matchers: [Cuckoo.ParameterMatcher<((Result) -> Void)>] = [wrap(matchable: completionHandler) { $0 }] - return cuckoo_manager.verify( - """ - stopRecording(_: @escaping (Result) -> Void) - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func mute() -> Cuckoo.__DoNotUse<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - mute() -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func unmute() -> Cuckoo.__DoNotUse<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - unmute() -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setQuality(to preset: M1) -> Cuckoo.__DoNotUse<(AVCaptureSession.Preset), AespaSession> where M1.MatchedType == AVCaptureSession.Preset { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureSession.Preset)>] = [wrap(matchable: preset) { $0 }] - return cuckoo_manager.verify( - """ - setQuality(to: AVCaptureSession.Preset) -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setPosition(to position: M1) -> Cuckoo.__DoNotUse<(AVCaptureDevice.Position), AespaSession> where M1.MatchedType == AVCaptureDevice.Position { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.Position)>] = [wrap(matchable: position) { $0 }] - return cuckoo_manager.verify( - """ - setPosition(to: AVCaptureDevice.Position) -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setOrientation(to orientation: M1) -> Cuckoo.__DoNotUse<(AVCaptureVideoOrientation), AespaSession> where M1.MatchedType == AVCaptureVideoOrientation { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoOrientation)>] = [wrap(matchable: orientation) { $0 }] - return cuckoo_manager.verify( - """ - setOrientation(to: AVCaptureVideoOrientation) -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setStabilization(mode: M1) -> Cuckoo.__DoNotUse<(AVCaptureVideoStabilizationMode), AespaSession> where M1.MatchedType == AVCaptureVideoStabilizationMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoStabilizationMode)>] = [wrap(matchable: mode) { $0 }] - return cuckoo_manager.verify( - """ - setStabilization(mode: AVCaptureVideoStabilizationMode) -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setAutofocusing(mode: M1) -> Cuckoo.__DoNotUse<(AVCaptureDevice.FocusMode), AespaSession> where M1.MatchedType == AVCaptureDevice.FocusMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.FocusMode)>] = [wrap(matchable: mode) { $0 }] - return cuckoo_manager.verify( - """ - setAutofocusing(mode: AVCaptureDevice.FocusMode) -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func zoom(factor: M1) -> Cuckoo.__DoNotUse<(CGFloat), AespaSession> where M1.MatchedType == CGFloat { - let matchers: [Cuckoo.ParameterMatcher<(CGFloat)>] = [wrap(matchable: factor) { $0 }] - return cuckoo_manager.verify( - """ - zoom(factor: CGFloat) -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func startRecordingWithError() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - startRecordingWithError() throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func stopRecording() -> Cuckoo.__DoNotUse<(), VideoFile> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - stopRecording() async throws -> VideoFile - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func muteWithError() -> Cuckoo.__DoNotUse<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - muteWithError() throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func unmuteWithError() -> Cuckoo.__DoNotUse<(), AespaSession> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - unmuteWithError() throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setQualityWithError(to preset: M1) -> Cuckoo.__DoNotUse<(AVCaptureSession.Preset), AespaSession> where M1.MatchedType == AVCaptureSession.Preset { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureSession.Preset)>] = [wrap(matchable: preset) { $0 }] - return cuckoo_manager.verify( - """ - setQualityWithError(to: AVCaptureSession.Preset) throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setPositionWithError(to position: M1) -> Cuckoo.__DoNotUse<(AVCaptureDevice.Position), AespaSession> where M1.MatchedType == AVCaptureDevice.Position { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.Position)>] = [wrap(matchable: position) { $0 }] - return cuckoo_manager.verify( - """ - setPositionWithError(to: AVCaptureDevice.Position) throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setOrientationWithError(to orientation: M1) -> Cuckoo.__DoNotUse<(AVCaptureVideoOrientation), AespaSession> where M1.MatchedType == AVCaptureVideoOrientation { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoOrientation)>] = [wrap(matchable: orientation) { $0 }] - return cuckoo_manager.verify( - """ - setOrientationWithError(to: AVCaptureVideoOrientation) throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setStabilizationWithError(mode: M1) -> Cuckoo.__DoNotUse<(AVCaptureVideoStabilizationMode), AespaSession> where M1.MatchedType == AVCaptureVideoStabilizationMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureVideoStabilizationMode)>] = [wrap(matchable: mode) { $0 }] - return cuckoo_manager.verify( - """ - setStabilizationWithError(mode: AVCaptureVideoStabilizationMode) throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setAutofocusingWithError(mode: M1) -> Cuckoo.__DoNotUse<(AVCaptureDevice.FocusMode), AespaSession> where M1.MatchedType == AVCaptureDevice.FocusMode { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.FocusMode)>] = [wrap(matchable: mode) { $0 }] - return cuckoo_manager.verify( - """ - setAutofocusingWithError(mode: AVCaptureDevice.FocusMode) throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func zoomWithError(factor: M1) -> Cuckoo.__DoNotUse<(CGFloat), AespaSession> where M1.MatchedType == CGFloat { - let matchers: [Cuckoo.ParameterMatcher<(CGFloat)>] = [wrap(matchable: factor) { $0 }] - return cuckoo_manager.verify( - """ - zoomWithError(factor: CGFloat) throws -> AespaSession - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func customize(_ tuner: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return cuckoo_manager.verify( - """ - customize(_: T) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func fetchVideoFiles(limit: M1) -> Cuckoo.__DoNotUse<(Int), [VideoFile]> where M1.MatchedType == Int { - let matchers: [Cuckoo.ParameterMatcher<(Int)>] = [wrap(matchable: limit) { $0 }] - return cuckoo_manager.verify( - """ - fetchVideoFiles(limit: Int) -> [VideoFile] - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func doctor() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - doctor() async throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - -public class AespaSessionStub: AespaSession { - - - - - public override var captureSession: AVCaptureSession { - get { - return DefaultValueRegistry.defaultValue(for: (AVCaptureSession).self) - } - - } - - - - - - public override var isMuted: Bool { - get { - return DefaultValueRegistry.defaultValue(for: (Bool).self) - } - - } - - - - - - public override var maxZoomFactor: CGFloat? { - get { - return DefaultValueRegistry.defaultValue(for: (CGFloat?).self) - } - - } - - - - - - public override var currentZoomFactor: CGFloat? { - get { - return DefaultValueRegistry.defaultValue(for: (CGFloat?).self) - } - - } - - - - - - public override var videoFilePublisher: AnyPublisher, Never> { - get { - return DefaultValueRegistry.defaultValue(for: (AnyPublisher, Never>).self) - } - - } - - - - - - public override var previewLayerPublisher: AnyPublisher { - get { - return DefaultValueRegistry.defaultValue(for: (AnyPublisher).self) - } - - } - - - - - - - - - - public override func startRecording() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public override func stopRecording(_ completionHandler: @escaping (Result) -> Void) { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public override func mute() -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func unmute() -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setQuality(to preset: AVCaptureSession.Preset) -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setPosition(to position: AVCaptureDevice.Position) -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setOrientation(to orientation: AVCaptureVideoOrientation) -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setStabilization(mode: AVCaptureVideoStabilizationMode) -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setAutofocusing(mode: AVCaptureDevice.FocusMode) -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func zoom(factor: CGFloat) -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func startRecordingWithError() throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public override func stopRecording() async throws -> VideoFile { - return DefaultValueRegistry.defaultValue(for: (VideoFile).self) - } - - - - - - public override func muteWithError() throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func unmuteWithError() throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setQualityWithError(to preset: AVCaptureSession.Preset) throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setPositionWithError(to position: AVCaptureDevice.Position) throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setOrientationWithError(to orientation: AVCaptureVideoOrientation) throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setStabilizationWithError(mode: AVCaptureVideoStabilizationMode) throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func setAutofocusingWithError(mode: AVCaptureDevice.FocusMode) throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func zoomWithError(factor: CGFloat) throws -> AespaSession { - return DefaultValueRegistry.defaultValue(for: (AespaSession).self) - } - - - - - - public override func customize(_ tuner: T) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public override func fetchVideoFiles(limit: Int) -> [VideoFile] { - return DefaultValueRegistry.defaultValue(for: ([VideoFile]).self) - } - - - - - - public override func doctor() async throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Core/AespaCoreAlbumManager.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaCoreAlbumManager.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation -import Photos - - - - - - - class MockAespaCoreAlbumManager: AespaCoreAlbumManager, Cuckoo.ClassMock { - - typealias MocksType = AespaCoreAlbumManager - - typealias Stubbing = __StubbingProxy_AespaCoreAlbumManager - typealias Verification = __VerificationProxy_AespaCoreAlbumManager - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true) - - - private var __defaultImplStub: AespaCoreAlbumManager? - - func enableDefaultImplementation(_ stub: AespaCoreAlbumManager) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - - - override func run(processor: T) async throws { - - return try await cuckoo_manager.callThrows( - """ - run(processor: T) async throws - """, - parameters: (processor), - escapingParameters: (processor), - superclassCall: - - await super.run(processor: processor) - , - defaultCall: await __defaultImplStub!.run(processor: processor)) - - } - - - - struct __StubbingProxy_AespaCoreAlbumManager: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - - func run(processor: M1) -> Cuckoo.ClassStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: processor) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreAlbumManager.self, method: - """ - run(processor: T) async throws - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaCoreAlbumManager: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - - - @discardableResult - func run(processor: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: processor) { $0 }] - return cuckoo_manager.verify( - """ - run(processor: T) async throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaCoreAlbumManagerStub: AespaCoreAlbumManager { - - - - - - - - - override func run(processor: T) async throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Core/AespaCoreFileManager.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaCoreFileManager.swift -// -// -// Created by 이영빈 on 2023/06/13 -import Cuckoo -@testable import Aespa - -import Foundation - - - - - - - class MockAespaCoreFileManager: AespaCoreFileManager, Cuckoo.ClassMock { - - typealias MocksType = AespaCoreFileManager - - typealias Stubbing = __StubbingProxy_AespaCoreFileManager - typealias Verification = __VerificationProxy_AespaCoreFileManager - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true) - - - private var __defaultImplStub: AespaCoreFileManager? - - func enableDefaultImplementation(_ stub: AespaCoreFileManager) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - - - override func fetch(albumName: String, count: Int) -> [VideoFile] { - - return cuckoo_manager.call( - """ - fetch(albumName: String, count: Int) -> [VideoFile] - """, - parameters: (albumName, count), - escapingParameters: (albumName, count), - superclassCall: - - super.fetch(albumName: albumName, count: count) - , - defaultCall: __defaultImplStub!.fetch(albumName: albumName, count: count)) - - } - - - - struct __StubbingProxy_AespaCoreFileManager: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - - func fetch(albumName: M1, count: M2) -> Cuckoo.ClassStubFunction<(String, Int), [VideoFile]> where M1.MatchedType == String, M2.MatchedType == Int { - let matchers: [Cuckoo.ParameterMatcher<(String, Int)>] = [wrap(matchable: albumName) { $0.0 }, wrap(matchable: count) { $0.1 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreFileManager.self, method: - """ - fetch(albumName: String, count: Int) -> [VideoFile] - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaCoreFileManager: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - - - @discardableResult - func fetch(albumName: M1, count: M2) -> Cuckoo.__DoNotUse<(String, Int), [VideoFile]> where M1.MatchedType == String, M2.MatchedType == Int { - let matchers: [Cuckoo.ParameterMatcher<(String, Int)>] = [wrap(matchable: albumName) { $0.0 }, wrap(matchable: count) { $0.1 }] - return cuckoo_manager.verify( - """ - fetch(albumName: String, count: Int) -> [VideoFile] - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaCoreFileManagerStub: AespaCoreFileManager { - - - - - - - - - override func fetch(albumName: String, count: Int) -> [VideoFile] { - return DefaultValueRegistry.defaultValue(for: ([VideoFile]).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Core/AespaCoreRecorder.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaCoreRecorder.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation -import Combine -import Foundation - - - - - - - class MockAespaCoreRecorder: AespaCoreRecorder, Cuckoo.ClassMock { - - typealias MocksType = AespaCoreRecorder - - typealias Stubbing = __StubbingProxy_AespaCoreRecorder - typealias Verification = __VerificationProxy_AespaCoreRecorder - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true) - - - private var __defaultImplStub: AespaCoreRecorder? - - func enableDefaultImplementation(_ stub: AespaCoreRecorder) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - - - override func run(processor: T) throws { - - return try cuckoo_manager.callThrows( - """ - run(processor: T) throws - """, - parameters: (processor), - escapingParameters: (processor), - superclassCall: - - super.run(processor: processor) - , - defaultCall: __defaultImplStub!.run(processor: processor)) - - } - - - - struct __StubbingProxy_AespaCoreRecorder: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - - func run(processor: M1) -> Cuckoo.ClassStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: processor) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreRecorder.self, method: - """ - run(processor: T) throws - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaCoreRecorder: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - - - @discardableResult - func run(processor: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: processor) { $0 }] - return cuckoo_manager.verify( - """ - run(processor: T) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaCoreRecorderStub: AespaCoreRecorder { - - - - - - - - - override func run(processor: T) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Core/AespaCoreSession+AespaCoreSessionRepresentable.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaCoreSession + AespaCoreSessionRepresentable.swift -// -// -// Created by Young Bin on 2023/06/08. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import Foundation - - - - - - -public class MockAespaCoreSessionRepresentable: AespaCoreSessionRepresentable, Cuckoo.ProtocolMock { - - public typealias MocksType = AespaCoreSessionRepresentable - - public typealias Stubbing = __StubbingProxy_AespaCoreSessionRepresentable - public typealias Verification = __VerificationProxy_AespaCoreSessionRepresentable - - public let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: false) - - - private var __defaultImplStub: AespaCoreSessionRepresentable? - - public func enableDefaultImplementation(_ stub: AespaCoreSessionRepresentable) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - public var avCaptureSession: AVCaptureSession { - get { - return cuckoo_manager.getter("avCaptureSession", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.avCaptureSession) - } - - } - - - - - - public var isRunning: Bool { - get { - return cuckoo_manager.getter("isRunning", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.isRunning) - } - - } - - - - - - public var audioDeviceInput: AVCaptureDeviceInput? { - get { - return cuckoo_manager.getter("audioDeviceInput", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.audioDeviceInput) - } - - } - - - - - - public var videoDeviceInput: AVCaptureDeviceInput? { - get { - return cuckoo_manager.getter("videoDeviceInput", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.videoDeviceInput) - } - - } - - - - - - public var movieFileOutput: AVCaptureMovieFileOutput? { - get { - return cuckoo_manager.getter("movieFileOutput", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.movieFileOutput) - } - - } - - - - - - public var previewLayer: AVCaptureVideoPreviewLayer { - get { - return cuckoo_manager.getter("previewLayer", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.previewLayer) - } - - } - - - - - - - - - - public func startRunning() { - - return cuckoo_manager.call( - """ - startRunning() - """, - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.startRunning()) - - } - - - - - - public func stopRunning() { - - return cuckoo_manager.call( - """ - stopRunning() - """, - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.stopRunning()) - - } - - - - - - public func addMovieInput() throws { - - return try cuckoo_manager.callThrows( - """ - addMovieInput() throws - """, - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.addMovieInput()) - - } - - - - - - public func removeMovieInput() { - - return cuckoo_manager.call( - """ - removeMovieInput() - """, - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.removeMovieInput()) - - } - - - - - - public func addAudioInput() throws { - - return try cuckoo_manager.callThrows( - """ - addAudioInput() throws - """, - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.addAudioInput()) - - } - - - - - - public func removeAudioInput() { - - return cuckoo_manager.call( - """ - removeAudioInput() - """, - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.removeAudioInput()) - - } - - - - - - public func addMovieFileOutput() throws { - - return try cuckoo_manager.callThrows( - """ - addMovieFileOutput() throws - """, - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.addMovieFileOutput()) - - } - - - - - - public func setCameraPosition(to position: AVCaptureDevice.Position, device deviceType: AVCaptureDevice.DeviceType?) throws { - - return try cuckoo_manager.callThrows( - """ - setCameraPosition(to: AVCaptureDevice.Position, device: AVCaptureDevice.DeviceType?) throws - """, - parameters: (position, deviceType), - escapingParameters: (position, deviceType), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.setCameraPosition(to: position, device: deviceType)) - - } - - - - - - public func setVideoQuality(to preset: AVCaptureSession.Preset) throws { - - return try cuckoo_manager.callThrows( - """ - setVideoQuality(to: AVCaptureSession.Preset) throws - """, - parameters: (preset), - escapingParameters: (preset), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.setVideoQuality(to: preset)) - - } - - - - public struct __StubbingProxy_AespaCoreSessionRepresentable: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - public init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - var avCaptureSession: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "avCaptureSession") - } - - - - - var isRunning: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "isRunning") - } - - - - - var audioDeviceInput: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "audioDeviceInput") - } - - - - - var videoDeviceInput: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "videoDeviceInput") - } - - - - - var movieFileOutput: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "movieFileOutput") - } - - - - - var previewLayer: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "previewLayer") - } - - - - - - func startRunning() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - startRunning() - """, parameterMatchers: matchers)) - } - - - - - func stopRunning() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - stopRunning() - """, parameterMatchers: matchers)) - } - - - - - func addMovieInput() -> Cuckoo.ProtocolStubNoReturnThrowingFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - addMovieInput() throws - """, parameterMatchers: matchers)) - } - - - - - func removeMovieInput() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - removeMovieInput() - """, parameterMatchers: matchers)) - } - - - - - func addAudioInput() -> Cuckoo.ProtocolStubNoReturnThrowingFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - addAudioInput() throws - """, parameterMatchers: matchers)) - } - - - - - func removeAudioInput() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - removeAudioInput() - """, parameterMatchers: matchers)) - } - - - - - func addMovieFileOutput() -> Cuckoo.ProtocolStubNoReturnThrowingFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - addMovieFileOutput() throws - """, parameterMatchers: matchers)) - } - - - - - func setCameraPosition(to position: M1, device deviceType: M2) -> Cuckoo.ProtocolStubNoReturnThrowingFunction<(AVCaptureDevice.Position, AVCaptureDevice.DeviceType?)> where M1.MatchedType == AVCaptureDevice.Position, M2.OptionalMatchedType == AVCaptureDevice.DeviceType { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.Position, AVCaptureDevice.DeviceType?)>] = [wrap(matchable: position) { $0.0 }, wrap(matchable: deviceType) { $0.1 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - setCameraPosition(to: AVCaptureDevice.Position, device: AVCaptureDevice.DeviceType?) throws - """, parameterMatchers: matchers)) - } - - - - - func setVideoQuality(to preset: M1) -> Cuckoo.ProtocolStubNoReturnThrowingFunction<(AVCaptureSession.Preset)> where M1.MatchedType == AVCaptureSession.Preset { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureSession.Preset)>] = [wrap(matchable: preset) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSessionRepresentable.self, method: - """ - setVideoQuality(to: AVCaptureSession.Preset) throws - """, parameterMatchers: matchers)) - } - - - } - - public struct __VerificationProxy_AespaCoreSessionRepresentable: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - public init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - var avCaptureSession: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "avCaptureSession", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var isRunning: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "isRunning", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var audioDeviceInput: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "audioDeviceInput", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var videoDeviceInput: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "videoDeviceInput", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var movieFileOutput: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "movieFileOutput", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - var previewLayer: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "previewLayer", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - - - @discardableResult - func startRunning() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - startRunning() - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func stopRunning() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - stopRunning() - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func addMovieInput() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - addMovieInput() throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func removeMovieInput() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - removeMovieInput() - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func addAudioInput() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - addAudioInput() throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func removeAudioInput() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - removeAudioInput() - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func addMovieFileOutput() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify( - """ - addMovieFileOutput() throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setCameraPosition(to position: M1, device deviceType: M2) -> Cuckoo.__DoNotUse<(AVCaptureDevice.Position, AVCaptureDevice.DeviceType?), Void> where M1.MatchedType == AVCaptureDevice.Position, M2.OptionalMatchedType == AVCaptureDevice.DeviceType { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice.Position, AVCaptureDevice.DeviceType?)>] = [wrap(matchable: position) { $0.0 }, wrap(matchable: deviceType) { $0.1 }] - return cuckoo_manager.verify( - """ - setCameraPosition(to: AVCaptureDevice.Position, device: AVCaptureDevice.DeviceType?) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func setVideoQuality(to preset: M1) -> Cuckoo.__DoNotUse<(AVCaptureSession.Preset), Void> where M1.MatchedType == AVCaptureSession.Preset { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureSession.Preset)>] = [wrap(matchable: preset) { $0 }] - return cuckoo_manager.verify( - """ - setVideoQuality(to: AVCaptureSession.Preset) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - -public class AespaCoreSessionRepresentableStub: AespaCoreSessionRepresentable { - - - - - public var avCaptureSession: AVCaptureSession { - get { - return DefaultValueRegistry.defaultValue(for: (AVCaptureSession).self) - } - - } - - - - - - public var isRunning: Bool { - get { - return DefaultValueRegistry.defaultValue(for: (Bool).self) - } - - } - - - - - - public var audioDeviceInput: AVCaptureDeviceInput? { - get { - return DefaultValueRegistry.defaultValue(for: (AVCaptureDeviceInput?).self) - } - - } - - - - - - public var videoDeviceInput: AVCaptureDeviceInput? { - get { - return DefaultValueRegistry.defaultValue(for: (AVCaptureDeviceInput?).self) - } - - } - - - - - - public var movieFileOutput: AVCaptureMovieFileOutput? { - get { - return DefaultValueRegistry.defaultValue(for: (AVCaptureMovieFileOutput?).self) - } - - } - - - - - - public var previewLayer: AVCaptureVideoPreviewLayer { - get { - return DefaultValueRegistry.defaultValue(for: (AVCaptureVideoPreviewLayer).self) - } - - } - - - - - - - - - - public func startRunning() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func stopRunning() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func addMovieInput() throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func removeMovieInput() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func addAudioInput() throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func removeAudioInput() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func addMovieFileOutput() throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func setCameraPosition(to position: AVCaptureDevice.Position, device deviceType: AVCaptureDevice.DeviceType?) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - public func setVideoQuality(to preset: AVCaptureSession.Preset) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Core/AespaCoreSession.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaCoreSessionManager.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation -import Combine -import Foundation -import UIKit - - - - - - - class MockAespaCoreSession: AespaCoreSession, Cuckoo.ClassMock { - - typealias MocksType = AespaCoreSession - - typealias Stubbing = __StubbingProxy_AespaCoreSession - typealias Verification = __VerificationProxy_AespaCoreSession - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true) - - - private var __defaultImplStub: AespaCoreSession? - - func enableDefaultImplementation(_ stub: AespaCoreSession) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - override var option: AespaOption { - get { - return cuckoo_manager.getter("option", - superclassCall: - - super.option - , - defaultCall: __defaultImplStub!.option) - } - - set { - cuckoo_manager.setter("option", - value: newValue, - superclassCall: - - super.option = newValue - , - defaultCall: __defaultImplStub!.option = newValue) - } - - } - - - - - - - - - - override func run(_ tuner: T) throws { - - return try cuckoo_manager.callThrows( - """ - run(_: T) throws - """, - parameters: (tuner), - escapingParameters: (tuner), - superclassCall: - - super.run(tuner) - , - defaultCall: __defaultImplStub!.run(tuner)) - - } - - - - - - override func run(_ tuner: T) throws { - - return try cuckoo_manager.callThrows( - """ - run(_: T) throws - """, - parameters: (tuner), - escapingParameters: (tuner), - superclassCall: - - super.run(tuner) - , - defaultCall: __defaultImplStub!.run(tuner)) - - } - - - - - - override func run(_ tuner: T) throws { - - return try cuckoo_manager.callThrows( - """ - run(_: T) throws - """, - parameters: (tuner), - escapingParameters: (tuner), - superclassCall: - - super.run(tuner) - , - defaultCall: __defaultImplStub!.run(tuner)) - - } - - - - - - override func run(_ processor: T) throws { - - return try cuckoo_manager.callThrows( - """ - run(_: T) throws - """, - parameters: (processor), - escapingParameters: (processor), - superclassCall: - - super.run(processor) - , - defaultCall: __defaultImplStub!.run(processor)) - - } - - - - struct __StubbingProxy_AespaCoreSession: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - var option: Cuckoo.ClassToBeStubbedProperty { - return .init(manager: cuckoo_manager, name: "option") - } - - - - - - func run(_ tuner: M1) -> Cuckoo.ClassStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSession.self, method: - """ - run(_: T) throws - """, parameterMatchers: matchers)) - } - - - - - func run(_ tuner: M1) -> Cuckoo.ClassStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSession.self, method: - """ - run(_: T) throws - """, parameterMatchers: matchers)) - } - - - - - func run(_ tuner: M1) -> Cuckoo.ClassStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSession.self, method: - """ - run(_: T) throws - """, parameterMatchers: matchers)) - } - - - - - func run(_ processor: M1) -> Cuckoo.ClassStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: processor) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaCoreSession.self, method: - """ - run(_: T) throws - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaCoreSession: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - var option: Cuckoo.VerifyProperty { - return .init(manager: cuckoo_manager, name: "option", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - - - @discardableResult - func run(_ tuner: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return cuckoo_manager.verify( - """ - run(_: T) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func run(_ tuner: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return cuckoo_manager.verify( - """ - run(_: T) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func run(_ tuner: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: tuner) { $0 }] - return cuckoo_manager.verify( - """ - run(_: T) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - - - @discardableResult - func run(_ processor: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: processor) { $0 }] - return cuckoo_manager.verify( - """ - run(_: T) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaCoreSessionStub: AespaCoreSession { - - - - - override var option: AespaOption { - get { - return DefaultValueRegistry.defaultValue(for: (AespaOption).self) - } - - set { } - - } - - - - - - - - - - override func run(_ tuner: T) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - override func run(_ tuner: T) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - override func run(_ tuner: T) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - - - override func run(_ processor: T) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Data/VideoFile.swift at 2023-06-13 08:25:34 +0000 - -// -// VideoFile.swift -// -// -// Created by 이영빈 on 2023/06/13 -import Cuckoo -@testable import Aespa - -import AVFoundation -import SwiftUI -import UIKit - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Processor/AespaProcessing.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaProcessing.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation -import Foundation -import Photos - - - - - - - class MockAespaFileOutputProcessing: AespaFileOutputProcessing, Cuckoo.ProtocolMock { - - typealias MocksType = AespaFileOutputProcessing - - typealias Stubbing = __StubbingProxy_AespaFileOutputProcessing - typealias Verification = __VerificationProxy_AespaFileOutputProcessing - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: false) - - - private var __defaultImplStub: AespaFileOutputProcessing? - - func enableDefaultImplementation(_ stub: AespaFileOutputProcessing) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - - - func process(_ output: AVCaptureFileOutput) throws { - - return try cuckoo_manager.callThrows( - """ - process(_: AVCaptureFileOutput) throws - """, - parameters: (output), - escapingParameters: (output), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.process(output)) - - } - - - - struct __StubbingProxy_AespaFileOutputProcessing: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - - func process(_ output: M1) -> Cuckoo.ProtocolStubNoReturnThrowingFunction<(AVCaptureFileOutput)> where M1.MatchedType == AVCaptureFileOutput { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureFileOutput)>] = [wrap(matchable: output) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaFileOutputProcessing.self, method: - """ - process(_: AVCaptureFileOutput) throws - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaFileOutputProcessing: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - - - @discardableResult - func process(_ output: M1) -> Cuckoo.__DoNotUse<(AVCaptureFileOutput), Void> where M1.MatchedType == AVCaptureFileOutput { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureFileOutput)>] = [wrap(matchable: output) { $0 }] - return cuckoo_manager.verify( - """ - process(_: AVCaptureFileOutput) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaFileOutputProcessingStub: AespaFileOutputProcessing { - - - - - - - - - func process(_ output: AVCaptureFileOutput) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - - - - - - - class MockAespaAssetProcessing: AespaAssetProcessing, Cuckoo.ProtocolMock { - - typealias MocksType = AespaAssetProcessing - - typealias Stubbing = __StubbingProxy_AespaAssetProcessing - typealias Verification = __VerificationProxy_AespaAssetProcessing - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: false) - - - private var __defaultImplStub: AespaAssetProcessing? - - func enableDefaultImplementation(_ stub: AespaAssetProcessing) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - - - func process(_ photoLibrary: PHPhotoLibrary, _ assetCollection: PHAssetCollection) async throws { - - return try await cuckoo_manager.callThrows( - """ - process(_: PHPhotoLibrary, _: PHAssetCollection) async throws - """, - parameters: (photoLibrary, assetCollection), - escapingParameters: (photoLibrary, assetCollection), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: await __defaultImplStub!.process(photoLibrary, assetCollection)) - - } - - - - struct __StubbingProxy_AespaAssetProcessing: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - - func process(_ photoLibrary: M1, _ assetCollection: M2) -> Cuckoo.ProtocolStubNoReturnThrowingFunction<(PHPhotoLibrary, PHAssetCollection)> where M1.MatchedType == PHPhotoLibrary, M2.MatchedType == PHAssetCollection { - let matchers: [Cuckoo.ParameterMatcher<(PHPhotoLibrary, PHAssetCollection)>] = [wrap(matchable: photoLibrary) { $0.0 }, wrap(matchable: assetCollection) { $0.1 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaAssetProcessing.self, method: - """ - process(_: PHPhotoLibrary, _: PHAssetCollection) async throws - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaAssetProcessing: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - - - @discardableResult - func process(_ photoLibrary: M1, _ assetCollection: M2) -> Cuckoo.__DoNotUse<(PHPhotoLibrary, PHAssetCollection), Void> where M1.MatchedType == PHPhotoLibrary, M2.MatchedType == PHAssetCollection { - let matchers: [Cuckoo.ParameterMatcher<(PHPhotoLibrary, PHAssetCollection)>] = [wrap(matchable: photoLibrary) { $0.0 }, wrap(matchable: assetCollection) { $0.1 }] - return cuckoo_manager.verify( - """ - process(_: PHPhotoLibrary, _: PHAssetCollection) async throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaAssetProcessingStub: AespaAssetProcessing { - - - - - - - - - func process(_ photoLibrary: PHPhotoLibrary, _ assetCollection: PHAssetCollection) async throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift at 2023-06-13 08:25:34 +0000 - -// -// AssetAddingProcessor.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import Foundation -import Photos - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Processor/Record/FinishRecordProcessor.swift at 2023-06-13 08:25:34 +0000 - -// -// FinishRecordingProcessor.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation -import Combine - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Processor/Record/StartRecordProcessor.swift at 2023-06-13 08:25:34 +0000 - -// -// RecordingStarter.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation -import Combine - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/AespaTuning.swift at 2023-06-13 08:25:34 +0000 - -// -// AespaTuning.swift -// -// -// Created by Young Bin on 2023/06/02. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import Combine -import Foundation - - - - - - -public class MockAespaSessionTuning: AespaSessionTuning, Cuckoo.ProtocolMock { - - public typealias MocksType = AespaSessionTuning - - public typealias Stubbing = __StubbingProxy_AespaSessionTuning - public typealias Verification = __VerificationProxy_AespaSessionTuning - - public let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: false) - - - private var __defaultImplStub: AespaSessionTuning? - - public func enableDefaultImplementation(_ stub: AespaSessionTuning) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - public var needTransaction: Bool { - get { - return cuckoo_manager.getter("needTransaction", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.needTransaction) - } - - } - - - - - - - - - - public func tune(_ session: T) throws { - - return try cuckoo_manager.callThrows( - """ - tune(_: T) throws - """, - parameters: (session), - escapingParameters: (session), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.tune(session)) - - } - - - - public struct __StubbingProxy_AespaSessionTuning: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - public init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - var needTransaction: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "needTransaction") - } - - - - - - func tune(_ session: M1) -> Cuckoo.ProtocolStubNoReturnThrowingFunction<(T)> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: session) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaSessionTuning.self, method: - """ - tune(_: T) throws - """, parameterMatchers: matchers)) - } - - - } - - public struct __VerificationProxy_AespaSessionTuning: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - public init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - var needTransaction: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "needTransaction", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - - - @discardableResult - func tune(_ session: M1) -> Cuckoo.__DoNotUse<(T), Void> where M1.MatchedType == T { - let matchers: [Cuckoo.ParameterMatcher<(T)>] = [wrap(matchable: session) { $0 }] - return cuckoo_manager.verify( - """ - tune(_: T) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - -public class AespaSessionTuningStub: AespaSessionTuning { - - - - - public var needTransaction: Bool { - get { - return DefaultValueRegistry.defaultValue(for: (Bool).self) - } - - } - - - - - - - - - - public func tune(_ session: T) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - - - - - - - class MockAespaConnectionTuning: AespaConnectionTuning, Cuckoo.ProtocolMock { - - typealias MocksType = AespaConnectionTuning - - typealias Stubbing = __StubbingProxy_AespaConnectionTuning - typealias Verification = __VerificationProxy_AespaConnectionTuning - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: false) - - - private var __defaultImplStub: AespaConnectionTuning? - - func enableDefaultImplementation(_ stub: AespaConnectionTuning) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - - - func tune(_ connection: AVCaptureConnection) throws { - - return try cuckoo_manager.callThrows( - """ - tune(_: AVCaptureConnection) throws - """, - parameters: (connection), - escapingParameters: (connection), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.tune(connection)) - - } - - - - struct __StubbingProxy_AespaConnectionTuning: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - - func tune(_ connection: M1) -> Cuckoo.ProtocolStubNoReturnThrowingFunction<(AVCaptureConnection)> where M1.MatchedType == AVCaptureConnection { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureConnection)>] = [wrap(matchable: connection) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaConnectionTuning.self, method: - """ - tune(_: AVCaptureConnection) throws - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaConnectionTuning: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - - - @discardableResult - func tune(_ connection: M1) -> Cuckoo.__DoNotUse<(AVCaptureConnection), Void> where M1.MatchedType == AVCaptureConnection { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureConnection)>] = [wrap(matchable: connection) { $0 }] - return cuckoo_manager.verify( - """ - tune(_: AVCaptureConnection) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaConnectionTuningStub: AespaConnectionTuning { - - - - - - - - - func tune(_ connection: AVCaptureConnection) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - - - - - - - class MockAespaDeviceTuning: AespaDeviceTuning, Cuckoo.ProtocolMock { - - typealias MocksType = AespaDeviceTuning - - typealias Stubbing = __StubbingProxy_AespaDeviceTuning - typealias Verification = __VerificationProxy_AespaDeviceTuning - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: false) - - - private var __defaultImplStub: AespaDeviceTuning? - - func enableDefaultImplementation(_ stub: AespaDeviceTuning) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - var needLock: Bool { - get { - return cuckoo_manager.getter("needLock", - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.needLock) - } - - } - - - - - - - - - - func tune(_ device: AVCaptureDevice) throws { - - return try cuckoo_manager.callThrows( - """ - tune(_: AVCaptureDevice) throws - """, - parameters: (device), - escapingParameters: (device), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.tune(device)) - - } - - - - struct __StubbingProxy_AespaDeviceTuning: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - - var needLock: Cuckoo.ProtocolToBeStubbedReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "needLock") - } - - - - - - func tune(_ device: M1) -> Cuckoo.ProtocolStubNoReturnThrowingFunction<(AVCaptureDevice)> where M1.MatchedType == AVCaptureDevice { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice)>] = [wrap(matchable: device) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockAespaDeviceTuning.self, method: - """ - tune(_: AVCaptureDevice) throws - """, parameterMatchers: matchers)) - } - - - } - - struct __VerificationProxy_AespaDeviceTuning: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - var needLock: Cuckoo.VerifyReadOnlyProperty { - return .init(manager: cuckoo_manager, name: "needLock", callMatcher: callMatcher, sourceLocation: sourceLocation) - } - - - - - - - @discardableResult - func tune(_ device: M1) -> Cuckoo.__DoNotUse<(AVCaptureDevice), Void> where M1.MatchedType == AVCaptureDevice { - let matchers: [Cuckoo.ParameterMatcher<(AVCaptureDevice)>] = [wrap(matchable: device) { $0 }] - return cuckoo_manager.verify( - """ - tune(_: AVCaptureDevice) throws - """, callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - - } -} - - - class AespaDeviceTuningStub: AespaDeviceTuning { - - - - - var needLock: Bool { - get { - return DefaultValueRegistry.defaultValue(for: (Bool).self) - } - - } - - - - - - - - - - func tune(_ device: AVCaptureDevice) throws { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// VideoOrientationTuner.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// VideoStabilizationTuner.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Device/AutoFocusTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// AutoFocusTuner.swift -// -// -// Created by Young Bin on 2023/06/10. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import Foundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Device/ZoomTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// ZoomTuner.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Session/AudioTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// File.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Session/CameraPositionTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// CameraPositionTuner.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Session/QualityTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// QualityTuner.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// SessionLauncher.swift -// -// -// Created by 이영빈 on 2023/06/02 -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Tuner/Session/SessionTerminationTuner.swift at 2023-06-13 08:25:34 +0000 - -// -// SessionTerminationTuner.swift -// -// -// Created by Young Bin on 2023/06/10. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Extension/AVFoundation+Extension.swift at 2023-06-13 08:25:34 +0000 - -// -// AVFoundation + Extension.swift -// -// -// Created by Young Bin on 2023/05/28. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Extension/SwiftUI+Extension.swift at 2023-06-13 08:25:34 +0000 - -// -// SwiftUI + Extension.swift -// -// -// Created by Young Bin on 2023/06/06. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import Combine -import SwiftUI - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Extension/UIKit+Extension.swift at 2023-06-13 08:25:34 +0000 - -// -// UIKit + Extension.swift -// -// -// Created by Young Bin on 2023/05/25. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import UIKit - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Log/Logger.swift at 2023-06-13 08:25:34 +0000 - -// -// LoggingManager.swift -// -// -// Created by Young Bin on 2023/05/27. -// - -import Cuckoo -@testable import Aespa - -import Foundation - - - - - - - class MockLogger: Logger, Cuckoo.ClassMock { - - typealias MocksType = Logger - - typealias Stubbing = __StubbingProxy_Logger - typealias Verification = __VerificationProxy_Logger - - let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true) - - - private var __defaultImplStub: Logger? - - func enableDefaultImplementation(_ stub: Logger) { - __defaultImplStub = stub - cuckoo_manager.enableDefaultStubImplementation() - } - - - - - - - - - struct __StubbingProxy_Logger: Cuckoo.StubbingProxy { - private let cuckoo_manager: Cuckoo.MockManager - - init(manager: Cuckoo.MockManager) { - self.cuckoo_manager = manager - } - - - } - - struct __VerificationProxy_Logger: Cuckoo.VerificationProxy { - private let cuckoo_manager: Cuckoo.MockManager - private let callMatcher: Cuckoo.CallMatcher - private let sourceLocation: Cuckoo.SourceLocation - - init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { - self.cuckoo_manager = manager - self.callMatcher = callMatcher - self.sourceLocation = sourceLocation - } - - - - - } -} - - - class LoggerStub: Logger { - - - - - -} - - - - - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Video/Album/AlbumImporter.swift at 2023-06-13 08:25:34 +0000 - -// -// VideoAlbumProvider.swift -// -// -// Created by Young Bin on 2023/05/27. -// - -import Cuckoo -@testable import Aespa - -import Foundation -import Photos -import UIKit - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Video/Authorization/AuthorizationChecker.swift at 2023-06-13 08:25:34 +0000 - -// -// AuthorizationManager.swift -// -// -// Created by Young Bin on 2023/05/25. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import Foundation - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Video/File/VideoFileGenerator.swift at 2023-06-13 08:25:34 +0000 - -// -// File.swift -// -// -// Created by Young Bin on 2023/05/27. -// - -import Cuckoo -@testable import Aespa - -import AVFoundation -import UIKit - -// MARK: - Mocks generated from file: ../../Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift at 2023-06-13 08:25:34 +0000 - -// -// VideoFilePathProvidingService.swift -// -// -// Created by Young Bin on 2023/05/25. -// - -import Cuckoo -@testable import Aespa - -import UIKit diff --git a/Tests/Aespa-iOS-testTests/Mock/MockFileManager.swift b/Tests/Aespa-iOS-testTests/Mock/MockFileManager.swift new file mode 100644 index 0000000..e646dbb --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Mock/MockFileManager.swift @@ -0,0 +1,34 @@ +// +// MockFileManager.swift +// Aespa-iOS-testTests +// +// Created by Young Bin on 2023/06/18. +// + +import Foundation + +class MockFileManager: FileManager { + var urlsStub: [URL]? + + override func urls( + for directory: FileManager.SearchPathDirectory, + in domainMask: FileManager.SearchPathDomainMask + ) -> [URL] { + urlsStubMethod() + } + + private func urlsStubMethod() -> [URL] { + guard let urlsStub else { + fatalError("Stub is not provided") + } + return urlsStub + } + + override func createDirectory( + atPath path: String, + withIntermediateDirectories createIntermediates: Bool, + attributes: [FileAttributeKey : Any]? = nil + ) throws { + return + } +} diff --git a/Tests/Aespa-iOS-testTests/Mock/Video/MockVideo.swift b/Tests/Aespa-iOS-testTests/Mock/Video/MockVideo.swift new file mode 100644 index 0000000..a87749c --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Mock/Video/MockVideo.swift @@ -0,0 +1,24 @@ +// +// MockVideo.swift +// Aespa-iOS-testTests +// +// Created by Young Bin on 2023/06/18. +// + +import Foundation + +class MockVideo { + let dirPath: URL + let path: URL? + init() { + let fileName = "video" + + dirPath = Bundle(for: type(of: self)).bundleURL + + if let path = Bundle(for: type(of: self)).path(forResource: fileName, ofType: "mp4") { + self.path = URL(fileURLWithPath: path) + } else { + self.path = nil + } + } +} diff --git a/Tests/Aespa-iOS-testTests/Mock/Video/video.mp4 b/Tests/Aespa-iOS-testTests/Mock/Video/video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..2b2ef86bbda85e441a6eaf1c0ce34f20ed2b3866 GIT binary patch literal 12857 zcmd6NcUV)+)^8dRdO~kP=m|KkE5!_5Kk_`%{q5(ZSCbh;cZ&`TG5>3Sgh< zh5cj({eV z8Nl)cvS57ft{zT6gdxEFkK%ut&gbwKkHf*o@l56{objprecVug#i2{Ses*p^{FASr z@4uXKCewN5IDnpof9m{KMjA*5h@Rmr{11u0vj6eRLL>qyIY6rh*a1XEoCmQx1N8Y9 z3|I+#8v=n~K_Dmy_~9f=U>E>{NL)Tbf@@9)gfkuV_MUD&9`jm0nh?a1mF#ThX5V}=md}s zpaQ^mK<8O!SOJs-pm>1N1Goaj&oBXC1Hc-9B_N*+pg#a?|2qxl1o8npKn?)q1?bsA z2b2Nx0rWON#Q-V`P&oQ`IO7QeKmj@QT`L2T}e;GzelXcevcEE=VjrMc^*Agod3?g%Uy^?X90M z1>s^EL07c~6(iKg6D@yLhe^E=LrIjw16M+X`smsEkG58N<{(Q+m-RKMUYuZ|@x`7{ zz27}!bGXDHZ^<9tQN=iz(YKf~Ndw|u$1|vHYDA}86t(Hq4KCt(^rdNhL&EGQo7hRK zyW%)yWmO4K{gJ3;UT`25mBTP-1Plm5CTTuczc#O|_3(gOqh7ulf%40zY zq#ch#OXI2@aeJze*n`j6fGCt|<}3GZ)T!nV-6!JYugMO#PIyvudJp*9o7T0?D|Iao z$zI9mWE)tF))ynhULarT+O{xwHsa%IuCYh_<+(SPYUu+uZ1mWM+i#6SnnV41>b(1`J8`@9X zDXPRJ?01TA2Gh%D4sA|(ihFqDG5W1s#HYmH5u5|U-li*7T*%MC&%gzrCgviXRPSLF zCWdPXuKK5~H!JeiUuKd@*lmun{+!y}=jw@yxem#pS@2ZNdi_hRo{|Nr&Aq(KOmN8GnnigQ(R2!v9htY2z?<8y))qPIYA-@ z9^=bD;c)uZGGZ~rEYT@(PkGuPuL#o9iyN&W#Eq6vd@YzF%TFq9$!haz1A84jdwILU z=w#w;dckNGm7t*KY*~cFD?dVN;NgkpgylVU$Ato$mGAat>+hRYnP29n(8(%#ku+bb zeE2(Xs!dv_J%B*!CuCxy*hM|6G0!0C!13U)ys%-+i)pJPfVe9FwcJ9Tc z*AeY<67)#|_HS+-To5R)TYJ|r&%hfuv-@QCZHWK+&sBrm58jvFHt|yu#HIkx&9^NjpT6AaatXhrT4S8?+EU2l^iN1fekbjy^sd-?o|vYU-OF8$yyI?CW#6L4ZIi@+aSF^mq zMLyhYKI^jt@ADrP;CB$K%}>`zA4#x9*NKKCo%nivU2hIsow((!;T%iz#7y?yBwctQmBGf;AIlwE6+9PZkeWxY@#BRS&;^jVdE8u7oZ+rHTmIsm z2ATT@C$QMNQq838M<_#R#pz+f@h%3zU8fJI@WZL+rkm3vh9QQV8|J*aP`a>cf(W;}^)H)+J-P*;qP5ac~k&KGBF(a^` zOIrGkjTsbNBygarwN!w(;l<}nBb(KE_goL77_VPZk!~lB84@j}cgMPdZogoc&1bBC z=lbJmY68!P=_J*4^JA#R9M^|8D|PFEL@s*q{7kkupPiijpUZAn=^Gan4cfPU%1fNL z=@DGGW$F)9s2qa(LU8dg$*&(p=O#gL zkVk2A8~JN`svg)9kqE7v&T+1`fswY=yy3(4ligsvUBpXX@}f->Npo#dGdwxiM*3dM z_=4PgLHdfW0e6&jiQ;?0DcVeSX=pg^5j+f-#s@CMpzA+(_LAE?I{p$J!K%>a zjk$*YxTx{q($Q4VV(t0_^z01#3X`V^N(e&eQ=-qA$d+G;!b)I*a!*l35#*VRX4E0G z?#+5HIo#yA$yfWUOOq(Oq2O0adJn1fqk4tEYwKr!>`nDZUf!H2xfnTys+B^^jPYP( zy3!WBFlx#LA#}5|*|-+U8sUdt5t$Pb6slWvoNZn29;*DFH+K4nx)fU~xc&XWfTU6g zU4|RN-u@83uP`n(HtXTsW%`ErOWl%0Ck^w-KhUHsjHjyZv{@4w@N5|in0+2xXpgR+ zluaZlfB0w~gcXmliTi~>pWC`x9%r&mq|CVTIG(2`k-%OI2uEV_9RIXYpmLnLZLY@T zlTUX%e`;i^@e=|vTt}zoGRmthU3s`OhupiGUnzqZl3P%qZ`AAG*AmnBWShwtm)65M z@n%Hx^5(+w?T^x(Er|801H{cJ;gpt?QcaAiHJhyq_ap-3+x&n+$MLsdYa2+L>Hrh` zmaDgEE<4`x)J+;NV+)fP%b>Y97KDrxAdyA5GOo}*PK-$3W#5A>jg(nusvRC9;5dd_ zV-vJ-l&_?2vozi?yKPdFs$|gI+=iaQE~i`!)OK`#_=HC|@3YYK`M7QU>jK&|d>rNY zi_CYZewG|LJ&MtWgmxY9-6V={z$BZL*|iY9|h!vSXLZ=xtLEoU{q_S#7DudA}D65RhVC1akL- z;jolV1ts5I-$@)99jkdt7lDN3br#HDhzQ>mtw;EyTFP}!wPs=Os}Ih49s@2cN7r}G z6&oN(7trCH7s=VTUry^3-^m`Kz`up7(@=Qp2=M7{l9StByYcZs<%2$PE@T=B1BCWVaZ{~_^^ZMkiwjh7%{JNTJQZfZ8 z5+Y`*7iDyNZa$~^);ET3wikg=e#(g`<|2wMJxl$vtkF4mw;lBPoM=Y1waatOL7T}n zid(OKvlf1Wh7i(bW%-V1I(U=8dKX&NGGx`s$*v|%6WX2IxF+hk zLlqb;XW&hyzG%R_I!s<4_{R#1u&yh!`;{1ZYiU$!_i2y_gDA&#Tandx6;FgzM9`&K zVvqopcFr*IdxUq{L*<-h+$h|jiG^0;yzp_fAM=&3LEOrYWIi9O!qci%Ob(nmb0h3w zIQKmQkNfhMm2ID{`s`TIDfujA;Q*vu9JJ5R*>{UJW-drc-gYogflG_UD~; zb%sbBna*XkL&Uq|tSD*+<+?6@;n8B1Gor|{f7LCxu=1FHshOc2;$C33o>Qr}Xs3P8 ztZw+jb`ZI(9^QbFNkAEeCjU_?krLz`_mz%Qo2T{*YCXn(M+KT@PW~8d9U2O8UnQOn zwn)U@E)BPZs>(*+CVhQZP-`mfDHAEuOYgAG5H~QXSd|A@h{NQIo>|yRSP&EBMm&utJiNu#)RYbh&wK14=Wx!u5$ zO&3ceiX)ZmqMbS$<0LCs**WQqf$-e?#tx*qqfau8G;%euHEPNNRzDTdG{tFi3PiZcE%bukBA%AN5I;^ zAPgwX|K-Kq-b@Zv)^|6pSM^BSx}&WV5h;7Li$>h?!7e3)FT4IPAp?<<*&B#tZc)1u z|H>Wmdq#O_jyZC6;L1X5{9`xTzRTnSI#(W6WUXq$MkP+vCdZkpI84Y94csT$L>IS3}jI4zniCCzN@T;}-@OP^3KR+JWfW2D^Rpq3Em$ zE1DmfN8CwC->HT*VH>`MVpNP4PZ0s>tZ>%5!4anE+MF;7Q-^6Ef+#FGmeiO;1J+y+ z92JxI@}+B)$+7PuelD)Lo5bp(eT_vgB}ik7cR)VPJG@QWXgWT0!_O-X8zU+!mw&@S z$MgsWO7?seuvGzF-Y-f;$62ID2>cXyF8btEGG&~`?8^CHU~og9#xdUISwGzpJDn20 z!%64<*vn%?r1&AL3CF7cb&QnJov(kE(+y1iFHiykZ$jA<89w<=wIhv$Q-AeWttR`c zORW)euQNV?rA#Q%!VD@ttL2NnInOO1lZ%SHjg!HyIU;RT&+TcVw4N+%>G6WVa@><% z+zCwJP0NMKO@Fvu|GG`?;3_01{+aKK-itF*)(;|bWbGrpDbNa_s8&K7?{qk*C?<)0 zeiM=bXXO^=ohptA_a!a*7_=W1cmBaN`1R1Mx|@1d*1XOf6<)ZT(r@FaCi8u+b(cA2 z3;MZ9nj{!R%F&wzhA6gg9$lTV3Ok8FU0`SZ)R`KDh^vSgbg+|WKzpqZNs5cnUcAHf!Fbm0cTr)la4hU=o=3gol{`m2DE#Gj{1R7A zBu2luKNEEElak9pFhc~|#}1G{~&gOX9i!LvU@1sF7fF1jhus^=ZEk6Jor%xtrLRzXV%_zyt((v9 z={3(4FPQ(e+HB9bkRK zC74Yo0?KW(FSR;6PJRpffF%OtV0wq;IK1YitxxD8dk%f%J?Hi|UfAOpn7JlHcdV9g zrjDYx%f2lg`W2vT43l>qyeh_!!)>Z`T)!f%B0d+Hn`(}R_Spz&1f^U4)b}oM=H*Fg z8{~-Yf*NgzrXi6l^UKEF7oF zhbHPIBt)W(ePYn56olRKdQj&;lM{I$5bW57QcKR&;Bh1zqasL{X+Ww+559Vn=g9;s zn?p^-b<#>Q^QaESIGvXWRAvvoGWtSqOAuqgv7Hg$)mcp;t!6RJ5pa$cX;iX?`gyj~ zm9O6eausIr+^M}`3^CDbJVJSDfVcC-B<#u zTIoAW=e3{@s;BZ6*-by8usEc4JjhGv9H5%Lnt>kQK@0vmri`Egbi;pWdvlK`xs2#`?Yk2 z>zc0d7NOgi2L^{H2cu>=r3yn1G1;+47wS8rRTZi(QIQqe$_)osEi;i^Ic*Wqx4V;l z*n-?Rry$t&_l0H-llKm2X%rsbydWIr=@~{9s}(u!jUN@JQle(B_%h)?DJR%1X{SlU zMCk>STG4g#tZ0%T4{9HJ8e;Y2gjwGvGVY=x`R9w3UyVuU%^;vduq`t~O|V{R_Es_O zZX@kI{=#<9^B|;Fy}AKOMOP#mr?oDZtoy+|zW;}{c3N~B1>247oyax|Oi<)|K&wTX={F1%LrovAhFifpYHpt&LGMgnMcSVfA|f{sRn zRqj!Fg)Ljf%`D{(0{ISc<`<6YbPm07F7N}eR6;R%Q$QQ5`7Vp>jMa#?7f0c`>8L&m zz0^Bpg-L3`yv1If94y{b7_()ekIG@fRoSvZT16+MTiWX`=d_jdni*rh-TZjprnx{o z2;1lkp=s3H*<*k~EU9jz`zi_z5VW}j??>vQ7dyAB$o7MRlj#(KS)gtd7|JBO&L~J^ zTU-EFY~S-%7oLWcrNNb!Mlv=hFD^RQj=*4&yBBy~o`hO+h{*KtZS-Q-#VH;1#*XZ+Js-iyR zECw;Q@H2Etm!b8j*VrTKy44_H@bJ`5;Lo;ZU{HU@iO(2!>VJ=`7Z^2 zF0Y_D$nhc5Q$ArNT#Tk3MLCMjYYZ!tUM>Ad10Q3btEu2-ntXtG?as;=~lX`44~1-EC+@=qIi*J~1#b zeG;A+7;ZTJ+j6Gexa2MFlx$`8FgyyY!Ghq8ps|Y)a_C5*=wf48<iZr8b25&RptKBIsPcm~@|NyG@i)AhlpcF{ViXtWx%(jza)=nBw|lhU1fc z2EC0n{qkke9_F@!_H9TP9DYfo`o$CD5yWHHr?GqEMX`aiWLvzFQX8`k;&E`Ha^nSS z*oQqjIokF|h(5-xrEBibq0jN(Z^U{(-|oGXNx@Ht+WV+R&HgC&Rtw)Q4aS@ka~JH* zL0_U{5KzO6)rQ9{Bv&oB!`EOp7GS3LtC405E)(~%b@3!?;w=(B4DW~L3 z)3L%D(tt}UWkM_)*{5wm^KvQTnIxPYQL;n7CwtBGy-$A;+tYf#n4cJhw%ZEalqkFd zhmP22Nwjgn?xRbOiBTjl@N~jphll+wuK~_xv;w-28})EwnG;#VefcJa>~=NqgqLB6sjl7{V%AyJ z8bV0(K$#=Z?aU11iw`W8M-CE}s6JoDi@@jOaJsz6AnNe0TV^W*6oOn4oVesa*Gx7{ zz762&ojApj;M>`DoFRTDPY1=txdy9SoMnRr273lr!@6TPY(_#%Ii|(PruE)OoorG( zy_2;NBni`sXA)wX%X$S%W6c?Zs9C)leS*&!CA&Otg#Ahx?mpFS%AJ)}6<|bTH2YC{ z;C^&qxWN3$z?M!1AtMTNo-c_L^!~L1_HOZ$@M2Q#c3l@V3ocvSEXwxP=B4zkR2PTI zqdxk6u8FZ3>6Y`tEIC-ri#Ptc0pcc=J-;nT3j_R4cW8SEL*I8S=HfJJB5M#d6kkNe zBPfPZv}o&|YV+qLZ%7BjIp{jtB9rUewtB_1?V&#f z)@dTOM+;`;oafxUN?u0YC$;>A1_p^LCO?8fbDn{@IZRVZouhwEF8AH>3&VA%iP+Ah z)f8XL%ir<*5tD9+$r>zOkp8DR(%PRxobMhK8yD*FP?h@>f4zR~vnsgW0vS6!E;S_C z@^h$|I}(w`&!jkqw5DfXj`n$}Jx5{v91(JdWqTnUC3REg!A*`fn-Q$$Cu?Z_ht6rd z68EHW?DBP-`j00swp=RPWBRIxn8$cKdF$+>P^=dWoexU`v)3%TVLg`4Viv~_`;^xC zEPu!opB5J*DNNSiWQ@-oBn^ECAUS_Jx=FTgVUT4r(GHq16MdrNdyelVGASxD$Aphm z#c&qi#Mryn;?5rYw5(jdn}ENn#G(|_U+M4P9kPC<%^? zBPgMQ70F|>|33U-t7CKQH6)IggJy}Kz+@M5zKFCGcsoypp&;!5_kyX%MMj1;VzHe& z-C>7Hm52EmpPtsauH#!JINSu-0S; zzO!0Q4}CWdD=+Xj&3c%-J~%x2nbq-it-DgsnD-DQEnKfu5k-|VuNmdbP!ASs~{A)T2#q)gSp6pc8x5?;u4RamVR!rh-xV=f# zdA`FyU#vr?@oNo-W+6!rBM;*!t!dH?OQ)6^LU3uqM%uj$SS0YqRg6YCX^apf0}i1< z`QXK_yHHasRz$87m_EP}Kfeo<*J4=7h`PLgr9gs zwoIy$cT<2kR9BzUk#d<592O@`5&wbd4Iw5GGcO}%^p0*~wXbAcqf$b}3Bp|>NX!fttg(#?0 zVM`csE697FE9O!p4Yw|QnUmX1W56+asGxY&P?rvmpSy1BwjUwPQi z)C5yWMpoA9?dy*BNndJiyja<#^^2FsnS&b5Hla*Sw1FoQs17p+_6!M}N7sj|Z@+?D zFM=R5T;8eAIlF1WDUqx;B%w+B72sFas25YXIC9qQQEK>f?Y2&kRL`9UM)4=2&wkja z;|d^H6dlY4@>HQ9qMa$0Yd0f%u#vvl3_*DlJo;vf@! z2$pBf#6_bU$}AJM&Ys#kfeT52Q5f4}RQ(up1Ea0PLNP6X3}-GD^Co!b?e`$!=1M7^ zIFgvpRiTcuSK2b2Yq;O`3SHh$fo&JVxF!aP`|CtS8{p9gPqrtK@~Yt)1+Q*IZ4_Ms z^s%sjx=oD${B(E{cxDI|#^jOzy(G*ECBGm=h_8L`iRS4Ly?$)Lox!H)3eyl{85D}4 zPFLH_7)@EG$HMO&ww%;p8QM`LS`xUoB;mJ60#mBd9y>zRMHlAZF8-!F6`>Q5jEtCN zVYyQZCei9{GG#mnCdU-O(Q)M7xT?2m{$6tK8W)q7U?|V0u*dXbqTvU@-4*nLoE$uz zq;hL^A3AAcqS68mKRJ<1`mmp0zo>7&{(~isx*~~-^UV`IlyXY!rB9U(EOR7i)55+o zt{P34n1liew-`;;?mVaL!HfIy+*#Vu;j+049r;)42y`?wQJJ=AJ}+J=Hf-27<+H_e zc)kPP*3!~O`Onk6h>Bm8HiLDm8&9iq){{(u`~@QZ8&SAtX1i8nAS|5w`J*8TtESR zY=z0Y0A6MH6{*nm($pmC_4AjEl%!15+C41trsmc%(IITNa}D1rzn@}QFb`UJ@vXc0 zcw~D?2~uInvuJTm@p9{>4}`0i$G%$g-cJgTlc%Q&nSpE$%yCL)Zy&?w&=MwC%B63% zgFWlbpnQk9$)d3zB?cR?86k)YC=#+Q2C`*}XDz`yuyTw_)#bCjwfvdRUEwH7b1rYD zvxy?QGas7UF(`r6CcW!>|M+}N!6On588=Pojj_Y{#&)8W1WP0b(r#}qWmPFuef;_R z_1i4cX8pn+SUuErRz|+XfZ_WfK~s&n$LxnPXwQ+p=av1F7h$$6CvXX5bAO-nRbA+Yo<_S9$_J9(2L}E@*}k|Gqw&* zvisJu%L7}l7^o%5rskzckHT59oOs^}KI5<=bLWm#-Yp|oM*ejUPrwxFX(8o5kvwC_ zqG4i#q=+j1WKf57P40YazKwrx{7jStST5A*DE_%_!9EGRnYuZzs}F%e5Xl_7u<(%J=Tcky=;bRUXKX zrPrbZ6J&yUT98(XjuB#h9I6{{L;}ygKU4r0-Udd=ZN}krQb_t-X|UHue0GfyH*ToP z=oEu1;~_8Afg{{>N`f}mCzmL=S9<)@1Lrx(3weOTm%)W@e-+MLd}u4L_p{Ne0$jhD%{m zsxtyxql2A=1PRgU*T?mb7gj6pGa1fNU9e_{J*vCN-*9LV@u0;GjBj6%^;?G}^s5av zb4gt!5E;&iY26zY2-XJeZ|aEsb&d4H6sBq+h5mSx3{FR(od}cH-dd=6Hh&AjKFEwC zMgH~)Y@JZ{x)tT>eqnn))#qHYR8<&OX?%R`SS(Hx4k|avm8HSpnZUi>9g$C<{2+Ty z7Pve7;+MC5SALs!6M`(IUmC*gQlrBy(^W`F3mSqofzDC|7gnR|sVlByNbbQ&7fHyO zQ`)gfvT&+%xbPD)BImmvgiQ(0b_e*TL1*?1GdEq|z;x9cf{CF}BUk;$Gv~>H3v2#z z9$EpO%Y@)TB-mVrdiUW*cjz6_VxKr@Q@p3ogTR5J!FCZAA!|AkP34?0HQ<9j&=8397fkKSbK!1f_oF_wnRrVv*ygqG^42OahXy8L|@a2@v`7@=^g)IHtuY z^oh)l!!;aLb0;bYGs(46_8t@kS>ok+JCU7g^oji=3wf!3uMYb!KG)$%y>L^x)wt@j z&u#_4g*)haGCRh!Ybd1A`*xf8YwOr6^Sg)Lm0qkC3>44gncWZ;b1Z71tv42?!nOlmNzs9NQQo=RJ_qPz1sBPp>mjTf zi{Ovax>KpuoQ$!`Z%%!|UI3FGTqF-LSsHOBqAbJc*-g*dvuq(uz6xLm#0uSaid;Uo z4?Ab5Q%j+gRra&7e4q6_7bh=!NL16GTMmdCQ)KbS1hywy>Sq;Ff{X3}I{%Rj{znpt l$+wjThJ%$6s>AS3@kb+npa`I&=s$I|{G$+{viM)g{{@jQA*uiX literal 0 HcmV?d00001 diff --git a/Tests/Aespa-iOS-testTests/Processor/AssetProcessorTests.swift b/Tests/Aespa-iOS-testTests/Processor/AssetProcessorTests.swift new file mode 100644 index 0000000..b19f771 --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Processor/AssetProcessorTests.swift @@ -0,0 +1,54 @@ +// +// AssetProcessorTests.swift +// Aespa-iOS-testTests +// +// Created by 이영빈 on 2023/06/16. +// + +import XCTest +import Photos +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class AssetProcessorTests: XCTestCase { + var library: MockAespaAssetLibraryRepresentable! + var collection: MockAespaAssetCollectionRepresentable! + + override func setUpWithError() throws { + library = MockAespaAssetLibraryRepresentable() + collection = MockAespaAssetCollectionRepresentable() + } + + override func tearDownWithError() throws { + library = nil + collection = nil + } + + func testAddition() async throws { + let url = URL(string: "/here/there.mp4")! + let accessLevel = PHAccessLevel.addOnly + let processor = AssetAdditionProcessor(filePath: url) + + stub(library) { proxy in + when(proxy.performChanges(anyClosure())).thenDoNothing() + when(proxy.requestAuthorization(for: equal(to: accessLevel))).thenReturn(.authorized) + } + + stub(collection) { proxy in + when(proxy.canAdd(any())).thenReturn(true) + } + + try await processor.process(library, collection) + + verify(library) + .performChanges(anyClosure()) + .with(returnType: Void.self) + + verify(library) + .requestAuthorization(for: equal(to: accessLevel)) + .with(returnType: PHAuthorizationStatus.self) + } +} diff --git a/Tests/Aespa-iOS-testTests/Processor/FileOutputProcessorTests.swift b/Tests/Aespa-iOS-testTests/Processor/FileOutputProcessorTests.swift new file mode 100644 index 0000000..d5e30c9 --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Processor/FileOutputProcessorTests.swift @@ -0,0 +1,63 @@ +// +// FileOutputProcessorTests.swift +// Aespa-iOS-testTests +// +// Created by 이영빈 on 2023/06/16. +// + +import XCTest +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class FileOutputProcessorTests: XCTestCase { + var fileOutput: MockAespaFileOutputRepresentable! + + override func setUpWithError() throws { + fileOutput = MockAespaFileOutputRepresentable() + } + + override func tearDownWithError() throws { + fileOutput = nil + } + + func testStartRecording() throws { + let url = URL(string: "/data/some.mp4")! + let delegate = MockDelegate() + let processor = StartRecordProcessor(filePath: url, delegate: delegate) + + stub(fileOutput) { proxy in + when(proxy.getConnection( + with: equal(to: AVMediaType.video)) + ).thenReturn(MockAespaCaptureConnectionRepresentable()) + + when(proxy.startRecording( + to: equal(to: url), recordingDelegate: equal(to: delegate)) + ).thenDoNothing() + } + + try processor.process(fileOutput) + verify(fileOutput) + .startRecording(to: equal(to: url), recordingDelegate: equal(to: delegate)) + .with(returnType: Void.self) + } + + func testStopRecording() throws { + let processor = FinishRecordProcessor() + + stub(fileOutput) { proxy in + when(proxy.stopRecording()).thenDoNothing() + } + + try processor.process(fileOutput) + verify(fileOutput) + .stopRecording() + .with(returnType: Void.self) + } +} + +fileprivate class MockDelegate: NSObject, AVCaptureFileOutputRecordingDelegate { + func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {} +} diff --git a/Tests/Aespa-iOS-testTests/Tuner/ConnectionTunerTests.swift b/Tests/Aespa-iOS-testTests/Tuner/ConnectionTunerTests.swift new file mode 100644 index 0000000..90767ff --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Tuner/ConnectionTunerTests.swift @@ -0,0 +1,61 @@ +// +// ConnectionTunerTests.swift +// Aespa-iOS-testTests +// +// Created by 이영빈 on 2023/06/16. +// + +import XCTest +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class ConnectionTunerTests: XCTestCase { + var connection: MockAespaCaptureConnectionRepresentable! + + override func setUpWithError() throws { + connection = MockAespaCaptureConnectionRepresentable() + } + + override func tearDownWithError() throws { + connection = nil + } + + func testOrientationTuner() throws { + let orientation = AVCaptureVideoOrientation.portrait + let tuner = VideoOrientationTuner(orientation: orientation) + + stub(connection) { proxy in + when(proxy.setOrientation(to: equal(to: orientation))).then { value in + when(proxy.videoOrientation.get).thenReturn(orientation) + } + } + + try tuner.tune(connection) + verify(connection) + .setOrientation(to: equal(to: orientation)) + .with(returnType: Void.self) + + XCTAssertEqual(connection.videoOrientation, orientation) + } + + func testStabilizationTuner() throws { + let mode = AVCaptureVideoStabilizationMode.auto + let tuner = VideoStabilizationTuner(stabilzationMode: mode) + + stub(connection) { proxy in + when(proxy.setStabilizationMode(to: equal(to: mode))).then { value in + when(proxy.preferredVideoStabilizationMode.get).thenReturn(mode) + } + } + + tuner.tune(connection) + verify(connection) + .setStabilizationMode(to: equal(to: mode)) + .with(returnType: Void.self) + + XCTAssertEqual(connection.preferredVideoStabilizationMode, mode) + } +} diff --git a/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift b/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift new file mode 100644 index 0000000..9af5169 --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift @@ -0,0 +1,65 @@ +// +// DeviceTunerTests.swift +// Aespa-iOS-testTests +// +// Created by 이영빈 on 2023/06/16. +// + +import XCTest +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class DeviceTunerTests: XCTestCase { + private var device: MockAespaCaptureDeviceRepresentable! + + override func setUpWithError() throws { + device = MockAespaCaptureDeviceRepresentable() + } + + override func tearDownWithError() throws { + device = nil + } + + func testAutoFocusTuner() throws { + let mode = AVCaptureDevice.FocusMode.locked + let tuner = AutoFocusTuner(mode: mode) + + stub(device) { proxy in + when(proxy.isFocusModeSupported(equal(to: mode))).thenReturn(true) + when(proxy.setFocusMode(equal(to: mode))).then { mode in + when(proxy.focusMode.get).thenReturn(mode) + } + } + + try tuner.tune(device) + verify(device) + .setFocusMode(equal(to: mode)) + .with(returnType: Void.self) + + XCTAssertEqual(device.focusMode, mode) + } + + func testZoomTuner() throws { + let factor = 1.23 + let tuner = ZoomTuner(zoomFactor: factor) + + stub(device) { proxy in + when(proxy.setZoomFactor(equal(to: factor))).then { factor in + when(proxy.videoZoomFactor.get).thenReturn(factor) + } + } + + tuner.tune(device) + verify(device) + .setZoomFactor(equal(to: factor)) + .with(returnType: Void.self) + + XCTAssertEqual(device.videoZoomFactor, factor) + } +} + +class MockDevice: AVCaptureDevice { +} diff --git a/Tests/Aespa-iOS-testTests/Tuner/SessionTunerTests.swift b/Tests/Aespa-iOS-testTests/Tuner/SessionTunerTests.swift index 164bbf2..14fcfa4 100644 --- a/Tests/Aespa-iOS-testTests/Tuner/SessionTunerTests.swift +++ b/Tests/Aespa-iOS-testTests/Tuner/SessionTunerTests.swift @@ -13,18 +13,15 @@ import Cuckoo @testable import Aespa final class SessionTunerTests: XCTestCase { - var mockSession: MockAespaCoreSession! var mockSessionProtocol: MockAespaCoreSessionRepresentable! override func setUpWithError() throws { - let option = AespaOption(albumName: "test") - mockSession = MockAespaCoreSession(option: option) mockSessionProtocol = MockAespaCoreSessionRepresentable() } override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. + mockSessionProtocol = nil } func testQualityTuner() throws { diff --git a/Tests/Aespa-iOS-testTests/Util/AlbumUtilTests.swift b/Tests/Aespa-iOS-testTests/Util/AlbumUtilTests.swift new file mode 100644 index 0000000..875b575 --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Util/AlbumUtilTests.swift @@ -0,0 +1,79 @@ +// +// AlbumUtilTests.swift +// Aespa-iOS-testTests +// +// Created by Young Bin on 2023/06/17. +// + +import XCTest +import Photos +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class AlbumUtilTests: XCTestCase { + var mockLibrary: MockAespaAssetLibraryRepresentable! + var mockAlbum: MockAespaAssetCollectionRepresentable! + + override func setUpWithError() throws { + mockLibrary = MockAespaAssetLibraryRepresentable() + mockAlbum = MockAespaAssetCollectionRepresentable() + } + + override func tearDownWithError() throws { + mockLibrary = nil + mockAlbum = nil + } + + func testAlbumImporter_albumExists() throws { + let albumName = "test" + let options = PHFetchOptions() + let mockAlbum = MockAespaAssetCollectionRepresentable() + + // Success case + stub(mockLibrary) { proxy in + when(proxy.fetchAlbum(title: albumName, + fetchOptions: equal(to: options))).thenReturn(mockAlbum) + when(proxy.performChangesAndWait(anyClosure())).thenDoNothing() + } + + let _: MockAespaAssetCollectionRepresentable = try AlbumImporter.getAlbum(name: albumName, in: mockLibrary, options) + + verify(mockLibrary) + .fetchAlbum(title: albumName, fetchOptions: equal(to: options)) + .with(returnType: MockAespaAssetCollectionRepresentable?.self) + + verify(mockLibrary, never()) + .performChangesAndWait(anyClosure()) + .with(returnType: Void.self) + } + + func testAlbumImporter_albumNotExists() throws { + let albumName = "test" + let options = PHFetchOptions() + let mockAlbum = MockAespaAssetCollectionRepresentable() + + // Success case + stub(mockLibrary) { proxy in + when(proxy.fetchAlbum(title: albumName, + fetchOptions: equal(to: options)) + as ProtocolStubFunction<(String, PHFetchOptions), MockAespaAssetCollectionRepresentable?> + ).thenReturn(nil) + + when(proxy.performChangesAndWait(anyClosure())).thenDoNothing() + } + + let result: MockAespaAssetCollectionRepresentable? = try? AlbumImporter.getAlbum(name: albumName, in: mockLibrary, options) + XCTAssertNil(result) + + verify(mockLibrary, times(2)) + .fetchAlbum(title: albumName, fetchOptions: equal(to: options)) + .with(returnType: MockAespaAssetCollectionRepresentable?.self) + + verify(mockLibrary, times(1)) + .performChangesAndWait(anyClosure()) + .with(returnType: Void.self) + } +} diff --git a/Tests/Aespa-iOS-testTests/Util/FileUtilTests.swift b/Tests/Aespa-iOS-testTests/Util/FileUtilTests.swift new file mode 100644 index 0000000..edcc85f --- /dev/null +++ b/Tests/Aespa-iOS-testTests/Util/FileUtilTests.swift @@ -0,0 +1,66 @@ +// +// FileGeneratorTests.swift +// Aespa-iOS-testTests +// +// Created by Young Bin on 2023/06/18. +// + +import XCTest +import Photos +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class FileGeneratorTests: XCTestCase { + let mockVideo = MockVideo() + var mockFileManager: MockFileManager! + + override func setUpWithError() throws { + mockFileManager = MockFileManager() + mockFileManager.urlsStub = [mockVideo.dirPath] + } + + override func tearDownWithError() throws { + mockFileManager = nil + } + + func testGenerate() throws { + let filePath = mockVideo.path! + let date = Date() + let file = VideoFileGenerator.generate(with: filePath, date: date) + + XCTAssertEqual(file.path, filePath) + XCTAssertEqual(file.generatedDate, date) + } + + func testGenerateThumbnail() throws { + let filePath = mockVideo.path! + let thumbnail = VideoFileGenerator.generateThumbnail(for: filePath) + + XCTAssertNotNil(thumbnail) + } + + func testRequestDirPath() throws { + let albumName = "Test" + let dirPath = try VideoFilePathProvider.requestDirectoryPath(from: mockFileManager, name: albumName) + + XCTAssertEqual(dirPath.lastPathComponent, albumName) + } + + func testRequestFilePath() throws { + let albumName = "Test" + let fileName = "Testfile" + let `extension` = "mp4" + let expectedSuffix = "/\(albumName)/\(fileName).\(`extension`)" + + let filePath = try VideoFilePathProvider.requestFilePath( + from: mockFileManager, + directoryName: albumName, + fileName: fileName, + extension: `extension`) + + XCTAssertTrue(filePath.absoluteString.hasSuffix(expectedSuffix)) + } +} diff --git a/Tests/Cuckoo/gen-mocks.sh b/Tests/Cuckoo/gen-mocks.sh deleted file mode 100755 index 3c8c83c..0000000 --- a/Tests/Cuckoo/gen-mocks.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -if [ ! -f run ]; then - curl -Lo run https://raw.githubusercontent.com/Brightify/Cuckoo/master/run && chmod +x run - ./run --download -fi - -PROJECT_NAME="Aespa" -TESTER_NAME="Aespa-iOS-test" -PACKAGE_SOURCE_PATH="../../Sources/Aespa" -OUTPUT_FILE="../${TESTER_NAME}Tests/Mock/GeneratedMocks.swift" -SWIFT_FILES=$(find "$PACKAGE_SOURCE_PATH" -type f -name "*.swift" -print0 | xargs -0) - -echo "✅ Generated Mocks File = ${OUTPUT_FILE}" -echo "✅ Mocks Input Directory = ${PACKAGE_SOURCE_PATH}" - -./run generate --testable "${PROJECT_NAME}" --output "${OUTPUT_FILE}" ${SWIFT_FILES} From b72f6e34ea38826cf3d42b613802f0a502c890f8 Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 17 Jun 2023 10:08:35 +0900 Subject: [PATCH 03/11] Add torchMode support(#8) --- Sources/Aespa/AespaSession.swift | 45 ++++++++++++++++++- .../AVCaptureDevice+AespaRepresentable.swift | 21 ++++++++- Sources/Aespa/Tuner/AespaTuning.swift | 4 ++ Sources/Aespa/Tuner/Device/TorchTuner.swift | 23 ++++++++++ .../Aespa-iOS-test.xcodeproj/project.pbxproj | 22 +++++++++ .../Tuner/DeviceTunerTests.swift | 21 +++++++++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 Sources/Aespa/Tuner/Device/TorchTuner.swift diff --git a/Sources/Aespa/AespaSession.swift b/Sources/Aespa/AespaSession.swift index e7fc13b..b32483e 100644 --- a/Sources/Aespa/AespaSession.swift +++ b/Sources/Aespa/AespaSession.swift @@ -307,7 +307,30 @@ open class AespaSession { return self } - + + /// Sets the torch mode and level for the video recording session. + /// + /// If an error occurs during the operation, the error is logged. + /// + /// - Parameters: + /// - mode: The desired torch mode (AVCaptureDevice.TorchMode). + /// - level: The desired torch level as a Float between 0.0 and 1.0. + /// + /// - Returns: Returns self, allowing additional settings to be configured. + /// + /// - Note: This function might throw an error if the torch mode is not supported, + /// or the specified level is not within the acceptable range. + @discardableResult + public func setTorch(mode: AVCaptureDevice.TorchMode, level: Float) -> AespaSession { + do { + try self.setTorchWitherror(mode: mode, level: level) + } catch let error { + Logger.log(error: error) // Logs any errors encountered during the operation + } + + return self + } + // MARK: - Throwing/// Starts the recording of a video session. /// /// - Throws: `AespaError` if the video file path request fails, orientation setting fails, or starting the recording fails. @@ -460,7 +483,25 @@ open class AespaSession { return self } - + /// Sets the torch mode and level for the video recording session. + /// + /// - Parameters: + /// - mode: The desired torch mode (AVCaptureDevice.TorchMode). + /// - level: The desired torch level as a Float between 0.0 and 1.0. + /// + /// - Returns: Returns self, allowing additional settings to be configured. + /// + /// - Throws: Throws an error if setting the torch mode or level fails. + /// + /// - Note: This function might throw an error if the torch mode is not supported, + /// or the specified level is not within the acceptable range. + @discardableResult + public func setTorchWitherror(mode: AVCaptureDevice.TorchMode, level: Float) throws -> AespaSession { + let tuner = TorchTuner(level: level, torchMode: mode) + try coreSession.run(tuner) + return self + } + // MARK: - Customizable /// This function provides a way to use a custom tuner to modify the current session. The tuner must conform to `AespaSessionTuning`. /// diff --git a/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift index 104899d..e1a1e09 100644 --- a/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift @@ -9,19 +9,36 @@ import Foundation import AVFoundation protocol AespaCaptureDeviceRepresentable { + var hasTorch: Bool { get } var focusMode: AVCaptureDevice.FocusMode { get set } + var flashMode: AVCaptureDevice.FlashMode { get set } var videoZoomFactor: CGFloat { get set } var maxResolution: Double? { get } func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool - func setFocusMode(_ focusMode: AVCaptureDevice.FocusMode) throws func setZoomFactor(_ factor: CGFloat) + func setFocusMode(_ focusMode: AVCaptureDevice.FocusMode) + func setTorchMode(_ torchMode: AVCaptureDevice.TorchMode) + func setTorchModeOn(level torchLevel: Float) throws } extension AVCaptureDevice: AespaCaptureDeviceRepresentable { - func setFocusMode(_ focusMode: FocusMode) throws { + func setTorchMode(_ torchMode: TorchMode) { + switch torchMode { + case .off: + self.torchMode = .off + case .on: + self.torchMode = .on + case .auto: + self.torchMode = .auto + @unknown default: + self.torchMode = .off + } + } + + func setFocusMode(_ focusMode: FocusMode) { self.focusMode = focusMode } diff --git a/Sources/Aespa/Tuner/AespaTuning.swift b/Sources/Aespa/Tuner/AespaTuning.swift index 5da761e..a536552 100644 --- a/Sources/Aespa/Tuner/AespaTuning.swift +++ b/Sources/Aespa/Tuner/AespaTuning.swift @@ -32,3 +32,7 @@ protocol AespaDeviceTuning { var needLock: Bool { get } func tune(_ device: T) throws } + +extension AespaDeviceTuning { + var needLock: Bool { true } +} diff --git a/Sources/Aespa/Tuner/Device/TorchTuner.swift b/Sources/Aespa/Tuner/Device/TorchTuner.swift new file mode 100644 index 0000000..6081f90 --- /dev/null +++ b/Sources/Aespa/Tuner/Device/TorchTuner.swift @@ -0,0 +1,23 @@ +// +// TorchTuner.swift +// +// +// Created by 이영빈 on 2023/06/17. +// + +import Foundation +import AVFoundation + +struct TorchTuner: AespaDeviceTuning { + let level: Float + let torchMode: AVCaptureDevice.TorchMode + + func tune(_ device: T) throws where T : AespaCaptureDeviceRepresentable { + guard device.hasTorch else { + throw AespaError.device(reason: .unsupported) + } + + device.setTorchMode(torchMode) + try device.setTorchModeOn(level: level) + } +} diff --git a/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj b/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj index 5a54d85..ee5da45 100644 --- a/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj +++ b/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj @@ -203,6 +203,7 @@ 07939F892A343B8D00DFA8BB /* Sources */, 07939F8A2A343B8D00DFA8BB /* Frameworks */, 07939F8B2A343B8D00DFA8BB /* Resources */, + 9B9CD51D2A3D3B430075B72F /* ShellScript */, ); buildRules = ( ); @@ -296,6 +297,27 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 9B9CD51D2A3D3B430075B72F /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + "$(PROJECT_DIR)/Aespa-iOS-testTests/Mock/GeneratedMocks.swift", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd ../Scripts\n./gen-mocks.sh\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 07939F892A343B8D00DFA8BB /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift b/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift index 9af5169..d5f232c 100644 --- a/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift +++ b/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift @@ -59,6 +59,27 @@ final class DeviceTunerTests: XCTestCase { XCTAssertEqual(device.videoZoomFactor, factor) } + + func testTorchTuner() throws { + let level: Float = 0.15 + let mode: AVCaptureDevice.TorchMode = .auto + let tuner = TorchTuner(level: level, torchMode: mode) + + stub(device) { proxy in + when(proxy.hasTorch.get).thenReturn(true) + when(proxy.setTorchMode(equal(to: mode))).thenDoNothing() + when(proxy.setTorchModeOn(level: level)).thenDoNothing() + } + + try tuner.tune(device) + verify(device) + .setTorchMode(equal(to: mode)) + .with(returnType: Void.self) + + verify(device) + .setTorchModeOn(level: level) + .with(returnType: Void.self) + } } class MockDevice: AVCaptureDevice { From 12ad43649a823ae560066c6d4b4258a76950aeaa Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 19 Jun 2023 13:14:53 +0900 Subject: [PATCH 04/11] Add photo capturing (#9) --- Sources/Aespa/AespaError.swift | 8 +++ Sources/Aespa/AespaSession.swift | 23 ++++++- Sources/Aespa/Core/AespaCoreCamera.swift | 63 +++++++++++++++++++ Sources/Aespa/Core/AespaCoreRecorder.swift | 2 +- Sources/Aespa/Core/AespaCoreSession.swift | 2 +- ...apturePhotoOutput+AespaRepresentable.swift | 20 ++++++ .../AespaCoreSession+AespaRepresentable.swift | 34 +++++++++- Sources/Aespa/Data/PhotoFile.swift | 30 +++++++++ Sources/Aespa/Data/VideoFile.swift | 1 - Sources/Aespa/Processor/AespaProcessing.swift | 6 +- .../Capture/CapturePhotoProcessor.swift | 26 ++++++++ .../Record/FinishRecordProcessor.swift | 2 +- .../Record/StartRecordProcessor.swift | 3 +- .../Aespa/Tuner/Device/AutoFocusTuner.swift | 2 +- .../Tuner/Session/SessionLaunchTuner.swift | 1 + .../Util/Video/File/PhotoFileGenerator.swift | 18 ++++++ 16 files changed, 227 insertions(+), 14 deletions(-) create mode 100644 Sources/Aespa/Core/AespaCoreCamera.swift create mode 100644 Sources/Aespa/Core/Representable/AVCapturePhotoOutput+AespaRepresentable.swift create mode 100644 Sources/Aespa/Data/PhotoFile.swift create mode 100644 Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift create mode 100644 Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift diff --git a/Sources/Aespa/AespaError.swift b/Sources/Aespa/AespaError.swift index a0f85f3..c0b069a 100644 --- a/Sources/Aespa/AespaError.swift +++ b/Sources/Aespa/AespaError.swift @@ -12,6 +12,7 @@ public enum AespaError: LocalizedError { case device(reason: DeviceErrorReason) case permission(reason: PermissionErrorReason) case album(reason: AlbumErrorReason) + case file(reason: FileErrorReason) public var errorDescription: String? { switch self { @@ -23,6 +24,8 @@ public enum AespaError: LocalizedError { return reason.rawValue case .album(reason: let reason): return reason.rawValue + case .file(reason: let reason): + return reason.rawValue } } } @@ -67,4 +70,9 @@ public extension AespaError { case notVideoURL = "Received URL is not a video type." } + + enum FileErrorReason: String { + case unableToFlatten = + "Cannot take a video because camera permissions are denied." + } } diff --git a/Sources/Aespa/AespaSession.swift b/Sources/Aespa/AespaSession.swift index b32483e..a67a9a6 100644 --- a/Sources/Aespa/AespaSession.swift +++ b/Sources/Aespa/AespaSession.swift @@ -22,6 +22,7 @@ open class AespaSession { private let option: AespaOption private let coreSession: AespaCoreSession private let recorder: AespaCoreRecorder + private let camera: AespaCoreCamera private let fileManager: AespaCoreFileManager private let albumManager: AespaCoreAlbumManager @@ -40,6 +41,7 @@ open class AespaSession { option: option, session: session, recorder: .init(core: session), + camera: .init(core: session), fileManager: .init(enableCaching: option.asset.useVideoFileCache), albumManager: .init(albumName: option.asset.albumName) ) @@ -49,12 +51,14 @@ open class AespaSession { option: AespaOption, session: AespaCoreSession, recorder: AespaCoreRecorder, + camera: AespaCoreCamera, fileManager: AespaCoreFileManager, albumManager: AespaCoreAlbumManager ) { self.option = option self.coreSession = session self.recorder = recorder + self.camera = camera self.fileManager = fileManager self.albumManager = albumManager @@ -155,7 +159,7 @@ open class AespaSession { ) { Task(priority: .utility) { do { - let videoFile = try await self.stopRecording() + let videoFile = try await self.stopRecordingWithError() return completionHandler(.success(videoFile)) } catch let error { Logger.log(error: error) @@ -352,13 +356,12 @@ open class AespaSession { try recorder.startRecording(in: filePath) } - /// Stops the ongoing video recording session and attempts to add the video file to the album. /// /// Supporting `async`, you can use this method in Swift Concurrency's context /// /// - Throws: `AespaError` if stopping the recording fails. - public func stopRecording() async throws -> VideoFile { + public func stopRecordingWithError() async throws -> VideoFile { let videoFilePath = try await recorder.stopRecording() let videoFile = VideoFileGenerator.generate(with: videoFilePath, date: Date()) @@ -368,6 +371,20 @@ open class AespaSession { return videoFile } + public func captureWithError(setting: AVCapturePhotoSettings) async throws -> PhotoFile { + let rawPhotoAsset = try await camera.capture(setting: setting) + guard let rawPhotoData = rawPhotoAsset.fileDataRepresentation() else { + throw AespaError.file(reason: .unableToFlatten) + } + + let photoFile = PhotoFileGenerator.generate(data: rawPhotoData, date: Date()) + + try await albumManager.addToAlbum(filePath: videoFilePath) + videoFileBufferSubject.send(.success(videoFile)) + + return videoFile + } + /// Mutes the audio input for the video recording session. /// /// - Throws: `AespaError` if the session fails to run the tuner. diff --git a/Sources/Aespa/Core/AespaCoreCamera.swift b/Sources/Aespa/Core/AespaCoreCamera.swift new file mode 100644 index 0000000..207aeb3 --- /dev/null +++ b/Sources/Aespa/Core/AespaCoreCamera.swift @@ -0,0 +1,63 @@ +// +// AespaCoreCamera.swift +// +// +// Created by Young Bin on 2023/06/18. +// + +import Combine +import Foundation +import AVFoundation + +/// Capturing a photo and responsible for notifying the result +class AespaCoreCamera: NSObject { + private let core: AespaCoreSession + + private let fileIOResultSubject = PassthroughSubject, Never>() + private var fileIOResultSubsciption: Cancellable? + + init(core: AespaCoreSession) { + self.core = core + } + + func run(processor: T) throws { + guard let output = core.photoOutput else { + throw AespaError.session(reason: .cannotFindConnection) + } + + try processor.process(output) + } +} + +extension AespaCoreCamera { + func capture(setting: AVCapturePhotoSettings) async throws -> AVCapturePhoto { + let processor = CapturePhotoProcessor(setting: setting, delegate: self) + try run(processor: processor) + + return try await withCheckedThrowingContinuation { continuation in + fileIOResultSubsciption = fileIOResultSubject + .subscribe(on: DispatchQueue.global()) + .sink(receiveValue: { result in + switch result { + case .success(let photo): + continuation.resume(returning: photo) + case .failure(let error): + continuation.resume(throwing: error) + } + }) + } + } +} + +extension AespaCoreCamera: AVCapturePhotoCaptureDelegate { + func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { + Logger.log(message: "Photo captured") + + if let error { + fileIOResultSubject.send(.failure(error)) + Logger.log(error: error) + } else { + fileIOResultSubject.send(.success(photo)) + } + } +} diff --git a/Sources/Aespa/Core/AespaCoreRecorder.swift b/Sources/Aespa/Core/AespaCoreRecorder.swift index e9f8829..ff2ef38 100644 --- a/Sources/Aespa/Core/AespaCoreRecorder.swift +++ b/Sources/Aespa/Core/AespaCoreRecorder.swift @@ -21,7 +21,7 @@ class AespaCoreRecorder: NSObject { self.core = core } - func run(processor: T) throws { + func run(processor: T) throws { guard let output = core.movieFileOutput else { throw AespaError.session(reason: .cannotFindConnection) } diff --git a/Sources/Aespa/Core/AespaCoreSession.swift b/Sources/Aespa/Core/AespaCoreSession.swift index fe6ae19..0a3938d 100644 --- a/Sources/Aespa/Core/AespaCoreSession.swift +++ b/Sources/Aespa/Core/AespaCoreSession.swift @@ -47,7 +47,7 @@ class AespaCoreSession: AVCaptureSession { try tuner.tune(connection) } - func run(_ processor: T) throws { + func run(_ processor: T) throws { guard let output = self.movieFileOutput else { throw AespaError.session(reason: .cannotFindConnection) } diff --git a/Sources/Aespa/Core/Representable/AVCapturePhotoOutput+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCapturePhotoOutput+AespaRepresentable.swift new file mode 100644 index 0000000..0e35ce2 --- /dev/null +++ b/Sources/Aespa/Core/Representable/AVCapturePhotoOutput+AespaRepresentable.swift @@ -0,0 +1,20 @@ +// +// AVCapturePhotoOutput+AespaRepresentable.swift +// +// +// Created by Young Bin on 2023/06/18. +// + +import Foundation +import AVFoundation + +protocol AespaPhotoOutputRepresentable { + func capturePhoto(with: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate) + func getConnection(with mediaType: AVMediaType) -> AespaCaptureConnectionRepresentable? +} + +extension AVCapturePhotoOutput: AespaPhotoOutputRepresentable { + func getConnection(with mediaType: AVMediaType) -> AespaCaptureConnectionRepresentable? { + return connection(with: mediaType) + } +} diff --git a/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift index bab9173..d1534be 100644 --- a/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift @@ -53,6 +53,10 @@ public protocol AespaCoreSessionRepresentable { /// Throws an error if the operation fails. func addMovieFileOutput() throws + /// Adds photo file output to the session. + /// Throws an error if the operation fails. + func addCapturePhotoOutput() throws + /// Sets the position of the camera. /// Throws an error if the operation fails. func setCameraPosition(to position: AVCaptureDevice.Position, device deviceType: AVCaptureDevice.DeviceType?) throws @@ -80,9 +84,20 @@ extension AespaCoreSession: AespaCoreSessionRepresentable { } var movieFileOutput: AVCaptureMovieFileOutput? { - let output = self.outputs.first as? AVCaptureMovieFileOutput + let output = self.outputs.first { + $0 as? AVCaptureMovieFileOutput != nil + } + - return output + return output as? AVCaptureMovieFileOutput + } + + var photoOutput: AVCapturePhotoOutput? { + let output = self.outputs.first { + $0 as? AVCapturePhotoOutput != nil + } + + return output as? AVCapturePhotoOutput } var previewLayer: AVCaptureVideoPreviewLayer { @@ -143,7 +158,6 @@ extension AespaCoreSession: AespaCoreSessionRepresentable { } } - func addMovieFileOutput() throws { guard self.movieFileOutput == nil else { // return itself if output is already set @@ -158,6 +172,20 @@ extension AespaCoreSession: AespaCoreSessionRepresentable { self.addOutput(fileOutput) } + func addCapturePhotoOutput() throws { + guard self.photoOutput == nil else { + // return itself if output is already set + return + } + + let photoOutput = AVCapturePhotoOutput() + guard self.canAddOutput(photoOutput) else { + throw AespaError.device(reason: .unableToSetOutput) + } + + self.addOutput(photoOutput) + } + // MARK: - Option related func setCameraPosition(to position: AVCaptureDevice.Position,device deviceType: AVCaptureDevice.DeviceType?) throws { let session = self diff --git a/Sources/Aespa/Data/PhotoFile.swift b/Sources/Aespa/Data/PhotoFile.swift new file mode 100644 index 0000000..1b71cc2 --- /dev/null +++ b/Sources/Aespa/Data/PhotoFile.swift @@ -0,0 +1,30 @@ +// +// PhotoFile.swift +// +// +// Created by 이영빈 on 2023/06/18. +// + +import UIKit +import SwiftUI +import Foundation + +public struct PhotoFile: Identifiable, Equatable { + public let id = UUID() + + /// A `Data` value containing image's raw data + public let data: Data + + /// A `Date` value keeps the date it's generated + public let generatedDate: Date + + /// An optional thumbnail generated from the video with `UIImage` type. + /// This will be `nil` if the thumbnail could not be generated for some reason. + public var thumbnail: UIImage? +} + +extension PhotoFile: Comparable { + public static func < (lhs: PhotoFile, rhs: PhotoFile) -> Bool { + lhs.generatedDate > rhs.generatedDate + } +} diff --git a/Sources/Aespa/Data/VideoFile.swift b/Sources/Aespa/Data/VideoFile.swift index 864fd74..6849c4f 100644 --- a/Sources/Aespa/Data/VideoFile.swift +++ b/Sources/Aespa/Data/VideoFile.swift @@ -28,7 +28,6 @@ public struct VideoFile: Equatable { /// UI related extension methods public extension VideoFile { - /// An optional thumbnail generated from the video with SwiftUI `Image` type. /// This will be `nil` if the thumbnail could not be generated for some reason. var thumbnailImage: Image? { diff --git a/Sources/Aespa/Processor/AespaProcessing.swift b/Sources/Aespa/Processor/AespaProcessing.swift index 03db488..b4422bb 100644 --- a/Sources/Aespa/Processor/AespaProcessing.swift +++ b/Sources/Aespa/Processor/AespaProcessing.swift @@ -9,7 +9,11 @@ import Photos import Foundation import AVFoundation -protocol AespaFileOutputProcessing { +protocol AespaCapturePhotoOutputProcessing { + func process(_ output: T) throws +} + +protocol AespaMovieFileOutputProcessing { func process(_ output: T) throws } diff --git a/Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift b/Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift new file mode 100644 index 0000000..bc5b3f9 --- /dev/null +++ b/Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift @@ -0,0 +1,26 @@ +// +// CapturePhotoProcessor.swift +// +// +// Created by 이영빈 on 2023/06/18. +// + +import AVFoundation + +struct CapturePhotoProcessor: AespaCapturePhotoOutputProcessing { + let setting: AVCapturePhotoSettings + let delegate: AVCapturePhotoCaptureDelegate + + init(setting: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate) { + self.setting = setting + self.delegate = delegate + } + + func process(_ output: T) throws where T : AespaPhotoOutputRepresentable { + guard output.getConnection(with: .video) != nil else { + throw AespaError.session(reason: .cannotFindConnection) + } + + output.capturePhoto(with: setting, delegate: delegate) + } +} diff --git a/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift b/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift index 3e17b39..949d614 100644 --- a/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift +++ b/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift @@ -8,7 +8,7 @@ import AVFoundation -struct FinishRecordProcessor: AespaFileOutputProcessing { +struct FinishRecordProcessor: AespaMovieFileOutputProcessing { func process(_ output: T) throws { output.stopRecording() } diff --git a/Sources/Aespa/Processor/Record/StartRecordProcessor.swift b/Sources/Aespa/Processor/Record/StartRecordProcessor.swift index 1ad9657..d219bb0 100644 --- a/Sources/Aespa/Processor/Record/StartRecordProcessor.swift +++ b/Sources/Aespa/Processor/Record/StartRecordProcessor.swift @@ -5,10 +5,9 @@ // Created by 이영빈 on 2023/06/02. // -import Combine import AVFoundation -struct StartRecordProcessor: AespaFileOutputProcessing { +struct StartRecordProcessor: AespaMovieFileOutputProcessing { let filePath: URL let delegate: AVCaptureFileOutputRecordingDelegate diff --git a/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift b/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift index 91bbf9a..cc97af2 100644 --- a/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift +++ b/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift @@ -17,6 +17,6 @@ struct AutoFocusTuner: AespaDeviceTuning { throw AespaError.device(reason: .unsupported) } - try device.setFocusMode(mode) + device.setFocusMode(mode) } } diff --git a/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift b/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift index f28ad8f..9d7dc1b 100644 --- a/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift +++ b/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift @@ -15,6 +15,7 @@ struct SessionLaunchTuner: AespaSessionTuning { try session.addMovieInput() try session.addMovieFileOutput() + try session.addCapturePhotoOutput() session.startRunning() } } diff --git a/Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift b/Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift new file mode 100644 index 0000000..b54c0f0 --- /dev/null +++ b/Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by 이영빈 on 2023/06/18. +// + +import UIKit +import Foundation + +struct PhotoFileGenerator { + static func generate(data: Data, date: Date) -> PhotoFile { + return PhotoFile( + data: data, + generatedDate: date, + thumbnail: UIImage(data: data)) + } +} From e51347838322854df78851c0951723ca0601b879 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 19 Jun 2023 13:28:08 +0900 Subject: [PATCH 05/11] Change test hosting app project --- .github/workflows/unit-test.yml | 5 +- .gitignore | 13 +- .swiftlint.yml | 54 +++ Scripts/gen-mocks.sh | 5 +- Sources/Aespa/AespaSession.swift | 37 +- .../Aespa/Core/AespaCoreAlbumManager.swift | 7 +- .../Asset/PhotoAssetAdditionProcessor.swift | 35 ++ ...wift => VideoAssetAdditionProcessor.swift} | 4 +- .../Video/File/VideoFilePathProvider.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 16 - .../xcshareddata/xcschemes/Aespa.xcscheme | 66 ---- Tests/Aespa-iOS-test.xctestplan | 32 -- Tests/Aespa-iOS-testTests/Mock/.gitkeep | 0 Tests/Package.swift | 3 + ...S-testTests.xctestplan => Test.xctestplan} | 8 +- .../project.pbxproj | 371 +++++++++--------- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 14 + .../xcshareddata/xcschemes/Test.xcscheme} | 26 +- .../xcschemes/TestHostApp.xcscheme} | 52 +-- .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../ContentView.swift | 4 +- .../Preview Assets.xcassets/Contents.json | 0 .../TestHostAppApp.swift} | 8 +- .../Mock/MockFileManager.swift | 0 .../Mock/Video/MockVideo.swift | 0 .../Mock/Video/video.mp4 | Bin .../Processor/AssetProcessorTests.swift | 2 +- .../Processor/FileOutputProcessorTests.swift | 0 .../Tuner/ConnectionTunerTests.swift | 0 .../Tuner/DeviceTunerTests.swift | 0 .../Tuner/SessionTunerTests.swift | 11 + .../Util/AlbumUtilTests.swift | 0 .../Util/FileUtilTests.swift | 0 37 files changed, 381 insertions(+), 394 deletions(-) create mode 100644 .swiftlint.yml create mode 100644 Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift rename Sources/Aespa/Processor/Asset/{AssetAdditionProcessor.swift => VideoAssetAdditionProcessor.swift} (94%) delete mode 100644 Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa.xcscheme delete mode 100644 Tests/Aespa-iOS-test.xctestplan delete mode 100644 Tests/Aespa-iOS-testTests/Mock/.gitkeep create mode 100644 Tests/Package.swift rename Tests/{Aespa-iOS-testTests.xctestplan => Test.xctestplan} (67%) rename Tests/{Aespa-iOS-test.xcodeproj => TestHostApp.xcodeproj}/project.pbxproj (51%) rename Tests/{Aespa-iOS-test.xcodeproj => TestHostApp.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename Tests/{Aespa-iOS-test.xcodeproj => TestHostApp.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename Tests/{Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme => TestHostApp.xcodeproj/xcshareddata/xcschemes/Test.xcscheme} (65%) rename Tests/{Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test.xcscheme => TestHostApp.xcodeproj/xcshareddata/xcschemes/TestHostApp.xcscheme} (59%) rename Tests/{Aespa-iOS-test => TestHostApp}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Tests/{Aespa-iOS-test => TestHostApp}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Tests/{Aespa-iOS-test => TestHostApp}/Assets.xcassets/Contents.json (100%) rename Tests/{Aespa-iOS-test => TestHostApp}/ContentView.swift (87%) rename Tests/{Aespa-iOS-test => TestHostApp}/Preview Content/Preview Assets.xcassets/Contents.json (100%) rename Tests/{Aespa-iOS-test/Aespa_iOS_testApp.swift => TestHostApp/TestHostAppApp.swift} (51%) rename Tests/{Aespa-iOS-testTests => Tests}/Mock/MockFileManager.swift (100%) rename Tests/{Aespa-iOS-testTests => Tests}/Mock/Video/MockVideo.swift (100%) rename Tests/{Aespa-iOS-testTests => Tests}/Mock/Video/video.mp4 (100%) rename Tests/{Aespa-iOS-testTests => Tests}/Processor/AssetProcessorTests.swift (95%) rename Tests/{Aespa-iOS-testTests => Tests}/Processor/FileOutputProcessorTests.swift (100%) rename Tests/{Aespa-iOS-testTests => Tests}/Tuner/ConnectionTunerTests.swift (100%) rename Tests/{Aespa-iOS-testTests => Tests}/Tuner/DeviceTunerTests.swift (100%) rename Tests/{Aespa-iOS-testTests => Tests}/Tuner/SessionTunerTests.swift (93%) rename Tests/{Aespa-iOS-testTests => Tests}/Util/AlbumUtilTests.swift (100%) rename Tests/{Aespa-iOS-testTests => Tests}/Util/FileUtilTests.swift (100%) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 2e9d2d0..17e70f2 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -39,11 +39,12 @@ jobs: # Now do test DERIVED_DATA_PATH="./DerivedData" - TEST_SCHEME="Aespa-iOS-test-scheme" + PROJECT_NAME="TestHostApp" + TEST_SCHEME="Test" xcodebuild test \ -verbose \ - -project ${ROOT_PATH}/Tests/Aespa-iOS-test.xcodeproj \ + -project ${ROOT_PATH}/Tests/${PROJECT_NAME}.xcodeproj \ -scheme ${TEST_SCHEME} \ -destination 'platform=iOS Simulator,name=iPhone 14 Pro,OS=latest' \ -derivedDataPath ${DERIVED_DATA_PATH} \ diff --git a/.gitignore b/.gitignore index 4d62266..37b2d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,16 +8,7 @@ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc -Aespa-test-env/cuckoo_generator - -Aespa-test-env/run - -Tests/Cuckoo/cuckoo_generator - -Tests/Cuckoo/run - -Tests/Aespa-iOS-testTests/Mock/GeneratedMocks.swift - Scripts/cuckoo_generator - Scripts/run + +Tests/Tests/Mock/GeneratedMocks.swift diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..aba3349 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,54 @@ +disabled_rules: # rule identifiers turned on by default to exclude from running + - colon + - comma + - control_statement +opt_in_rules: # some rules are turned off by default, so you need to opt-in + - empty_count # Find all the available rules by running: `swiftlint rules` + +# Alternatively, specify all rules explicitly by uncommenting this option: +# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this +# - empty_parameters +# - vertical_whitespace + +analyzer_rules: # Rules run by `swiftlint analyze` + - explicit_self + +included: # paths to include during linting. `--path` is ignored if present. + - Source +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Carthage + - Tests + - Source/ExcludedFolder + - Source/ExcludedFile.swift + - Source/*/ExcludedFile.swift # Exclude files with a wildcard + +# If true, SwiftLint will not fail if no lintable files are found. +allow_zero_lintable_files: false + +# configurable rules can be customized from this configuration file +# binary rules can set their severity level +force_cast: warning # implicitly +force_try: + severity: warning # explicitly +# rules that have both warning and error levels, can set just the warning level +# implicitly +line_length: 110 +# they can set both implicitly with an array +type_body_length: + - 300 # warning + - 400 # error +# or they can set both explicitly +file_length: + warning: 500 + error: 1200 +# naming rules can set warnings/errors for min_length and max_length +# additionally they can set excluded names +type_name: + max_length: # warning and error + warning: 40 + error: 50 + allowed_symbols: ["_"] +identifier_name: + allowed_symbols: ["_"] + +reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) diff --git a/Scripts/gen-mocks.sh b/Scripts/gen-mocks.sh index f917447..12d9256 100755 --- a/Scripts/gen-mocks.sh +++ b/Scripts/gen-mocks.sh @@ -12,15 +12,14 @@ if [ ! -f run ]; then fi PROJECT_NAME="Aespa" -TESTER_NAME="Aespa-iOS-test" +TESTER_NAME="TestHostApp" PACKAGE_SOURCE_PATH="${ROOT_PATH}/Sources/Aespa" -OUTPUT_FILE="${ROOT_PATH}/Tests/${TESTER_NAME}Tests/Mock/GeneratedMocks.swift" +OUTPUT_FILE="${ROOT_PATH}/Tests/Tests/Mock/GeneratedMocks.swift" SWIFT_FILES=$(find "$PACKAGE_SOURCE_PATH" -type f -name "*.swift" -print0 | xargs -0) echo "✅ Generated Mocks File = ${OUTPUT_FILE}" echo "✅ Mocks Input Directory = ${PACKAGE_SOURCE_PATH}" - ./run --download generate --testable "${PROJECT_NAME}" --output "${OUTPUT_FILE}" ${SWIFT_FILES} # Check the exit status of the last command diff --git a/Sources/Aespa/AespaSession.swift b/Sources/Aespa/AespaSession.swift index a67a9a6..b78e95b 100644 --- a/Sources/Aespa/AespaSession.swift +++ b/Sources/Aespa/AespaSession.swift @@ -26,6 +26,7 @@ open class AespaSession { private let fileManager: AespaCoreFileManager private let albumManager: AespaCoreAlbumManager + private let photoFileBufferSubject: CurrentValueSubject?, Never> private let videoFileBufferSubject: CurrentValueSubject?, Never> private let previewLayerSubject: CurrentValueSubject @@ -63,6 +64,7 @@ open class AespaSession { self.albumManager = albumManager self.videoFileBufferSubject = .init(nil) + self.photoFileBufferSubject = .init(nil) self.previewLayerSubject = .init(nil) self.previewLayer = AVCaptureVideoPreviewLayer(session: session) @@ -117,7 +119,17 @@ open class AespaSession { .compactMap({ $0 }) .eraseToAnyPublisher() } - + + public var photoFilePublisher: AnyPublisher, Never> { + photoFileBufferSubject.handleEvents(receiveOutput: { status in + if case .failure(let error) = status { + Logger.log(error: error) + } + }) + .compactMap({ $0 }) + .eraseToAnyPublisher() + } + /// This publisher is responsible for emitting updates to the preview layer. /// /// A log message is printed to the console every time a new layer is pushed. @@ -168,6 +180,21 @@ open class AespaSession { } } + public func capturePhoto( + setting: AVCapturePhotoSettings, + _ completionHandler: @escaping (Result) -> Void = { _ in } + ) { + Task(priority: .utility) { + do { + let photoFile = try await self.captureWithError(setting: setting) + return completionHandler(.success(photoFile)) + } catch let error { + Logger.log(error: error) + return completionHandler(.failure(error)) + } + } + } + /// Mutes the audio input for the video recording session. /// /// If an error occurs during the operation, the error is logged. @@ -377,12 +404,12 @@ open class AespaSession { throw AespaError.file(reason: .unableToFlatten) } - let photoFile = PhotoFileGenerator.generate(data: rawPhotoData, date: Date()) + try await albumManager.addToAlbum(imageData: rawPhotoData) - try await albumManager.addToAlbum(filePath: videoFilePath) - videoFileBufferSubject.send(.success(videoFile)) + let photoFile = PhotoFileGenerator.generate(data: rawPhotoData, date: Date()) + photoFileBufferSubject.send(.success(photoFile)) - return videoFile + return photoFile } /// Mutes the audio input for the video recording session. diff --git a/Sources/Aespa/Core/AespaCoreAlbumManager.swift b/Sources/Aespa/Core/AespaCoreAlbumManager.swift index 28d6e72..75b33ac 100644 --- a/Sources/Aespa/Core/AespaCoreAlbumManager.swift +++ b/Sources/Aespa/Core/AespaCoreAlbumManager.swift @@ -40,7 +40,12 @@ class AespaCoreAlbumManager { extension AespaCoreAlbumManager { func addToAlbum(filePath: URL) async throws { - let processor = AssetAdditionProcessor(filePath: filePath) + let processor = VideoAssetAdditionProcessor(filePath: filePath) + try await run(processor: processor) + } + + func addToAlbum(imageData: Data) async throws { + let processor = PhotoAssetAdditionProcessor(imageData: imageData) try await run(processor: processor) } } diff --git a/Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift b/Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift new file mode 100644 index 0000000..c97a4e3 --- /dev/null +++ b/Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift @@ -0,0 +1,35 @@ +// +// PhotoAssetAdditionProcessor.swift +// +// +// Created by 이영빈 on 2023/06/19. +// + +import Photos +import Foundation + +struct PhotoAssetAdditionProcessor: AespaAssetProcessing { + let imageData: Data + + func process(_ photoLibrary: T, _ assetCollection: U) async throws { + guard + case .authorized = await photoLibrary.requestAuthorization(for: .addOnly) + else { + let error = AespaError.album(reason: .unabledToAccess) + Logger.log(error: error) + throw error + } + + try await add(imageData: imageData, to: assetCollection, photoLibrary) + Logger.log(message: "File is added to album") + } + + /// Add the video to the app's album roll + func add(imageData: Data, to album: U, _ photoLibrary: T) async throws -> Void { + try await photoLibrary.performChanges { + // Request creating an asset from the image. + let creationRequest = PHAssetCreationRequest.forAsset() + creationRequest.addResource(with: .photo, data: imageData, options: nil) + } + } +} diff --git a/Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift b/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift similarity index 94% rename from Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift rename to Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift index 91d83e4..ef55f61 100644 --- a/Sources/Aespa/Processor/Asset/AssetAdditionProcessor.swift +++ b/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift @@ -1,5 +1,5 @@ // -// AssetAddingProcessor.swift +// VideoAssetAdditionProcessor.swift // // // Created by 이영빈 on 2023/06/02. @@ -8,7 +8,7 @@ import Photos import Foundation -struct AssetAdditionProcessor: AespaAssetProcessing { +struct VideoAssetAdditionProcessor: AespaAssetProcessing { let filePath: URL func process(_ photoLibrary: T, _ assetCollection: U) async throws { diff --git a/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift b/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift index 5e45988..e5c7408 100644 --- a/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift +++ b/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift @@ -34,7 +34,7 @@ struct VideoFilePathProvider { // Set directory if doesn't exist if fileManager.fileExists(atPath: directoryPathURL.path) == false { - try FileManager.default.createDirectory( + try fileManager.createDirectory( atPath: directoryPathURL.path, withIntermediateDirectories: true, attributes: nil) diff --git a/Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 11ca10b..0000000 --- a/Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "Cuckoo", - "repositoryURL": "https://github.com/Brightify/Cuckoo", - "state": { - "branch": null, - "revision": "475322e609d009c284464c886ca08cfe421137a9", - "version": "1.10.3" - } - } - ] - }, - "version": 1 -} diff --git a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa.xcscheme b/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa.xcscheme deleted file mode 100644 index 31d5232..0000000 --- a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa.xcscheme +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/Aespa-iOS-test.xctestplan b/Tests/Aespa-iOS-test.xctestplan deleted file mode 100644 index 421fb11..0000000 --- a/Tests/Aespa-iOS-test.xctestplan +++ /dev/null @@ -1,32 +0,0 @@ -{ - "configurations" : [ - { - "id" : "1D1026B7-1AE7-47C9-A8DE-7047D3049461", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - "codeCoverage" : { - "targets" : [ - { - "containerPath" : "container:..", - "identifier" : "Aespa", - "name" : "Aespa" - } - ] - } - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:Aespa-iOS-test.xcodeproj", - "identifier" : "07939F9C2A343B8E00DFA8BB", - "name" : "Aespa-iOS-testTests" - } - } - ], - "version" : 1 -} diff --git a/Tests/Aespa-iOS-testTests/Mock/.gitkeep b/Tests/Aespa-iOS-testTests/Mock/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Tests/Package.swift b/Tests/Package.swift new file mode 100644 index 0000000..da8e907 --- /dev/null +++ b/Tests/Package.swift @@ -0,0 +1,3 @@ +import PackageDescription + +let package = Package() diff --git a/Tests/Aespa-iOS-testTests.xctestplan b/Tests/Test.xctestplan similarity index 67% rename from Tests/Aespa-iOS-testTests.xctestplan rename to Tests/Test.xctestplan index 4ffc41e..04a65f0 100644 --- a/Tests/Aespa-iOS-testTests.xctestplan +++ b/Tests/Test.xctestplan @@ -1,7 +1,7 @@ { "configurations" : [ { - "id" : "F430D1B4-52B5-4C85-8BEB-263A506AFE77", + "id" : "E98AB7E2-A5C9-449F-BC03-D7CEEAB04AB9", "name" : "Test Scheme Action", "options" : { @@ -23,9 +23,9 @@ { "parallelizable" : true, "target" : { - "containerPath" : "container:Aespa-iOS-test.xcodeproj", - "identifier" : "07939F9C2A343B8E00DFA8BB", - "name" : "Aespa-iOS-testTests" + "containerPath" : "container:TestHostApp.xcodeproj", + "identifier" : "9C727D132A3FEF9900EF9472", + "name" : "TestHostAppTests" } } ], diff --git a/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj b/Tests/TestHostApp.xcodeproj/project.pbxproj similarity index 51% rename from Tests/Aespa-iOS-test.xcodeproj/project.pbxproj rename to Tests/TestHostApp.xcodeproj/project.pbxproj index ee5da45..d138b43 100644 --- a/Tests/Aespa-iOS-test.xcodeproj/project.pbxproj +++ b/Tests/TestHostApp.xcodeproj/project.pbxproj @@ -7,69 +7,68 @@ objects = { /* Begin PBXBuildFile section */ - 07939F912A343B8D00DFA8BB /* Aespa_iOS_testApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07939F902A343B8D00DFA8BB /* Aespa_iOS_testApp.swift */; }; - 07939F932A343B8D00DFA8BB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07939F922A343B8D00DFA8BB /* ContentView.swift */; }; - 07939F952A343B8E00DFA8BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07939F942A343B8E00DFA8BB /* Assets.xcassets */; }; - 07939F982A343B8E00DFA8BB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07939F972A343B8E00DFA8BB /* Preview Assets.xcassets */; }; - 07F359B82A347CA400F4EF16 /* Cuckoo in Frameworks */ = {isa = PBXBuildFile; productRef = 07F359B72A347CA400F4EF16 /* Cuckoo */; }; - 07F359BA2A347DF600F4EF16 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F359B92A347DF600F4EF16 /* GeneratedMocks.swift */; }; - 07F359BE2A3489C000F4EF16 /* SessionTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F359BD2A3489C000F4EF16 /* SessionTunerTests.swift */; }; - 07FBEE722A3DA67E003CC5FD /* Aespa in Frameworks */ = {isa = PBXBuildFile; productRef = 07FBEE712A3DA67E003CC5FD /* Aespa */; }; - 07FBEE762A3DD55C003CC5FD /* AlbumUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE752A3DD55C003CC5FD /* AlbumUtilTests.swift */; }; - 07FBEE782A3E7500003CC5FD /* FileUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE772A3E7500003CC5FD /* FileUtilTests.swift */; }; - 07FBEE7A2A3E77B5003CC5FD /* video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 07FBEE792A3E77B5003CC5FD /* video.mp4 */; }; - 07FBEE7D2A3E77D8003CC5FD /* MockVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE7C2A3E77D8003CC5FD /* MockVideo.swift */; }; - 07FBEE822A3E7F95003CC5FD /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBEE812A3E7F95003CC5FD /* MockFileManager.swift */; }; - 9B24721A2A3C212500D82A2E /* DeviceTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2472192A3C212500D82A2E /* DeviceTunerTests.swift */; }; - 9B24721C2A3C310D00D82A2E /* ConnectionTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B24721B2A3C310D00D82A2E /* ConnectionTunerTests.swift */; }; - 9B4975BD2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4975BC2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift */; }; - 9B4975BF2A3CA0BF0068FA35 /* AssetProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4975BE2A3CA0BF0068FA35 /* AssetProcessorTests.swift */; }; + 9C4BBE5B2A400E450071C84F /* SessionTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE4D2A400E450071C84F /* SessionTunerTests.swift */; }; + 9C4BBE5C2A400E450071C84F /* DeviceTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE4E2A400E450071C84F /* DeviceTunerTests.swift */; }; + 9C4BBE5D2A400E450071C84F /* ConnectionTunerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE4F2A400E450071C84F /* ConnectionTunerTests.swift */; }; + 9C4BBE5E2A400E450071C84F /* AlbumUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE512A400E450071C84F /* AlbumUtilTests.swift */; }; + 9C4BBE5F2A400E450071C84F /* FileUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE522A400E450071C84F /* FileUtilTests.swift */; }; + 9C4BBE602A400E450071C84F /* video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 9C4BBE552A400E450071C84F /* video.mp4 */; }; + 9C4BBE612A400E450071C84F /* MockVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE562A400E450071C84F /* MockVideo.swift */; }; + 9C4BBE622A400E450071C84F /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE572A400E450071C84F /* MockFileManager.swift */; }; + 9C4BBE632A400E450071C84F /* AssetProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE592A400E450071C84F /* AssetProcessorTests.swift */; }; + 9C4BBE642A400E450071C84F /* FileOutputProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE5A2A400E450071C84F /* FileOutputProcessorTests.swift */; }; + 9C4BBE662A400F830071C84F /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE652A400F830071C84F /* GeneratedMocks.swift */; }; + 9C727D082A3FEF9600EF9472 /* TestHostAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C727D072A3FEF9600EF9472 /* TestHostAppApp.swift */; }; + 9C727D0A2A3FEF9600EF9472 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C727D092A3FEF9600EF9472 /* ContentView.swift */; }; + 9C727D0C2A3FEF9800EF9472 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C727D0B2A3FEF9800EF9472 /* Assets.xcassets */; }; + 9C727D0F2A3FEF9800EF9472 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C727D0E2A3FEF9800EF9472 /* Preview Assets.xcassets */; }; + 9C727D502A3FF03200EF9472 /* Cuckoo in Frameworks */ = {isa = PBXBuildFile; productRef = 9C727D4F2A3FF03200EF9472 /* Cuckoo */; }; + 9C727D572A3FF0B100EF9472 /* Aespa in Frameworks */ = {isa = PBXBuildFile; productRef = 9C727D562A3FF0B100EF9472 /* Aespa */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 07939F9E2A343B8E00DFA8BB /* PBXContainerItemProxy */ = { + 9C727D152A3FEF9900EF9472 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 07939F852A343B8D00DFA8BB /* Project object */; + containerPortal = 9C727CFC2A3FEF9600EF9472 /* Project object */; proxyType = 1; - remoteGlobalIDString = 07939F8C2A343B8D00DFA8BB; - remoteInfo = "Aespa-iOS-test"; + remoteGlobalIDString = 9C727D032A3FEF9600EF9472; + remoteInfo = TestHostApp; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 07939F8D2A343B8D00DFA8BB /* Aespa-iOS-test.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Aespa-iOS-test.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 07939F902A343B8D00DFA8BB /* Aespa_iOS_testApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aespa_iOS_testApp.swift; sourceTree = ""; }; - 07939F922A343B8D00DFA8BB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 07939F942A343B8E00DFA8BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 07939F972A343B8E00DFA8BB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 07939F9D2A343B8E00DFA8BB /* Aespa-iOS-testTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Aespa-iOS-testTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 07F359B92A347DF600F4EF16 /* GeneratedMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; - 07F359BD2A3489C000F4EF16 /* SessionTunerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTunerTests.swift; sourceTree = ""; }; - 07FBEE742A3DA69A003CC5FD /* Aespa */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Aespa; path = ..; sourceTree = ""; }; - 07FBEE752A3DD55C003CC5FD /* AlbumUtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumUtilTests.swift; sourceTree = ""; }; - 07FBEE772A3E7500003CC5FD /* FileUtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtilTests.swift; sourceTree = ""; }; - 07FBEE792A3E77B5003CC5FD /* video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = video.mp4; sourceTree = ""; }; - 07FBEE7C2A3E77D8003CC5FD /* MockVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVideo.swift; sourceTree = ""; }; - 07FBEE812A3E7F95003CC5FD /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; - 9B2472192A3C212500D82A2E /* DeviceTunerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTunerTests.swift; sourceTree = ""; }; - 9B24721B2A3C310D00D82A2E /* ConnectionTunerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTunerTests.swift; sourceTree = ""; }; - 9B24721E2A3C366100D82A2E /* Aespa-iOS-testTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Aespa-iOS-testTests.xctestplan"; sourceTree = ""; }; - 9B4975BC2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOutputProcessorTests.swift; sourceTree = ""; }; - 9B4975BE2A3CA0BF0068FA35 /* AssetProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetProcessorTests.swift; sourceTree = ""; }; - 9CA7CFFB2A380754000B11B3 /* Aespa-iOS-test.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Aespa-iOS-test.xctestplan"; sourceTree = ""; }; + 9C4BBE4A2A3FF4870071C84F /* Test.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Test.xctestplan; sourceTree = ""; }; + 9C4BBE4D2A400E450071C84F /* SessionTunerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTunerTests.swift; sourceTree = ""; }; + 9C4BBE4E2A400E450071C84F /* DeviceTunerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceTunerTests.swift; sourceTree = ""; }; + 9C4BBE4F2A400E450071C84F /* ConnectionTunerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionTunerTests.swift; sourceTree = ""; }; + 9C4BBE512A400E450071C84F /* AlbumUtilTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumUtilTests.swift; sourceTree = ""; }; + 9C4BBE522A400E450071C84F /* FileUtilTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtilTests.swift; sourceTree = ""; }; + 9C4BBE552A400E450071C84F /* video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = video.mp4; sourceTree = ""; }; + 9C4BBE562A400E450071C84F /* MockVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockVideo.swift; sourceTree = ""; }; + 9C4BBE572A400E450071C84F /* MockFileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; + 9C4BBE592A400E450071C84F /* AssetProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetProcessorTests.swift; sourceTree = ""; }; + 9C4BBE5A2A400E450071C84F /* FileOutputProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileOutputProcessorTests.swift; sourceTree = ""; }; + 9C4BBE652A400F830071C84F /* GeneratedMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; + 9C727D042A3FEF9600EF9472 /* TestHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C727D072A3FEF9600EF9472 /* TestHostAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHostAppApp.swift; sourceTree = ""; }; + 9C727D092A3FEF9600EF9472 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 9C727D0B2A3FEF9800EF9472 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9C727D0E2A3FEF9800EF9472 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 9C727D142A3FEF9900EF9472 /* TestHostAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestHostAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C727D542A3FF09400EF9472 /* Aespa */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Aespa; path = ..; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 07939F8A2A343B8D00DFA8BB /* Frameworks */ = { + 9C727D012A3FEF9600EF9472 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 07F359B82A347CA400F4EF16 /* Cuckoo in Frameworks */, - 07FBEE722A3DA67E003CC5FD /* Aespa in Frameworks */, + 9C727D502A3FF03200EF9472 /* Cuckoo in Frameworks */, + 9C727D572A3FF0B100EF9472 /* Aespa in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 07939F9A2A343B8E00DFA8BB /* Frameworks */ = { + 9C727D112A3FEF9900EF9472 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -79,183 +78,182 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 07939F842A343B8D00DFA8BB = { + 9C4BBE4B2A400E450071C84F /* Tests */ = { isa = PBXGroup; children = ( - 07FBEE732A3DA69A003CC5FD /* Packages */, - 9B24721E2A3C366100D82A2E /* Aespa-iOS-testTests.xctestplan */, - 9CA7CFFB2A380754000B11B3 /* Aespa-iOS-test.xctestplan */, - 07939F8F2A343B8D00DFA8BB /* Aespa-iOS-test */, - 07939FA02A343B8E00DFA8BB /* Aespa-iOS-testTests */, - 07939F8E2A343B8D00DFA8BB /* Products */, - 07F359AF2A34449B00F4EF16 /* Frameworks */, + 9C4BBE532A400E450071C84F /* Mock */, + 9C4BBE4C2A400E450071C84F /* Tuner */, + 9C4BBE502A400E450071C84F /* Util */, + 9C4BBE582A400E450071C84F /* Processor */, ); + path = Tests; sourceTree = ""; }; - 07939F8E2A343B8D00DFA8BB /* Products */ = { + 9C4BBE4C2A400E450071C84F /* Tuner */ = { isa = PBXGroup; children = ( - 07939F8D2A343B8D00DFA8BB /* Aespa-iOS-test.app */, - 07939F9D2A343B8E00DFA8BB /* Aespa-iOS-testTests.xctest */, + 9C4BBE4D2A400E450071C84F /* SessionTunerTests.swift */, + 9C4BBE4E2A400E450071C84F /* DeviceTunerTests.swift */, + 9C4BBE4F2A400E450071C84F /* ConnectionTunerTests.swift */, ); - name = Products; + path = Tuner; sourceTree = ""; }; - 07939F8F2A343B8D00DFA8BB /* Aespa-iOS-test */ = { + 9C4BBE502A400E450071C84F /* Util */ = { isa = PBXGroup; children = ( - 07939F902A343B8D00DFA8BB /* Aespa_iOS_testApp.swift */, - 07939F922A343B8D00DFA8BB /* ContentView.swift */, - 07939F942A343B8E00DFA8BB /* Assets.xcassets */, - 07939F962A343B8E00DFA8BB /* Preview Content */, + 9C4BBE512A400E450071C84F /* AlbumUtilTests.swift */, + 9C4BBE522A400E450071C84F /* FileUtilTests.swift */, ); - path = "Aespa-iOS-test"; + path = Util; sourceTree = ""; }; - 07939F962A343B8E00DFA8BB /* Preview Content */ = { + 9C4BBE532A400E450071C84F /* Mock */ = { isa = PBXGroup; children = ( - 07939F972A343B8E00DFA8BB /* Preview Assets.xcassets */, + 9C4BBE652A400F830071C84F /* GeneratedMocks.swift */, + 9C4BBE542A400E450071C84F /* Video */, + 9C4BBE572A400E450071C84F /* MockFileManager.swift */, ); - path = "Preview Content"; + path = Mock; sourceTree = ""; }; - 07939FA02A343B8E00DFA8BB /* Aespa-iOS-testTests */ = { + 9C4BBE542A400E450071C84F /* Video */ = { isa = PBXGroup; children = ( - 07FBEE6E2A3D9C02003CC5FD /* Util */, - 9B4975BB2A3C8BA50068FA35 /* Processor */, - 0795E11F2A35A2FC001AD4DC /* Tuner */, - 07F359AA2A3443EC00F4EF16 /* Mock */, + 9C4BBE552A400E450071C84F /* video.mp4 */, + 9C4BBE562A400E450071C84F /* MockVideo.swift */, ); - path = "Aespa-iOS-testTests"; + path = Video; sourceTree = ""; }; - 0795E11F2A35A2FC001AD4DC /* Tuner */ = { + 9C4BBE582A400E450071C84F /* Processor */ = { isa = PBXGroup; children = ( - 07F359BD2A3489C000F4EF16 /* SessionTunerTests.swift */, - 9B2472192A3C212500D82A2E /* DeviceTunerTests.swift */, - 9B24721B2A3C310D00D82A2E /* ConnectionTunerTests.swift */, + 9C4BBE592A400E450071C84F /* AssetProcessorTests.swift */, + 9C4BBE5A2A400E450071C84F /* FileOutputProcessorTests.swift */, ); - path = Tuner; + path = Processor; sourceTree = ""; }; - 07F359AA2A3443EC00F4EF16 /* Mock */ = { + 9C727CFB2A3FEF9600EF9472 = { isa = PBXGroup; children = ( - 07FBEE7B2A3E77CF003CC5FD /* Video */, - 07F359B92A347DF600F4EF16 /* GeneratedMocks.swift */, - 07FBEE812A3E7F95003CC5FD /* MockFileManager.swift */, + 9C4BBE4A2A3FF4870071C84F /* Test.xctestplan */, + 9C727D532A3FF09400EF9472 /* Packages */, + 9C727D062A3FEF9600EF9472 /* TestHostApp */, + 9C4BBE4B2A400E450071C84F /* Tests */, + 9C727D052A3FEF9600EF9472 /* Products */, + 9C727D552A3FF0B100EF9472 /* Frameworks */, ); - path = Mock; sourceTree = ""; }; - 07F359AF2A34449B00F4EF16 /* Frameworks */ = { + 9C727D052A3FEF9600EF9472 /* Products */ = { isa = PBXGroup; children = ( + 9C727D042A3FEF9600EF9472 /* TestHostApp.app */, + 9C727D142A3FEF9900EF9472 /* TestHostAppTests.xctest */, ); - name = Frameworks; + name = Products; sourceTree = ""; }; - 07FBEE6E2A3D9C02003CC5FD /* Util */ = { + 9C727D062A3FEF9600EF9472 /* TestHostApp */ = { isa = PBXGroup; children = ( - 07FBEE752A3DD55C003CC5FD /* AlbumUtilTests.swift */, - 07FBEE772A3E7500003CC5FD /* FileUtilTests.swift */, + 9C727D072A3FEF9600EF9472 /* TestHostAppApp.swift */, + 9C727D092A3FEF9600EF9472 /* ContentView.swift */, + 9C727D0B2A3FEF9800EF9472 /* Assets.xcassets */, + 9C727D0D2A3FEF9800EF9472 /* Preview Content */, ); - path = Util; + path = TestHostApp; sourceTree = ""; }; - 07FBEE732A3DA69A003CC5FD /* Packages */ = { + 9C727D0D2A3FEF9800EF9472 /* Preview Content */ = { isa = PBXGroup; children = ( - 07FBEE742A3DA69A003CC5FD /* Aespa */, + 9C727D0E2A3FEF9800EF9472 /* Preview Assets.xcassets */, ); - name = Packages; + path = "Preview Content"; sourceTree = ""; }; - 07FBEE7B2A3E77CF003CC5FD /* Video */ = { + 9C727D532A3FF09400EF9472 /* Packages */ = { isa = PBXGroup; children = ( - 07FBEE792A3E77B5003CC5FD /* video.mp4 */, - 07FBEE7C2A3E77D8003CC5FD /* MockVideo.swift */, + 9C727D542A3FF09400EF9472 /* Aespa */, ); - path = Video; + name = Packages; sourceTree = ""; }; - 9B4975BB2A3C8BA50068FA35 /* Processor */ = { + 9C727D552A3FF0B100EF9472 /* Frameworks */ = { isa = PBXGroup; children = ( - 9B4975BC2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift */, - 9B4975BE2A3CA0BF0068FA35 /* AssetProcessorTests.swift */, ); - path = Processor; + name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 07939F8C2A343B8D00DFA8BB /* Aespa-iOS-test */ = { + 9C727D032A3FEF9600EF9472 /* TestHostApp */ = { isa = PBXNativeTarget; - buildConfigurationList = 07939FB12A343B8E00DFA8BB /* Build configuration list for PBXNativeTarget "Aespa-iOS-test" */; + buildConfigurationList = 9C727D282A3FEF9900EF9472 /* Build configuration list for PBXNativeTarget "TestHostApp" */; buildPhases = ( - 07939F892A343B8D00DFA8BB /* Sources */, - 07939F8A2A343B8D00DFA8BB /* Frameworks */, - 07939F8B2A343B8D00DFA8BB /* Resources */, - 9B9CD51D2A3D3B430075B72F /* ShellScript */, + 9C727D002A3FEF9600EF9472 /* Sources */, + 9C727D012A3FEF9600EF9472 /* Frameworks */, + 9C727D022A3FEF9600EF9472 /* Resources */, + 9C727D582A3FF18500EF9472 /* Generate mock file */, ); buildRules = ( ); dependencies = ( ); - name = "Aespa-iOS-test"; + name = TestHostApp; packageProductDependencies = ( - 07F359B72A347CA400F4EF16 /* Cuckoo */, - 07FBEE712A3DA67E003CC5FD /* Aespa */, + 9C727D4F2A3FF03200EF9472 /* Cuckoo */, + 9C727D562A3FF0B100EF9472 /* Aespa */, ); - productName = "Aespa-iOS-test"; - productReference = 07939F8D2A343B8D00DFA8BB /* Aespa-iOS-test.app */; + productName = TestHostApp; + productReference = 9C727D042A3FEF9600EF9472 /* TestHostApp.app */; productType = "com.apple.product-type.application"; }; - 07939F9C2A343B8E00DFA8BB /* Aespa-iOS-testTests */ = { + 9C727D132A3FEF9900EF9472 /* TestHostAppTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 07939FB42A343B8E00DFA8BB /* Build configuration list for PBXNativeTarget "Aespa-iOS-testTests" */; + buildConfigurationList = 9C727D2B2A3FEF9900EF9472 /* Build configuration list for PBXNativeTarget "TestHostAppTests" */; buildPhases = ( - 07939F992A343B8E00DFA8BB /* Sources */, - 07939F9A2A343B8E00DFA8BB /* Frameworks */, - 07939F9B2A343B8E00DFA8BB /* Resources */, + 9C727D102A3FEF9900EF9472 /* Sources */, + 9C727D112A3FEF9900EF9472 /* Frameworks */, + 9C727D122A3FEF9900EF9472 /* Resources */, ); buildRules = ( ); dependencies = ( - 07939F9F2A343B8E00DFA8BB /* PBXTargetDependency */, + 9C727D162A3FEF9900EF9472 /* PBXTargetDependency */, ); - name = "Aespa-iOS-testTests"; - productName = "Aespa-iOS-testTests"; - productReference = 07939F9D2A343B8E00DFA8BB /* Aespa-iOS-testTests.xctest */; + name = TestHostAppTests; + productName = TestHostAppTests; + productReference = 9C727D142A3FEF9900EF9472 /* TestHostAppTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 07939F852A343B8D00DFA8BB /* Project object */ = { + 9C727CFC2A3FEF9600EF9472 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 1430; TargetAttributes = { - 07939F8C2A343B8D00DFA8BB = { + 9C727D032A3FEF9600EF9472 = { CreatedOnToolsVersion = 14.3; }; - 07939F9C2A343B8E00DFA8BB = { + 9C727D132A3FEF9900EF9472 = { CreatedOnToolsVersion = 14.3; - TestTargetID = 07939F8C2A343B8D00DFA8BB; + TestTargetID = 9C727D032A3FEF9600EF9472; }; }; }; - buildConfigurationList = 07939F882A343B8D00DFA8BB /* Build configuration list for PBXProject "Aespa-iOS-test" */; + buildConfigurationList = 9C727CFF2A3FEF9600EF9472 /* Build configuration list for PBXProject "TestHostApp" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -263,42 +261,42 @@ en, Base, ); - mainGroup = 07939F842A343B8D00DFA8BB; + mainGroup = 9C727CFB2A3FEF9600EF9472; packageReferences = ( - 07F359B62A347CA400F4EF16 /* XCRemoteSwiftPackageReference "Cuckoo" */, + 9C727D4E2A3FF03200EF9472 /* XCRemoteSwiftPackageReference "Cuckoo" */, ); - productRefGroup = 07939F8E2A343B8D00DFA8BB /* Products */; + productRefGroup = 9C727D052A3FEF9600EF9472 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 07939F8C2A343B8D00DFA8BB /* Aespa-iOS-test */, - 07939F9C2A343B8E00DFA8BB /* Aespa-iOS-testTests */, + 9C727D032A3FEF9600EF9472 /* TestHostApp */, + 9C727D132A3FEF9900EF9472 /* TestHostAppTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 07939F8B2A343B8D00DFA8BB /* Resources */ = { + 9C727D022A3FEF9600EF9472 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 07939F982A343B8E00DFA8BB /* Preview Assets.xcassets in Resources */, - 07939F952A343B8E00DFA8BB /* Assets.xcassets in Resources */, + 9C727D0F2A3FEF9800EF9472 /* Preview Assets.xcassets in Resources */, + 9C727D0C2A3FEF9800EF9472 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 07939F9B2A343B8E00DFA8BB /* Resources */ = { + 9C727D122A3FEF9900EF9472 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 07FBEE7A2A3E77B5003CC5FD /* video.mp4 in Resources */, + 9C4BBE602A400E450071C84F /* video.mp4 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 9B9CD51D2A3D3B430075B72F /* ShellScript */ = { + 9C727D582A3FF18500EF9472 /* Generate mock file */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -307,10 +305,11 @@ ); inputPaths = ( ); + name = "Generate mock file"; outputFileListPaths = ( ); outputPaths = ( - "$(PROJECT_DIR)/Aespa-iOS-testTests/Mock/GeneratedMocks.swift", + "$(PROJECT_DIR)/$(PRODUCT_NAME)Tests/Mock/GeneratedMocks.swift", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -319,44 +318,44 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 07939F892A343B8D00DFA8BB /* Sources */ = { + 9C727D002A3FEF9600EF9472 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 07939F932A343B8D00DFA8BB /* ContentView.swift in Sources */, - 07939F912A343B8D00DFA8BB /* Aespa_iOS_testApp.swift in Sources */, + 9C727D0A2A3FEF9600EF9472 /* ContentView.swift in Sources */, + 9C727D082A3FEF9600EF9472 /* TestHostAppApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 07939F992A343B8E00DFA8BB /* Sources */ = { + 9C727D102A3FEF9900EF9472 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9B24721A2A3C212500D82A2E /* DeviceTunerTests.swift in Sources */, - 9B4975BF2A3CA0BF0068FA35 /* AssetProcessorTests.swift in Sources */, - 07F359BE2A3489C000F4EF16 /* SessionTunerTests.swift in Sources */, - 9B4975BD2A3C8C0C0068FA35 /* FileOutputProcessorTests.swift in Sources */, - 07FBEE762A3DD55C003CC5FD /* AlbumUtilTests.swift in Sources */, - 07FBEE782A3E7500003CC5FD /* FileUtilTests.swift in Sources */, - 9B24721C2A3C310D00D82A2E /* ConnectionTunerTests.swift in Sources */, - 07FBEE7D2A3E77D8003CC5FD /* MockVideo.swift in Sources */, - 07F359BA2A347DF600F4EF16 /* GeneratedMocks.swift in Sources */, - 07FBEE822A3E7F95003CC5FD /* MockFileManager.swift in Sources */, + 9C4BBE5C2A400E450071C84F /* DeviceTunerTests.swift in Sources */, + 9C4BBE612A400E450071C84F /* MockVideo.swift in Sources */, + 9C4BBE5E2A400E450071C84F /* AlbumUtilTests.swift in Sources */, + 9C4BBE5F2A400E450071C84F /* FileUtilTests.swift in Sources */, + 9C4BBE622A400E450071C84F /* MockFileManager.swift in Sources */, + 9C4BBE5B2A400E450071C84F /* SessionTunerTests.swift in Sources */, + 9C4BBE642A400E450071C84F /* FileOutputProcessorTests.swift in Sources */, + 9C4BBE5D2A400E450071C84F /* ConnectionTunerTests.swift in Sources */, + 9C4BBE632A400E450071C84F /* AssetProcessorTests.swift in Sources */, + 9C4BBE662A400F830071C84F /* GeneratedMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 07939F9F2A343B8E00DFA8BB /* PBXTargetDependency */ = { + 9C727D162A3FEF9900EF9472 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 07939F8C2A343B8D00DFA8BB /* Aespa-iOS-test */; - targetProxy = 07939F9E2A343B8E00DFA8BB /* PBXContainerItemProxy */; + target = 9C727D032A3FEF9600EF9472 /* TestHostApp */; + targetProxy = 9C727D152A3FEF9900EF9472 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 07939FAF2A343B8E00DFA8BB /* Debug */ = { + 9C727D262A3FEF9900EF9472 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -392,6 +391,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -416,7 +416,7 @@ }; name = Debug; }; - 07939FB02A343B8E00DFA8BB /* Release */ = { + 9C727D272A3FEF9900EF9472 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -452,6 +452,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -470,17 +471,16 @@ }; name = Release; }; - 07939FB22A343B8E00DFA8BB /* Debug */ = { + 9C727D292A3FEF9900EF9472 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Aespa-iOS-test/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"TestHostApp/Preview Content\""; DEVELOPMENT_TEAM = W6QHM4Y43Z; ENABLE_PREVIEWS = YES; - ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -492,28 +492,24 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-test"; + PRODUCT_BUNDLE_IDENTIFIER = co.enebin.TestHostApp; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 07939FB32A343B8E00DFA8BB /* Release */ = { + 9C727D2A2A3FEF9900EF9472 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Aespa-iOS-test/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"TestHostApp/Preview Content\""; DEVELOPMENT_TEAM = W6QHM4Y43Z; ENABLE_PREVIEWS = YES; - ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -525,18 +521,15 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-test"; + PRODUCT_BUNDLE_IDENTIFIER = co.enebin.TestHostApp; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - 07939FB52A343B8E00DFA8BB /* Debug */ = { + 9C727D2C2A3FEF9900EF9472 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; @@ -547,16 +540,16 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-testTests"; + PRODUCT_BUNDLE_IDENTIFIER = co.enebin.TestHostAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Aespa-iOS-test.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Aespa-iOS-test"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestHostApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TestHostApp"; }; name = Debug; }; - 07939FB62A343B8E00DFA8BB /* Release */ = { + 9C727D2D2A3FEF9900EF9472 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; @@ -567,41 +560,41 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.enebin.Aespa-iOS-testTests"; + PRODUCT_BUNDLE_IDENTIFIER = co.enebin.TestHostAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Aespa-iOS-test.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Aespa-iOS-test"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestHostApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TestHostApp"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 07939F882A343B8D00DFA8BB /* Build configuration list for PBXProject "Aespa-iOS-test" */ = { + 9C727CFF2A3FEF9600EF9472 /* Build configuration list for PBXProject "TestHostApp" */ = { isa = XCConfigurationList; buildConfigurations = ( - 07939FAF2A343B8E00DFA8BB /* Debug */, - 07939FB02A343B8E00DFA8BB /* Release */, + 9C727D262A3FEF9900EF9472 /* Debug */, + 9C727D272A3FEF9900EF9472 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 07939FB12A343B8E00DFA8BB /* Build configuration list for PBXNativeTarget "Aespa-iOS-test" */ = { + 9C727D282A3FEF9900EF9472 /* Build configuration list for PBXNativeTarget "TestHostApp" */ = { isa = XCConfigurationList; buildConfigurations = ( - 07939FB22A343B8E00DFA8BB /* Debug */, - 07939FB32A343B8E00DFA8BB /* Release */, + 9C727D292A3FEF9900EF9472 /* Debug */, + 9C727D2A2A3FEF9900EF9472 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 07939FB42A343B8E00DFA8BB /* Build configuration list for PBXNativeTarget "Aespa-iOS-testTests" */ = { + 9C727D2B2A3FEF9900EF9472 /* Build configuration list for PBXNativeTarget "TestHostAppTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 07939FB52A343B8E00DFA8BB /* Debug */, - 07939FB62A343B8E00DFA8BB /* Release */, + 9C727D2C2A3FEF9900EF9472 /* Debug */, + 9C727D2D2A3FEF9900EF9472 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -609,7 +602,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 07F359B62A347CA400F4EF16 /* XCRemoteSwiftPackageReference "Cuckoo" */ = { + 9C727D4E2A3FF03200EF9472 /* XCRemoteSwiftPackageReference "Cuckoo" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Brightify/Cuckoo"; requirement = { @@ -620,16 +613,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 07F359B72A347CA400F4EF16 /* Cuckoo */ = { + 9C727D4F2A3FF03200EF9472 /* Cuckoo */ = { isa = XCSwiftPackageProductDependency; - package = 07F359B62A347CA400F4EF16 /* XCRemoteSwiftPackageReference "Cuckoo" */; + package = 9C727D4E2A3FF03200EF9472 /* XCRemoteSwiftPackageReference "Cuckoo" */; productName = Cuckoo; }; - 07FBEE712A3DA67E003CC5FD /* Aespa */ = { + 9C727D562A3FF0B100EF9472 /* Aespa */ = { isa = XCSwiftPackageProductDependency; productName = Aespa; }; /* End XCSwiftPackageProductDependency section */ }; - rootObject = 07939F852A343B8D00DFA8BB /* Project object */; + rootObject = 9C727CFC2A3FEF9600EF9472 /* Project object */; } diff --git a/Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Tests/TestHostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Tests/TestHostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Tests/Aespa-iOS-test.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..69b96b4 --- /dev/null +++ b/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "cuckoo", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Brightify/Cuckoo", + "state" : { + "revision" : "475322e609d009c284464c886ca08cfe421137a9", + "version" : "1.10.3" + } + } + ], + "version" : 2 +} diff --git a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme b/Tests/TestHostApp.xcodeproj/xcshareddata/xcschemes/Test.xcscheme similarity index 65% rename from Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme rename to Tests/TestHostApp.xcodeproj/xcshareddata/xcschemes/Test.xcscheme index c13cc3a..1d86c0e 100644 --- a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test-scheme.xcscheme +++ b/Tests/TestHostApp.xcodeproj/xcshareddata/xcschemes/Test.xcscheme @@ -5,22 +5,6 @@ - - - - - - @@ -39,10 +23,10 @@ parallelizable = "YES"> + BlueprintIdentifier = "9C727D132A3FEF9900EF9472" + BuildableName = "TestHostAppTests.xctest" + BlueprintName = "TestHostAppTests" + ReferencedContainer = "container:TestHostApp.xcodeproj"> diff --git a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test.xcscheme b/Tests/TestHostApp.xcodeproj/xcshareddata/xcschemes/TestHostApp.xcscheme similarity index 59% rename from Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test.xcscheme rename to Tests/TestHostApp.xcodeproj/xcshareddata/xcschemes/TestHostApp.xcscheme index d0244b7..fdadaf4 100644 --- a/Tests/Aespa-iOS-test.xcodeproj/xcshareddata/xcschemes/Aespa-iOS-test.xcscheme +++ b/Tests/TestHostApp.xcodeproj/xcshareddata/xcschemes/TestHostApp.xcscheme @@ -14,10 +14,10 @@ buildForAnalyzing = "YES"> + BlueprintIdentifier = "9C727D032A3FEF9600EF9472" + BuildableName = "TestHostApp.app" + BlueprintName = "TestHostApp" + ReferencedContainer = "container:TestHostApp.xcodeproj"> @@ -26,34 +26,18 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> - - - - + BlueprintIdentifier = "9C727D132A3FEF9900EF9472" + BuildableName = "TestHostAppTests.xctest" + BlueprintName = "TestHostAppTests" + ReferencedContainer = "container:TestHostApp.xcodeproj"> @@ -72,10 +56,10 @@ runnableDebuggingMode = "0"> + BlueprintIdentifier = "9C727D032A3FEF9600EF9472" + BuildableName = "TestHostApp.app" + BlueprintName = "TestHostApp" + ReferencedContainer = "container:TestHostApp.xcodeproj"> @@ -89,10 +73,10 @@ runnableDebuggingMode = "0"> + BlueprintIdentifier = "9C727D032A3FEF9600EF9472" + BuildableName = "TestHostApp.app" + BlueprintName = "TestHostApp" + ReferencedContainer = "container:TestHostApp.xcodeproj"> diff --git a/Tests/Aespa-iOS-test/Assets.xcassets/AccentColor.colorset/Contents.json b/Tests/TestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Tests/Aespa-iOS-test/Assets.xcassets/AccentColor.colorset/Contents.json rename to Tests/TestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Tests/Aespa-iOS-test/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/TestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Tests/Aespa-iOS-test/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Tests/TestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Tests/Aespa-iOS-test/Assets.xcassets/Contents.json b/Tests/TestHostApp/Assets.xcassets/Contents.json similarity index 100% rename from Tests/Aespa-iOS-test/Assets.xcassets/Contents.json rename to Tests/TestHostApp/Assets.xcassets/Contents.json diff --git a/Tests/Aespa-iOS-test/ContentView.swift b/Tests/TestHostApp/ContentView.swift similarity index 87% rename from Tests/Aespa-iOS-test/ContentView.swift rename to Tests/TestHostApp/ContentView.swift index acabb04..7017224 100644 --- a/Tests/Aespa-iOS-test/ContentView.swift +++ b/Tests/TestHostApp/ContentView.swift @@ -1,8 +1,8 @@ // // ContentView.swift -// Aespa-iOS-test +// TestHostApp // -// Created by Young Bin on 2023/06/10. +// Created by 이영빈 on 2023/06/19. // import SwiftUI diff --git a/Tests/Aespa-iOS-test/Preview Content/Preview Assets.xcassets/Contents.json b/Tests/TestHostApp/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from Tests/Aespa-iOS-test/Preview Content/Preview Assets.xcassets/Contents.json rename to Tests/TestHostApp/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Tests/Aespa-iOS-test/Aespa_iOS_testApp.swift b/Tests/TestHostApp/TestHostAppApp.swift similarity index 51% rename from Tests/Aespa-iOS-test/Aespa_iOS_testApp.swift rename to Tests/TestHostApp/TestHostAppApp.swift index 6426db0..f0330ff 100644 --- a/Tests/Aespa-iOS-test/Aespa_iOS_testApp.swift +++ b/Tests/TestHostApp/TestHostAppApp.swift @@ -1,14 +1,14 @@ // -// Aespa_iOS_testApp.swift -// Aespa-iOS-test +// TestHostAppApp.swift +// TestHostApp // -// Created by Young Bin on 2023/06/10. +// Created by 이영빈 on 2023/06/19. // import SwiftUI @main -struct Aespa_iOS_testApp: App { +struct TestHostAppApp: App { var body: some Scene { WindowGroup { ContentView() diff --git a/Tests/Aespa-iOS-testTests/Mock/MockFileManager.swift b/Tests/Tests/Mock/MockFileManager.swift similarity index 100% rename from Tests/Aespa-iOS-testTests/Mock/MockFileManager.swift rename to Tests/Tests/Mock/MockFileManager.swift diff --git a/Tests/Aespa-iOS-testTests/Mock/Video/MockVideo.swift b/Tests/Tests/Mock/Video/MockVideo.swift similarity index 100% rename from Tests/Aespa-iOS-testTests/Mock/Video/MockVideo.swift rename to Tests/Tests/Mock/Video/MockVideo.swift diff --git a/Tests/Aespa-iOS-testTests/Mock/Video/video.mp4 b/Tests/Tests/Mock/Video/video.mp4 similarity index 100% rename from Tests/Aespa-iOS-testTests/Mock/Video/video.mp4 rename to Tests/Tests/Mock/Video/video.mp4 diff --git a/Tests/Aespa-iOS-testTests/Processor/AssetProcessorTests.swift b/Tests/Tests/Processor/AssetProcessorTests.swift similarity index 95% rename from Tests/Aespa-iOS-testTests/Processor/AssetProcessorTests.swift rename to Tests/Tests/Processor/AssetProcessorTests.swift index b19f771..2da96d4 100644 --- a/Tests/Aespa-iOS-testTests/Processor/AssetProcessorTests.swift +++ b/Tests/Tests/Processor/AssetProcessorTests.swift @@ -30,7 +30,7 @@ final class AssetProcessorTests: XCTestCase { func testAddition() async throws { let url = URL(string: "/here/there.mp4")! let accessLevel = PHAccessLevel.addOnly - let processor = AssetAdditionProcessor(filePath: url) + let processor = VideoAssetAdditionProcessor(filePath: url) stub(library) { proxy in when(proxy.performChanges(anyClosure())).thenDoNothing() diff --git a/Tests/Aespa-iOS-testTests/Processor/FileOutputProcessorTests.swift b/Tests/Tests/Processor/FileOutputProcessorTests.swift similarity index 100% rename from Tests/Aespa-iOS-testTests/Processor/FileOutputProcessorTests.swift rename to Tests/Tests/Processor/FileOutputProcessorTests.swift diff --git a/Tests/Aespa-iOS-testTests/Tuner/ConnectionTunerTests.swift b/Tests/Tests/Tuner/ConnectionTunerTests.swift similarity index 100% rename from Tests/Aespa-iOS-testTests/Tuner/ConnectionTunerTests.swift rename to Tests/Tests/Tuner/ConnectionTunerTests.swift diff --git a/Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift b/Tests/Tests/Tuner/DeviceTunerTests.swift similarity index 100% rename from Tests/Aespa-iOS-testTests/Tuner/DeviceTunerTests.swift rename to Tests/Tests/Tuner/DeviceTunerTests.swift diff --git a/Tests/Aespa-iOS-testTests/Tuner/SessionTunerTests.swift b/Tests/Tests/Tuner/SessionTunerTests.swift similarity index 93% rename from Tests/Aespa-iOS-testTests/Tuner/SessionTunerTests.swift rename to Tests/Tests/Tuner/SessionTunerTests.swift index 14fcfa4..6e1b708 100644 --- a/Tests/Aespa-iOS-testTests/Tuner/SessionTunerTests.swift +++ b/Tests/Tests/Tuner/SessionTunerTests.swift @@ -71,8 +71,11 @@ final class SessionTunerTests: XCTestCase { func testSessionLaunchTuner_whenNotRunning() throws { stub(mockSessionProtocol) { proxy in when(proxy.isRunning.get).thenReturn(false) + when(proxy.addMovieInput()).thenDoNothing() when(proxy.addMovieFileOutput()).thenDoNothing() + when(proxy.addCapturePhotoOutput()).thenDoNothing() + when(proxy.startRunning()).thenDoNothing() } @@ -87,6 +90,10 @@ final class SessionTunerTests: XCTestCase { .addMovieFileOutput() .with(returnType: Void.self) + verify(mockSessionProtocol) + .addCapturePhotoOutput() + .with(returnType: Void.self) + verify(mockSessionProtocol) .startRunning() .with(returnType: Void.self) @@ -108,6 +115,10 @@ final class SessionTunerTests: XCTestCase { .addMovieFileOutput() .with(returnType: Void.self) + verify(mockSessionProtocol, never()) + .addCapturePhotoOutput() + .with(returnType: Void.self) + verify(mockSessionProtocol, never()) .startRunning() .with(returnType: Void.self) diff --git a/Tests/Aespa-iOS-testTests/Util/AlbumUtilTests.swift b/Tests/Tests/Util/AlbumUtilTests.swift similarity index 100% rename from Tests/Aespa-iOS-testTests/Util/AlbumUtilTests.swift rename to Tests/Tests/Util/AlbumUtilTests.swift diff --git a/Tests/Aespa-iOS-testTests/Util/FileUtilTests.swift b/Tests/Tests/Util/FileUtilTests.swift similarity index 100% rename from Tests/Aespa-iOS-testTests/Util/FileUtilTests.swift rename to Tests/Tests/Util/FileUtilTests.swift From 38793722a1440a290cca364a9ba4a7b0f0331750 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 19 Jun 2023 13:46:36 +0900 Subject: [PATCH 06/11] Add tests for photo capturing --- .../Photos+AespaRepresentable.swift | 4 +- .../Asset/VideoAssetAdditionProcessor.swift | 2 +- Tests/TestHostApp.xcodeproj/project.pbxproj | 14 +++-- .../Tests/Processor/AssetProcessorTests.swift | 23 ++++++++- .../CapturePhotoOutputProcessorTests.swift | 51 +++++++++++++++++++ 5 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 Tests/Tests/Processor/CapturePhotoOutputProcessorTests.swift diff --git a/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift index 0177a26..8b8d7bb 100644 --- a/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift @@ -19,7 +19,7 @@ protocol AespaAssetCollectionRepresentable { var underlyingAssetCollection: PHAssetCollection { get } var localizedTitle: String? { get } - func canAdd(_ filePath: URL) -> Bool + func canAdd(video filePath: URL) -> Bool } extension PHPhotoLibrary: AespaAssetLibraryRepresentable { @@ -41,7 +41,7 @@ extension PHPhotoLibrary: AespaAssetLibraryRepresentable { extension PHAssetCollection: AespaAssetCollectionRepresentable { var underlyingAssetCollection: PHAssetCollection { self } - func canAdd(_ filePath: URL) -> Bool { + func canAdd(video filePath: URL) -> Bool { let asset = AVAsset(url: filePath) let tracks = asset.tracks(withMediaType: AVMediaType.video) diff --git a/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift b/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift index ef55f61..3c14ce6 100644 --- a/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift +++ b/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift @@ -26,7 +26,7 @@ struct VideoAssetAdditionProcessor: AespaAssetProcessing { /// Add the video to the app's album roll func add(video path: URL, to album: U, _ photoLibrary: T) async throws -> Void { - guard album.canAdd(path) else { + guard album.canAdd(video: path) else { throw AespaError.album(reason: .notVideoURL) } diff --git a/Tests/TestHostApp.xcodeproj/project.pbxproj b/Tests/TestHostApp.xcodeproj/project.pbxproj index d138b43..3a65b51 100644 --- a/Tests/TestHostApp.xcodeproj/project.pbxproj +++ b/Tests/TestHostApp.xcodeproj/project.pbxproj @@ -15,9 +15,10 @@ 9C4BBE602A400E450071C84F /* video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 9C4BBE552A400E450071C84F /* video.mp4 */; }; 9C4BBE612A400E450071C84F /* MockVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE562A400E450071C84F /* MockVideo.swift */; }; 9C4BBE622A400E450071C84F /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE572A400E450071C84F /* MockFileManager.swift */; }; - 9C4BBE632A400E450071C84F /* AssetProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE592A400E450071C84F /* AssetProcessorTests.swift */; }; 9C4BBE642A400E450071C84F /* FileOutputProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE5A2A400E450071C84F /* FileOutputProcessorTests.swift */; }; 9C4BBE662A400F830071C84F /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE652A400F830071C84F /* GeneratedMocks.swift */; }; + 9C4BBE682A4011460071C84F /* CapturePhotoOutputProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE672A4011460071C84F /* CapturePhotoOutputProcessorTests.swift */; }; + 9C4BBE6C2A4013F90071C84F /* AssetProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4BBE6B2A4013F80071C84F /* AssetProcessorTests.swift */; }; 9C727D082A3FEF9600EF9472 /* TestHostAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C727D072A3FEF9600EF9472 /* TestHostAppApp.swift */; }; 9C727D0A2A3FEF9600EF9472 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C727D092A3FEF9600EF9472 /* ContentView.swift */; }; 9C727D0C2A3FEF9800EF9472 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C727D0B2A3FEF9800EF9472 /* Assets.xcassets */; }; @@ -46,9 +47,10 @@ 9C4BBE552A400E450071C84F /* video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = video.mp4; sourceTree = ""; }; 9C4BBE562A400E450071C84F /* MockVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockVideo.swift; sourceTree = ""; }; 9C4BBE572A400E450071C84F /* MockFileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; - 9C4BBE592A400E450071C84F /* AssetProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetProcessorTests.swift; sourceTree = ""; }; 9C4BBE5A2A400E450071C84F /* FileOutputProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileOutputProcessorTests.swift; sourceTree = ""; }; 9C4BBE652A400F830071C84F /* GeneratedMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; + 9C4BBE672A4011460071C84F /* CapturePhotoOutputProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturePhotoOutputProcessorTests.swift; sourceTree = ""; }; + 9C4BBE6B2A4013F80071C84F /* AssetProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetProcessorTests.swift; sourceTree = ""; }; 9C727D042A3FEF9600EF9472 /* TestHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9C727D072A3FEF9600EF9472 /* TestHostAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHostAppApp.swift; sourceTree = ""; }; 9C727D092A3FEF9600EF9472 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -83,8 +85,8 @@ children = ( 9C4BBE532A400E450071C84F /* Mock */, 9C4BBE4C2A400E450071C84F /* Tuner */, - 9C4BBE502A400E450071C84F /* Util */, 9C4BBE582A400E450071C84F /* Processor */, + 9C4BBE502A400E450071C84F /* Util */, ); path = Tests; sourceTree = ""; @@ -130,8 +132,9 @@ 9C4BBE582A400E450071C84F /* Processor */ = { isa = PBXGroup; children = ( - 9C4BBE592A400E450071C84F /* AssetProcessorTests.swift */, + 9C4BBE6B2A4013F80071C84F /* AssetProcessorTests.swift */, 9C4BBE5A2A400E450071C84F /* FileOutputProcessorTests.swift */, + 9C4BBE672A4011460071C84F /* CapturePhotoOutputProcessorTests.swift */, ); path = Processor; sourceTree = ""; @@ -338,8 +341,9 @@ 9C4BBE622A400E450071C84F /* MockFileManager.swift in Sources */, 9C4BBE5B2A400E450071C84F /* SessionTunerTests.swift in Sources */, 9C4BBE642A400E450071C84F /* FileOutputProcessorTests.swift in Sources */, + 9C4BBE6C2A4013F90071C84F /* AssetProcessorTests.swift in Sources */, 9C4BBE5D2A400E450071C84F /* ConnectionTunerTests.swift in Sources */, - 9C4BBE632A400E450071C84F /* AssetProcessorTests.swift in Sources */, + 9C4BBE682A4011460071C84F /* CapturePhotoOutputProcessorTests.swift in Sources */, 9C4BBE662A400F830071C84F /* GeneratedMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/Tests/Processor/AssetProcessorTests.swift b/Tests/Tests/Processor/AssetProcessorTests.swift index 2da96d4..f44b221 100644 --- a/Tests/Tests/Processor/AssetProcessorTests.swift +++ b/Tests/Tests/Processor/AssetProcessorTests.swift @@ -27,7 +27,7 @@ final class AssetProcessorTests: XCTestCase { collection = nil } - func testAddition() async throws { + func testVideoAddition() async throws { let url = URL(string: "/here/there.mp4")! let accessLevel = PHAccessLevel.addOnly let processor = VideoAssetAdditionProcessor(filePath: url) @@ -51,4 +51,25 @@ final class AssetProcessorTests: XCTestCase { .requestAuthorization(for: equal(to: accessLevel)) .with(returnType: PHAuthorizationStatus.self) } + + func testPhotoAddition() async throws { + let imageData = try XCTUnwrap(UIImage(systemName: "person")?.pngData()) + let accessLevel = PHAccessLevel.addOnly + let processor = PhotoAssetAdditionProcessor(imageData: imageData) + + stub(library) { proxy in + when(proxy.performChanges(anyClosure())).thenDoNothing() + when(proxy.requestAuthorization(for: equal(to: accessLevel))).thenReturn(.authorized) + } + + try await processor.process(library, collection) + + verify(library) + .performChanges(anyClosure()) + .with(returnType: Void.self) + + verify(library) + .requestAuthorization(for: equal(to: accessLevel)) + .with(returnType: PHAuthorizationStatus.self) + } } diff --git a/Tests/Tests/Processor/CapturePhotoOutputProcessorTests.swift b/Tests/Tests/Processor/CapturePhotoOutputProcessorTests.swift new file mode 100644 index 0000000..020b910 --- /dev/null +++ b/Tests/Tests/Processor/CapturePhotoOutputProcessorTests.swift @@ -0,0 +1,51 @@ +// +// CapturePhotoOutputProcessorTests.swift +// TestHostAppTests +// +// Created by 이영빈 on 2023/06/19. +// + +import XCTest +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class CapturePhotoOutputProcessorTests: XCTestCase { + var photoOutput: MockAespaPhotoOutputRepresentable! + + override func setUpWithError() throws { + photoOutput = MockAespaPhotoOutputRepresentable() + } + + override func tearDownWithError() throws { + photoOutput = nil + } + + func testCapture() throws { + let setting = AVCapturePhotoSettings() + let delegate = MockDelegate() + let processor = CapturePhotoProcessor(setting: setting, delegate: delegate) + + stub(photoOutput) { proxy in + when(proxy.capturePhoto(with: equal(to: setting), delegate: equal(to: delegate))).thenDoNothing() + when(proxy.getConnection(with: equal(to: .video))).thenReturn(MockAespaCaptureConnectionRepresentable()) + } + + try processor.process(photoOutput) + verify(photoOutput) + .capturePhoto(with: equal(to: setting), delegate: equal(to: delegate)) + .with(returnType: Void.self) + + verify(photoOutput) + .getConnection(with: equal(to: .video)) + .with(returnType: AespaCaptureConnectionRepresentable?.self) + } +} + +fileprivate class MockDelegate: NSObject, AVCapturePhotoCaptureDelegate { + func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { + return + } +} From e14d119d9ccaa729dae2742aecc126eb82a3748f Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 19 Jun 2023 18:12:06 +0900 Subject: [PATCH 07/11] =?UTF-8?q?Apply=20swiftlint=20Add=20comments=20?= =?UTF-8?q?=E3=85=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swiftlint.yml | 51 +++--- Sources/Aespa/Aespa.swift | 26 ++-- Sources/Aespa/AespaError.swift | 10 +- Sources/Aespa/AespaOption.swift | 37 +++-- Sources/Aespa/AespaSession.swift | 145 +++++++++++------- .../Aespa/Core/AespaCoreAlbumManager.swift | 11 +- Sources/Aespa/Core/AespaCoreCamera.swift | 18 ++- Sources/Aespa/Core/AespaCoreFileManager.swift | 12 +- Sources/Aespa/Core/AespaCoreRecorder.swift | 18 ++- Sources/Aespa/Core/AespaCoreSession.swift | 20 +-- ...CaptureConnection+AespaRepresentable.swift | 5 +- .../AVCaptureDevice+AespaRepresentable.swift | 12 +- ...CaptureFileOutput+AespaRepresentable.swift | 4 +- .../AespaCoreSession+AespaRepresentable.swift | 105 +++++++------ .../Photos+AespaRepresentable.swift | 23 ++- Sources/Aespa/Data/PhotoFile.swift | 6 +- Sources/Aespa/Data/VideoFile.swift | 6 +- .../Aespa/Data/VideoFileCachingProxy.swift | 38 ++--- .../Asset/PhotoAssetAdditionProcessor.swift | 15 +- .../Asset/VideoAssetAdditionProcessor.swift | 17 +- .../Capture/CapturePhotoProcessor.swift | 8 +- .../Record/FinishRecordProcessor.swift | 1 - .../Record/StartRecordProcessor.swift | 4 +- Sources/Aespa/Tuner/AespaTuning.swift | 17 +- .../Connection/VideoOrientationTuner.swift | 2 +- .../Connection/VideoStabilizationTuner.swift | 2 +- .../Aespa/Tuner/Device/AutoFocusTuner.swift | 4 +- Sources/Aespa/Tuner/Device/TorchTuner.swift | 6 +- Sources/Aespa/Tuner/Session/AudioTuner.swift | 2 +- .../Tuner/Session/CameraPositionTuner.swift | 5 +- .../Tuner/Session/SessionLaunchTuner.swift | 6 +- .../Session/SessionTerminationTuner.swift | 4 +- .../Extension/AVFoundation+Extension.swift | 8 +- .../Util/Extension/SwiftUI+Extension.swift | 9 +- .../Util/Extension/UIKit+Extension.swift | 4 +- Sources/Aespa/Util/Log/Logger.swift | 10 +- .../Util/Video/Album/AlbumImporter.swift | 7 +- .../Authorization/AuthorizationChecker.swift | 8 +- .../Util/Video/File/PhotoFileGenerator.swift | 2 +- .../Util/Video/File/VideoFileGenerator.swift | 11 +- .../Video/File/VideoFilePathProvider.swift | 10 +- Tests/TestHostApp.xcodeproj/project.pbxproj | 22 ++- .../Tests/Processor/AssetProcessorTests.swift | 2 +- 43 files changed, 428 insertions(+), 305 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index aba3349..4c54959 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,54 +1,43 @@ -disabled_rules: # rule identifiers turned on by default to exclude from running +disabled_rules: - colon - comma - control_statement -opt_in_rules: # some rules are turned off by default, so you need to opt-in - - empty_count # Find all the available rules by running: `swiftlint rules` + - trailing_whitespace + - nesting + - type_body_length + - file_length + - no_fallthrough_only -# Alternatively, specify all rules explicitly by uncommenting this option: -# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this -# - empty_parameters -# - vertical_whitespace +opt_in_rules: + - missing_docs -analyzer_rules: # Rules run by `swiftlint analyze` +analyzer_rules: - explicit_self -included: # paths to include during linting. `--path` is ignored if present. - - Source -excluded: # paths to ignore during linting. Takes precedence over `included`. - - Carthage - - Tests - - Source/ExcludedFolder - - Source/ExcludedFile.swift - - Source/*/ExcludedFile.swift # Exclude files with a wildcard +included: + - Sources/* +excluded: + - Tests/* # If true, SwiftLint will not fail if no lintable files are found. allow_zero_lintable_files: false -# configurable rules can be customized from this configuration file -# binary rules can set their severity level force_cast: warning # implicitly force_try: severity: warning # explicitly -# rules that have both warning and error levels, can set just the warning level # implicitly -line_length: 110 -# they can set both implicitly with an array -type_body_length: - - 300 # warning - - 400 # error -# or they can set both explicitly -file_length: - warning: 500 - error: 1200 -# naming rules can set warnings/errors for min_length and max_length -# additionally they can set excluded names +line_length: 120 + type_name: max_length: # warning and error warning: 40 error: 50 allowed_symbols: ["_"] + identifier_name: allowed_symbols: ["_"] -reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) +missing_docs: + included: Sources/* + +reporter: "xcode" diff --git a/Sources/Aespa/Aespa.swift b/Sources/Aespa/Aespa.swift index 535489e..eef26e6 100644 --- a/Sources/Aespa/Aespa.swift +++ b/Sources/Aespa/Aespa.swift @@ -9,7 +9,7 @@ open class Aespa { /// The core `AespaSession` that manages the actual video recording session. private static var core: AespaSession? - + /// Creates a new `AespaSession` with the given options. /// /// - Parameters: @@ -17,20 +17,22 @@ open class Aespa { /// - Returns: The newly created `AespaSession`. public static func session(with option: AespaOption) -> AespaSession { let newCore = AespaSession(option: option) - + core = newCore - + // Check logging option Logger.enableLogging = option.log.loggingEnabled - + return newCore } - + /// Configures the `AespaSession` for recording. /// Call this method to start the flow of data from the capture session’s inputs to its outputs. /// - /// This method ensures that necessary permissions are granted and the session is properly configured before starting. - /// If either the session isn't configured or the necessary permissions aren't granted, it throws an error. + /// This method ensures that necessary permissions are granted + /// and the session is properly configured before starting. + /// If either the session isn't configured or the necessary permissions aren't granted, + /// it throws an error. /// /// - Warning: This method is synchronous and blocks until the session starts running or it fails, /// which it reports by posting an `AVCaptureSessionRuntimeError` notification. @@ -38,18 +40,18 @@ open class Aespa { guard let core = core else { throw AespaError.session(reason: .notConfigured) } - + guard case .permitted = await AuthorizationChecker.checkCaptureAuthorizationStatus() else { throw AespaError.permission(reason: .denied) } - + try core.startSession() - + Logger.log(message: "Session is configured successfully") } - + /// Terminates the current `AespaSession`. /// /// If a session has been started, it stops the session and releases resources. @@ -58,7 +60,7 @@ open class Aespa { guard let core = core else { return } - + try core.terminateSession() Logger.log(message: "Session is terminated successfully") } diff --git a/Sources/Aespa/AespaError.swift b/Sources/Aespa/AespaError.swift index c0b069a..cc3b0be 100644 --- a/Sources/Aespa/AespaError.swift +++ b/Sources/Aespa/AespaError.swift @@ -13,7 +13,7 @@ public enum AespaError: LocalizedError { case permission(reason: PermissionErrorReason) case album(reason: AlbumErrorReason) case file(reason: FileErrorReason) - + public var errorDescription: String? { switch self { case .session(reason: let reason): @@ -43,7 +43,7 @@ public extension AespaError { case cannotFindDevice = "Couldn't find device. Check if you've added device properly" } - + enum DeviceErrorReason: String { case invalid = "Unable to set up camera device. Please check camera usage permission." @@ -56,12 +56,12 @@ public extension AespaError { case unsupported = "Unsupported device (supported on iPhone XR and later devices)" } - + enum PermissionErrorReason: String { case denied = "Cannot take a video because camera permissions are denied." } - + enum AlbumErrorReason: String { case unabledToAccess = "Unable to access album" @@ -70,7 +70,7 @@ public extension AespaError { case notVideoURL = "Received URL is not a video type." } - + enum FileErrorReason: String { case unableToFlatten = "Cannot take a video because camera permissions are denied." diff --git a/Sources/Aespa/AespaOption.swift b/Sources/Aespa/AespaOption.swift index 14ae8a6..c140640 100644 --- a/Sources/Aespa/AespaOption.swift +++ b/Sources/Aespa/AespaOption.swift @@ -14,8 +14,16 @@ public typealias FileNamingRule = () -> String /// `AespaOption` allows customization of various aspects of the video recording process, /// such as the video asset configuration, session settings and logging preferences. public struct AespaOption { + /// `Asset` configuration object which encapsulates options related to the media assets such as + /// the album name, file extension and naming convention for the files. public let asset: Asset + + /// `Session` configuration object which holds the settings to be applied for the `Aespa` session, + /// such as auto video orientation. public let session: Session + + /// `Log` configuration object which determines the logging behaviour during the session, + /// such as enabling or disabling logging. public let log: Log /// Creates an `AespaOption` with specified album name and an option to enable logging. @@ -29,7 +37,7 @@ public struct AespaOption { session: Session(), log: Log(loggingEnabled: enableLogging)) } - + /// Creates an `AespaOption` with specified asset, session and log options. /// /// - Parameters: @@ -44,22 +52,23 @@ public struct AespaOption { } public extension AespaOption { - /// `Asset` provides options for configuring the video assets, such as the album name, file naming rule, and file extension. + /// `Asset` provides options for configuring the video assets, + /// such as the album name, file naming rule, and file extension. struct Asset { /// The name of the album where recorded videos will be saved. let albumName: String - + /// A `Boolean` flag that determines to use in-memory cache for `VideoFile` /// /// It's set `true` by default. let useVideoFileCache: Bool - + /// The file extension for the recorded videos. let fileNameHandler: FileNamingRule - + /// The rule for naming video files. let fileExtension: String - + init( albumName: String, useVideoFileCache: Bool = true, @@ -72,15 +81,17 @@ public extension AespaOption { self.fileNameHandler = fileNameHandler } } - - /// `Session` provides options for configuring the video recording session, such as automatic video orientation. + + /// `Session` provides options for configuring the video recording session, + /// such as automatic video orientation. struct Session { /// A Boolean value that determines whether video orientation should be automatic. var autoVideoOrientationEnabled: Bool = true - /// An `AVCaptureDevice.DeviceType` value that determines camera device. If not specified, the device is automatically selected. + /// An `AVCaptureDevice.DeviceType` value that determines camera device. + /// If not specified, the device is automatically selected. var cameraDevicePreference: AVCaptureDevice.DeviceType? } - + /// `Log` provides an option for enabling or disabling logging. struct Log { var loggingEnabled: Bool = true @@ -96,19 +107,19 @@ public extension AespaOption { var rule: FileNamingRule { return { formatter.string(from: Date()) } } - + /// Creates a `Timestamp` file naming rule. init() { formatter = DateFormatter() formatter.dateFormat = "yyyy_MM_dd_HH_mm_ss" } } - + struct Random { let rule: FileNamingRule = { UUID().uuidString } } } - + /// `FileExtension` provides supported file extensions. enum FileExtension: String { case mp4 diff --git a/Sources/Aespa/AespaSession.swift b/Sources/Aespa/AespaSession.swift index b78e95b..3973c58 100644 --- a/Sources/Aespa/AespaSession.swift +++ b/Sources/Aespa/AespaSession.swift @@ -10,7 +10,8 @@ import Combine import Foundation import AVFoundation -/// The `AespaSession` is a Swift interface which provides a wrapper around the `AVFoundation`'s `AVCaptureSession`, +/// The `AespaSession` is a Swift interface which provides a wrapper +/// around the `AVFoundation`'s `AVCaptureSession`, /// simplifying its use for video capture. /// /// The interface allows you to start and stop recording, manage device input and output, @@ -25,11 +26,11 @@ open class AespaSession { private let camera: AespaCoreCamera private let fileManager: AespaCoreFileManager private let albumManager: AespaCoreAlbumManager - + private let photoFileBufferSubject: CurrentValueSubject?, Never> private let videoFileBufferSubject: CurrentValueSubject?, Never> private let previewLayerSubject: CurrentValueSubject - + /// A `UIKit` layer that you use to display video as it is being captured by an input device. /// /// - Note: If you're looking for a `View` for `SwiftUI`, use `preview` @@ -37,7 +38,7 @@ open class AespaSession { convenience init(option: AespaOption) { let session = AespaCoreSession(option: option) - + self.init( option: option, session: session, @@ -47,7 +48,7 @@ open class AespaSession { albumManager: .init(albumName: option.asset.albumName) ) } - + init( option: AespaOption, session: AespaCoreSession, @@ -62,25 +63,26 @@ open class AespaSession { self.camera = camera self.fileManager = fileManager self.albumManager = albumManager - + self.videoFileBufferSubject = .init(nil) self.photoFileBufferSubject = .init(nil) self.previewLayerSubject = .init(nil) - + self.previewLayer = AVCaptureVideoPreviewLayer(session: session) - + // Add first video file to buffer if it exists if let firstVideoFile = fileManager.fetch(albumName: option.asset.albumName, count: 1).first { videoFileBufferSubject.send(.success(firstVideoFile)) } } - + // MARK: - vars /// This property exposes the underlying `AVCaptureSession` that `Aespa` currently utilizes. /// /// While you can directly interact with this object, it is strongly recommended to avoid modifications /// that could yield unpredictable behavior. - /// If you require custom configurations, consider utilizing the `custom` function we offer whenever possible. + /// If you require custom configurations, + /// consider utilizing the `custom` function we offer whenever possible. public var captureSession: AVCaptureSession { return coreSession } @@ -119,7 +121,11 @@ open class AespaSession { .compactMap({ $0 }) .eraseToAnyPublisher() } - + + /// The publisher that broadcasts the result of a photo file operation. + /// It emits a `Result` object containing a `PhotoFile` on success or an `Error` on failure, + /// and never fails itself. This can be used to observe the photo capturing process and handle + /// the results asynchronously. public var photoFilePublisher: AnyPublisher, Never> { photoFileBufferSubject.handleEvents(receiveOutput: { status in if case .failure(let error) = status { @@ -129,7 +135,7 @@ open class AespaSession { .compactMap({ $0 }) .eraseToAnyPublisher() } - + /// This publisher is responsible for emitting updates to the preview layer. /// /// A log message is printed to the console every time a new layer is pushed. @@ -165,7 +171,8 @@ open class AespaSession { /// - Parameter completionHandler: A closure that handles the result of the operation. /// It's called with a `Result` object that encapsulates either a `VideoFile` instance. /// - /// - Note: It is recommended to use the ``stopRecording() async throws`` for more straightforward error handling. + /// - Note: It is recommended to use the ``stopRecording() async throws`` + /// for more straightforward error handling. public func stopRecording( _ completionHandler: @escaping (Result) -> Void = { _ in } ) { @@ -179,7 +186,22 @@ open class AespaSession { } } } - + + /// Asynchronously captures a photo using the specified `AVCapturePhotoSettings`. + /// + /// This function utilizes the `captureWithError(setting:)` function to perform the actual photo capture, + /// while handling any errors that may occur. If the photo capture is successful, it will return a `PhotoFile` + /// object through the provided completion handler. + /// + /// In case of an error during the photo capture process, the error will be logged and also returned via + /// the completion handler. + /// + /// - Parameters: + /// - setting: The `AVCapturePhotoSettings` to use when capturing the photo. + /// - completionHandler: A closure to be invoked once the photo capture process is completed. This + /// closure takes a `Result` type where `Success` contains a `PhotoFile` object and + /// `Failure` contains an `Error` object. By default, the closure does nothing. + /// public func capturePhoto( setting: AVCapturePhotoSettings, _ completionHandler: @escaping (Result) -> Void = { _ in } @@ -194,7 +216,7 @@ open class AespaSession { } } } - + /// Mutes the audio input for the video recording session. /// /// If an error occurs during the operation, the error is logged. @@ -207,7 +229,7 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } @@ -223,7 +245,7 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } @@ -241,7 +263,7 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } @@ -259,7 +281,7 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } @@ -280,13 +302,14 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } /// Sets the stabilization mode for the video recording session. /// - /// - Parameter mode: An `AVCaptureVideoStabilizationMode` value indicating the stabilization mode to be set. + /// - Parameter mode: An `AVCaptureVideoStabilizationMode` value + /// indicating the stabilization mode to be set. /// /// If an error occurs during the operation, the error is logged. /// @@ -298,10 +321,10 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } - + /// Sets the autofocusing mode for the video recording session. /// /// - Parameter mode: The focus mode for the capture device. @@ -316,11 +339,10 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } - /// Sets the zoom factor for the video recording session. /// /// - Parameter factor: A `CGFloat` value indicating the zoom factor to be set. @@ -335,10 +357,10 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } - + /// Sets the torch mode and level for the video recording session. /// /// If an error occurs during the operation, the error is logged. @@ -358,13 +380,14 @@ open class AespaSession { } catch let error { Logger.log(error: error) // Logs any errors encountered during the operation } - + return self } - + // MARK: - Throwing/// Starts the recording of a video session. /// - /// - Throws: `AespaError` if the video file path request fails, orientation setting fails, or starting the recording fails. + /// - Throws: `AespaError` if the video file path request fails, + /// orientation setting fails, or starting the recording fails. /// /// - Note: If `autoVideoOrientation` option is enabled, /// it sets the orientation according to the current device orientation. @@ -375,14 +398,14 @@ open class AespaSession { directoryName: option.asset.albumName, fileName: fileName, extension: "mp4") - + if option.session.autoVideoOrientationEnabled { try setOrientationWithError(to: UIDevice.current.orientation.toVideoOrientation) } - + try recorder.startRecording(in: filePath) } - + /// Stops the ongoing video recording session and attempts to add the video file to the album. /// /// Supporting `async`, you can use this method in Swift Concurrency's context @@ -391,27 +414,39 @@ open class AespaSession { public func stopRecordingWithError() async throws -> VideoFile { let videoFilePath = try await recorder.stopRecording() let videoFile = VideoFileGenerator.generate(with: videoFilePath, date: Date()) - + try await albumManager.addToAlbum(filePath: videoFilePath) videoFileBufferSubject.send(.success(videoFile)) - + return videoFile } - + + /// Asynchronously captures a photo with the specified `AVCapturePhotoSettings`. + /// + /// The captured photo is flattened into a `Data` object, and then added to an album. A `PhotoFile` + /// object is then created using the raw photo data and the current date. This `PhotoFile` is sent + /// through the `photoFileBufferSubject` and then returned to the caller. + /// + /// If any part of this process fails, an `AespaError` is thrown. + /// + /// - Parameter setting: The `AVCapturePhotoSettings` to use when capturing the photo. + /// - Returns: A `PhotoFile` object representing the captured photo. + /// - Throws: An `AespaError` if there is an issue capturing the photo, + /// flattening it into a `Data` object, or adding it to the album. public func captureWithError(setting: AVCapturePhotoSettings) async throws -> PhotoFile { let rawPhotoAsset = try await camera.capture(setting: setting) guard let rawPhotoData = rawPhotoAsset.fileDataRepresentation() else { throw AespaError.file(reason: .unableToFlatten) } - + try await albumManager.addToAlbum(imageData: rawPhotoData) - + let photoFile = PhotoFileGenerator.generate(data: rawPhotoData, date: Date()) photoFileBufferSubject.send(.success(photoFile)) - + return photoFile } - + /// Mutes the audio input for the video recording session. /// /// - Throws: `AespaError` if the session fails to run the tuner. @@ -486,7 +521,8 @@ open class AespaSession { /// Sets the stabilization mode for the video recording session. /// - /// - Parameter mode: An `AVCaptureVideoStabilizationMode` value indicating the stabilization mode to be set. + /// - Parameter mode: An `AVCaptureVideoStabilizationMode` value + /// indicating the stabilization mode to be set. /// /// - Throws: `AespaError` if the session fails to run the tuner. /// @@ -497,7 +533,7 @@ open class AespaSession { try coreSession.run(tuner) return self } - + /// Sets the autofocusing mode for the video recording session. /// /// - Parameter mode: The focus mode(`AVCaptureDevice.FocusMode`) for the session. @@ -512,7 +548,6 @@ open class AespaSession { return self } - /// Sets the zoom factor for the video recording session. /// /// - Parameter factor: A `CGFloat` value indicating the zoom factor to be set. @@ -545,9 +580,10 @@ open class AespaSession { try coreSession.run(tuner) return self } - + // MARK: - Customizable - /// This function provides a way to use a custom tuner to modify the current session. The tuner must conform to `AespaSessionTuning`. + /// This function provides a way to use a custom tuner to modify the current session. + /// The tuner must conform to `AespaSessionTuning`. /// /// - Parameter tuner: An instance that conforms to `AespaSessionTuning`. /// - Throws: If the session fails to run the tuner. @@ -555,9 +591,9 @@ open class AespaSession { try coreSession.run(tuner) } - // MARK: - Utilities - /// Fetches a list of recorded video files. The number of files fetched is controlled by the limit parameter. + /// Fetches a list of recorded video files. + /// The number of files fetched is controlled by the limit parameter. /// /// It is recommended not to be called in main thread. /// @@ -567,13 +603,14 @@ open class AespaSession { public func fetchVideoFiles(limit: Int = 0) -> [VideoFile] { return fileManager.fetch(albumName: option.asset.albumName, count: limit) } - + /// Checks if essential conditions to start recording are satisfied. /// This includes checking for capture authorization, if the session is running, /// if there is an existing connection and if a device is attached. /// /// - Throws: `AespaError.permission` if capture authorization is denied. - /// - Throws: `AespaError.session` if the session is not running, cannot find a connection, or cannot find a device. + /// - Throws: `AespaError.session` if the session is not running, + /// cannot find a connection, or cannot find a device. public func doctor() async throws { // Check authorization status guard @@ -581,16 +618,16 @@ open class AespaSession { else { throw AespaError.permission(reason: .denied) } - + guard coreSession.isRunning else { throw AespaError.session(reason: .notRunning) } - + // Check if connection exists guard coreSession.movieFileOutput != nil else { throw AespaError.session(reason: .cannotFindConnection) } - + // Check if device is attached guard coreSession.videoDeviceInput != nil else { throw AespaError.session(reason: .cannotFindDevice) @@ -603,10 +640,10 @@ extension AespaSession { func startSession() throws { let tuner = SessionLaunchTuner() try coreSession.run(tuner) - + previewLayerSubject.send(previewLayer) } - + func terminateSession() throws { let tuner = SessionTerminationTuner() try coreSession.run(tuner) diff --git a/Sources/Aespa/Core/AespaCoreAlbumManager.swift b/Sources/Aespa/Core/AespaCoreAlbumManager.swift index 75b33ac..e8b964e 100644 --- a/Sources/Aespa/Core/AespaCoreAlbumManager.swift +++ b/Sources/Aespa/Core/AespaCoreAlbumManager.swift @@ -8,18 +8,19 @@ import Photos import AVFoundation -/// Retreive the video(url) from `FileManager` based local storage and add the video to the pre-defined album roll +/// Retreive the video(url) from `FileManager` based local storage +/// and add the video to the pre-defined album roll class AespaCoreAlbumManager { // Dependencies private let photoLibrary: PHPhotoLibrary private let albumName: String private var album: PHAssetCollection? - + convenience init(albumName: String) { let photoLibrary = PHPhotoLibrary.shared() self.init(albumName: albumName, photoLibrary) } - + init( albumName: String, _ photoLibrary: PHPhotoLibrary @@ -27,7 +28,7 @@ class AespaCoreAlbumManager { self.albumName = albumName self.photoLibrary = photoLibrary } - + func run(processor: T) async throws { if let album { try await processor.process(photoLibrary, album) @@ -43,7 +44,7 @@ extension AespaCoreAlbumManager { let processor = VideoAssetAdditionProcessor(filePath: filePath) try await run(processor: processor) } - + func addToAlbum(imageData: Data) async throws { let processor = PhotoAssetAdditionProcessor(imageData: imageData) try await run(processor: processor) diff --git a/Sources/Aespa/Core/AespaCoreCamera.swift b/Sources/Aespa/Core/AespaCoreCamera.swift index 207aeb3..b86e7d6 100644 --- a/Sources/Aespa/Core/AespaCoreCamera.swift +++ b/Sources/Aespa/Core/AespaCoreCamera.swift @@ -12,19 +12,19 @@ import AVFoundation /// Capturing a photo and responsible for notifying the result class AespaCoreCamera: NSObject { private let core: AespaCoreSession - + private let fileIOResultSubject = PassthroughSubject, Never>() private var fileIOResultSubsciption: Cancellable? - + init(core: AespaCoreSession) { self.core = core } - + func run(processor: T) throws { guard let output = core.photoOutput else { throw AespaError.session(reason: .cannotFindConnection) } - + try processor.process(output) } } @@ -33,7 +33,7 @@ extension AespaCoreCamera { func capture(setting: AVCapturePhotoSettings) async throws -> AVCapturePhoto { let processor = CapturePhotoProcessor(setting: setting, delegate: self) try run(processor: processor) - + return try await withCheckedThrowingContinuation { continuation in fileIOResultSubsciption = fileIOResultSubject .subscribe(on: DispatchQueue.global()) @@ -50,9 +50,13 @@ extension AespaCoreCamera { } extension AespaCoreCamera: AVCapturePhotoCaptureDelegate { - func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { + func photoOutput( + _ output: AVCapturePhotoOutput, + didFinishProcessingPhoto photo: AVCapturePhoto, + error: Error? + ) { Logger.log(message: "Photo captured") - + if let error { fileIOResultSubject.send(.failure(error)) Logger.log(error: error) diff --git a/Sources/Aespa/Core/AespaCoreFileManager.swift b/Sources/Aespa/Core/AespaCoreFileManager.swift index 394a7e9..05b94f3 100644 --- a/Sources/Aespa/Core/AespaCoreFileManager.swift +++ b/Sources/Aespa/Core/AespaCoreFileManager.swift @@ -10,9 +10,9 @@ import Foundation class AespaCoreFileManager { private var videoFileProxyDictionary: [String: VideoFileCachingProxy] private let enableCaching: Bool - + let systemFileManager: FileManager - + init( enableCaching: Bool, fileManager: FileManager = .default @@ -21,20 +21,20 @@ class AespaCoreFileManager { self.enableCaching = enableCaching self.systemFileManager = fileManager } - + /// If `count` is `0`, return all existing files func fetch(albumName: String, count: Int) -> [VideoFile] { guard count >= 0 else { return [] } - + guard let proxy = videoFileProxyDictionary[albumName] else { videoFileProxyDictionary[albumName] = VideoFileCachingProxy( albumName: albumName, enableCaching: enableCaching, fileManager: systemFileManager) - + return fetch(albumName: albumName, count: count) } - + let files = proxy.fetch(count: count) Logger.log(message: "\(files.count) Video files fetched") return files diff --git a/Sources/Aespa/Core/AespaCoreRecorder.swift b/Sources/Aespa/Core/AespaCoreRecorder.swift index ff2ef38..bf1c2b9 100644 --- a/Sources/Aespa/Core/AespaCoreRecorder.swift +++ b/Sources/Aespa/Core/AespaCoreRecorder.swift @@ -12,20 +12,20 @@ import AVFoundation /// Start, stop recording and responsible for notifying the result of recording class AespaCoreRecorder: NSObject { private let core: AespaCoreSession - + /// Notify the end of recording private let fileIOResultSubject = PassthroughSubject, Never>() private var fileIOResultSubsciption: Cancellable? - + init(core: AespaCoreSession) { self.core = core } - + func run(processor: T) throws { guard let output = core.movieFileOutput else { throw AespaError.session(reason: .cannotFindConnection) } - + try processor.process(output) } } @@ -34,10 +34,10 @@ extension AespaCoreRecorder { func startRecording(in filePath: URL) throws { try run(processor: StartRecordProcessor(filePath: filePath, delegate: self)) } - + func stopRecording() async throws -> URL { try run(processor: FinishRecordProcessor()) - + return try await withCheckedThrowingContinuation { continuation in fileIOResultSubsciption = fileIOResultSubject.sink { _ in // Do nothing on completion; we're only interested in values. @@ -54,7 +54,11 @@ extension AespaCoreRecorder { } extension AespaCoreRecorder: AVCaptureFileOutputRecordingDelegate { - func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { + func fileOutput( + _ output: AVCaptureFileOutput, + didStartRecordingTo fileURL: URL, + from connections: [AVCaptureConnection] + ) { Logger.log(message: "Recording started") } diff --git a/Sources/Aespa/Core/AespaCoreSession.swift b/Sources/Aespa/Core/AespaCoreSession.swift index 0a3938d..44ed487 100644 --- a/Sources/Aespa/Core/AespaCoreSession.swift +++ b/Sources/Aespa/Core/AespaCoreSession.swift @@ -12,46 +12,46 @@ import AVFoundation class AespaCoreSession: AVCaptureSession { var option: AespaOption - + init(option: AespaOption) { self.option = option } - + func run(_ tuner: T) throws { if tuner.needTransaction { self.beginConfiguration() } defer { if tuner.needTransaction { self.commitConfiguration() } } - + try tuner.tune(self) } - + func run(_ tuner: T) throws { guard let device = self.videoDeviceInput?.device else { throw AespaError.device(reason: .invalid) } - + if tuner.needLock { try device.lockForConfiguration() } defer { if tuner.needLock { device.unlockForConfiguration() } } - + try tuner.tune(device) } - + func run(_ tuner: T) throws { guard let connection = self.connections.first else { throw AespaError.session(reason: .cannotFindConnection) } - + try tuner.tune(connection) } - + func run(_ processor: T) throws { guard let output = self.movieFileOutput else { throw AespaError.session(reason: .cannotFindConnection) } - + try processor.process(output) } } diff --git a/Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift index 22b6709..7b04560 100644 --- a/Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/AVCaptureConnection+AespaRepresentable.swift @@ -11,7 +11,7 @@ import AVFoundation protocol AespaCaptureConnectionRepresentable { var videoOrientation: AVCaptureVideoOrientation { get set } var preferredVideoStabilizationMode: AVCaptureVideoStabilizationMode { get set } - + func setOrientation(to orientation: AVCaptureVideoOrientation) func setStabilizationMode(to mode: AVCaptureVideoStabilizationMode) } @@ -20,9 +20,8 @@ extension AVCaptureConnection: AespaCaptureConnectionRepresentable { func setOrientation(to orientation: AVCaptureVideoOrientation) { self.videoOrientation = orientation } - + func setStabilizationMode(to mode: AVCaptureVideoStabilizationMode) { self.preferredVideoStabilizationMode = mode } } - diff --git a/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift index e1a1e09..8cc66e9 100644 --- a/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/AVCaptureDevice+AespaRepresentable.swift @@ -13,11 +13,11 @@ protocol AespaCaptureDeviceRepresentable { var focusMode: AVCaptureDevice.FocusMode { get set } var flashMode: AVCaptureDevice.FlashMode { get set } var videoZoomFactor: CGFloat { get set } - + var maxResolution: Double? { get } - + func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool - + func setZoomFactor(_ factor: CGFloat) func setFocusMode(_ focusMode: AVCaptureDevice.FocusMode) func setTorchMode(_ torchMode: AVCaptureDevice.TorchMode) @@ -37,15 +37,15 @@ extension AVCaptureDevice: AespaCaptureDeviceRepresentable { self.torchMode = .off } } - + func setFocusMode(_ focusMode: FocusMode) { self.focusMode = focusMode } - + func setZoomFactor(_ factor: CGFloat) { self.videoZoomFactor = factor } - + var maxResolution: Double? { var maxResolution: Double = 0 for format in self.formats { diff --git a/Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift index c8f2d77..f8c3dd4 100644 --- a/Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/AVCaptureFileOutput+AespaRepresentable.swift @@ -10,7 +10,9 @@ import AVFoundation protocol AespaFileOutputRepresentable { func stopRecording() - func startRecording(to outputFileURL: URL, recordingDelegate delegate: AVCaptureFileOutputRecordingDelegate) + func startRecording( + to outputFileURL: URL, + recordingDelegate delegate: AVCaptureFileOutputRecordingDelegate) func getConnection(with mediaType: AVMediaType) -> AespaCaptureConnectionRepresentable? } diff --git a/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift index d1534be..dddf3b3 100644 --- a/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/AespaCoreSession+AespaRepresentable.swift @@ -8,59 +8,63 @@ import Foundation import AVFoundation -/// `AespaCoreSessionRepresentable` defines a set of requirements for classes or structs that interact with `AVCaptureDeviceInput` -/// and `AVCaptureMovieFileOutput` to setup and configure a camera session for recording videos. +/// `AespaCoreSessionRepresentable` defines a set of requirements for classes or +/// structs that interact with `AVCaptureDeviceInput` and +/// `AVCaptureMovieFileOutput` to setup and configure a camera session for recording videos. public protocol AespaCoreSessionRepresentable { /// The `AVCaptureSession` that coordinates the flow of data from AV input devices to outputs. var avCaptureSession: AVCaptureSession { get } /// A Boolean value indicating whether the capture session is running. var isRunning: Bool { get } - + /// The `AVCaptureDeviceInput` representing the audio input. var audioDeviceInput: AVCaptureDeviceInput? { get } - + /// The `AVCaptureDeviceInput` representing the video input. var videoDeviceInput: AVCaptureDeviceInput? { get } - + /// The `AVCaptureMovieFileOutput` for the video file output. var movieFileOutput: AVCaptureMovieFileOutput? { get } - + /// The `AVCaptureVideoPreviewLayer` for previewing the video being recorded. var previewLayer: AVCaptureVideoPreviewLayer { get } - + /// Starts the capture session. This method is synchronous and blocks until the session starts. func startRunning() - + /// Stops the capture session. This method is synchronous and blocks until the session stops. func stopRunning() - + /// Adds movie input to the recording session. /// Throws an error if the operation fails. func addMovieInput() throws - + /// Removes movie input from the recording session if it exists. func removeMovieInput() - + /// Adds audio input to the recording session. /// Throws an error if the operation fails. func addAudioInput() throws - + /// Removes audio input from the recording session if it exists. func removeAudioInput() - + /// Adds movie file output to the recording session. /// Throws an error if the operation fails. func addMovieFileOutput() throws - + /// Adds photo file output to the session. /// Throws an error if the operation fails. func addCapturePhotoOutput() throws - + /// Sets the position of the camera. /// Throws an error if the operation fails. - func setCameraPosition(to position: AVCaptureDevice.Position, device deviceType: AVCaptureDevice.DeviceType?) throws - + func setCameraPosition( + to position: AVCaptureDevice.Position, + device deviceType: AVCaptureDevice.DeviceType? + ) throws + /// Sets the video quality preset. func setVideoQuality(to preset: AVCaptureSession.Preset) throws } @@ -75,56 +79,54 @@ extension AespaCoreSession: AespaCoreSessionRepresentable { .filter { $0.device.hasMediaType(.audio) } // Find audio input .first } - + var videoDeviceInput: AVCaptureDeviceInput? { return self.inputs .compactMap { $0 as? AVCaptureDeviceInput } // Get inputs .filter { $0.device.hasMediaType(.video) } // Find video input .first } - + var movieFileOutput: AVCaptureMovieFileOutput? { let output = self.outputs.first { $0 as? AVCaptureMovieFileOutput != nil } - return output as? AVCaptureMovieFileOutput } - + var photoOutput: AVCapturePhotoOutput? { let output = self.outputs.first { $0 as? AVCapturePhotoOutput != nil } - + return output as? AVCapturePhotoOutput } - + var previewLayer: AVCaptureVideoPreviewLayer { let previewLayer = AVCaptureVideoPreviewLayer(session: self) previewLayer.connection?.videoOrientation = .portrait // Fixed value for now - + return previewLayer } - + // MARK: - Input and output - + func addMovieInput() throws { // Add video input guard let videoDevice = AVCaptureDevice.default(for: AVMediaType.video) else { throw AespaError.device(reason: .unableToSetInput) } - + let videoInput = try AVCaptureDeviceInput(device: videoDevice) guard self.canAddInput(videoInput) else { throw AespaError.device(reason: .unableToSetInput) } - + self.addInput(videoInput) } - - - func removeMovieInput() { + + func removeMovieInput() { guard let videoDevice = AVCaptureDevice.default(for: AVMediaType.video), let videoInput = try? AVCaptureDeviceInput(device: videoDevice) @@ -134,66 +136,67 @@ extension AespaCoreSession: AespaCoreSessionRepresentable { self.removeInput(videoInput) } - - + func addAudioInput() throws { // Add microphone input guard let audioDevice = AVCaptureDevice.default(for: AVMediaType.audio) else { throw AespaError.device(reason: .unableToSetInput) } - + let audioInput = try AVCaptureDeviceInput(device: audioDevice) - + guard self.canAddInput(audioInput) else { throw AespaError.device(reason: .unableToSetInput) } - + self.addInput(audioInput) } - - + func removeAudioInput() { if let audioDeviceInput { self.removeInput(audioDeviceInput) } } - + func addMovieFileOutput() throws { guard self.movieFileOutput == nil else { // return itself if output is already set return } - + let fileOutput = AVCaptureMovieFileOutput() guard self.canAddOutput(fileOutput) else { throw AespaError.device(reason: .unableToSetOutput) } - + self.addOutput(fileOutput) } - + func addCapturePhotoOutput() throws { guard self.photoOutput == nil else { // return itself if output is already set return } - + let photoOutput = AVCapturePhotoOutput() guard self.canAddOutput(photoOutput) else { throw AespaError.device(reason: .unableToSetOutput) } - + self.addOutput(photoOutput) } - + // MARK: - Option related - func setCameraPosition(to position: AVCaptureDevice.Position,device deviceType: AVCaptureDevice.DeviceType?) throws { + func setCameraPosition( + to position: AVCaptureDevice.Position, + device deviceType: AVCaptureDevice.DeviceType? + ) throws { let session = self - + if let videoDeviceInput { session.removeInput(videoDeviceInput) } - + let device: AVCaptureDevice if let deviceType, @@ -205,7 +208,7 @@ extension AespaCoreSession: AespaCoreSessionRepresentable { } else { throw AespaError.device(reason: .invalid) } - + let deviceInput = try AVCaptureDeviceInput(device: device) if session.canAddInput(deviceInput) { session.addInput(deviceInput) @@ -213,10 +216,10 @@ extension AespaCoreSession: AespaCoreSessionRepresentable { throw AespaError.device(reason: .unableToSetInput) } } - + func setVideoQuality(to preset: AVCaptureSession.Preset) { let session = self - + session.sessionPreset = preset } } diff --git a/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift b/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift index 8b8d7bb..ae586b9 100644 --- a/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift +++ b/Sources/Aespa/Core/Representable/Photos+AespaRepresentable.swift @@ -12,27 +12,34 @@ protocol AespaAssetLibraryRepresentable { func performChanges(_ changes: @escaping () -> Void) async throws func performChangesAndWait(_ changeBlock: @escaping () -> Void) throws func requestAuthorization(for accessLevel: PHAccessLevel) async -> PHAuthorizationStatus - func fetchAlbum(title: String, fetchOptions: PHFetchOptions) -> Collection? + func fetchAlbum( + title: String, + fetchOptions: + PHFetchOptions + ) -> Collection? } protocol AespaAssetCollectionRepresentable { var underlyingAssetCollection: PHAssetCollection { get } var localizedTitle: String? { get } - + func canAdd(video filePath: URL) -> Bool } extension PHPhotoLibrary: AespaAssetLibraryRepresentable { - func fetchAlbum(title: String, fetchOptions: PHFetchOptions) -> Collection? { + func fetchAlbum( + title: String, + fetchOptions: PHFetchOptions + ) -> Collection? { fetchOptions.predicate = NSPredicate(format: "title = %@", title) - + let collections = PHAssetCollection.fetchAssetCollections( with: .album, subtype: .any, options: fetchOptions ) - + return collections.firstObject as? Collection } - + func requestAuthorization(for accessLevel: PHAccessLevel) async -> PHAuthorizationStatus { await PHPhotoLibrary.requestAuthorization(for: accessLevel) } @@ -40,11 +47,11 @@ extension PHPhotoLibrary: AespaAssetLibraryRepresentable { extension PHAssetCollection: AespaAssetCollectionRepresentable { var underlyingAssetCollection: PHAssetCollection { self } - + func canAdd(video filePath: URL) -> Bool { let asset = AVAsset(url: filePath) let tracks = asset.tracks(withMediaType: AVMediaType.video) - + return !tracks.isEmpty } } diff --git a/Sources/Aespa/Data/PhotoFile.swift b/Sources/Aespa/Data/PhotoFile.swift index 1b71cc2..f332709 100644 --- a/Sources/Aespa/Data/PhotoFile.swift +++ b/Sources/Aespa/Data/PhotoFile.swift @@ -11,13 +11,13 @@ import Foundation public struct PhotoFile: Identifiable, Equatable { public let id = UUID() - + /// A `Data` value containing image's raw data public let data: Data - + /// A `Date` value keeps the date it's generated public let generatedDate: Date - + /// An optional thumbnail generated from the video with `UIImage` type. /// This will be `nil` if the thumbnail could not be generated for some reason. public var thumbnail: UIImage? diff --git a/Sources/Aespa/Data/VideoFile.swift b/Sources/Aespa/Data/VideoFile.swift index 6849c4f..379cf03 100644 --- a/Sources/Aespa/Data/VideoFile.swift +++ b/Sources/Aespa/Data/VideoFile.swift @@ -17,10 +17,10 @@ import AVFoundation public struct VideoFile: Equatable { /// A `Date` value keeps the date it's generated public let generatedDate: Date - + /// The path to the video file. public let path: URL - + /// An optional thumbnail generated from the video with `UIImage` type. /// This will be `nil` if the thumbnail could not be generated for some reason. public var thumbnail: UIImage? @@ -34,7 +34,7 @@ public extension VideoFile { if let thumbnail { return Image(uiImage: thumbnail) } - + return nil } } diff --git a/Sources/Aespa/Data/VideoFileCachingProxy.swift b/Sources/Aespa/Data/VideoFileCachingProxy.swift index e1a9430..f453d5b 100644 --- a/Sources/Aespa/Data/VideoFileCachingProxy.swift +++ b/Sources/Aespa/Data/VideoFileCachingProxy.swift @@ -10,22 +10,22 @@ import Foundation class VideoFileCachingProxy { private let albumName: String private let cacheEnabled: Bool - + private let fileManager: FileManager - + private var cache: [URL: VideoFile] = [:] private var lastModificationDate: Date? - + init(albumName: String, enableCaching: Bool, fileManager: FileManager) { self.albumName = albumName self.cacheEnabled = enableCaching self.fileManager = fileManager - + DispatchQueue.global().async { self.updateCache() } } - + /// If `count` is `0`, return all existing files func fetch(count: Int) -> [VideoFile] { guard @@ -34,32 +34,32 @@ class VideoFileCachingProxy { else { return [] } - + guard cacheEnabled else { invalidateCache() return fetchFile(from: albumDirectory, count: count).sorted() } - + guard let directoryAttributes = try? fileManager.attributesOfItem(atPath: albumDirectory.path), let currentModificationDate = directoryAttributes[.modificationDate] as? Date else { return [] } - + // Check if the directory has been modified since last fetch if let lastModificationDate = self.lastModificationDate, lastModificationDate == currentModificationDate { return fetchSortedFiles(count: count) } - + // Update cache and lastModificationDate updateCache() self.lastModificationDate = currentModificationDate - + return fetchSortedFiles(count: count) } - + // Invalidate the cache if needed, for example when a file is added or removed func invalidateCache() { cache.removeAll() @@ -67,17 +67,17 @@ class VideoFileCachingProxy { } } - private extension VideoFileCachingProxy { func updateCache() { guard - let albumDirectory = try? VideoFilePathProvider.requestDirectoryPath(from: fileManager, name: albumName), + let albumDirectory = try? VideoFilePathProvider.requestDirectoryPath(from: fileManager, + name: albumName), let filePaths = try? fileManager.contentsOfDirectory(atPath: albumDirectory.path) else { Logger.log(message: "Cannot access to saved video file") return } - + var newCache: [URL: VideoFile] = [:] filePaths.forEach { fileName in let filePath = albumDirectory.appendingPathComponent(fileName) @@ -89,16 +89,16 @@ private extension VideoFileCachingProxy { } cache = newCache } - + func fetchFile(from albumDirectory: URL, count: Int) -> [VideoFile] { guard count >= 0 else { return [] } - + let files = Array(cache.values) let sortedFiles = files.sorted() let prefixFiles = (count == 0) ? sortedFiles : Array(sortedFiles.prefix(count)) return prefixFiles } - + func createVideoFile(for filePath: URL) -> VideoFile? { guard let fileAttributes = try? fileManager.attributesOfItem(atPath: filePath.path), @@ -107,10 +107,10 @@ private extension VideoFileCachingProxy { Logger.log(message: "Cannot access to saved video file") return nil } - + return VideoFileGenerator.generate(with: filePath, date: creationDate) } - + func fetchSortedFiles(count: Int) -> [VideoFile] { let files = cache.values.sorted() return count == 0 ? files : Array(files.prefix(count)) diff --git a/Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift b/Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift index c97a4e3..fe63ce3 100644 --- a/Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift +++ b/Sources/Aespa/Processor/Asset/PhotoAssetAdditionProcessor.swift @@ -11,7 +11,12 @@ import Foundation struct PhotoAssetAdditionProcessor: AespaAssetProcessing { let imageData: Data - func process(_ photoLibrary: T, _ assetCollection: U) async throws { + func process< + T: AespaAssetLibraryRepresentable, U: AespaAssetCollectionRepresentable + >( + _ photoLibrary: T, + _ assetCollection: U + ) async throws { guard case .authorized = await photoLibrary.requestAuthorization(for: .addOnly) else { @@ -25,7 +30,13 @@ struct PhotoAssetAdditionProcessor: AespaAssetProcessing { } /// Add the video to the app's album roll - func add(imageData: Data, to album: U, _ photoLibrary: T) async throws -> Void { + func add< + T: AespaAssetLibraryRepresentable, U: AespaAssetCollectionRepresentable + >( + imageData: Data, + to album: U, + _ photoLibrary: T + ) async throws { try await photoLibrary.performChanges { // Request creating an asset from the image. let creationRequest = PHAssetCreationRequest.forAsset() diff --git a/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift b/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift index 3c14ce6..8daab77 100644 --- a/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift +++ b/Sources/Aespa/Processor/Asset/VideoAssetAdditionProcessor.swift @@ -11,7 +11,12 @@ import Foundation struct VideoAssetAdditionProcessor: AespaAssetProcessing { let filePath: URL - func process(_ photoLibrary: T, _ assetCollection: U) async throws { + func process< + T: AespaAssetLibraryRepresentable, U: AespaAssetCollectionRepresentable + >( + _ photoLibrary: T, + _ assetCollection: U + ) async throws { guard case .authorized = await photoLibrary.requestAuthorization(for: .addOnly) else { @@ -25,14 +30,20 @@ struct VideoAssetAdditionProcessor: AespaAssetProcessing { } /// Add the video to the app's album roll - func add(video path: URL, to album: U, _ photoLibrary: T) async throws -> Void { + func add< + T: AespaAssetLibraryRepresentable, U: AespaAssetCollectionRepresentable + >(video path: URL, + to album: U, + _ photoLibrary: T + ) async throws { guard album.canAdd(video: path) else { throw AespaError.album(reason: .notVideoURL) } return try await photoLibrary.performChanges { guard - let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path), + let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo( + atFileURL: path), let placeholder = assetChangeRequest.placeholderForCreatedAsset, let albumChangeRequest = PHAssetCollectionChangeRequest(for: album.underlyingAssetCollection) else { diff --git a/Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift b/Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift index bc5b3f9..78f6b47 100644 --- a/Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift +++ b/Sources/Aespa/Processor/Capture/CapturePhotoProcessor.swift @@ -10,17 +10,17 @@ import AVFoundation struct CapturePhotoProcessor: AespaCapturePhotoOutputProcessing { let setting: AVCapturePhotoSettings let delegate: AVCapturePhotoCaptureDelegate - + init(setting: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate) { self.setting = setting self.delegate = delegate } - - func process(_ output: T) throws where T : AespaPhotoOutputRepresentable { + + func process(_ output: T) throws where T: AespaPhotoOutputRepresentable { guard output.getConnection(with: .video) != nil else { throw AespaError.session(reason: .cannotFindConnection) } - + output.capturePhoto(with: setting, delegate: delegate) } } diff --git a/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift b/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift index 949d614..c8baff0 100644 --- a/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift +++ b/Sources/Aespa/Processor/Record/FinishRecordProcessor.swift @@ -5,7 +5,6 @@ // Created by 이영빈 on 2023/06/02. // - import AVFoundation struct FinishRecordProcessor: AespaMovieFileOutputProcessing { diff --git a/Sources/Aespa/Processor/Record/StartRecordProcessor.swift b/Sources/Aespa/Processor/Record/StartRecordProcessor.swift index d219bb0..e320cdd 100644 --- a/Sources/Aespa/Processor/Record/StartRecordProcessor.swift +++ b/Sources/Aespa/Processor/Record/StartRecordProcessor.swift @@ -10,12 +10,12 @@ import AVFoundation struct StartRecordProcessor: AespaMovieFileOutputProcessing { let filePath: URL let delegate: AVCaptureFileOutputRecordingDelegate - + func process(_ output: T) throws { guard output.getConnection(with: .video) != nil else { throw AespaError.session(reason: .cannotFindConnection) } - + output.startRecording(to: filePath, recordingDelegate: delegate) } } diff --git a/Sources/Aespa/Tuner/AespaTuning.swift b/Sources/Aespa/Tuner/AespaTuning.swift index a536552..a4f40e6 100644 --- a/Sources/Aespa/Tuner/AespaTuning.swift +++ b/Sources/Aespa/Tuner/AespaTuning.swift @@ -9,14 +9,30 @@ import Combine import Foundation import AVFoundation +/// `AespaSessionTuning` defines a set of requirements for classes or structs that aim to adjust settings +/// for an `AespaCoreSessionRepresentable`. +/// /// - Warning: Do not `begin` or `commit` session change yourself. It can cause deadlock. /// Instead, use `needTransaction` flag public protocol AespaSessionTuning { + /// Determines if a transaction is required for this particular tuning operation. + /// Default is `true`, indicating a transaction is generally needed. var needTransaction: Bool { get } + + /// Applies the specific tuning implementation to a given `AespaCoreSessionRepresentable` session. + /// It is expected that each concrete implementation of `AespaSessionTuning` will provide its own + /// tuning adjustments here. + /// + /// - Parameter session: The `AespaCoreSessionRepresentable` session to be adjusted. + /// + /// - Throws: An error if any problems occur during the tuning process. func tune(_ session: T) throws } +/// Default implementation for `AespaSessionTuning`. public extension AespaSessionTuning { + /// By default, tuning operations need a transaction. This can be overridden by specific tuners + /// if a transaction isn't necessary for their operation. var needTransaction: Bool { true } } @@ -25,7 +41,6 @@ protocol AespaConnectionTuning { func tune(_ connection: T) throws } - /// - Warning: Do not `lock` or `release` device yourself. It can cause deadlock. /// Instead, use `needLock` flag protocol AespaDeviceTuning { diff --git a/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift b/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift index 957fc8f..e64c697 100644 --- a/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift +++ b/Sources/Aespa/Tuner/Connection/VideoOrientationTuner.swift @@ -9,7 +9,7 @@ import AVFoundation struct VideoOrientationTuner: AespaConnectionTuning { var orientation: AVCaptureVideoOrientation - + func tune(_ connection: T) throws { connection.setOrientation(to: orientation) } diff --git a/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift b/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift index c1b0ad4..ad555b2 100644 --- a/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift +++ b/Sources/Aespa/Tuner/Connection/VideoStabilizationTuner.swift @@ -9,7 +9,7 @@ import AVFoundation struct VideoStabilizationTuner: AespaConnectionTuning { var stabilzationMode: AVCaptureVideoStabilizationMode - + func tune(_ connection: T) { connection.setStabilizationMode(to: stabilzationMode) } diff --git a/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift b/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift index cc97af2..2b04f43 100644 --- a/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift +++ b/Sources/Aespa/Tuner/Device/AutoFocusTuner.swift @@ -11,12 +11,12 @@ import AVFoundation struct AutoFocusTuner: AespaDeviceTuning { let needLock = true let mode: AVCaptureDevice.FocusMode - + func tune(_ device: T) throws { guard device.isFocusModeSupported(mode) else { throw AespaError.device(reason: .unsupported) } - + device.setFocusMode(mode) } } diff --git a/Sources/Aespa/Tuner/Device/TorchTuner.swift b/Sources/Aespa/Tuner/Device/TorchTuner.swift index 6081f90..7a2cac5 100644 --- a/Sources/Aespa/Tuner/Device/TorchTuner.swift +++ b/Sources/Aespa/Tuner/Device/TorchTuner.swift @@ -11,12 +11,12 @@ import AVFoundation struct TorchTuner: AespaDeviceTuning { let level: Float let torchMode: AVCaptureDevice.TorchMode - - func tune(_ device: T) throws where T : AespaCaptureDeviceRepresentable { + + func tune(_ device: T) throws where T: AespaCaptureDeviceRepresentable { guard device.hasTorch else { throw AespaError.device(reason: .unsupported) } - + device.setTorchMode(torchMode) try device.setTorchModeOn(level: level) } diff --git a/Sources/Aespa/Tuner/Session/AudioTuner.swift b/Sources/Aespa/Tuner/Session/AudioTuner.swift index e6fd254..3bf89ae 100644 --- a/Sources/Aespa/Tuner/Session/AudioTuner.swift +++ b/Sources/Aespa/Tuner/Session/AudioTuner.swift @@ -10,7 +10,7 @@ import AVFoundation struct AudioTuner: AespaSessionTuning { let needTransaction = true var isMuted: Bool // default zoom factor - + func tune(_ session: T) throws { if isMuted { session.removeAudioInput() diff --git a/Sources/Aespa/Tuner/Session/CameraPositionTuner.swift b/Sources/Aespa/Tuner/Session/CameraPositionTuner.swift index 747d3ff..fd0da89 100644 --- a/Sources/Aespa/Tuner/Session/CameraPositionTuner.swift +++ b/Sources/Aespa/Tuner/Session/CameraPositionTuner.swift @@ -11,14 +11,13 @@ struct CameraPositionTuner: AespaSessionTuning { let needTransaction = true var position: AVCaptureDevice.Position var devicePreference: AVCaptureDevice.DeviceType? - + init(position: AVCaptureDevice.Position, devicePreference: AVCaptureDevice.DeviceType? = nil) { self.position = position self.devicePreference = devicePreference } - + func tune(_ session: T) throws { try session.setCameraPosition(to: position, device: devicePreference) } } - diff --git a/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift b/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift index 9d7dc1b..7fc5934 100644 --- a/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift +++ b/Sources/Aespa/Tuner/Session/SessionLaunchTuner.swift @@ -9,15 +9,13 @@ import AVFoundation struct SessionLaunchTuner: AespaSessionTuning { let needTransaction = false - + func tune(_ session: T) throws { guard session.isRunning == false else { return } - + try session.addMovieInput() try session.addMovieFileOutput() try session.addCapturePhotoOutput() session.startRunning() } } - - diff --git a/Sources/Aespa/Tuner/Session/SessionTerminationTuner.swift b/Sources/Aespa/Tuner/Session/SessionTerminationTuner.swift index d0f163f..bfdf531 100644 --- a/Sources/Aespa/Tuner/Session/SessionTerminationTuner.swift +++ b/Sources/Aespa/Tuner/Session/SessionTerminationTuner.swift @@ -9,10 +9,10 @@ import AVFoundation struct SessionTerminationTuner: AespaSessionTuning { let needTransaction = false - + func tune(_ session: T) { guard session.isRunning else { return } - + session.removeAudioInput() session.removeMovieInput() session.stopRunning() diff --git a/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift b/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift index fa8a103..8b3cb92 100644 --- a/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift +++ b/Sources/Aespa/Util/Extension/AVFoundation+Extension.swift @@ -9,10 +9,12 @@ import AVFoundation extension AVCaptureDevice.Position { var chooseBestCamera: AVCaptureDevice? { - let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInTripleCamera, .builtInWideAngleCamera], + let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, + .builtInTripleCamera, + .builtInWideAngleCamera], mediaType: .video, position: self) - + // Sort the devices by resolution let sortedDevices = discoverySession.devices.sorted { (device1, device2) -> Bool in guard let maxResolution1 = device1.maxResolution, @@ -21,7 +23,7 @@ extension AVCaptureDevice.Position { } return maxResolution1 > maxResolution2 } - + // Return the device with the highest resolution, or nil if no devices were found return sortedDevices.first } diff --git a/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift b/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift index 8c5192b..ae8309b 100644 --- a/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift +++ b/Sources/Aespa/Util/Extension/SwiftUI+Extension.swift @@ -12,7 +12,8 @@ import AVFoundation public extension AespaSession { /// A `SwiftUI` `View` that you use to display video as it is being captured by an input device. /// - /// - Parameter gravity: Define `AVLayerVideoGravity` for preview's orientation. `.resizeAspectFill` by default. + /// - Parameter gravity: Define `AVLayerVideoGravity` for preview's orientation. + /// .resizeAspectFill` by default. /// /// - Returns: `some UIViewRepresentable` which can coordinate other `View` components func preview(gravity: AVLayerVideoGravity = .resizeAspectFill) -> some UIViewControllerRepresentable { @@ -20,7 +21,7 @@ public extension AespaSession { } } -fileprivate struct Preview: UIViewControllerRepresentable { +private struct Preview: UIViewControllerRepresentable { let previewLayer: AVCaptureVideoPreviewLayer let gravity: AVLayerVideoGravity @@ -35,14 +36,14 @@ fileprivate struct Preview: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { let viewController = UIViewController() viewController.view.backgroundColor = .clear - + return viewController } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { previewLayer.videoGravity = gravity uiViewController.view.layer.addSublayer(previewLayer) - + previewLayer.frame = uiViewController.view.bounds } diff --git a/Sources/Aespa/Util/Extension/UIKit+Extension.swift b/Sources/Aespa/Util/Extension/UIKit+Extension.swift index 69e1979..bd1402b 100644 --- a/Sources/Aespa/Util/Extension/UIKit+Extension.swift +++ b/Sources/Aespa/Util/Extension/UIKit+Extension.swift @@ -12,7 +12,7 @@ extension UIDeviceOrientation { var toVideoOrientation: AVCaptureVideoOrientation { let currentOrientation = UIDevice.current.orientation let previewOrientation: AVCaptureVideoOrientation - + switch currentOrientation { case .portrait: previewOrientation = .portrait @@ -25,7 +25,7 @@ extension UIDeviceOrientation { default: previewOrientation = .portrait } - + return previewOrientation } } diff --git a/Sources/Aespa/Util/Log/Logger.swift b/Sources/Aespa/Util/Log/Logger.swift index e5e38dc..1321fc3 100644 --- a/Sources/Aespa/Util/Log/Logger.swift +++ b/Sources/Aespa/Util/Log/Logger.swift @@ -9,14 +9,18 @@ import Foundation class Logger { static var enableLogging = true - + static func log(message: String) { if enableLogging { print("[Aespa] \(message)") } } - - static func log(error: Error, file: String = (#file as NSString).lastPathComponent, method: String = #function) { + + static func log( + error: Error, + file: String = (#file as NSString).lastPathComponent, + method: String = #function + ) { if enableLogging { print("[Aespa : error] [\(file) : \(method)] - \(error) : \(error.localizedDescription)") } diff --git a/Sources/Aespa/Util/Video/Album/AlbumImporter.swift b/Sources/Aespa/Util/Video/Album/AlbumImporter.swift index 8976ffd..cbfc0c3 100644 --- a/Sources/Aespa/Util/Video/Album/AlbumImporter.swift +++ b/Sources/Aespa/Util/Video/Album/AlbumImporter.swift @@ -11,7 +11,10 @@ import Photos import UIKit struct AlbumImporter { - static func getAlbum( + static func getAlbum< + Library: AespaAssetLibraryRepresentable, + Collection: AespaAssetCollectionRepresentable + >( name: String, in photoLibrary: Library, retry: Bool = true, @@ -28,7 +31,7 @@ struct AlbumImporter { throw AespaError.album(reason: .unabledToAccess) } } - + static private func createAlbum( name: String, in photoLibrary: Library diff --git a/Sources/Aespa/Util/Video/Authorization/AuthorizationChecker.swift b/Sources/Aespa/Util/Video/Authorization/AuthorizationChecker.swift index 71c5436..7ed0093 100644 --- a/Sources/Aespa/Util/Video/Authorization/AuthorizationChecker.swift +++ b/Sources/Aespa/Util/Video/Authorization/AuthorizationChecker.swift @@ -13,7 +13,7 @@ struct AuthorizationChecker { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: return .permitted - + case .notDetermined: let isPermissionGranted = await AVCaptureDevice.requestAccess(for: .video) if isPermissionGranted { @@ -21,13 +21,13 @@ struct AuthorizationChecker { } else { fallthrough } - + case .denied: fallthrough - + case .restricted: fallthrough - + @unknown default: return .notPermitted } diff --git a/Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift b/Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift index b54c0f0..2f00a83 100644 --- a/Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift +++ b/Sources/Aespa/Util/Video/File/PhotoFileGenerator.swift @@ -11,7 +11,7 @@ import Foundation struct PhotoFileGenerator { static func generate(data: Data, date: Date) -> PhotoFile { return PhotoFile( - data: data, + data: data, generatedDate: date, thumbnail: UIImage(data: data)) } diff --git a/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift b/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift index b7f7929..d399c11 100644 --- a/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift +++ b/Sources/Aespa/Util/Video/File/VideoFileGenerator.swift @@ -15,18 +15,19 @@ struct VideoFileGenerator { path: path, thumbnail: VideoFileGenerator.generateThumbnail(for: path)) } - + static func generateThumbnail(for path: URL) -> UIImage? { let asset = AVURLAsset(url: path, options: nil) - + let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true imageGenerator.maximumSize = .init(width: 250, height: 250) - + do { - let cgImage = try imageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) + let cgImage = try imageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), + actualTime: nil) let thumbnail = UIImage(cgImage: cgImage) - + return thumbnail } catch let error { Logger.log(error: error) diff --git a/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift b/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift index e5c7408..7e97437 100644 --- a/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift +++ b/Sources/Aespa/Util/Video/File/VideoFilePathProvider.swift @@ -18,10 +18,10 @@ struct VideoFilePathProvider { let filePath = directoryPath .appendingPathComponent(fileName) .appendingPathExtension(`extension`) - + return filePath } - + static func requestDirectoryPath(from fileManager: FileManager, name: String) throws -> URL { guard let albumPath = fileManager.urls(for: .documentDirectory, @@ -29,9 +29,9 @@ struct VideoFilePathProvider { else { throw AespaError.album(reason: .unabledToAccess) } - + let directoryPathURL = albumPath.appendingPathComponent(name, isDirectory: true) - + // Set directory if doesn't exist if fileManager.fileExists(atPath: directoryPathURL.path) == false { try fileManager.createDirectory( @@ -39,7 +39,7 @@ struct VideoFilePathProvider { withIntermediateDirectories: true, attributes: nil) } - + return directoryPathURL } } diff --git a/Tests/TestHostApp.xcodeproj/project.pbxproj b/Tests/TestHostApp.xcodeproj/project.pbxproj index 3a65b51..0ff88d1 100644 --- a/Tests/TestHostApp.xcodeproj/project.pbxproj +++ b/Tests/TestHostApp.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ 9C727D012A3FEF9600EF9472 /* Frameworks */, 9C727D022A3FEF9600EF9472 /* Resources */, 9C727D582A3FF18500EF9472 /* Generate mock file */, + 9CFF96C02A401CDB003EF921 /* Lint files */, ); buildRules = ( ); @@ -312,12 +313,31 @@ outputFileListPaths = ( ); outputPaths = ( - "$(PROJECT_DIR)/$(PRODUCT_NAME)Tests/Mock/GeneratedMocks.swift", + "$(PROJECT_DIR)/Tests/Mock/GeneratedMocks.swift", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "cd ../Scripts\n./gen-mocks.sh\n"; }; + 9CFF96C02A401CDB003EF921 /* Lint files */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Lint files"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd ../\n\nif [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/Tests/Tests/Processor/AssetProcessorTests.swift b/Tests/Tests/Processor/AssetProcessorTests.swift index f44b221..682f863 100644 --- a/Tests/Tests/Processor/AssetProcessorTests.swift +++ b/Tests/Tests/Processor/AssetProcessorTests.swift @@ -38,7 +38,7 @@ final class AssetProcessorTests: XCTestCase { } stub(collection) { proxy in - when(proxy.canAdd(any())).thenReturn(true) + when(proxy.canAdd(video: any())).thenReturn(true) } try await processor.process(library, collection) From dc3833451323171d54a1610697f6f785258ae87a Mon Sep 17 00:00:00 2001 From: enebin Date: Tue, 20 Jun 2023 09:32:59 +0900 Subject: [PATCH 08/11] Add proxy tests --- .../Aespa/Data/VideoFileCachingProxy.swift | 3 +- Tests/Test.xctestplan | 1 - Tests/TestHostApp.xcodeproj/project.pbxproj | 12 ++++ .../Data/VideoFileCachingProxyTests.swift | 71 +++++++++++++++++++ Tests/Tests/Mock/MockFileManager.swift | 24 +++++++ 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 Tests/Tests/Data/VideoFileCachingProxyTests.swift diff --git a/Sources/Aespa/Data/VideoFileCachingProxy.swift b/Sources/Aespa/Data/VideoFileCachingProxy.swift index f453d5b..07e44bf 100644 --- a/Sources/Aespa/Data/VideoFileCachingProxy.swift +++ b/Sources/Aespa/Data/VideoFileCachingProxy.swift @@ -48,8 +48,7 @@ class VideoFileCachingProxy { } // Check if the directory has been modified since last fetch - if let lastModificationDate = self.lastModificationDate, - lastModificationDate == currentModificationDate { + if let lastModificationDate, lastModificationDate == currentModificationDate { return fetchSortedFiles(count: count) } diff --git a/Tests/Test.xctestplan b/Tests/Test.xctestplan index 04a65f0..0a56ad8 100644 --- a/Tests/Test.xctestplan +++ b/Tests/Test.xctestplan @@ -21,7 +21,6 @@ }, "testTargets" : [ { - "parallelizable" : true, "target" : { "containerPath" : "container:TestHostApp.xcodeproj", "identifier" : "9C727D132A3FEF9900EF9472", diff --git a/Tests/TestHostApp.xcodeproj/project.pbxproj b/Tests/TestHostApp.xcodeproj/project.pbxproj index 0ff88d1..dc4c9aa 100644 --- a/Tests/TestHostApp.xcodeproj/project.pbxproj +++ b/Tests/TestHostApp.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 9C727D0F2A3FEF9800EF9472 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C727D0E2A3FEF9800EF9472 /* Preview Assets.xcassets */; }; 9C727D502A3FF03200EF9472 /* Cuckoo in Frameworks */ = {isa = PBXBuildFile; productRef = 9C727D4F2A3FF03200EF9472 /* Cuckoo */; }; 9C727D572A3FF0B100EF9472 /* Aespa in Frameworks */ = {isa = PBXBuildFile; productRef = 9C727D562A3FF0B100EF9472 /* Aespa */; }; + 9CF0FE2D2A40574200FEE8C9 /* VideoFileCachingProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF0FE2C2A40574200FEE8C9 /* VideoFileCachingProxyTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -58,6 +59,7 @@ 9C727D0E2A3FEF9800EF9472 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 9C727D142A3FEF9900EF9472 /* TestHostAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestHostAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9C727D542A3FF09400EF9472 /* Aespa */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Aespa; path = ..; sourceTree = ""; }; + 9CF0FE2C2A40574200FEE8C9 /* VideoFileCachingProxyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFileCachingProxyTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -84,6 +86,7 @@ isa = PBXGroup; children = ( 9C4BBE532A400E450071C84F /* Mock */, + 9CF0FE2B2A40573000FEE8C9 /* Data */, 9C4BBE4C2A400E450071C84F /* Tuner */, 9C4BBE582A400E450071C84F /* Processor */, 9C4BBE502A400E450071C84F /* Util */, @@ -194,6 +197,14 @@ name = Frameworks; sourceTree = ""; }; + 9CF0FE2B2A40573000FEE8C9 /* Data */ = { + isa = PBXGroup; + children = ( + 9CF0FE2C2A40574200FEE8C9 /* VideoFileCachingProxyTests.swift */, + ); + path = Data; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -354,6 +365,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9CF0FE2D2A40574200FEE8C9 /* VideoFileCachingProxyTests.swift in Sources */, 9C4BBE5C2A400E450071C84F /* DeviceTunerTests.swift in Sources */, 9C4BBE612A400E450071C84F /* MockVideo.swift in Sources */, 9C4BBE5E2A400E450071C84F /* AlbumUtilTests.swift in Sources */, diff --git a/Tests/Tests/Data/VideoFileCachingProxyTests.swift b/Tests/Tests/Data/VideoFileCachingProxyTests.swift new file mode 100644 index 0000000..02e5bb1 --- /dev/null +++ b/Tests/Tests/Data/VideoFileCachingProxyTests.swift @@ -0,0 +1,71 @@ +// +// VideoFileCachingProxyTests.swift +// TestHostAppTests +// +// Created by 이영빈 on 2023/06/19. +// + +import XCTest +import AVFoundation + +import Cuckoo + +@testable import Aespa + +final class VideoFileCachingProxyTests: XCTestCase { + var sut: VideoFileCachingProxy! + var mockFileManager: MockFileManager! + var expectedVideoFile: VideoFile { VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) } + var attributes: [FileAttributeKey: Any] { [.modificationDate: Date(), .creationDate: Date()] } + + override func setUpWithError() throws { + mockFileManager = MockFileManager() + mockFileManager.urlsStub = [expectedVideoFile.path] + mockFileManager.attributesOfItemStub = attributes + mockFileManager.contentsOfDirectoryStub = ["/path/to/mock/file"] + + sut = VideoFileCachingProxy(albumName: "Test", enableCaching: true, fileManager: mockFileManager) + } + + override func tearDownWithError() throws { + sut = nil + mockFileManager = nil + } + + func testFetch_withCacheEnabledAndUnmodifiedFileSystem_returnsCachedFiles() { + let expectedVideoFile = VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) + + mockFileManager.urlsStub = [expectedVideoFile.path] + mockFileManager.attributesOfItemStub = attributes + + _ = sut.fetch(count: 0) // Initial fetch to populate the cache + + let fetchedFiles = sut.fetch(count: 1) + XCTAssertEqual(fetchedFiles.first, expectedVideoFile, + "Fetched file should match the initially cached file when the file system is not modified") + } + + func testFetch_withCacheDisabled_returnsFilesFromFileSystem() { + sut = VideoFileCachingProxy(albumName: "Test", enableCaching: false, fileManager: mockFileManager) + mockFileManager.urlsStub = [expectedVideoFile.path] + + + let fetchedFiles = sut.fetch(count: 1) + XCTAssertEqual(fetchedFiles.first, expectedVideoFile, + "Fetched file should match the file from file system when caching is disabled") + } + + func testFetch_withCacheEnabledAndModifiedFileSystem_updatesCacheAndReturnsUpdatedFiles() { + let initialVideoFile = VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) + mockFileManager.urlsStub = [initialVideoFile.path] + mockFileManager.attributesOfItemStub = attributes + _ = sut.fetch(count: 0) // Initial fetch to populate the cache + + mockFileManager.urlsStub = [expectedVideoFile.path] + mockFileManager.attributesOfItemStub = [.modificationDate: Date().addingTimeInterval(1)] // Ensure modification date is different + + let fetchedFiles = sut.fetch(count: 1) + XCTAssertEqual(fetchedFiles.first, expectedVideoFile, + "Fetched file should match the updated file from file system when the file system is modified") + } +} diff --git a/Tests/Tests/Mock/MockFileManager.swift b/Tests/Tests/Mock/MockFileManager.swift index e646dbb..2156917 100644 --- a/Tests/Tests/Mock/MockFileManager.swift +++ b/Tests/Tests/Mock/MockFileManager.swift @@ -9,6 +9,8 @@ import Foundation class MockFileManager: FileManager { var urlsStub: [URL]? + var attributesOfItemStub: [FileAttributeKey: Any]? + var contentsOfDirectoryStub: [String]? override func urls( for directory: FileManager.SearchPathDirectory, @@ -31,4 +33,26 @@ class MockFileManager: FileManager { ) throws { return } + + override func attributesOfItem(atPath path: String) throws -> [FileAttributeKey : Any] { + try attributesOfItemStubMethod(atPath: path) + } + + private func attributesOfItemStubMethod(atPath path: String) throws -> [FileAttributeKey : Any] { + guard let attributesOfItemStub else { + fatalError("Stub is not provided") + } + return attributesOfItemStub + } + + override func contentsOfDirectory(atPath path: String) throws -> [String] { + try contentsOfDirectoryStubMethod(atPath: path) + } + + private func contentsOfDirectoryStubMethod(atPath path: String) throws -> [String] { + guard let contentsOfDirectoryStub else { + fatalError("Stub is not provided") + } + return contentsOfDirectoryStub + } } From e1f3fed935dbda030264d9759b2d3d1c01fe226b Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Tue, 20 Jun 2023 12:29:46 +0900 Subject: [PATCH 09/11] Separate cache data structure --- Sources/Aespa/Core/AespaCoreFileManager.swift | 9 ++- Sources/Aespa/Data/URLCacheStorage.swift | 47 +++++++++++ Sources/Aespa/Data/VideoFile.swift | 8 +- .../Aespa/Data/VideoFileCachingProxy.swift | 65 +++++++-------- .../Data/VideoFileCachingProxyTests.swift | 79 +++++++++++-------- 5 files changed, 140 insertions(+), 68 deletions(-) create mode 100644 Sources/Aespa/Data/URLCacheStorage.swift diff --git a/Sources/Aespa/Core/AespaCoreFileManager.swift b/Sources/Aespa/Core/AespaCoreFileManager.swift index 05b94f3..ef4c017 100644 --- a/Sources/Aespa/Core/AespaCoreFileManager.swift +++ b/Sources/Aespa/Core/AespaCoreFileManager.swift @@ -25,10 +25,17 @@ class AespaCoreFileManager { /// If `count` is `0`, return all existing files func fetch(albumName: String, count: Int) -> [VideoFile] { guard count >= 0 else { return [] } + + guard let albumDirectory = try? VideoFilePathProvider.requestDirectoryPath(from: systemFileManager, + name: albumName) + else { + Logger.log(message: "Cannot fetch album directory so `fetch` will return empty array.") + return [] + } guard let proxy = videoFileProxyDictionary[albumName] else { videoFileProxyDictionary[albumName] = VideoFileCachingProxy( - albumName: albumName, + albumDirectory: albumDirectory, enableCaching: enableCaching, fileManager: systemFileManager) diff --git a/Sources/Aespa/Data/URLCacheStorage.swift b/Sources/Aespa/Data/URLCacheStorage.swift new file mode 100644 index 0000000..43a2383 --- /dev/null +++ b/Sources/Aespa/Data/URLCacheStorage.swift @@ -0,0 +1,47 @@ +// +// URLCacheStorage.swift +// +// +// Created by 이영빈 on 2023/06/20. +// + +import Foundation + +protocol URLCache { + associatedtype File + + func get(_ url: URL) -> File? + func store(_ file: File, at filePath: URL) + func renew(with filePair: [URL: File]) + func empty() + + var all: [File] { get } +} + +class URLCacheStorage: URLCache { + private var storage: [URL: File] + + init() { + self.storage = [:] + } + + func get(_ filePath: URL) -> File? { + storage[filePath] + } + + func store(_ file: File, at filePath: URL) { + storage[filePath] = file + } + + func renew(with filePair: [URL: File]) { + storage = filePair + } + + func empty() { + storage.removeAll() + } + + var all: [File] { + Array(storage.values) + } +} diff --git a/Sources/Aespa/Data/VideoFile.swift b/Sources/Aespa/Data/VideoFile.swift index 379cf03..b440b57 100644 --- a/Sources/Aespa/Data/VideoFile.swift +++ b/Sources/Aespa/Data/VideoFile.swift @@ -14,7 +14,7 @@ import AVFoundation /// This struct holds information about the video file, including a unique identifier (`id`), /// the path to the video file (`path`), and an optional thumbnail image (`thumbnail`) /// generated from the video. -public struct VideoFile: Equatable { +public struct VideoFile { /// A `Date` value keeps the date it's generated public let generatedDate: Date @@ -39,6 +39,12 @@ public extension VideoFile { } } +extension VideoFile: Equatable { + public static func == (lhs: VideoFile, rhs: VideoFile ) -> Bool { + lhs.path == rhs.path + } +} + extension VideoFile: Identifiable { public var id: URL { self.path diff --git a/Sources/Aespa/Data/VideoFileCachingProxy.swift b/Sources/Aespa/Data/VideoFileCachingProxy.swift index 07e44bf..4a321de 100644 --- a/Sources/Aespa/Data/VideoFileCachingProxy.swift +++ b/Sources/Aespa/Data/VideoFileCachingProxy.swift @@ -8,38 +8,39 @@ import Foundation class VideoFileCachingProxy { - private let albumName: String + private let albumDirectory: URL private let cacheEnabled: Bool private let fileManager: FileManager - private var cache: [URL: VideoFile] = [:] + private var cacheStroage: URLCacheStorage private var lastModificationDate: Date? - init(albumName: String, enableCaching: Bool, fileManager: FileManager) { - self.albumName = albumName + init( + albumDirectory: URL, + enableCaching: Bool, + fileManager: FileManager, + cacheStorage: URLCacheStorage = .init() + ) { + self.albumDirectory = albumDirectory + self.cacheStroage = cacheStorage + self.cacheEnabled = enableCaching self.fileManager = fileManager DispatchQueue.global().async { - self.updateCache() + self.update(cacheStorage) } } /// If `count` is `0`, return all existing files func fetch(count: Int) -> [VideoFile] { - guard - let albumDirectory = try? VideoFilePathProvider.requestDirectoryPath(from: fileManager, - name: albumName) - else { - return [] - } - guard cacheEnabled else { invalidateCache() - return fetchFile(from: albumDirectory, count: count).sorted() + return fetchSortedFiles(count: count) } + // Get directory's last modification date guard let directoryAttributes = try? fileManager.attributesOfItem(atPath: albumDirectory.path), let currentModificationDate = directoryAttributes[.modificationDate] as? Date @@ -49,53 +50,47 @@ class VideoFileCachingProxy { // Check if the directory has been modified since last fetch if let lastModificationDate, lastModificationDate == currentModificationDate { + // If it's corrupted, newly fetch files return fetchSortedFiles(count: count) } // Update cache and lastModificationDate - updateCache() - self.lastModificationDate = currentModificationDate + update(cacheStroage) + lastModificationDate = currentModificationDate return fetchSortedFiles(count: count) } // Invalidate the cache if needed, for example when a file is added or removed func invalidateCache() { - cache.removeAll() lastModificationDate = nil + cacheStroage.empty() } } private extension VideoFileCachingProxy { - func updateCache() { + func update(_ storage: URLCacheStorage) { guard - let albumDirectory = try? VideoFilePathProvider.requestDirectoryPath(from: fileManager, - name: albumName), let filePaths = try? fileManager.contentsOfDirectory(atPath: albumDirectory.path) else { Logger.log(message: "Cannot access to saved video file") return } - var newCache: [URL: VideoFile] = [:] - filePaths.forEach { fileName in + let newPair = filePaths.reduce([URL: VideoFile]()) { previousDictionary, fileName in let filePath = albumDirectory.appendingPathComponent(fileName) - if let cachedFile = cache[filePath] { - newCache[filePath] = cachedFile + var dictionary = previousDictionary + + if let videoFile = storage.get(filePath) { + dictionary[filePath] = videoFile } else if let videoFile = createVideoFile(for: filePath) { - newCache[filePath] = videoFile + dictionary[filePath] = videoFile } + + return dictionary } - cache = newCache - } - - func fetchFile(from albumDirectory: URL, count: Int) -> [VideoFile] { - guard count >= 0 else { return [] } - - let files = Array(cache.values) - let sortedFiles = files.sorted() - let prefixFiles = (count == 0) ? sortedFiles : Array(sortedFiles.prefix(count)) - return prefixFiles + + storage.renew(with: newPair) } func createVideoFile(for filePath: URL) -> VideoFile? { @@ -111,7 +106,7 @@ private extension VideoFileCachingProxy { } func fetchSortedFiles(count: Int) -> [VideoFile] { - let files = cache.values.sorted() + let files = cacheStroage.all.sorted() return count == 0 ? files : Array(files.prefix(count)) } } diff --git a/Tests/Tests/Data/VideoFileCachingProxyTests.swift b/Tests/Tests/Data/VideoFileCachingProxyTests.swift index 02e5bb1..b1f7aa3 100644 --- a/Tests/Tests/Data/VideoFileCachingProxyTests.swift +++ b/Tests/Tests/Data/VideoFileCachingProxyTests.swift @@ -15,16 +15,13 @@ import Cuckoo final class VideoFileCachingProxyTests: XCTestCase { var sut: VideoFileCachingProxy! var mockFileManager: MockFileManager! - var expectedVideoFile: VideoFile { VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) } - var attributes: [FileAttributeKey: Any] { [.modificationDate: Date(), .creationDate: Date()] } override func setUpWithError() throws { mockFileManager = MockFileManager() - mockFileManager.urlsStub = [expectedVideoFile.path] - mockFileManager.attributesOfItemStub = attributes - mockFileManager.contentsOfDirectoryStub = ["/path/to/mock/file"] - - sut = VideoFileCachingProxy(albumName: "Test", enableCaching: true, fileManager: mockFileManager) + + mockFileManager.urlsStub = [givenDirectoryPath] + mockFileManager.attributesOfItemStub = givenAttributes + mockFileManager.contentsOfDirectoryStub = [givenVideoFile.path.lastPathComponent] } override func tearDownWithError() throws { @@ -33,39 +30,59 @@ final class VideoFileCachingProxyTests: XCTestCase { } func testFetch_withCacheEnabledAndUnmodifiedFileSystem_returnsCachedFiles() { - let expectedVideoFile = VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) - - mockFileManager.urlsStub = [expectedVideoFile.path] - mockFileManager.attributesOfItemStub = attributes - + sut = VideoFileCachingProxy(albumName: albumName, + enableCaching: true, + fileManager: mockFileManager) + _ = sut.fetch(count: 0) // Initial fetch to populate the cache + let expectedVideoFile = givenVideoFile let fetchedFiles = sut.fetch(count: 1) - XCTAssertEqual(fetchedFiles.first, expectedVideoFile, - "Fetched file should match the initially cached file when the file system is not modified") + + XCTAssertEqual(fetchedFiles.first, expectedVideoFile, "Fetched file should match the initially cached file") } - func testFetch_withCacheDisabled_returnsFilesFromFileSystem() { - sut = VideoFileCachingProxy(albumName: "Test", enableCaching: false, fileManager: mockFileManager) - mockFileManager.urlsStub = [expectedVideoFile.path] - +// func testFetch_withCacheDisabled_returnsFilesFromFileSystem() { +// sut = VideoFileCachingProxy(albumName: "Test", enableCaching: false, fileManager: mockFileManager) +// mockFileManager.urlsStub = [expectedVideoFile.path] +// +// +// let fetchedFiles = sut.fetch(count: 1) +// XCTAssertEqual(fetchedFiles.first, expectedVideoFile, +// "Fetched file should match the file from file system when caching is disabled") +// } +// +// func testFetch_withCacheEnabledAndModifiedFileSystem_updatesCacheAndReturnsUpdatedFiles() { +// let initialVideoFile = VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) +// mockFileManager.urlsStub = [initialVideoFile.path] +// mockFileManager.attributesOfItemStub = attributes +// _ = sut.fetch(count: 0) // Initial fetch to populate the cache +// +// mockFileManager.urlsStub = [expectedVideoFile.path] +// mockFileManager.attributesOfItemStub = [.modificationDate: Date().addingTimeInterval(1)] // Ensure modification date is different +// +// let fetchedFiles = sut.fetch(count: 1) +// XCTAssertEqual(fetchedFiles.first, expectedVideoFile, +// "Fetched file should match the updated file from file system when the file system is modified") +// } +} - let fetchedFiles = sut.fetch(count: 1) - XCTAssertEqual(fetchedFiles.first, expectedVideoFile, - "Fetched file should match the file from file system when caching is disabled") +fileprivate extension VideoFileCachingProxyTests { + var albumName: String { + "Test" } - func testFetch_withCacheEnabledAndModifiedFileSystem_updatesCacheAndReturnsUpdatedFiles() { - let initialVideoFile = VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) - mockFileManager.urlsStub = [initialVideoFile.path] - mockFileManager.attributesOfItemStub = attributes - _ = sut.fetch(count: 0) // Initial fetch to populate the cache + var givenDirectoryPath: URL { + URL(fileURLWithPath: "/path/to/mock/", isDirectory: true) + } - mockFileManager.urlsStub = [expectedVideoFile.path] - mockFileManager.attributesOfItemStub = [.modificationDate: Date().addingTimeInterval(1)] // Ensure modification date is different + var givenVideoFile: VideoFile { + VideoFile(generatedDate: Date(), + path: URL(fileURLWithPath: givenDirectoryPath.relativePath + "/\(albumName)/" + "file")) + } - let fetchedFiles = sut.fetch(count: 1) - XCTAssertEqual(fetchedFiles.first, expectedVideoFile, - "Fetched file should match the updated file from file system when the file system is modified") + var givenAttributes: [FileAttributeKey: Any] { + [.modificationDate: Date(), .creationDate: Date()] } + } From 7d7ace49db66c406eb142efbbf86300793fba969 Mon Sep 17 00:00:00 2001 From: enebin Date: Tue, 20 Jun 2023 20:49:08 +0900 Subject: [PATCH 10/11] Add proxy tests --- .github/workflows/unit-test.yml | 3 +- Sources/Aespa/Core/AespaCoreFileManager.swift | 4 +- .../Data/{ => Proxy}/URLCacheStorage.swift | 9 +- .../{ => Proxy}/VideoFileCachingProxy.swift | 70 +++++--- .../xcshareddata/swiftpm/Package.resolved | 24 +-- .../Data/VideoFileCachingProxyTests.swift | 166 ++++++++++++++---- Tests/Tests/Mock/MockFileManager.swift | 7 +- 7 files changed, 199 insertions(+), 84 deletions(-) rename Sources/Aespa/Data/{ => Proxy}/URLCacheStorage.swift (77%) rename Sources/Aespa/Data/{ => Proxy}/VideoFileCachingProxy.swift (58%) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 17e70f2..ef63683 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -49,7 +49,8 @@ jobs: -destination 'platform=iOS Simulator,name=iPhone 14 Pro,OS=latest' \ -derivedDataPath ${DERIVED_DATA_PATH} \ -enableCodeCoverage YES \ - | xcpretty --color + | xcpretty --color \ + || exit 1 # Check if the tests failed if [ $? -ne 0 ]; then diff --git a/Sources/Aespa/Core/AespaCoreFileManager.swift b/Sources/Aespa/Core/AespaCoreFileManager.swift index ef4c017..13886bc 100644 --- a/Sources/Aespa/Core/AespaCoreFileManager.swift +++ b/Sources/Aespa/Core/AespaCoreFileManager.swift @@ -8,7 +8,7 @@ import Foundation class AespaCoreFileManager { - private var videoFileProxyDictionary: [String: VideoFileCachingProxy] + private var videoFileProxyDictionary: [String: VideoFileCachingProxy>] private let enableCaching: Bool let systemFileManager: FileManager @@ -35,7 +35,7 @@ class AespaCoreFileManager { guard let proxy = videoFileProxyDictionary[albumName] else { videoFileProxyDictionary[albumName] = VideoFileCachingProxy( - albumDirectory: albumDirectory, + albumDirectory: albumDirectory, enableCaching: enableCaching, fileManager: systemFileManager) diff --git a/Sources/Aespa/Data/URLCacheStorage.swift b/Sources/Aespa/Data/Proxy/URLCacheStorage.swift similarity index 77% rename from Sources/Aespa/Data/URLCacheStorage.swift rename to Sources/Aespa/Data/Proxy/URLCacheStorage.swift index 43a2383..dde0402 100644 --- a/Sources/Aespa/Data/URLCacheStorage.swift +++ b/Sources/Aespa/Data/Proxy/URLCacheStorage.swift @@ -7,18 +7,17 @@ import Foundation -protocol URLCache { +protocol URLCaching { associatedtype File func get(_ url: URL) -> File? func store(_ file: File, at filePath: URL) - func renew(with filePair: [URL: File]) func empty() var all: [File] { get } } -class URLCacheStorage: URLCache { +final class URLCacheStorage: URLCaching { private var storage: [URL: File] init() { @@ -33,10 +32,6 @@ class URLCacheStorage: URLCache { storage[filePath] = file } - func renew(with filePair: [URL: File]) { - storage = filePair - } - func empty() { storage.removeAll() } diff --git a/Sources/Aespa/Data/VideoFileCachingProxy.swift b/Sources/Aespa/Data/Proxy/VideoFileCachingProxy.swift similarity index 58% rename from Sources/Aespa/Data/VideoFileCachingProxy.swift rename to Sources/Aespa/Data/Proxy/VideoFileCachingProxy.swift index 4a321de..942f666 100644 --- a/Sources/Aespa/Data/VideoFileCachingProxy.swift +++ b/Sources/Aespa/Data/Proxy/VideoFileCachingProxy.swift @@ -7,20 +7,20 @@ import Foundation -class VideoFileCachingProxy { +class VideoFileCachingProxy> { private let albumDirectory: URL private let cacheEnabled: Bool private let fileManager: FileManager - private var cacheStroage: URLCacheStorage - private var lastModificationDate: Date? + private var cacheStroage: CacheStorage + private(set) var lastModificationDate: Date? init( albumDirectory: URL, enableCaching: Bool, fileManager: FileManager, - cacheStorage: URLCacheStorage = .init() + cacheStorage: CacheStorage = URLCacheStorage() ) { self.albumDirectory = albumDirectory self.cacheStroage = cacheStorage @@ -28,16 +28,17 @@ class VideoFileCachingProxy { self.cacheEnabled = enableCaching self.fileManager = fileManager - DispatchQueue.global().async { - self.update(cacheStorage) + if enableCaching { + DispatchQueue.global().async { + self.update(cacheStorage) + } } } /// If `count` is `0`, return all existing files - func fetch(count: Int) -> [VideoFile] { + func fetch(count: Int = 0) -> [VideoFile] { guard cacheEnabled else { - invalidateCache() - return fetchSortedFiles(count: count) + return fetchSortedFiles(count: count, usingCache: cacheEnabled) } // Get directory's last modification date @@ -51,25 +52,25 @@ class VideoFileCachingProxy { // Check if the directory has been modified since last fetch if let lastModificationDate, lastModificationDate == currentModificationDate { // If it's corrupted, newly fetch files - return fetchSortedFiles(count: count) + return fetchSortedFiles(count: count, usingCache: cacheEnabled) } // Update cache and lastModificationDate update(cacheStroage) lastModificationDate = currentModificationDate - return fetchSortedFiles(count: count) + return fetchSortedFiles(count: count, usingCache: cacheEnabled) } // Invalidate the cache if needed, for example when a file is added or removed - func invalidateCache() { + func invalidate() { lastModificationDate = nil cacheStroage.empty() } } private extension VideoFileCachingProxy { - func update(_ storage: URLCacheStorage) { + func update(_ storage: CacheStorage) { guard let filePaths = try? fileManager.contentsOfDirectory(atPath: albumDirectory.path) else { @@ -77,22 +78,42 @@ private extension VideoFileCachingProxy { return } - let newPair = filePaths.reduce([URL: VideoFile]()) { previousDictionary, fileName in + filePaths.forEach { fileName in let filePath = albumDirectory.appendingPathComponent(fileName) - var dictionary = previousDictionary - if let videoFile = storage.get(filePath) { - dictionary[filePath] = videoFile - } else if let videoFile = createVideoFile(for: filePath) { - dictionary[filePath] = videoFile + guard storage.get(filePath) == nil else { + return + } + + guard let videoFile = createVideoFile(for: filePath) else { + return + } + + storage.store(videoFile, at: filePath) + } + } + + func fetchSortedFiles(count: Int, usingCache: Bool) -> [VideoFile] { + let files: [VideoFile] + if usingCache { + files = cacheStroage.all.sorted() + } else { + guard let filePaths = try? fileManager.contentsOfDirectory(atPath: albumDirectory.path) else { + Logger.log(message: "Cannot access to saved video file") + return [] } - return dictionary + files = filePaths + .map { fileName in + let filePath = albumDirectory.appendingPathComponent(fileName) + return createVideoFile(for: filePath) + } + .compactMap({$0}) } - storage.renew(with: newPair) + return count == 0 ? files : Array(files.prefix(count)) } - + func createVideoFile(for filePath: URL) -> VideoFile? { guard let fileAttributes = try? fileManager.attributesOfItem(atPath: filePath.path), @@ -104,9 +125,4 @@ private extension VideoFileCachingProxy { return VideoFileGenerator.generate(with: filePath, date: creationDate) } - - func fetchSortedFiles(count: Int) -> [VideoFile] { - let files = cacheStroage.all.sorted() - return count == 0 ? files : Array(files.prefix(count)) - } } diff --git a/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 69b96b4..11ca10b 100644 --- a/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tests/TestHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,16 @@ { - "pins" : [ - { - "identity" : "cuckoo", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Brightify/Cuckoo", - "state" : { - "revision" : "475322e609d009c284464c886ca08cfe421137a9", - "version" : "1.10.3" + "object": { + "pins": [ + { + "package": "Cuckoo", + "repositoryURL": "https://github.com/Brightify/Cuckoo", + "state": { + "branch": null, + "revision": "475322e609d009c284464c886ca08cfe421137a9", + "version": "1.10.3" + } } - } - ], - "version" : 2 + ] + }, + "version": 1 } diff --git a/Tests/Tests/Data/VideoFileCachingProxyTests.swift b/Tests/Tests/Data/VideoFileCachingProxyTests.swift index b1f7aa3..75ec90a 100644 --- a/Tests/Tests/Data/VideoFileCachingProxyTests.swift +++ b/Tests/Tests/Data/VideoFileCachingProxyTests.swift @@ -13,76 +13,172 @@ import Cuckoo @testable import Aespa final class VideoFileCachingProxyTests: XCTestCase { - var sut: VideoFileCachingProxy! + var sut: VideoFileCachingProxy>! + + var mockCache: MockURLCaching! var mockFileManager: MockFileManager! override func setUpWithError() throws { mockFileManager = MockFileManager() + mockCache = MockURLCaching() + // Mock file maanager simulates like it has the `givenVideoFile` mockFileManager.urlsStub = [givenDirectoryPath] mockFileManager.attributesOfItemStub = givenAttributes mockFileManager.contentsOfDirectoryStub = [givenVideoFile.path.lastPathComponent] + + // Default stub + let expectedVideoFile = givenVideoFile + let expectedFilePath = givenVideoFile.path + stub(mockCache) { proxy in + when(proxy.store(equal(to: expectedVideoFile), at: equal(to: expectedFilePath))).thenDoNothing() + when(proxy.get(equal(to: expectedFilePath))).thenReturn(expectedVideoFile) + when(proxy.all.get).thenReturn([expectedVideoFile]) + when(proxy.empty()).thenDoNothing() + } } override func tearDownWithError() throws { sut = nil mockFileManager = nil } + + func testFetchWithoutCount() { + let givenFiles = [ + givenVideoFile.path.lastPathComponent + "1", + givenVideoFile.path.lastPathComponent + "2", + givenVideoFile.path.lastPathComponent + "3" + ] + + mockFileManager.contentsOfDirectoryStub = givenFiles + + sut = VideoFileCachingProxy(albumDirectory: givenAlbumDirectory, + enableCaching: false, + fileManager: mockFileManager, + cacheStorage: mockCache) + + let fetchedFiles = sut.fetch() + XCTAssertEqual(fetchedFiles.count, givenFiles.count) + } + + func testFetchWithCount() { + let givenFiles = [ + givenVideoFile.path.lastPathComponent + "1", + givenVideoFile.path.lastPathComponent + "2", + givenVideoFile.path.lastPathComponent + "3" + ] + + mockFileManager.contentsOfDirectoryStub = givenFiles + + sut = VideoFileCachingProxy(albumDirectory: givenAlbumDirectory, + enableCaching: false, + fileManager: mockFileManager, + cacheStorage: mockCache) + + let fetchedFiles = sut.fetch(count: 2) + XCTAssertEqual(fetchedFiles.count, 2) + } + + func testInvalidate() { + sut = VideoFileCachingProxy(albumDirectory: givenAlbumDirectory, + enableCaching: true, + fileManager: mockFileManager, + cacheStorage: mockCache) + + sut.invalidate() + + XCTAssertNil(sut.lastModificationDate) + verify(mockCache) + .empty() + .with(returnType: Void.self) + } + + func testProxyCache() { + let expectedVideoFile = givenVideoFile + let expectedFilePath = givenVideoFile.path + let modificationDate = Date() - func testFetch_withCacheEnabledAndUnmodifiedFileSystem_returnsCachedFiles() { - sut = VideoFileCachingProxy(albumName: albumName, + mockFileManager.contentsOfDirectoryStub = [expectedFilePath.lastPathComponent] + mockFileManager.attributesOfItemStub = [.modificationDate: modificationDate, + .creationDate: Date()] + + // Cache will be filled with a data it has in the `FileManager` + sut = VideoFileCachingProxy(albumDirectory: givenAlbumDirectory, enableCaching: true, - fileManager: mockFileManager) - - _ = sut.fetch(count: 0) // Initial fetch to populate the cache + fileManager: mockFileManager, + cacheStorage: mockCache) + + // Assume empty cache + stub(mockCache) { proxy in + when(proxy.get(equal(to: expectedFilePath))).thenReturn(nil) + when(proxy.all.get).thenReturn([]) + } + + verify(mockCache) + .get(equal(to: expectedFilePath)) + .with(returnType: VideoFile?.self) + + // Assume cache filled + stub(mockCache) { proxy in + when(proxy.get(equal(to: expectedFilePath))).thenReturn(expectedVideoFile) + when(proxy.all.get).thenReturn([expectedVideoFile]) + } + + let fetchedFiles = sut.fetch(count: 1) + + verify(mockCache, times(1)) // Only includes previous invoke + .store(equal(to: expectedVideoFile), at: equal(to: expectedFilePath)) + .with(returnType: Void.self) + + XCTAssertEqual(sut.lastModificationDate, modificationDate) + XCTAssertEqual(fetchedFiles.first, expectedVideoFile, "Fetched file should match the initially cached file") + } + func testProxyNotUsingCache() { let expectedVideoFile = givenVideoFile + let expectedFilePath = givenVideoFile.path + + // Cache shouldn't be filled with a data it has in the `FileManager` + sut = VideoFileCachingProxy(albumDirectory: givenAlbumDirectory, + enableCaching: false, + fileManager: mockFileManager, + cacheStorage: mockCache) + let fetchedFiles = sut.fetch(count: 1) + verify(mockCache, never()) + .store(any(VideoFile.self), at: any(URL.self)) + + verify(mockCache, never()) + .get(any(URL.self)) + + verify(mockCache, never()) + .all.get() + + XCTAssertTrue(mockFileManager.contentsOfDirectoryCalled) XCTAssertEqual(fetchedFiles.first, expectedVideoFile, "Fetched file should match the initially cached file") } - -// func testFetch_withCacheDisabled_returnsFilesFromFileSystem() { -// sut = VideoFileCachingProxy(albumName: "Test", enableCaching: false, fileManager: mockFileManager) -// mockFileManager.urlsStub = [expectedVideoFile.path] -// -// -// let fetchedFiles = sut.fetch(count: 1) -// XCTAssertEqual(fetchedFiles.first, expectedVideoFile, -// "Fetched file should match the file from file system when caching is disabled") -// } -// -// func testFetch_withCacheEnabledAndModifiedFileSystem_updatesCacheAndReturnsUpdatedFiles() { -// let initialVideoFile = VideoFile(generatedDate: Date(), path: URL(fileURLWithPath: "/path/to/mock/file")) -// mockFileManager.urlsStub = [initialVideoFile.path] -// mockFileManager.attributesOfItemStub = attributes -// _ = sut.fetch(count: 0) // Initial fetch to populate the cache -// -// mockFileManager.urlsStub = [expectedVideoFile.path] -// mockFileManager.attributesOfItemStub = [.modificationDate: Date().addingTimeInterval(1)] // Ensure modification date is different -// -// let fetchedFiles = sut.fetch(count: 1) -// XCTAssertEqual(fetchedFiles.first, expectedVideoFile, -// "Fetched file should match the updated file from file system when the file system is modified") -// } } fileprivate extension VideoFileCachingProxyTests { - var albumName: String { - "Test" + var givenFilename: String { + "File" } - + var givenDirectoryPath: URL { URL(fileURLWithPath: "/path/to/mock/", isDirectory: true) } + + var givenAlbumDirectory: URL { + URL(fileURLWithPath: "\(givenDirectoryPath.relativePath)/Test", isDirectory: true) + } var givenVideoFile: VideoFile { VideoFile(generatedDate: Date(), - path: URL(fileURLWithPath: givenDirectoryPath.relativePath + "/\(albumName)/" + "file")) + path: givenAlbumDirectory.appendingPathComponent(givenFilename)) } var givenAttributes: [FileAttributeKey: Any] { [.modificationDate: Date(), .creationDate: Date()] } - } diff --git a/Tests/Tests/Mock/MockFileManager.swift b/Tests/Tests/Mock/MockFileManager.swift index 2156917..4bff55a 100644 --- a/Tests/Tests/Mock/MockFileManager.swift +++ b/Tests/Tests/Mock/MockFileManager.swift @@ -10,7 +10,9 @@ import Foundation class MockFileManager: FileManager { var urlsStub: [URL]? var attributesOfItemStub: [FileAttributeKey: Any]? + var contentsOfDirectoryStub: [String]? + var contentsOfDirectoryCalled: Bool = false override func urls( for directory: FileManager.SearchPathDirectory, @@ -46,7 +48,10 @@ class MockFileManager: FileManager { } override func contentsOfDirectory(atPath path: String) throws -> [String] { - try contentsOfDirectoryStubMethod(atPath: path) + let result = try contentsOfDirectoryStubMethod(atPath: path) + contentsOfDirectoryCalled = true + + return result } private func contentsOfDirectoryStubMethod(atPath path: String) throws -> [String] { From 62d922087ec0cf4d25908ee83949aacaf615de8b Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Wed, 21 Jun 2023 14:35:44 +0900 Subject: [PATCH 11/11] Fix failing test cases --- .../Data/Proxy/VideoFileCachingProxy.swift | 10 ++--- .../Data/VideoFileCachingProxyTests.swift | 44 ++++++++++++++++--- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/Sources/Aespa/Data/Proxy/VideoFileCachingProxy.swift b/Sources/Aespa/Data/Proxy/VideoFileCachingProxy.swift index 942f666..8550453 100644 --- a/Sources/Aespa/Data/Proxy/VideoFileCachingProxy.swift +++ b/Sources/Aespa/Data/Proxy/VideoFileCachingProxy.swift @@ -27,12 +27,6 @@ class VideoFileCachingProxy> { self.cacheEnabled = enableCaching self.fileManager = fileManager - - if enableCaching { - DispatchQueue.global().async { - self.update(cacheStorage) - } - } } /// If `count` is `0`, return all existing files @@ -67,6 +61,10 @@ class VideoFileCachingProxy> { lastModificationDate = nil cacheStroage.empty() } + + func renew() { + update(cacheStroage) + } } private extension VideoFileCachingProxy { diff --git a/Tests/Tests/Data/VideoFileCachingProxyTests.swift b/Tests/Tests/Data/VideoFileCachingProxyTests.swift index 75ec90a..8f50149 100644 --- a/Tests/Tests/Data/VideoFileCachingProxyTests.swift +++ b/Tests/Tests/Data/VideoFileCachingProxyTests.swift @@ -93,6 +93,30 @@ final class VideoFileCachingProxyTests: XCTestCase { .with(returnType: Void.self) } + func testRenew() { + sut = VideoFileCachingProxy(albumDirectory: givenAlbumDirectory, + enableCaching: true, + fileManager: mockFileManager, + cacheStorage: mockCache) + // Given a file + mockFileManager.contentsOfDirectoryStub = [givenVideoFile.path.lastPathComponent] + + // Given empty cache + stub(mockCache) { proxy in + when(proxy.get(any(URL.self))).thenReturn(nil) + when(proxy.all.get).thenReturn([]) + } + + // When renew is called + sut.renew() + + // Then cache should be updated... + let expectedFile = givenVideoFile + verify(mockCache) + .store(equal(to: expectedFile), at: equal(to: expectedFile.path)) + .with(returnType: Void.self) + } + func testProxyCache() { let expectedVideoFile = givenVideoFile let expectedFilePath = givenVideoFile.path @@ -108,25 +132,36 @@ final class VideoFileCachingProxyTests: XCTestCase { fileManager: mockFileManager, cacheStorage: mockCache) - // Assume empty cache + // Given empty cache stub(mockCache) { proxy in when(proxy.get(equal(to: expectedFilePath))).thenReturn(nil) when(proxy.all.get).thenReturn([]) } + // When renew the cache + sut.renew() + + // Then... verify(mockCache) .get(equal(to: expectedFilePath)) .with(returnType: VideoFile?.self) - // Assume cache filled + verify(mockCache) + .store(equal(to: expectedVideoFile), at: equal(to: expectedFilePath)) + .with(returnType: Void.self) + + + // Given cache filled stub(mockCache) { proxy in when(proxy.get(equal(to: expectedFilePath))).thenReturn(expectedVideoFile) when(proxy.all.get).thenReturn([expectedVideoFile]) } + // When fetch the files let fetchedFiles = sut.fetch(count: 1) - - verify(mockCache, times(1)) // Only includes previous invoke + + // Then... + verify(mockCache, times(1)) // Only includes previous invoke - which means store's not called additionally .store(equal(to: expectedVideoFile), at: equal(to: expectedFilePath)) .with(returnType: Void.self) @@ -136,7 +171,6 @@ final class VideoFileCachingProxyTests: XCTestCase { func testProxyNotUsingCache() { let expectedVideoFile = givenVideoFile - let expectedFilePath = givenVideoFile.path // Cache shouldn't be filled with a data it has in the `FileManager` sut = VideoFileCachingProxy(albumDirectory: givenAlbumDirectory,