Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dea50fc
Only run `initWithCameraName` on background thread.
renefloor Jul 7, 2021
da83b51
run camera start async
renefloor Jul 7, 2021
277d923
Start with migrating to ThreadSafeFlutterResult
renefloor Jul 8, 2021
9bd75bd
Migrated all results to thread safe class
renefloor Jul 9, 2021
a079af9
make data nonNullable
renefloor Jul 9, 2021
bd7d8eb
copyright
renefloor Jul 9, 2021
f0a2bb0
Merge remote-tracking branch 'flutter/master' into bugfix/camera-thre…
renefloor Jul 9, 2021
4125244
Added method channel tests
renefloor Jul 12, 2021
71e321e
Extend unit test
renefloor Jul 13, 2021
bdfde1a
replace dispatch loop with notifications
renefloor Jul 13, 2021
e6b848c
Made test much cleaner
renefloor Jul 13, 2021
d59a80d
add return for error
renefloor Jul 13, 2021
0f35095
changelog and pubspec
renefloor Jul 13, 2021
0b63dea
Add documentation
renefloor Jul 13, 2021
8bc557f
Merge branch 'master' into bugfix/camera-threading
renefloor Jul 13, 2021
b53ab3e
Make interface an extension
renefloor Jul 14, 2021
b6acf82
Increase timeout
renefloor Jul 14, 2021
9f31156
Merge remote-tracking branch 'flutter/master' into bugfix/camera-thre…
renefloor Aug 16, 2021
a4cf79e
update broken test
renefloor Aug 16, 2021
2b84db2
Documentation improvements
renefloor Aug 16, 2021
92c2d00
Make result methods verbs
renefloor Aug 16, 2021
2722cf4
small doc changes
renefloor Aug 16, 2021
4a1155a
remove notification center
renefloor Aug 17, 2021
04566ae
Added unit test for thread safe result
renefloor Aug 17, 2021
57c63ae
add extra comment
renefloor Aug 17, 2021
5c69728
Merge branch 'master' of https://github.com/flutter/plugins into bugf…
mvanbeusekom Oct 8, 2021
e2ba654
Revert removing handleMethodCallAsync.
mvanbeusekom Oct 8, 2021
17d1f5d
Fix unit tests
mvanbeusekom Oct 11, 2021
a0be148
Merge remote-tracking branch 'upstream/master' into bugfix/camera-thr…
mvanbeusekom Oct 11, 2021
ee453bc
Removed left over GIT merge tag
mvanbeusekom Oct 14, 2021
cd48ddd
Applied feedback on PR
mvanbeusekom Oct 14, 2021
98fe08a
Merge remote-tracking branch 'upstream/master' into bugfix/camera-thr…
mvanbeusekom Oct 14, 2021
a4e5262
Remove development team from project file
mvanbeusekom Oct 14, 2021
39d1438
Remove custom setting from XCScheme
mvanbeusekom Oct 14, 2021
3a12cbf
Clean up mock
mvanbeusekom Oct 14, 2021
8e8bdfc
Clean reference to CameraPlugin
mvanbeusekom Oct 14, 2021
3e86910
Apply feedback from PR.
mvanbeusekom Oct 18, 2021
9093887
Merge remote-tracking branch 'upstream/master' into bugfix/camera-thr…
mvanbeusekom Oct 18, 2021
1d51203
Apply review feedback.
mvanbeusekom Oct 18, 2021
f0b9f53
Merge remote-tracking branch 'upstream/master' into bugfix/camera-thr…
mvanbeusekom Oct 20, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.4+3

* Fix registerTexture and result being called on background thread on iOS.

## 0.9.4+2

* Updated package description;
Expand Down
104 changes: 61 additions & 43 deletions packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ @interface FLTCam : NSObject <FlutterTexture,

- (void)applyFocusMode;
- (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice;
- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y;
- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y;
@end

@interface CameraFocusTests : XCTestCase
Expand Down Expand Up @@ -128,11 +128,11 @@ - (void)testSetFocusPointWithResult_SetsFocusPointOfInterest {
[_camera setValue:_mockDevice forKey:@"captureDevice"];

// Run test
[_camera
setFocusPointWithResult:^void(id _Nullable result) {
}
x:1
y:1];
[_camera setFocusPointWithResult:[[FLTThreadSafeFlutterResult alloc]
initWithResult:^(id _Nullable result){
}]
x:1
y:1];

// Verify the focus point of interest has been set
OCMVerify([_mockDevice setFocusPointOfInterest:CGPointMake(1, 1)]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 camera.Test;
@import XCTest;
@import AVFoundation;
#import <OCMock/OCMock.h>
#import "MockFLTThreadSafeFlutterResult.h"

@interface CameraMethodChannelTests : XCTestCase
@property(readonly, nonatomic) CameraPlugin *camera;
@property(readonly, nonatomic) MockFLTThreadSafeFlutterResult *resultObject;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the comment in the other file: please remove these and make them local to the test. (Having the object under test be part of the fixture state is always a major red flag to me unless there's a very compelling reason.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@end

@implementation CameraMethodChannelTests

- (void)setUp {
_camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil];

XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];

// Set up mocks for initWithCameraName method
Comment thread
stuartmorgan-g marked this conversation as resolved.
id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]);
OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]])
.andReturn([AVCaptureInput alloc]);

