Index: chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java |
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4ed9c7cbde5251c82acd5d8f46f651910898db02 |
--- /dev/null |
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java |
@@ -0,0 +1,440 @@ |
+// Copyright 2017 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.compositor; |
+ |
+import android.app.Activity; |
+import android.graphics.PixelFormat; |
+import android.view.Surface; |
+import android.view.SurfaceHolder; |
+import android.view.SurfaceView; |
+import android.widget.FrameLayout; |
+ |
+import static org.hamcrest.Matchers.lessThan; |
+import static org.junit.Assert.assertEquals; |
+import static org.junit.Assert.assertNotNull; |
+import static org.junit.Assert.assertThat; |
+import static org.junit.Assert.assertTrue; |
+import static org.mockito.ArgumentMatchers.anyInt; |
+import static org.mockito.ArgumentMatchers.eq; |
+import static org.mockito.Mockito.times; |
+import static org.mockito.Mockito.verify; |
+ |
+import org.junit.Before; |
+import org.junit.Test; |
+import org.junit.runner.RunWith; |
+import org.mockito.ArgumentMatchers; |
+import org.mockito.Mock; |
+import org.mockito.MockitoAnnotations; |
+import org.robolectric.Robolectric; |
+import org.robolectric.Shadows; |
+import org.robolectric.annotation.Config; |
+import org.robolectric.annotation.Implementation; |
+import org.robolectric.annotation.Implements; |
+import org.robolectric.shadows.ShadowLooper; |
+import org.robolectric.shadows.ShadowSurfaceView; |
+ |
+import org.chromium.base.test.util.Feature; |
+import org.chromium.testing.local.LocalRobolectricTestRunner; |
+ |
+import java.util.Set; |
+ |
+/** |
+ * Unit tests for the CompositorSurfaceManager. |
+ */ |
+@RunWith(LocalRobolectricTestRunner.class) |
+@Config(manifest = Config.NONE) |
+public class CompositorSurfaceManagerTest { |
+ @Mock |
+ private SurfaceHolder.Callback mCallback; |
+ |
+ private CompositorSurfaceManager mManager; |
+ |
+ private FrameLayout mLayout; |
+ |
+ /** |
+ * Implementation of a SurfaceView shadow that provides additional functionality for controlling |
+ * the state of the underlying (fake) Surface. |
+ */ |
+ @Implements(SurfaceView.class) |
+ public static class MyShadowSurfaceView extends ShadowSurfaceView { |
+ private final MyFakeSurfaceHolder mHolder = new MyFakeSurfaceHolder(); |
+ |
+ /** |
+ * Robolectric's FakeSurfaceHolder doesn't keep track of the format, etc. |
+ */ |
+ public static class MyFakeSurfaceHolder extends ShadowSurfaceView.FakeSurfaceHolder { |
+ /** |
+ * Fake surface that lets us control whether it's valid or not. |
+ */ |
+ public static class MyFakeSurface extends Surface { |
+ public boolean valid = false; |
+ |
+ @Override |
+ public boolean isValid() { |
+ return valid; |
+ } |
+ } |
+ |
+ private int mFormat = PixelFormat.UNKNOWN; |
+ private final MyFakeSurface mSurface = new MyFakeSurface(); |
+ |
+ @Implementation |
+ public void setFormat(int format) { |
+ mFormat = format; |
+ } |
+ |
+ public int getFormat() { |
+ return mFormat; |
+ } |
+ |
+ // Return a surface that we can control if it's valid or not. |
+ @Override |
+ public Surface getSurface() { |
+ return getFakeSurface(); |
+ } |
+ |
+ public MyFakeSurface getFakeSurface() { |
+ return mSurface; |
+ } |
+ } |
+ |
+ public MyShadowSurfaceView() {} |
+ |
+ @Implementation |
+ public SurfaceHolder getHolder() { |
+ return getMyFakeSurfaceHolder(); |
+ } |
+ |
+ @Override |
+ public FakeSurfaceHolder getFakeSurfaceHolder() { |
+ return getMyFakeSurfaceHolder(); |
+ } |
+ |
+ public MyFakeSurfaceHolder getMyFakeSurfaceHolder() { |
+ return mHolder; |
+ } |
+ } |
+ |
+ @Before |
+ public void beforeTest() { |
+ MockitoAnnotations.initMocks(this); |
+ Activity activity = Robolectric.buildActivity(Activity.class).setup().get(); |
+ mLayout = new FrameLayout(activity); |
+ mManager = new CompositorSurfaceManager(mLayout, mCallback); |
+ } |
+ |
+ private void runDelayedTasks() { |
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
+ } |
+ |
+ /** |
+ * Return the callback for |view|, or null. Will get mad if there's more than one. |
+ */ |
+ private SurfaceHolder.Callback callbackFor(SurfaceView view) { |
+ MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(view); |
+ ShadowSurfaceView.FakeSurfaceHolder viewHolder = viewShadow.getFakeSurfaceHolder(); |
+ Set<SurfaceHolder.Callback> callbacks = viewHolder.getCallbacks(); |
+ // Zero or one is okay. |
+ assertThat(callbacks.size(), lessThan(2)); |
+ |
+ if (callbacks.size() == 1) return callbacks.iterator().next(); |
+ |
+ return null; |
+ } |
+ |
+ private MyShadowSurfaceView.MyFakeSurfaceHolder fakeHolderFor(SurfaceView view) { |
+ MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(view); |
+ return viewShadow.getMyFakeSurfaceHolder(); |
+ } |
+ |
+ private void setSurfaceValid(SurfaceView view, boolean valid) { |
+ fakeHolderFor(view).getFakeSurface().valid = valid; |
+ } |
+ |
+ /** |
+ * Find and return the SurfaceView with format |format|. |
+ */ |
+ private SurfaceView findSurface(int format) { |
+ final int childCount = mLayout.getChildCount(); |
+ for (int i = 0; i < childCount; i++) { |
+ final SurfaceView child = (SurfaceView) mLayout.getChildAt(i); |
+ if (fakeHolderFor(child).getFormat() == format) return child; |
+ } |
+ |
+ return null; |
+ } |
+ |
+ /** |
+ * Request the pixel format |format|, and return the SurfaceView for it if it's attached. You |
+ * are responsible for sending surfaceCreated / Changed to |mManager| if you want it to think |
+ * that Android has provided the Surface. |
+ */ |
+ private SurfaceView requestSurface(int format) { |
+ mManager.requestSurface(format); |
+ runDelayedTasks(); |
+ |
+ return findSurface(format); |
+ } |
+ |
+ /** |
+ * Request format |format|, and send created / changed callbacks to |mManager| as if Android |
+ * had provided the underlying Surface. |
+ */ |
+ private SurfaceView requestThenCreateSurface(int format) { |
+ SurfaceView view = requestSurface(format); |
+ setSurfaceValid(view, true); |
+ callbackFor(view).surfaceCreated(view.getHolder()); |
+ final int actualFormat = |
+ (format == PixelFormat.OPAQUE) ? PixelFormat.RGB_565 : PixelFormat.RGBA_8888; |
+ final int width = 320; |
+ final int height = 240; |
+ callbackFor(view).surfaceChanged(view.getHolder(), actualFormat, width, height); |
+ |
+ return view; |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testRequestOpaqueSurface() { |
+ // Request a SurfaceView, and test in detail that it worked. |
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
+ verify(mCallback, times(0)).surfaceCreated(ArgumentMatchers.<SurfaceHolder>any()); |
+ verify(mCallback, times(0)) |
+ .surfaceChanged( |
+ ArgumentMatchers.<SurfaceHolder>any(), anyInt(), anyInt(), anyInt()); |
+ verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHolder>any()); |
+ |
+ // Check that there's an opaque SurfaceView . |
+ assertEquals(1, mLayout.getChildCount()); |
+ assertTrue(fakeHolderFor(opaque).getFormat() == PixelFormat.OPAQUE); |
+ |
+ // Verify that we are notified when the surface is created. |
+ callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHolder>any()); |
+ |
+ // Verify that we are notified when the surface is changed. |
+ final int format = PixelFormat.RGB_565; |
+ final int width = 320; |
+ final int height = 240; |
+ callbackFor(opaque).surfaceChanged(opaque.getHolder(), format, width, height); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceChanged(opaque.getHolder(), format, width, height); |
+ verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHolder>any()); |
+ |
+ // Verify that we are notified when the surface is destroyed. |
+ callbackFor(opaque).surfaceDestroyed(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceChanged(opaque.getHolder(), format, width, height); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testRequestOpaqueThenTranslucentSurface() { |
+ // Request opaque then translucent. |
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
+ SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCENT); |
+ |
+ // Verify that we received a destroy for |opaque| and created / changed for |translucent|. |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceCreated(translucent.getHolder()); |
+ verify(mCallback, times(1)) |
+ .surfaceChanged(eq(translucent.getHolder()), anyInt(), anyInt(), anyInt()); |
+ |
+ // Both views should be present. |
+ assertEquals(2, mLayout.getChildCount()); |
+ |
+ // Only the translucent surface should be left. Note that the old view is still valid. |
+ mManager.doneWithUnownedSurface(); |
+ runDelayedTasks(); |
+ assertEquals(1, mLayout.getChildCount()); |
+ assertNotNull(findSurface(PixelFormat.TRANSLUCENT)); |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testRequestSameSurface() { |
+ // Request an opaque surface, get it, then request it again. Verify that we get synthetic |
+ // create / destroy callbacks. |
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
+ |
+ // Surface is curerntly valid. Request again. We should get back a destroy and create. |
+ assertEquals(opaque, requestSurface(PixelFormat.OPAQUE)); |
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(2)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ assertEquals(1, mLayout.getChildCount()); |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testRequestSameSurfaceBeforeReady() { |
+ // Request an opaque surface, then request it again before the first one shows up. |
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(0)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
+ |
+ // Request again. We shouldn't get any callbacks, since the surface is still pending. |
+ assertEquals(opaque, requestSurface(PixelFormat.OPAQUE)); |
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(0)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
+ |
+ // Only the opaque view should be attached. |
+ assertEquals(1, mLayout.getChildCount()); |
+ |
+ // When the surface is created, we should get notified created / changed, but not destroyed. |
+ callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ |
+ callbackFor(opaque).surfaceChanged(opaque.getHolder(), PixelFormat.RGB_565, 320, 240); |
+ verify(mCallback, times(1)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testRequestDifferentSurfacesBeforeReady() { |
+ // Request an opaque surface, then request the translucent one before the it one shows up. |
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(0)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
+ |
+ // Request translucent. We should get no callbacks, but both views should be attached. |
+ SurfaceView translucent = requestSurface(PixelFormat.TRANSLUCENT); |
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(0)).surfaceCreated(translucent.getHolder()); |
+ assertEquals(2, mLayout.getChildCount()); |
+ |
+ // If the opaque surface arrives, we shouldn't hear about it. It should be detached, since |
+ // we've requested the other one. |
+ callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
+ runDelayedTasks(); |
+ assertEquals(1, mLayout.getChildCount()); |
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(0)).surfaceCreated(translucent.getHolder()); |
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
+ |
+ // When we create the translucent surface, we should be notified. |
+ callbackFor(translucent).surfaceCreated(translucent.getHolder()); |
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceCreated(translucent.getHolder()); |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testPendingSurfaceChangedCallback() { |
+ // Request an opaque surface, and request it again between 'created' and 'changed'. We |
+ // should get a synthetic 'created', but a real 'changed' callback. |
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
+ callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
+ runDelayedTasks(); |
+ |
+ // Sanity check. |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(0)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ |
+ // Re-request while 'changed' is still pending. We should get a synthetic 'destroyed' and |
+ // synthetic 'created'. |
+ assertEquals(opaque, requestSurface(PixelFormat.OPAQUE)); |
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ verify(mCallback, times(0)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ |
+ // Send 'changed', and expect that we'll receive it. |
+ callbackFor(opaque).surfaceChanged(opaque.getHolder(), PixelFormat.RGB_565, 320, 240); |
+ verify(mCallback, times(1)) |
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt()); |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testJellyBeanWorkaround() { |
+ // See if recreateSurfaceForJellyBean destroys / re-creates the surface. |
+ // should get a synthetic 'created', but a real 'changed' callback. |
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ assertEquals(1, mLayout.getChildCount()); |
+ |
+ // We should be notified that the surface was destroyed via synthetic callback, and the |
+ // surface should be detached. |
+ mManager.recreateSurfaceForJellyBean(); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ assertEquals(0, mLayout.getChildCount()); |
+ |
+ // When the surface really is destroyed, it should be re-attached. We should not be |
+ // notified again, though. |
+ callbackFor(opaque).surfaceDestroyed(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ assertEquals(1, mLayout.getChildCount()); |
+ |
+ // When the surface is re-created, we should be notified. |
+ callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ assertEquals(1, mLayout.getChildCount()); |
+ } |
+ |
+ @Test |
+ @Feature("Compositor") |
+ @Config(shadows = {MyShadowSurfaceView.class}) |
+ public void testRequestSurfaceDuringDestruction() { |
+ // If we re-request a surface while we're tearing it down, it should be re-attached and |
+ // given back to us once the destruction completes. |
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
+ SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCENT); |
+ mManager.doneWithUnownedSurface(); |
+ |
+ // The transparent surface should be attached, and the opaque one detached. |
+ assertEquals(1, mLayout.getChildCount()); |
+ assertNotNull(findSurface(PixelFormat.TRANSLUCENT)); |
+ |
+ // Re-request the opaque surface. Nothing should happen until it's destroyed. It should |
+ // not be re-attached, since that is also deferred until destruction. |
+ assertEquals(null, requestSurface(PixelFormat.OPAQUE)); |
+ assertEquals(1, mLayout.getChildCount()); |
+ assertNotNull(findSurface(PixelFormat.TRANSLUCENT)); |
+ |
+ // When the opaque surface is destroyed, then it should be re-attached. No callbacks shoud |
+ // have arrived yet, except for initial creation and (synthetic) destroyed when we got the |
+ // translucent surface. |
+ callbackFor(opaque).surfaceDestroyed(opaque.getHolder()); |
+ assertEquals(2, mLayout.getChildCount()); |
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ verify(mCallback, times(0)).surfaceDestroyed(translucent.getHolder()); |
+ |
+ // When the opaque surface becomes available, we'll get the synthetic destroy for the |
+ // translucent one that we lost ownership of, and the real create for the opaque one. |
+ callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
+ assertEquals(2, mLayout.getChildCount()); |
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
+ verify(mCallback, times(1)).surfaceDestroyed(translucent.getHolder()); |
+ } |
+} |