Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 0b969a4

Browse files
authored
[camera]remove "selfRef" for SavePhotoDelegate and ensure thread safety (#4780)
1 parent 6407c3e commit 0b969a4

14 files changed

Lines changed: 272 additions & 82 deletions

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.4+13
2+
3+
* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup.
4+
15
## 0.9.4+12
26

37
* Skips unnecessary AppDelegate setup for unit tests on iOS.

packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 50;
6+
objectVersion = 46;
77
objects = {
88

99
/* Begin PBXBuildFile section */
@@ -23,11 +23,12 @@
2323
E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; };
2424
E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; };
2525
E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; };
26+
E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; };
27+
E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; };
2628
E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; };
2729
E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; };
2830
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; };
2931
E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; };
30-
E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */; };
3132
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; };
3233
F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; };
3334
/* End PBXBuildFile section */
@@ -85,11 +86,12 @@
8586
E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = "<group>"; };
8687
E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = "<group>"; };
8788
E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = "<group>"; };
89+
E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = "<group>"; };
90+
E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = "<group>"; };
8891
E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = "<group>"; };
8992
E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = "<group>"; };
9093
E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = "<group>"; };
9194
E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = "<group>"; };
92-
E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleBufferQueueTests.m; sourceTree = "<group>"; };
9395
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = "<group>"; };
9496
F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = "<group>"; };
9597
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = "<group>"; };
@@ -126,8 +128,9 @@
126128
E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */,
127129
E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */,
128130
E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */,
129-
E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */,
130131
E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */,
132+
E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */,
133+
E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */,
131134
E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */,
132135
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
133136
F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */,
@@ -396,12 +399,13 @@
396399
isa = PBXSourcesBuildPhase;
397400
buildActionMask = 2147483647;
398401
files = (
399-
E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */,
400402
03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */,
401403
033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */,
404+
E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */,
402405
E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */,
403406
03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */,
404407
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
408+
E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */,
405409
E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */,
406410
F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */,
407411
334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import camera;
6+
@import camera.Test;
7+
@import AVFoundation;
8+
@import XCTest;
9+
#import <OCMock/OCMock.h>
10+
11+
@interface FLTCamPhotoCaptureTests : XCTestCase
12+
13+
@end
14+
15+
@implementation FLTCamPhotoCaptureTests
16+
17+
- (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError {
18+
XCTestExpectation *errorExpectation =
19+
[self expectationWithDescription:
20+
@"Must send error to result if save photo delegate completes with error."];
21+
22+
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
23+
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
24+
(void *)FLTCaptureSessionQueueSpecific, NULL);
25+
FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue];
26+
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
27+
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
28+
OCMStub([mockSettings photoSettings]).andReturn(settings);
29+
30+
NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil];
31+
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
32+
OCMStub([mockResult sendError:error]).andDo(^(NSInvocation *invocation) {
33+
[errorExpectation fulfill];
34+
});
35+
36+
id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
37+
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
38+
.andDo(^(NSInvocation *invocation) {
39+
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
40+
// Completion runs on IO queue.
41+
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
42+
dispatch_async(ioQueue, ^{
43+
delegate.completionHandler(nil, error);
44+
});
45+
});
46+
cam.capturePhotoOutput = mockOutput;
47+
48+
// `FLTCam::captureToFile` runs on capture session queue.
49+
dispatch_async(captureSessionQueue, ^{
50+
[cam captureToFile:mockResult];
51+
});
52+
53+
[self waitForExpectationsWithTimeout:1 handler:nil];
54+
}
55+
56+
- (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath {
57+
XCTestExpectation *pathExpectation =
58+
[self expectationWithDescription:
59+
@"Must send file path to result if save photo delegate completes with file path."];
60+
61+
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
62+
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
63+
(void *)FLTCaptureSessionQueueSpecific, NULL);
64+
FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue];
65+
66+
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
67+
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
68+
OCMStub([mockSettings photoSettings]).andReturn(settings);
69+
70+
NSString *filePath = @"test";
71+
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
72+
OCMStub([mockResult sendSuccessWithData:filePath]).andDo(^(NSInvocation *invocation) {
73+
[pathExpectation fulfill];
74+
});
75+
76+
id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
77+
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
78+
.andDo(^(NSInvocation *invocation) {
79+
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
80+
// Completion runs on IO queue.
81+
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
82+
dispatch_async(ioQueue, ^{
83+
delegate.completionHandler(filePath, nil);
84+
});
85+
});
86+
cam.capturePhotoOutput = mockOutput;
87+
88+
// `FLTCam::captureToFile` runs on capture session queue.
89+
dispatch_async(captureSessionQueue, ^{
90+
[cam captureToFile:mockResult];
91+
});
92+
[self waitForExpectationsWithTimeout:1 handler:nil];
93+
}
94+
95+
/// Creates an `FLTCam` that runs its operations on a given capture session queue.
96+
- (FLTCam *)createFLTCamWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue {
97+
id inputMock = OCMClassMock([AVCaptureDeviceInput class]);
98+
OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]])
99+
.andReturn(inputMock);
100+
101+
id sessionMock = OCMClassMock([AVCaptureSession class]);
102+
OCMStub([sessionMock alloc]).andReturn(sessionMock);
103+
OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op
104+
OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES);
105+
106+
return [[FLTCam alloc] initWithCameraName:@"camera"
107+
resolutionPreset:@"medium"
108+
enableAudio:true
109+
orientation:UIDeviceOrientationPortrait
110+
captureSessionQueue:captureSessionQueue
111+
error:nil];
112+
}
113+
114+
@end

packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m renamed to packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
@import XCTest;
99
#import <OCMock/OCMock.h>
1010

11-
@interface SampleBufferQueueTests : XCTestCase
11+
@interface FLTCamSampleBufferTests : XCTestCase
1212

1313
@end
1414

15-
@implementation SampleBufferQueueTests
15+
@implementation FLTCamSampleBufferTests
1616

1717
- (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue {
1818
id inputMock = OCMClassMock([AVCaptureDeviceInput class]);

packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,44 @@ @interface FLTSavePhotoDelegateTests : XCTestCase
1414

1515
@implementation FLTSavePhotoDelegateTests
1616

17-
- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToCapture {
18-
NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil];
19-
dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
20-
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
21-
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
22-
result:mockResult
23-
ioQueue:ioQueue];
17+
- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture {
18+
XCTestExpectation *completionExpectation =
19+
[self expectationWithDescription:@"Must complete with error if failed to capture photo."];
2420

25-
[delegate handlePhotoCaptureResultWithError:error
21+
NSError *captureError = [NSError errorWithDomain:@"test" code:0 userInfo:nil];
22+
dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
23+
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc]
24+
initWithPath:@"test"
25+
ioQueue:ioQueue
26+
completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) {
27+
XCTAssertEqualObjects(captureError, error);
28+
XCTAssertNil(path);
29+
[completionExpectation fulfill];
30+
}];
31+
32+
[delegate handlePhotoCaptureResultWithError:captureError
2633
photoDataProvider:^NSData * {
2734
return nil;
2835
}];
29-
OCMVerify([mockResult sendError:error]);
36+
[self waitForExpectationsWithTimeout:1 handler:nil];
3037
}
3138

32-
- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite {
33-
XCTestExpectation *resultExpectation =
34-
[self expectationWithDescription:@"Must send IOError to the result if failed to write file."];
39+
- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite {
40+
XCTestExpectation *completionExpectation =
41+
[self expectationWithDescription:@"Must complete with error if failed to write file."];
3542
dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
36-
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
3743

3844
NSError *ioError = [NSError errorWithDomain:@"IOError"
3945
code:0
4046
userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}];
41-
42-
OCMStub([mockResult sendErrorWithCode:@"IOError"
43-
message:@"Unable to write file"
44-
details:ioError.localizedDescription])
45-
.andDo(^(NSInvocation *invocation) {
46-
[resultExpectation fulfill];
47-
});
48-
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
49-
result:mockResult
50-
ioQueue:ioQueue];
47+
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc]
48+
initWithPath:@"test"
49+
ioQueue:ioQueue
50+
completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) {
51+
XCTAssertEqualObjects(ioError, error);
52+
XCTAssertNil(path);
53+
[completionExpectation fulfill];
54+
}];
5155

5256
// We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
5357
// `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
@@ -63,23 +67,25 @@ - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite {
6367
[self waitForExpectationsWithTimeout:1 handler:nil];
6468
}
6569

66-
- (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite {
67-
XCTestExpectation *resultExpectation = [self
68-
expectationWithDescription:@"Must send file path to the result if success to write file."];
70+
- (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite {
71+
XCTestExpectation *completionExpectation =
72+
[self expectationWithDescription:@"Must complete with file path if success to write file."];
6973

7074
dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
71-
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
72-
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
73-
result:mockResult
74-
ioQueue:ioQueue];
75-
OCMStub([mockResult sendSuccessWithData:delegate.path]).andDo(^(NSInvocation *invocation) {
76-
[resultExpectation fulfill];
77-
});
75+
NSString *filePath = @"test";
76+
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc]
77+
initWithPath:filePath
78+
ioQueue:ioQueue
79+
completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) {
80+
XCTAssertNil(error);
81+
XCTAssertEqualObjects(filePath, path);
82+
[completionExpectation fulfill];
83+
}];
7884

7985
// We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
8086
// `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
8187
id mockData = OCMPartialMock([NSData data]);
82-
OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]])
88+
OCMStub([mockData writeToFile:filePath options:NSDataWritingAtomic error:[OCMArg setTo:nil]])
8389
.andReturn(YES);
8490

8591
[delegate handlePhotoCaptureResultWithError:nil
@@ -94,16 +100,12 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue
94100
[self expectationWithDescription:@"Data provider must run on io queue."];
95101
XCTestExpectation *writeFileQueueExpectation =
96102
[self expectationWithDescription:@"File writing must run on io queue"];
97-
XCTestExpectation *resultExpectation = [self
98-
expectationWithDescription:@"Must send file path to the result if success to write file."];
103+
XCTestExpectation *completionExpectation =
104+
[self expectationWithDescription:@"Must complete with file path if success to write file."];
99105

100106
dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
101107
const char *ioQueueSpecific = "io_queue_specific";
102108
dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL);
103-
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
104-
OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
105-
[resultExpectation fulfill];
106-
});
107109

108110
// We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
109111
// `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
@@ -116,9 +118,14 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue
116118
})
117119
.andReturn(YES);
118120

119-
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
120-
result:mockResult
121-
ioQueue:ioQueue];
121+
NSString *filePath = @"test";
122+
FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc]
123+
initWithPath:filePath
124+
ioQueue:ioQueue
125+
completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) {
126+
[completionExpectation fulfill];
127+
}];
128+
122129
[delegate handlePhotoCaptureResultWithError:nil
123130
photoDataProvider:^NSData * {
124131
if (dispatch_get_specific(ioQueueSpecific)) {

0 commit comments

Comments
 (0)