id avCaptureSessionMock = OCMClassMock([AVCaptureSession class]);
OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock);
OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES);

_resultObject = [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation];
}
Comment thread
renefloor marked this conversation as resolved.
Outdated

- (void)testCreate_ShouldCallResultOnMainThread {
// Set up method call
FlutterMethodCall *call = [FlutterMethodCall
methodCallWithMethodName:@"create"
arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}];

[self->_camera handleMethodCallAsync:call result:self->_resultObject];

// Verify the result
NSDictionary *dictionaryResult = (NSDictionary *)_resultObject.receivedResult;
XCTAssertNotNil(dictionaryResult);
XCTAssert([[dictionaryResult allKeys] containsObject:@"cameraId"]);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ - (void)setUp {
self.cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:self.mockMessenger];
}

- (void)tearDown {
self.mockMessenger = nil;
self.cameraPlugin = nil;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually much rather the ivars, the setup, and the teardown all be removed, and those two lines move into the test. Per the article I linked in a recent PR comment, stateless fixtures >> stateful fixtures, and in this case we're only saving two lines of boilerplate (assuming we eventually have more tests in this file; right now this is actually more code in order to theoretically reduce future duplication.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

- (void)testOrientationNotifications {
id mockMessenger = self.mockMessenger;
[mockMessenger setExpectationOrderMatters:YES];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,36 @@
@import XCTest;
@import AVFoundation;
#import <OCMock/OCMock.h>
#import "MockFLTThreadSafeFlutterResult.h"

@interface FLTCam : NSObject <FlutterTexture,
AVCaptureVideoDataOutputSampleBufferDelegate,
AVCaptureAudioDataOutputSampleBufferDelegate>
@property(assign, nonatomic) BOOL isPreviewPaused;
- (void)pausePreviewWithResult:(FlutterResult)result;
- (void)resumePreviewWithResult:(FlutterResult)result;
- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result;
- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result;
@end

@interface CameraPreviewPauseTests : XCTestCase
@property(readonly, nonatomic) FLTCam* camera;
@property(readonly, nonatomic) FLTCam *camera;
@property(readonly, nonatomic) MockFLTThreadSafeFlutterResult *resultObject;
@end

@implementation CameraPreviewPauseTests

- (void)setUp {
_camera = [[FLTCam alloc] init];

_resultObject = [[MockFLTThreadSafeFlutterResult alloc] init];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be local (and while you're here, you could move _camera into the tests. It's one line, and locality of setup of the object under test is absolutely worth one extra line of boilerplate.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

- (void)testPausePreviewWithResult_shouldPausePreview {
XCTestExpectation* resultExpectation =
[self expectationWithDescription:@"Succeeding result with nil value"];
[_camera pausePreviewWithResult:^void(id _Nullable result) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:2.0 handler:nil];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you still need this (and an expectation passed to the MockFLTThreadSafeFlutterResult initialization) so that the assertions are guaranteed to run after the result callback?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, the [CameraPlugin pausePreviewWithResult:] method that is tested has a very simple (synchronised) implementation and will not run its logic on a different queue (dispatching on different queue is done by the [CameraPlugin handleMethodCall] method).

Also the result object that is passed in is a simple mock implementation of the FLTThreadSafeFlutterResult class which simply echo's the value that is received by calling the [MockFLTThreadSafeFlutterResult sendSuccessWithData:] method through the MockFLTThreadSafeFlutterResult.receivedResult property.

So as far as I understand everything will run synchronously after each other.

[_camera pausePreviewWithResult:_resultObject];
XCTAssertTrue(_camera.isPreviewPaused);
}

- (void)testResumePreviewWithResult_shouldResumePreview {
XCTestExpectation* resultExpectation =
[self expectationWithDescription:@"Succeeding result with nil value"];
[_camera resumePreviewWithResult:^void(id _Nullable result) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:2.0 handler:nil];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See explanation above.

[_camera resumePreviewWithResult:_resultObject];
XCTAssertFalse(_camera.isPreviewPaused);
}

Expand Down
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.

#ifndef MockFLTThreadSafeFlutterResult_h
#define MockFLTThreadSafeFlutterResult_h

/**
* Extends FLTThreadSafeFlutterResult to give tests the ability to wait on the result and
* read the received result.
*/
@interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult
@property(readonly, nonatomic) XCTestExpectation *_Nonnull expectation;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this use the nonnull @property decoration?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is possible, I used the Xcode "Fix" feature when the analyser warned me to specify a nonnull attribute and it added the _Nonnull declaration behind the pointer. It can be replaced with the nonnull attribute in the @property aswell (see update).

@property(nonatomic, nullable) id receivedResult;

- (instancetype _Nonnull)initWithExpectation:(XCTestExpectation *_Nonnull)expectation;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be nonnull as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@end

#endif /* MockFLTThreadSafeFlutterResult_h */
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 "MockFLTThreadSafeFlutterResult.h"

@implementation MockFLTThreadSafeFlutterResult
/**
* Initializes the MockFLTThreadSafeFlutterResult.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overrides of superclass methods don't need (and shouldn't have, to avoid duplication that can drift over time) declaration comments, as the interface there should have the declaration comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

- (instancetype)init {
self = [super init];
return self;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But also this method shouldn't exist, because it's not doing anything.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.


/**
* Initializes the MockFLTThreadSafeFlutterResult with an expectation.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a declaration comment, so should be in the header. It should also describe what the expectation is (i.e., that it's fulfilled when a result is called), so that the comment is actually providing useful information.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved into the header file and added additional description.

- (instancetype)initWithExpectation:(XCTestExpectation *)expectation {
self = [super init];
_expectation = expectation;
return self;
}

/**
* Called when result is successful.
*
* Stores the data in the `receivedResult` property and fulfills the expectation.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove per above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

- (void)sendSuccessWithData:(id)data {
_receivedResult = data;
[self->_expectation fulfill];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Property access should not use self-> except in cases that require it. This should either be _expectation or self.expectation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}

/**
* Called when result is successful.
*
* Fulfills the expectation.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

- (void)sendSuccess {
_receivedResult = nil;
[self->_expectation fulfill];
}
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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;

@interface ThreadSafeFlutterResultTests : XCTestCase
@end

@implementation ThreadSafeFlutterResultTests
- (void)testAsyncSendSuccess_ShouldCallResultOnMainThread {
XCTestExpectation* expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];

FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
XCTAssert(NSThread.isMainThread);
[expectation fulfill];
}];
dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL);
dispatch_async(dispatchQueue, ^{
[threadSafeFlutterResult sendSuccess];
});

[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
}

- (void)testSyncSendSuccess_ShouldCallResultOnMainThread {
XCTestExpectation* expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];

FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
XCTAssert(NSThread.isMainThread);
[expectation fulfill];
}];
[threadSafeFlutterResult sendSuccess];
[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
}

- (void)testSendNotImplemented_ShouldSendNotImplementedToFlutterResult {
XCTestExpectation* expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];

FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
XCTAssert([result isKindOfClass:FlutterMethodNotImplemented.class]);
[expectation fulfill];
}];
dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL);
dispatch_async(dispatchQueue, ^{
[threadSafeFlutterResult sendNotImplemented];
});

[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
}

- (void)testSendErrorDetails_ShouldSendErrorToFlutterResult {
NSString* errorCode = @"errorCode";
NSString* errorMessage = @"message";
NSString* errorDetails = @"error details";
XCTestExpectation* expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];

FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
XCTAssert([result isKindOfClass:FlutterError.class]);
FlutterError* error = (FlutterError*)result;
XCTAssertEqualObjects(error.code, errorCode);
XCTAssertEqualObjects(error.message, errorMessage);
XCTAssertEqualObjects(error.details, errorDetails);
[expectation fulfill];
}];
dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL);
dispatch_async(dispatchQueue, ^{
[threadSafeFlutterResult sendErrorWithCode:errorCode message:errorMessage details:errorDetails];
});

[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
}

- (void)testSendNSError_ShouldSendErrorToFlutterResult {
NSError* originalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:404 userInfo:nil];
XCTestExpectation* expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];

FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
XCTAssert([result isKindOfClass:FlutterError.class]);
FlutterError* error = (FlutterError*)result;
NSString* constructedErrorCode =
[NSString stringWithFormat:@"Error %d", (int)originalError.code];
XCTAssertEqualObjects(error.code, constructedErrorCode);
[expectation fulfill];
}];
dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL);
dispatch_async(dispatchQueue, ^{
[threadSafeFlutterResult sendError:originalError];
});

[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
}

- (void)testSendResult_ShouldSendResultToFlutterResult {
NSString* resultData = @"resultData";
XCTestExpectation* expectation =
[[XCTestExpectation alloc] initWithDescription:@"Result finished"];

FLTThreadSafeFlutterResult* threadSafeFlutterResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {
XCTAssertEqualObjects(result, resultData);
[expectation fulfill];
}];
dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL);
dispatch_async(dispatchQueue, ^{
[threadSafeFlutterResult sendSuccessWithData:resultData];
});

[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1];
}
@end
Loading