Skip to content

Commit

Permalink
[camera] Remove OCMock from FLTCamPhotoCaptureTests, FLTSavePhotoDele…
Browse files Browse the repository at this point in the history
…gateTests and StreamingTests (#8590)

Continuation of OCMock removal from tests related to flutter/flutter#119109. Introduces two new protocols `FLTFileWriter` and `FLTCapturePhotoOutput`.
  • Loading branch information
mchudy authored Feb 10, 2025
1 parent 970d858 commit fec5ec5
Show file tree
Hide file tree
Showing 19 changed files with 329 additions and 97 deletions.
4 changes: 4 additions & 0 deletions packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.18+3

* Refactors implementations to reduce usage of OCMock in internal testing.

## 0.9.18+2

* Refactors implementations to reduce usage of OCMock in internal testing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
7FA99E592D22C75300582559 /* CameraExposureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FA99E582D22C75300582559 /* CameraExposureTests.m */; };
7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FCEDD342D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m */; };
7FCEDD362D43C2B900EA1CA8 /* MockCaptureDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FCEDD322D43C2B900EA1CA8 /* MockCaptureDevice.m */; };
7FD582122D579650003B1200 /* MockWritableData.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FD582112D579650003B1200 /* MockWritableData.m */; };
7FD582202D579ECC003B1200 /* MockCapturePhotoOutput.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FD5821F2D579ECC003B1200 /* MockCapturePhotoOutput.m */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
Expand Down Expand Up @@ -102,6 +104,10 @@
7FCEDD322D43C2B900EA1CA8 /* MockCaptureDevice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockCaptureDevice.m; sourceTree = "<group>"; };
7FCEDD332D43C2B900EA1CA8 /* MockDeviceOrientationProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockDeviceOrientationProvider.h; sourceTree = "<group>"; };
7FCEDD342D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockDeviceOrientationProvider.m; sourceTree = "<group>"; };
7FD582112D579650003B1200 /* MockWritableData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockWritableData.m; sourceTree = "<group>"; };
7FD582132D57965A003B1200 /* MockWritableData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockWritableData.h; sourceTree = "<group>"; };
7FD5821F2D579ECC003B1200 /* MockCapturePhotoOutput.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockCapturePhotoOutput.m; sourceTree = "<group>"; };
7FD582212D579ED9003B1200 /* MockCapturePhotoOutput.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockCapturePhotoOutput.h; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -199,6 +205,8 @@
7F8FD2272D4BFA8D001AF2C1 /* MockGlobalEventApi.h */,
7FCEDD312D43C2B900EA1CA8 /* MockCaptureDevice.h */,
7FCEDD322D43C2B900EA1CA8 /* MockCaptureDevice.m */,
7FD582212D579ED9003B1200 /* MockCapturePhotoOutput.h */,
7FD5821F2D579ECC003B1200 /* MockCapturePhotoOutput.m */,
7FCEDD332D43C2B900EA1CA8 /* MockDeviceOrientationProvider.h */,
7FCEDD342D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m */,
7F29EB282D26A59000740257 /* MockCameraDeviceDiscoverer.m */,
Expand All @@ -207,6 +215,8 @@
7F29EB212D269ED500740257 /* MockEventChannel.m */,
7F29EB3E2D281C5800740257 /* MockCaptureSession.h */,
7F29EB402D281C7E00740257 /* MockCaptureSession.m */,
7FD582112D579650003B1200 /* MockWritableData.m */,
7FD582132D57965A003B1200 /* MockWritableData.h */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -512,13 +522,15 @@
43ED1537282570DE00EB00DE /* AvailableCamerasTest.m in Sources */,
E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */,
334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
7FD582202D579ECC003B1200 /* MockCapturePhotoOutput.m in Sources */,
CEF6611A2B5E36A500D33FD4 /* CameraSessionPresetsTests.m in Sources */,
E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */,
7F29EB292D26A59000740257 /* MockCameraDeviceDiscoverer.m in Sources */,
788A065A27B0E02900533D74 /* StreamingTest.m in Sources */,
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */,
7F29EB412D281C7E00740257 /* MockCaptureSession.m in Sources */,
E01EE4A82799F3A5008C1950 /* QueueUtilsTests.m in Sources */,
7FD582122D579650003B1200 /* MockWritableData.m in Sources */,
7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */,
7FCEDD362D43C2B900EA1CA8 /* MockCaptureDevice.m in Sources */,
7F8FD22C2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
#endif
@import AVFoundation;
@import XCTest;
#import <OCMock/OCMock.h>

