Skip to content

[camera_avfoundation] Tests backfilling - part 4 #8854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.9.18+11

* Backfills unit tests for the `FLTCam` class.
* Refactors implementation to allow mocking of `AVCaptureVideoDataOutput` in tests.

## 0.9.18+10

* Backfills unit tests for the `FLTCam` class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@
97DB234D2D566D0700CEFE66 /* CameraPreviewPauseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */; };
E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */; };
E11D6A912D82C7740031E6C5 /* FLTCamExposureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */; };
E11D6A8F2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */; };
E12C4FF62D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */; };
E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */; };
E16602952D8471C0003CFE12 /* FLTCamZoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */; };
E1A5F4E32D80259C0005BA64 /* FLTCamSetFlashModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */; };
E15139182D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */; };
E1A5F4E32D80259C0005BA64 /* FLTCamSetFlashModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */; };
E1FFEAAD2D6C8DD700B14107 /* MockFLTCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */; };
E1FFEAAF2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */; };
E1FFEAB12D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */; };
Expand Down Expand Up @@ -148,10 +151,13 @@
E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraTestUtils.h; sourceTree = "<group>"; };
E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraTestUtils.m; sourceTree = "<group>"; };
E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamExposureTests.swift; sourceTree = "<group>"; };
E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCaptureVideoDataOutput.swift; sourceTree = "<group>"; };
E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginDelegatingMethodTests.swift; sourceTree = "<group>"; };
E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCameraPermissionManager.swift; sourceTree = "<group>"; };
E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamZoomTests.swift; sourceTree = "<group>"; };
E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetFlashModeTests.swift; sourceTree = "<group>"; };
E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetDeviceOrientationTests.swift; sourceTree = "<group>"; };
E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetFlashModeTests.swift; sourceTree = "<group>"; };
E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCam.swift; sourceTree = "<group>"; };
E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginCreateCameraTests.swift; sourceTree = "<group>"; };
E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginInitializeCameraTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -212,6 +218,7 @@
977A25212D5A49EC00931E34 /* FLTCamFocusTests.swift */,
E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */,
E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */,
E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -254,6 +261,7 @@
970ADABF2D6764CC00EFDCD9 /* MockEventChannel.swift */,
E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */,
970ADABD2D6740A900EFDCD9 /* MockWritableData.swift */,
E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -557,6 +565,7 @@
7F8FD22F2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m in Sources */,
E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */,
97922B0D2D6380C300A9B4CF /* SampleBufferTests.swift in Sources */,
E15139182D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift in Sources */,
972CA92B2D5A1D8C004B846F /* CameraPropertiesTests.swift in Sources */,
E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */,
978296CF2D5F744B0009BDD3 /* PhotoCaptureTests.swift in Sources */,
Expand All @@ -577,6 +586,7 @@
978D90B42D5F630300CD817E /* StreamingTests.swift in Sources */,
7F29EB412D281C7E00740257 /* MockCaptureSession.m in Sources */,
7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */,
E11D6A8F2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift in Sources */,
977CAC9F2D5E5180001E5DC3 /* ThreadSafeEventChannelTests.swift 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 @@ -101,11 +101,10 @@ private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
}

override func recommendedVideoSettingsForAssetWriter(
withFileType fileType: AVFileType, for output: AVCaptureVideoDataOutput
withFileType fileType: AVFileType, for output: FLTCaptureVideoDataOutput
) -> [String: Any]? {
return [:]
}

}

final class CameraSettingsTests: XCTestCase {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 AVFoundation
import XCTest

@testable import camera_avfoundation

final class FLTCamSetDeviceOrientationTests: XCTestCase {
private func createCamera() -> (FLTCam, MockCaptureConnection, MockCaptureConnection) {
let configuration = FLTCreateTestCameraConfiguration()
let camera = FLTCreateCamWithConfiguration(configuration)

let mockCapturePhotoOutput = MockCapturePhotoOutput()
let mockPhotoCaptureConnection = MockCaptureConnection()
mockPhotoCaptureConnection.isVideoOrientationSupported = true

mockCapturePhotoOutput.connectionWithMediaTypeStub = { _ in mockPhotoCaptureConnection }
camera.capturePhotoOutput = mockCapturePhotoOutput

let mockCaptureVideoDataOutput = MockCaptureVideoDataOutput()
let mockVideoCaptureConnection = MockCaptureConnection()
mockVideoCaptureConnection.isVideoOrientationSupported = true

mockCaptureVideoDataOutput.connectionWithMediaTypeStub = { _ in mockVideoCaptureConnection }
camera.captureVideoOutput = mockCaptureVideoDataOutput

return (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection)
}

func testSetDeviceOrientation_setsOrientationsOfCaptureConnections() {
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()
var photoSetVideoOrientationCalled = false
mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in
// Device orientation is flipped compared to video orientation. When UIDeviceOrientation
// is landscape left the video orientation should be landscape right.
XCTAssertEqual(orientation, .landscapeRight)
photoSetVideoOrientationCalled = true
}

var videoSetVideoOrientationCalled = false
mockVideoCaptureConnection.setVideoOrientationStub = { orientation in
// Device orientation is flipped compared to video orientation. When UIDeviceOrientation
// is landscape left the video orientation should be landscape right.
XCTAssertEqual(orientation, .landscapeRight)
videoSetVideoOrientationCalled = true
}

camera.setDeviceOrientation(.landscapeLeft)

XCTAssertTrue(photoSetVideoOrientationCalled)
XCTAssertTrue(videoSetVideoOrientationCalled)
}

func
testSetDeviceOrientation_setsLockedOrientationsOfCaptureConnection_ifCaptureOrientationIsLocked()
{
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()
var photoSetVideoOrientationCalled = false
mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in
XCTAssertEqual(orientation, .portraitUpsideDown)
photoSetVideoOrientationCalled = true
}

var videoSetVideoOrientationCalled = false
mockVideoCaptureConnection.setVideoOrientationStub = { orientation in
XCTAssertEqual(orientation, .portraitUpsideDown)
videoSetVideoOrientationCalled = true
}

camera.lockCapture(FCPPlatformDeviceOrientation.portraitDown)

camera.setDeviceOrientation(.landscapeLeft)

XCTAssertTrue(photoSetVideoOrientationCalled)
XCTAssertTrue(videoSetVideoOrientationCalled)
}

func testSetDeviceOrientation_doesNotSetOrientations_ifRecordingIsInProgress() {
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()

camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)

mockPhotoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() }
mockVideoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() }

camera.setDeviceOrientation(.landscapeLeft)
}

func testSetDeviceOrientation_doesNotSetOrientations_forDuplicateUpdates() {
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()
var photoSetVideoOrientationCallCount = 0
mockPhotoCaptureConnection.setVideoOrientationStub = { _ in
photoSetVideoOrientationCallCount += 1
}

var videoSetVideoOrientationCallCount = 0
mockVideoCaptureConnection.setVideoOrientationStub = { _ in
videoSetVideoOrientationCallCount += 1
}

camera.setDeviceOrientation(.landscapeRight)
camera.setDeviceOrientation(.landscapeRight)

XCTAssertEqual(photoSetVideoOrientationCallCount, 1)
XCTAssertEqual(videoSetVideoOrientationCallCount, 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, assign, getter=isVideoMirroringSupported) BOOL supportsVideoMirroring;
@property(nonatomic, assign, getter=isVideoOrientationSupported) BOOL supportsVideoOrientation;

// Stub that is called when the corresponding public method is called.
@property(nonatomic, copy) void (^setVideoOrientationStub)(AVCaptureVideoOrientation);

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@

@implementation MockCaptureConnection

- (void)setVideoOrientation:(AVCaptureVideoOrientation)videoOrientation {
if (self.setVideoOrientationStub) {
_setVideoOrientationStub(videoOrientation);
} else {
_videoOrientation = videoOrientation;
}
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
@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) AVCapturePhotoOutput *avOutput;
@property(nonatomic, strong) NSArray<AVVideoCodecType> *availablePhotoCodecTypes;
@property(nonatomic, assign) BOOL highResolutionCaptureEnabled;
@property(nonatomic, strong) NSArray<NSNumber *> *supportedFlashModes;
Expand All @@ -21,6 +21,10 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, copy) void (^capturePhotoWithSettingsStub)
(AVCapturePhotoSettings *, NSObject<AVCapturePhotoCaptureDelegate> *);

// Stub that is called when the corresponding public method is called.
@property(nonatomic, copy) NSObject<FLTCaptureConnection> * (^connectionWithMediaTypeStub)
(AVMediaType);

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ - (void)capturePhotoWithSettings:(AVCapturePhotoSettings *)settings
}
}

- (nullable AVCaptureConnection *)connectionWithMediaType:(nonnull AVMediaType)mediaType {
return nil;
- (nullable NSObject<FLTCaptureConnection> *)connectionWithMediaType:
(nonnull AVMediaType)mediaType {
if (self.connectionWithMediaTypeStub) {
return self.connectionWithMediaTypeStub(mediaType);
} else {
return NULL;
}
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.

/// Mock implementation of `FLTCaptureVideoDataOutput` protocol which allows injecting a custom
/// implementation.
class MockCaptureVideoDataOutput: NSObject, FLTCaptureVideoDataOutput {

var avOutput = AVCaptureVideoDataOutput()
var alwaysDiscardsLateVideoFrames = false
var videoSettings: [String: Any] = [:]

var connectionWithMediaTypeStub: ((AVMediaType) -> FLTCaptureConnection?)?

func connection(withMediaType mediaType: AVMediaType) -> FLTCaptureConnection? {
return connectionWithMediaTypeStub?(mediaType)
}

func setSampleBufferDelegate(
_ sampleBufferDelegate: AVCaptureVideoDataOutputSampleBufferDelegate?,
queue sampleBufferCallbackQueue: DispatchQueue?
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private class FakeMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
}

override func recommendedVideoSettingsForAssetWriter(
withFileType fileType: AVFileType, for output: AVCaptureVideoDataOutput
withFileType fileType: AVFileType, for output: FLTCaptureVideoDataOutput
) -> [String: Any]? {
return [:]
}
Expand Down Expand Up @@ -100,7 +100,7 @@ final class CameraSampleBufferTests: XCTestCase {
let captureSessionQueue = DispatchQueue(label: "testing")
let camera = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue)
XCTAssertEqual(
captureSessionQueue, camera.captureVideoOutput.sampleBufferCallbackQueue,
captureSessionQueue, camera.captureVideoOutput.avOutput.sampleBufferCallbackQueue,
"Sample buffer callback queue must be the capture session queue.")
}

Expand All @@ -110,7 +110,8 @@ final class CameraSampleBufferTests: XCTestCase {
let capturedPixelBuffer = CMSampleBufferGetImageBuffer(capturedSampleBuffer)!
// Mimic sample buffer callback when captured a new video sample.
camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: capturedSampleBuffer, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: capturedSampleBuffer,
from: connectionMock)
let deliveredPixelBuffer = camera.copyPixelBuffer()?.takeRetainedValue()
XCTAssertEqual(
deliveredPixelBuffer, capturedPixelBuffer,
Expand All @@ -129,7 +130,7 @@ final class CameraSampleBufferTests: XCTestCase {
camera.resumeVideoRecording()

camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: sampleBuffer, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: sampleBuffer, from: connectionMock)

let finalRetainCount = CFGetRetainCount(sampleBuffer)
XCTAssertEqual(
Expand Down Expand Up @@ -166,7 +167,7 @@ final class CameraSampleBufferTests: XCTestCase {
camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock)
camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock)
camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock)

let expectedSamples = ["video", "audio"]
Expand Down Expand Up @@ -207,17 +208,17 @@ final class CameraSampleBufferTests: XCTestCase {
camera.pauseVideoRecording()
camera.resumeVideoRecording()
camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock)
camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock)

XCTAssert(videoAppended && audioAppended, "Video or audio was not appended.")
}

func testDidOutputSampleBufferMustNotAppendSampleWhenReadyForMoreMediaDataIsFalse() {
let (camera, writerMock, adaptorMock, inputMock, connectionMock) = createCamera()
let (camera, _, adaptorMock, inputMock, connectionMock) = createCamera()

let videoSample = FLTCreateTestSampleBuffer().takeRetainedValue()

Expand All @@ -232,18 +233,18 @@ final class CameraSampleBufferTests: XCTestCase {
inputMock.readyForMoreMediaData = true
sampleAppended = false
camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
XCTAssertTrue(sampleAppended, "Sample was not appended.")

inputMock.readyForMoreMediaData = false
sampleAppended = false
camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
XCTAssertFalse(sampleAppended, "Sample cannot be appended when readyForMoreMediaData is NO.")
}

func testStopVideoRecordingWithCompletionMustCallCompletion() {
let (camera, writerMock, adaptorMock, inputMock, _) = createCamera()
let (camera, writerMock, _, _, _) = createCamera()

var status = AVAssetWriter.Status.unknown
writerMock.startWritingStub = {
Expand Down Expand Up @@ -291,13 +292,13 @@ final class CameraSampleBufferTests: XCTestCase {

let startWritingCalledBefore = startWritingCalled
camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
XCTAssert(
(startWritingCalledBefore && videoAppended) || (startWritingCalled && !videoAppended),
"The startWriting was called between sample creation and appending.")

camera.captureOutput(
camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock)
XCTAssert(videoAppended, "Video was not appended.")
}

Expand Down
Loading