-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[camera] Handle "create" method call in main thread on iOS #4007
Changes from all commits
f8d7ea0
b6f6d9e
e846c64
b16f1d7
c336bcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// 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; | ||
@import XCTest; | ||
|
||
#import <OCMock/OCMock.h> | ||
|
||
@interface CameraPlugin () | ||
- (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry | ||
messenger:(NSObject<FlutterBinaryMessenger> *)messenger; | ||
- (BOOL)handleMethodCallSync:(FlutterMethodCall *)call result:(FlutterResult)result; | ||
- (BOOL)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing with private methods should only be done when a test can't be done with public methods. I don't see why accessing these should be necessary. |
||
@end | ||
|
||
@interface CameraPluginTests : XCTestCase | ||
@property(strong, nonatomic) id mockRegistrar; | ||
@property(strong, nonatomic) id mockMessenger; | ||
@property(strong, nonatomic) CameraPlugin *plugin; | ||
@end | ||
|
||
@implementation CameraPluginTests | ||
|
||
- (void)setUp { | ||
[super setUp]; | ||
self.mockRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); | ||
self.mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); | ||
OCMStub([self.mockRegistrar messenger]).andReturn(self.mockMessenger); | ||
self.plugin = [[CameraPlugin alloc] initWithRegistry:self.mockRegistrar | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't make the object under test stateful. Test fixtures should have as little state as possible so that it's obvious what's happening within each test, and that's especially true of seeing how the object under test is being set up. |
||
messenger:self.mockMessenger]; | ||
} | ||
|
||
- (void)testHandleMethodCallSync_ShouldHandleSyncMethods { | ||
id methodCallMock = OCMClassMock([FlutterMethodCall class]); | ||
OCMStub([methodCallMock method]).andReturn(@"create"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this a mock? Mocking is a tool of last resort; |
||
|
||
BOOL result = [[self plugin] handleMethodCallSync:methodCallMock | ||
result:^(id _Nullable result){ | ||
}]; | ||
|
||
XCTAssertTrue(result); | ||
} | ||
|
||
- (void)testHandleMethodCallSync_ShouldNotHandleAsyncMethods { | ||
id methodCallMock = OCMClassMock([FlutterMethodCall class]); | ||
OCMStub([methodCallMock method]).andReturn(@"initialize"); | ||
|
||
BOOL result = [[self plugin] handleMethodCallSync:methodCallMock | ||
result:^(id _Nullable result){ | ||
}]; | ||
|
||
XCTAssertFalse(result); | ||
} | ||
|
||
- (void)testHandleMethodCallAsync_ShouldHandleAsyncMethods { | ||
id methodCallMock = OCMClassMock([FlutterMethodCall class]); | ||
OCMStub([methodCallMock method]).andReturn(@"initialize"); | ||
|
||
BOOL result = [[self plugin] handleMethodCallAsync:methodCallMock | ||
result:^(id _Nullable result){ | ||
}]; | ||
|
||
XCTAssertTrue(result); | ||
} | ||
|
||
- (void)testHandleMethodCallAsync_ShouldNotHandleSyncMethods { | ||
id methodCallMock = OCMClassMock([FlutterMethodCall class]); | ||
OCMStub([methodCallMock method]).andReturn(@"create"); | ||
|
||
BOOL result = [[self plugin] handleMethodCallAsync:methodCallMock | ||
result:^(id _Nullable result){ | ||
}]; | ||
|
||
XCTAssertFalse(result); | ||
} | ||
|
||
- (void)testHandleMethodCall_ShouldNotCallAsyncHandlerForSyncMethod { | ||
id methodCallMock = OCMClassMock([FlutterMethodCall class]); | ||
OCMStub([methodCallMock method]).andReturn(@"create"); | ||
id mockedPlugin = OCMPartialMock(self.plugin); | ||
id result = ^(id _Nullable result) { | ||
}; | ||
OCMStub([mockedPlugin handleMethodCallSync:methodCallMock result:result]).andReturn(true); | ||
OCMStub([mockedPlugin handleMethodCallAsync:methodCallMock result:result]).andReturn(false); | ||
|
||
[[self plugin] handleMethodCall:methodCallMock result:result]; | ||
|
||
OCMVerify([mockedPlugin handleMethodCallSync:methodCallMock result:result]); | ||
OCMVerify(never(), [mockedPlugin handleMethodCallAsync:methodCallMock result:result]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of these tests are testing for an implementation detail rather than testing that the thing we want to be true—that interactions with the engine are on the main thread. We should test for what we want to assert, not for an implementation that we believe has the properties we want. As one of many reasons this is true: would these tests fail if someone accidentally swapped which internal method call was done on a queue and which one wasn't? See #3778 for an example where I fixed a similar issue in another plugin, and handled it with assertions in the code and tests designed to exercise the assertions without assuming things about the implementation details of the plugin itself. |
||
} | ||
|
||
- (void)testHandleMethodCall_ShouldCallAsyncHandlerForAsyncMethod { | ||
id methodCallMock = OCMClassMock([FlutterMethodCall class]); | ||
OCMStub([methodCallMock method]).andReturn(@"initialize"); | ||
id mockedPlugin = OCMPartialMock(self.plugin); | ||
id result = ^(id _Nullable result) { | ||
}; | ||
OCMStub([mockedPlugin handleMethodCallSync:methodCallMock result:result]).andReturn(false); | ||
OCMStub([mockedPlugin handleMethodCallAsync:methodCallMock result:result]).andReturn(true); | ||
|
||
[[self plugin] handleMethodCall:methodCallMock result:result]; | ||
|
||
OCMVerify([mockedPlugin handleMethodCallSync:methodCallMock result:result]); | ||
OCMVerify([mockedPlugin handleMethodCallAsync:methodCallMock result:result]); | ||
} | ||
|
||
@end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1271,6 +1271,7 @@ - (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry | |
_messenger = messenger; | ||
[self initDeviceEventMethodChannel]; | ||
[self startOrientationListener]; | ||
|
||
return self; | ||
} | ||
|
||
|
@@ -1315,13 +1316,48 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result | |
_dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); | ||
} | ||
|
||
// Invoke the plugin on another dispatch queue to avoid blocking the UI. | ||
// Handle method calls on platform thread for those that require it. | ||
if ([self handleMethodCallSync:call result:result]) { | ||
return; | ||
} | ||
|
||
// Otherwise invoke the plugin on another dispatch queue to avoid blocking the UI. | ||
dispatch_async(_dispatchQueue, ^{ | ||
[self handleMethodCallAsync:call result:result]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've commented on the issue to get clarity, but I believe all uses of Delegating to a queue should very likely be limited to just expensive operations, rather than a default behavior. |
||
}); | ||
} | ||
|
||
- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result { | ||
- (BOOL)handleMethodCallSync:(FlutterMethodCall *)call result:(FlutterResult)result { | ||
if ([@"create" isEqualToString:call.method]) { | ||
NSString *cameraName = call.arguments[@"cameraName"]; | ||
NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; | ||
NSNumber *enableAudio = call.arguments[@"enableAudio"]; | ||
NSError *error; | ||
FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName | ||
resolutionPreset:resolutionPreset | ||
enableAudio:[enableAudio boolValue] | ||
orientation:[[UIDevice currentDevice] orientation] | ||
dispatchQueue:_dispatchQueue | ||
error:&error]; | ||
if (error) { | ||
result(getFlutterError(error)); | ||
} else { | ||
if (_camera) { | ||
[_camera close]; | ||
} | ||
int64_t textureId = [_registry registerTexture:cam]; | ||
_camera = cam; | ||
|
||
result(@{ | ||
@"cameraId" : @(textureId), | ||
}); | ||
} | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
- (BOOL)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result { | ||
if ([@"availableCameras" isEqualToString:call.method]) { | ||
if (@available(iOS 10.0, *)) { | ||
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession | ||
|
@@ -1353,31 +1389,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re | |
result(reply); | ||
} else { | ||
result(FlutterMethodNotImplemented); | ||
} | ||
} else if ([@"create" isEqualToString:call.method]) { | ||
NSString *cameraName = call.arguments[@"cameraName"]; | ||
NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; | ||
NSNumber *enableAudio = call.arguments[@"enableAudio"]; | ||
NSError *error; | ||
FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName | ||
resolutionPreset:resolutionPreset | ||
enableAudio:[enableAudio boolValue] | ||
orientation:[[UIDevice currentDevice] orientation] | ||
dispatchQueue:_dispatchQueue | ||
error:&error]; | ||
|
||
if (error) { | ||
result(getFlutterError(error)); | ||
} else { | ||
if (_camera) { | ||
[_camera close]; | ||
} | ||
int64_t textureId = [_registry registerTexture:cam]; | ||
_camera = cam; | ||
|
||
result(@{ | ||
@"cameraId" : @(textureId), | ||
}); | ||
return false; | ||
} | ||
} else if ([@"startImageStream" isEqualToString:call.method]) { | ||
[_camera startImageStreamWithMessenger:_messenger]; | ||
|
@@ -1483,8 +1495,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re | |
[_camera setFocusPointWithResult:result x:x y:y]; | ||
} else { | ||
result(FlutterMethodNotImplemented); | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
@end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file hasn't been added to the project, so isn't being run.