#import "CameraTestUtils.h"
#import "MockCaptureDevice.h"
#import "MockCapturePhotoOutput.h"

/// Includes test cases related to photo capture operations for FLTCam class.
@interface FLTCamPhotoCaptureTests : XCTestCase
Expand All @@ -27,22 +29,19 @@ - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsW
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
(void *)FLTCaptureSessionQueueSpecific, NULL);
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
OCMStub([mockSettings photoSettings]).andReturn(settings);

NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil];

id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
MockCapturePhotoOutput *mockOutput = [[MockCapturePhotoOutput alloc] init];
mockOutput.capturePhotoWithSettingsStub =
^(AVCapturePhotoSettings *settings, NSObject<AVCapturePhotoCaptureDelegate> *photoDelegate) {
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
// Completion runs on IO queue.
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
dispatch_async(ioQueue, ^{
delegate.completionHandler(nil, error);
});
});
};
cam.capturePhotoOutput = mockOutput;

// `FLTCam::captureToFile` runs on capture session queue.
Expand All @@ -67,22 +66,18 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi
(void *)FLTCaptureSessionQueueSpecific, NULL);
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);

AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
OCMStub([mockSettings photoSettings]).andReturn(settings);

NSString *filePath = @"test";

id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
MockCapturePhotoOutput *mockOutput = [[MockCapturePhotoOutput alloc] init];
mockOutput.capturePhotoWithSettingsStub =
^(AVCapturePhotoSettings *settings, NSObject<AVCapturePhotoCaptureDelegate> *photoDelegate) {
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
// Completion runs on IO queue.
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
dispatch_async(ioQueue, ^{
delegate.completionHandler(filePath, nil);
});
});
};
cam.capturePhotoOutput = mockOutput;

// `FLTCam::captureToFile` runs on capture session queue.
Expand All @@ -105,27 +100,19 @@ - (void)testCaptureToFile_mustReportFileExtensionWithHeifWhenHEVCIsAvailableAndF
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);
[cam setImageFileFormat:FCPPlatformImageFileFormatHeif];

AVCapturePhotoSettings *settings =
[AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey : AVVideoCodecTypeHEVC}];

id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
OCMStub([mockSettings photoSettingsWithFormat:OCMOCK_ANY]).andReturn(settings);

id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
// Set availablePhotoCodecTypes to HEVC
NSArray *codecTypes = @[ AVVideoCodecTypeHEVC ];
OCMStub([mockOutput availablePhotoCodecTypes]).andReturn(codecTypes);

OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
MockCapturePhotoOutput *mockOutput = [[MockCapturePhotoOutput alloc] init];
mockOutput.availablePhotoCodecTypes = @[ AVVideoCodecTypeHEVC ];
mockOutput.capturePhotoWithSettingsStub =
^(AVCapturePhotoSettings *settings, NSObject<AVCapturePhotoCaptureDelegate> *photoDelegate) {
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
// Completion runs on IO queue.
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
dispatch_async(ioQueue, ^{
delegate.completionHandler(delegate.filePath, nil);
});
});
};
cam.capturePhotoOutput = mockOutput;

