From bd6d8cdaa5d9f8e6fc1425fb5ef54e6b9d6f8883 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Fri, 8 Sep 2017 18:43:17 +0200 Subject: [PATCH] CameraCallbacks tests --- .../otaliastudios/cameraview/BaseTest.java | 37 +++ .../cameraview/CameraCallbacksTest.java | 301 ++++++++++++++++++ .../cameraview/CameraViewTest.java | 2 +- .../cameraview/CropHelperTest.java | 50 +-- .../otaliastudios/cameraview/MockPreview.java | 5 + .../cameraview/PinchGestureLayoutTest.java | 22 +- .../otaliastudios/cameraview/PreviewTest.java | 4 +- .../cameraview/ScrollGestureLayoutTest.java | 9 +- .../cameraview/TapGestureLayoutTest.java | 10 +- .../otaliastudios/cameraview/CameraView.java | 2 +- .../com/otaliastudios/cameraview/Task.java | 16 +- 11 files changed, 375 insertions(+), 83 deletions(-) create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java index 7f505479..bcaa0f69 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java @@ -2,6 +2,9 @@ package com.otaliastudios.cameraview; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.YuvImage; import android.os.Handler; import android.os.Looper; import android.support.test.InstrumentationRegistry; @@ -11,6 +14,16 @@ import android.view.View; import org.junit.Before; import org.junit.Rule; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class BaseTest { @@ -38,4 +51,28 @@ public class BaseTest { public static void waitUi() { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } + + + public static byte[] mockJpeg(int width, int height) { + Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + source.compress(Bitmap.CompressFormat.JPEG, 100, os); + return os.toByteArray(); + } + + public static YuvImage mockYuv(int width, int height) { + YuvImage y = mock(YuvImage.class); + when(y.getWidth()).thenReturn(width); + when(y.getHeight()).thenReturn(height); + when(y.compressToJpeg(any(Rect.class), anyInt(), any(OutputStream.class))).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + Rect rect = (Rect) invocation.getArguments()[0]; + OutputStream stream = (OutputStream) invocation.getArguments()[2]; + stream.write(mockJpeg(rect.width(), rect.height())); + return true; + } + }); + return y; + } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java new file mode 100644 index 00000000..dc54aa04 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java @@ -0,0 +1,301 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.YuvImage; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.ViewGroup; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import static junit.framework.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class CameraCallbacksTest extends BaseTest { + + private CameraView camera; + private CameraView.CameraCallbacks callbacks; + private CameraListener listener; + private MockCameraController mockController; + private MockPreview mockPreview; + private Task task; + + + @Before + public void setUp() { + ui(new Runnable() { + @Override + public void run() { + Context context = context(); + listener = mock(CameraListener.class); + camera = new CameraView(context) { + @Override + protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { + mockController = new MockCameraController(callbacks, preview); + return mockController; + } + + @Override + protected Preview instantiatePreview(Context context, ViewGroup container) { + mockPreview = new MockPreview(context, container); + return mockPreview; + } + + @Override + protected boolean checkPermissions(SessionType sessionType) { + return true; + } + }; + camera.addCameraListener(listener); + callbacks = camera.mCameraCallbacks; + task = new Task<>(); + task.listen(); + } + }); + } + + @After + public void tearDown() { + camera.removeCameraListener(listener); + camera = null; + mockController = null; + mockPreview = null; + callbacks = null; + listener = null; + } + + // Completes our task. + private Answer completeTask() { + return new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + task.end(true); + return null; + } + }; + } + + @Test + public void testDontDispatchIfRemoved() { + camera.removeCameraListener(listener); + doAnswer(completeTask()).when(listener).onCameraOpened(null); + callbacks.dispatchOnCameraOpened(null); + + assertNull(task.await(200)); + verify(listener, never()).onCameraOpened(null); + } + + @Test + public void testDontDispatchIfCleared() { + camera.clearCameraListeners(); + doAnswer(completeTask()).when(listener).onCameraOpened(null); + callbacks.dispatchOnCameraOpened(null); + + assertNull(task.await(200)); + verify(listener, never()).onCameraOpened(null); + } + + @Test + public void testDispatchOnCameraOpened() { + doAnswer(completeTask()).when(listener).onCameraOpened(null); + callbacks.dispatchOnCameraOpened(null); + + assertNotNull(task.await(200)); + verify(listener, times(1)).onCameraOpened(null); + } + + @Test + public void testDispatchOnCameraClosed() { + doAnswer(completeTask()).when(listener).onCameraClosed(); + callbacks.dispatchOnCameraClosed(); + + assertNotNull(task.await(200)); + verify(listener, times(1)).onCameraClosed(); + } + + @Test + public void testDispatchOnVideoTaken() { + doAnswer(completeTask()).when(listener).onVideoTaken(null); + callbacks.dispatchOnVideoTaken(null); + + assertNotNull(task.await(200)); + verify(listener, times(1)).onVideoTaken(null); + } + + @Test + public void testDispatchOnZoomChanged() { + doAnswer(completeTask()).when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); + callbacks.dispatchOnZoomChanged(0f, null); + + assertNotNull(task.await(200)); + verify(listener, times(1)).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); + } + + @Test + public void testDispatchOnExposureCorrectionChanged() { + doAnswer(completeTask()).when(listener).onExposureCorrectionChanged(0f, null, null); + callbacks.dispatchOnExposureCorrectionChanged(0f, null, null); + + assertNotNull(task.await(200)); + verify(listener, times(1)).onExposureCorrectionChanged(0f, null, null); + } + + @Test + public void testDispatchOnFocusStart() { + // Enable tap gesture. + // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); + camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); + + PointF point = new PointF(); + doAnswer(completeTask()).when(listener).onFocusStart(point); + callbacks.dispatchOnFocusStart(Gesture.TAP, point); + + assertNotNull(task.await(200)); + verify(listener, times(1)).onFocusStart(point); + // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusStart(point); + } + + @Test + public void testDispatchOnFocusEnd() { + // Enable tap gesture. + // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); + camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); + + PointF point = new PointF(); + boolean success = true; + doAnswer(completeTask()).when(listener).onFocusEnd(success, point); + callbacks.dispatchOnFocusEnd(Gesture.TAP, success, point); + + assertNotNull(task.await(200)); + verify(listener, times(1)).onFocusEnd(success, point); + // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusEnd(success); + } + + @Test + public void testOrientationCallbacks_deviceOnly() { + doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); + + // Assert not called. Both methods must be called. + callbacks.onDeviceOrientationChanged(0); + assertNull(task.await(200)); + verify(listener, never()).onOrientationChanged(anyInt()); + } + + @Test + public void testOrientationCallbacks_displayOnly() { + doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); + + // Assert not called. Both methods must be called. + callbacks.onDisplayOffsetChanged(0); + assertNull(task.await(200)); + verify(listener, never()).onOrientationChanged(anyInt()); + } + + @Test + public void testOrientationCallbacks_both() { + doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); + + // Assert called. + callbacks.onDisplayOffsetChanged(0); + callbacks.onDeviceOrientationChanged(90); + assertNotNull(task.await(200)); + verify(listener, times(1)).onOrientationChanged(anyInt()); + } + + + @Test + public void testProcessJpeg() { + int[] viewDim = new int[]{ 200, 200 }; + int[] imageDim = new int[]{ 1000, 1600 }; + + // With crop flag: expect a 1:1 ratio. + int[] output = testProcessImage(true, true, viewDim, imageDim); + assertEquals(output[0], 1000); + assertEquals(output[1], 1000); + + // Without crop flag: expect original ratio. + output = testProcessImage(true, false, viewDim, imageDim); + assertEquals(output[0], imageDim[0]); + assertEquals(output[1], imageDim[1]); + } + + @Test + public void testProcessYuv() { + int[] viewDim = new int[]{ 200, 200 }; + int[] imageDim = new int[]{ 1000, 1600 }; + + // With crop flag: expect a 1:1 ratio. + int[] output = testProcessImage(false, true, viewDim, imageDim); + assertEquals(output[0], 1000); + assertEquals(output[1], 1000); + + // Without crop flag: expect original ratio. + output = testProcessImage(false, false, viewDim, imageDim); + assertEquals(output[0], imageDim[0]); + assertEquals(output[1], imageDim[1]); + } + + private int[] testProcessImage(boolean jpeg, boolean crop, int[] viewDim, int[] imageDim) { + // End our task when onPictureTaken is called. Take note of the result. + final Task jpegTask = new Task<>(); + jpegTask.listen(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + jpegTask.end((byte[]) invocation.getArguments()[0]); + return null; + } + }).when(listener).onPictureTaken(any(byte[].class)); + + // Fake our own dimensions. + camera.setTop(0); + camera.setBottom(viewDim[1]); + camera.setLeft(0); + camera.setRight(viewDim[0]); + + // Ensure the image will (not) be cropped. + camera.setCropOutput(crop); + mockPreview.setIsCropping(crop); + + // Create fake JPEG array and trigger the process. + if (jpeg) { + callbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false); + } else { + callbacks.processSnapshot(mockYuv(imageDim[0], imageDim[1]), true, false); + } + + // Wait for result and get out dimensions. + byte[] result = jpegTask.await(800); + assertNotNull(result); + Bitmap bitmap = BitmapFactory.decodeByteArray(result, 0, result.length); + return new int[]{ bitmap.getWidth(), bitmap.getHeight() }; + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 62f75880..283581ad 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -40,7 +40,7 @@ public class CameraViewTest extends BaseTest { ui(new Runnable() { @Override public void run() { - Context context = InstrumentationRegistry.getContext(); + Context context = context(); cameraView = new CameraView(context) { @Override protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java index 993f3bd9..3ca017b8 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java @@ -30,7 +30,7 @@ import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @SmallTest -public class CropHelperTest { +public class CropHelperTest extends BaseTest { @Test public void testCropFromYuv() { @@ -38,46 +38,30 @@ public class CropHelperTest { testCropFromYuv(1600, 1600, AspectRatio.of(9, 16)); } - private void testCropFromYuv(final int w, final int h, final AspectRatio target) { - final boolean wider = target.toFloat() > ((float) w / (float) h); - - // Not sure how to test YuvImages... - YuvImage i = mock(YuvImage.class); - when(i.getWidth()).thenReturn(w); - when(i.getHeight()).thenReturn(h); - when(i.compressToJpeg(any(Rect.class), anyInt(), any(OutputStream.class))).thenAnswer(new Answer() { - @Override - public Boolean answer(InvocationOnMock iom) throws Throwable { - Object[] args = iom.getArguments(); - Rect rect = (Rect) args[0]; - - // Assert. - AspectRatio ratio = AspectRatio.of(rect.width(), rect.height()); - assertEquals(target, ratio); - if (wider) { // width must match. - assertEquals(rect.width(), w); - } else { - assertEquals(rect.height(), h); - } - return true; - } - }); - CropHelper.cropToJpeg(i, target, 100); - } - @Test public void testCropFromJpeg() { testCropFromJpeg(1600, 1600, AspectRatio.of(16, 9)); testCropFromJpeg(1600, 1600, AspectRatio.of(9, 16)); } - private void testCropFromJpeg(int w, int h, AspectRatio target) { + private void testCropFromYuv(final int w, final int h, final AspectRatio target) { final boolean wider = target.toFloat() > ((float) w / (float) h); + byte[] b = CropHelper.cropToJpeg(mockYuv(w, h), target, 100); + Bitmap result = BitmapFactory.decodeByteArray(b, 0, b.length); - Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - source.compress(Bitmap.CompressFormat.JPEG, 100, os); - byte[] b = CropHelper.cropToJpeg(os.toByteArray(), target, 100); + // Assert. + AspectRatio ratio = AspectRatio.of(result.getWidth(), result.getHeight()); + assertEquals(target, ratio); + if (wider) { // width must match. + assertEquals(result.getWidth(), w); + } else { + assertEquals(result.getHeight(), h); + } + } + + private void testCropFromJpeg(int w, int h, AspectRatio target) { + final boolean wider = target.toFloat() > ((float) w / (float) h); + byte[] b = CropHelper.cropToJpeg(mockJpeg(w, h), target, 100); Bitmap result = BitmapFactory.decodeByteArray(b, 0, b.length); // Assert. diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java index e011c617..514fbd21 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java @@ -13,6 +13,11 @@ public class MockPreview extends Preview { super(context, parent, null); } + public void setIsCropping(boolean crop) { + getView().setScaleX(crop ? 2 : 1); + getView().setScaleY(crop ? 2 : 1); + } + @NonNull @Override protected View onCreateView(Context context, ViewGroup parent) { diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java index d1af6e1d..8205d356 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java @@ -2,33 +2,13 @@ package com.otaliastudios.cameraview; import android.content.Context; -import android.support.test.espresso.Espresso; -import android.support.test.espresso.UiController; import android.support.test.espresso.ViewAction; -import android.support.test.espresso.action.CoordinatesProvider; -import android.support.test.espresso.action.GeneralLocation; -import android.support.test.espresso.action.GeneralSwipeAction; -import android.support.test.espresso.action.MotionEvents; -import android.support.test.espresso.action.PrecisionDescriber; -import android.support.test.espresso.action.Press; -import android.support.test.espresso.action.Swipe; -import android.support.test.espresso.action.ViewActions; -import android.support.test.espresso.assertion.ViewAssertions; -import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; -import android.view.View; -import org.hamcrest.Matcher; -import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.concurrent.TimeUnit; - -import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -69,7 +49,7 @@ public class PinchGestureLayoutTest extends GestureLayoutTest { Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER, InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); onLayout().perform(a); - Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + Gesture found = touch.await(500); assertEquals(found, Gesture.TAP); Size size = rule.getActivity().getContentSize(); @@ -60,7 +56,7 @@ public class TapGestureLayoutTest extends GestureLayoutTest { touch.listen(); touch.start(); onLayout().perform(click()); - Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + Gesture found = touch.await(500); assertNull(found); } @@ -72,7 +68,7 @@ public class TapGestureLayoutTest extends GestureLayoutTest { Tap.LONG, GeneralLocation.CENTER, Press.FINGER, InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); onLayout().perform(a); - Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + Gesture found = touch.await(500); assertEquals(found, Gesture.LONG_TAP); Size size = rule.getActivity().getContentSize(); assertEquals(layout.getPoints()[0].x, (size.getWidth() / 2f), 1f); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index e6c22a26..ae89b7df 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -59,7 +59,7 @@ public class CameraView extends FrameLayout { private HashMap mGestureMap = new HashMap<>(4); // Components - private CameraCallbacks mCameraCallbacks; + CameraCallbacks mCameraCallbacks; private OrientationHelper mOrientationHelper; private CameraController mCameraController; private Preview mPreviewImpl; diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java index 16ef3150..aa2a92e0 100644 --- a/cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java +++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java @@ -44,19 +44,15 @@ class Task { } } + T await(long millis) { + return await(millis, TimeUnit.MILLISECONDS); + } + T await() { - try { - mLatch.await(); - } catch (Exception e) { - e.printStackTrace(); - } - T result = mResult; - mResult = null; - mLatch = null; - return result; + return await(1, TimeUnit.MINUTES); } - T await(long time, @NonNull TimeUnit unit) { + private T await(long time, @NonNull TimeUnit unit) { try { mLatch.await(time, unit); } catch (Exception e) {