// `FLTCam::captureToFile` runs on capture session queue.
dispatch_async(captureSessionQueue, ^{
[cam captureToFileWithCompletion:^(NSString *filePath, FlutterError *error) {
Expand All @@ -146,22 +133,18 @@ - (void)testCaptureToFile_mustReportFileExtensionWithJpgWhenHEVCNotAvailableAndF
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);
[cam setImageFileFormat:FCPPlatformImageFileFormatHeif];

AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
OCMStub([mockSettings photoSettings]).andReturn(settings);

id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);

OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
MockCapturePhotoOutput *mockOutput = [[MockCapturePhotoOutput alloc] init];
mockOutput.capturePhotoWithSettingsStub =
^(AVCapturePhotoSettings *settings, NSObject<AVCapturePhotoCaptureDelegate> *photoDelegate) {
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
// Completion runs on IO queue.
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
dispatch_async(ioQueue, ^{
delegate.completionHandler(delegate.filePath, nil);
});
});
};
cam.capturePhotoOutput = mockOutput;

// `FLTCam::captureToFile` runs on capture session queue.
dispatch_async(captureSessionQueue, ^{
[cam captureToFileWithCompletion:^(NSString *filePath, FlutterError *error) {
Expand All @@ -176,12 +159,18 @@ - (void)testCaptureToFile_handlesTorchMode {
XCTestExpectation *pathExpectation =
[self expectationWithDescription:
@"Must send file path to result if save photo delegate completes with file path."];

id captureDeviceMock = OCMProtocolMock(@protocol(FLTCaptureDevice));
OCMStub([captureDeviceMock hasTorch]).andReturn(YES);
OCMStub([captureDeviceMock isTorchAvailable]).andReturn(YES);
OCMStub([captureDeviceMock torchMode]).andReturn(AVCaptureTorchModeAuto);
OCMExpect([captureDeviceMock setTorchMode:AVCaptureTorchModeOn]);
XCTestExpectation *setTorchExpectation =
[self expectationWithDescription:@"Should set torch mode to AVCaptureTorchModeOn."];

MockCaptureDevice *captureDeviceMock = [[MockCaptureDevice alloc] init];
captureDeviceMock.hasTorch = YES;
captureDeviceMock.isTorchAvailable = YES;
captureDeviceMock.torchMode = AVCaptureTorchModeAuto;
captureDeviceMock.setTorchModeStub = ^(AVCaptureTorchMode mode) {
if (mode == AVCaptureTorchModeOn) {
[setTorchExpectation fulfill];
}
};

dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
Expand All @@ -194,22 +183,18 @@ - (void)testCaptureToFile_handlesTorchMode {
},
nil);

AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
OCMStub([mockSettings photoSettings]).andReturn(settings);

NSString *filePath = @"test";

id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
MockCapturePhotoOutput *mockOutput = [[MockCapturePhotoOutput alloc] init];
mockOutput.capturePhotoWithSettingsStub =
^(AVCapturePhotoSettings *settings, NSObject<AVCapturePhotoCaptureDelegate> *photoDelegate) {
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
// Completion runs on IO queue.
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
dispatch_async(ioQueue, ^{
delegate.completionHandler(filePath, nil);
});
});
};
cam.capturePhotoOutput = mockOutput;

// `FLTCam::captureToFile` runs on capture session queue.
Expand All @@ -223,6 +208,5 @@ - (void)testCaptureToFile_handlesTorchMode {
}];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
OCMVerifyAll(captureDeviceMock);
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
#endif
@import AVFoundation;
@import XCTest;
#import <OCMock/OCMock.h>

#import "MockWritableData.h"

@interface FLTSavePhotoDelegateTests : XCTestCase

Expand Down Expand Up @@ -55,16 +56,16 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite {
[completionExpectation fulfill];
}];

// Do not use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
// `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
id mockData = OCMPartialMock([NSData data]);
OCMStub([mockData writeToFile:OCMOCK_ANY
options:NSDataWritingAtomic
error:[OCMArg setTo:ioError]])
.andReturn(NO);
MockWritableData *mockWritableData = [[MockWritableData alloc] init];
mockWritableData.writeToFileStub =
^BOOL(NSString *path, NSDataWritingOptions options, NSError *__autoreleasing *error) {
*error = ioError;
return NO;
};

[delegate handlePhotoCaptureResultWithError:nil
photoDataProvider:^NSData * {
return mockData;
photoDataProvider:^NSObject<FLTWritableData> * {
return mockWritableData;
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
Expand All @@ -84,15 +85,15 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite {
[completionExpectation fulfill];
}];

// Do not use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
// `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
id mockData = OCMPartialMock([NSData data]);
OCMStub([mockData writeToFile:filePath options:NSDataWritingAtomic error:[OCMArg setTo:nil]])
.andReturn(YES);
MockWritableData *mockWritableData = [[MockWritableData alloc] init];
mockWritableData.writeToFileStub =
^BOOL(NSString *path, NSDataWritingOptions options, NSError *__autoreleasing *error) {
return YES;
};

[delegate handlePhotoCaptureResultWithError:nil
photoDataProvider:^NSData * {
return mockData;
photoDataProvider:^NSObject<FLTWritableData> * {
return mockWritableData;
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
Expand All @@ -109,16 +110,14 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue
const char *ioQueueSpecific = "io_queue_specific";
dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL);

// Do not use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
// `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
id mockData = OCMPartialMock([NSData data]);
OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]])
.andDo(^(NSInvocation *invocation) {
MockWritableData *mockWritableData = [[MockWritableData alloc] init];
mockWritableData.writeToFileStub =
^BOOL(NSString *path, NSDataWritingOptions options, NSError *__autoreleasing *error) {
if (dispatch_get_specific(ioQueueSpecific)) {
[writeFileQueueExpectation fulfill];
}
})
.andReturn(YES);
return YES;
};

NSString *filePath = @"test";
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc]
Expand All @@ -129,11 +128,11 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue
}];

[delegate handlePhotoCaptureResultWithError:nil
photoDataProvider:^NSData * {
photoDataProvider:^NSObject<FLTWritableData> * {
if (dispatch_get_specific(ioQueueSpecific)) {
[dataProviderQueueExpectation fulfill];
}
return mockData;
return mockWritableData;
}];

[self waitForExpectationsWithTimeout:1 handler:nil];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@import camera_avfoundation;
@import AVFoundation;

/// Mock implementation of `FLTCapturePhotoOutput` protocol which allows injecting a custom
/// implementation.
@interface MockCapturePhotoOutput : NSObject <FLTCapturePhotoOutput>

// Properties re-declared as read/write so a mocked value can be set during testing.
@property(nonatomic, strong) AVCapturePhotoOutput *photoOutput;
@property(nonatomic, strong) NSArray<AVVideoCodecType> *availablePhotoCodecTypes;
@property(nonatomic, assign) BOOL highResolutionCaptureEnabled;
@property(nonatomic, strong) NSArray<NSNumber *> *supportedFlashModes;

// Stub that is called when the corresponding public method is called.
@property(nonatomic, copy) void (^capturePhotoWithSettingsStub)
(AVCapturePhotoSettings *, NSObject<AVCapturePhotoCaptureDelegate> *);

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "MockCapturePhotoOutput.h"

@implementation MockCapturePhotoOutput
- (void)capturePhotoWithSettings:(AVCapturePhotoSettings *)settings
delegate:(id<AVCapturePhotoCaptureDelegate>)delegate {
if (self.capturePhotoWithSettingsStub) {
self.capturePhotoWithSettingsStub(settings, delegate);
}
}

- (nullable AVCaptureConnection *)connectionWithMediaType:(nonnull AVMediaType)mediaType {
return nil;
}

@end
Loading

0 comments on commit fec5ec5

Please sign in to comment.