From 7ba0fddef0de5d87e5bdd93787ef7114726ca247 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 12 Jun 2024 13:53:11 -0700 Subject: [PATCH 1/7] Refactor FlutterRendererTest, prepare for ActivityScenarioRule. --- .../engine/renderer/FlutterEngineRule.java | 86 ++++++++ .../engine/renderer/FlutterRendererTest.java | 191 +++++++++--------- .../test_runner/src/main/AndroidManifest.xml | 2 + 3 files changed, 187 insertions(+), 92 deletions(-) create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java new file mode 100644 index 0000000000000..d3a8beb57348d --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java @@ -0,0 +1,86 @@ + +package io.flutter.embedding.engine.renderer; + +import android.content.Context; +import android.content.Intent; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterLoader; + +/** + * Prepares and returns a {@link FlutterEngine} and {@link Intent} primed with an engine for tests. + */ +public final class FlutterEngineRule extends TestWatcher { + private final static Context ctx = ApplicationProvider.getApplicationContext(); + private final static String cachedEngineId = "flutter_engine_rule_cached_engine"; + private FlutterJNI flutterJNI; + private FlutterEngine flutterEngine; + private boolean jniIsAttached = true; + + @Override + protected void starting(Description description) { + // Setup mock JNI. + flutterJNI = mock(FlutterJNI.class); + when(flutterJNI.isAttached()).thenAnswer(i -> jniIsAttached); + + // We will not try to load plugins in these tests. + FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); + when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(false); + + // Create an engine. + flutterEngine = new FlutterEngine(ctx, mockFlutterLoader, flutterJNI); + + // Place it in the engine cache. + FlutterEngineCache.getInstance().put(cachedEngineId, flutterEngine); + } + + @Override + protected void finished(Description description) { + FlutterEngineCache.getInstance().clear(); + } + + /** + * Returns a Mockito-mocked version of {@link FlutterJNI}. + * + * @return an instance that is already considered attached. + */ + FlutterJNI getFlutterJNI() { + return this.flutterJNI; + } + + /** + * Returns a pre-configured engine. + * + * @return flutter engine using the mock provided by {{@link #getFlutterJNI()}}. + */ + FlutterEngine getFlutterEngine() { + return this.flutterEngine; + } + + /** + * Sets what {@link FlutterJNI#isAttached()} returns. If not invoked, defaults to true. + * @param isAttached whether to consider JNI attached. + */ + void setJniIsAttached(boolean isAttached) { + this.jniIsAttached = isAttached; + } + + /** + * Creates an intent with {@link FlutterEngine} instance already provided. + * @return intent, i.e. to use with {@link androidx.test.ext.junit.rules.ActivityScenarioRule}. + */ + Intent makeIntent() { + return FlutterActivity.withCachedEngine(cachedEngineId).build(ctx); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index f19da788d3759..5938180c916e3 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -9,12 +9,13 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verifyNoInteractions; import static org.robolectric.Shadows.shadowOf; import android.graphics.Canvas; @@ -23,12 +24,18 @@ import android.media.Image; import android.os.Looper; import android.view.Surface; + +import androidx.lifecycle.Lifecycle; +import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; + +import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.view.TextureRegistry; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -37,10 +44,13 @@ @Config(manifest = Config.NONE) @RunWith(AndroidJUnit4.class) public class FlutterRendererTest { + @Rule(order = 1) + public final FlutterEngineRule engineRule = new FlutterEngineRule(); + + @Rule(order = 2) + public final ActivityScenarioRule scenarioRule = new ActivityScenarioRule<>(engineRule.makeIntent()); private FlutterJNI fakeFlutterJNI; - private Surface fakeSurface; - private Surface fakeSurface2; @Before public void init() { @@ -50,16 +60,14 @@ public void init() { @Before public void setup() { - fakeFlutterJNI = mock(FlutterJNI.class); - fakeSurface = mock(Surface.class); - fakeSurface2 = mock(Surface.class); + fakeFlutterJNI = engineRule.getFlutterJNI(); } @Test public void itForwardsSurfaceCreationNotificationToFlutterJNI() { // Setup the test. Surface fakeSurface = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); // Execute the behavior under test. flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -72,7 +80,7 @@ public void itForwardsSurfaceCreationNotificationToFlutterJNI() { public void itForwardsSurfaceChangeNotificationToFlutterJNI() { // Setup the test. Surface fakeSurface = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -87,7 +95,7 @@ public void itForwardsSurfaceChangeNotificationToFlutterJNI() { public void itForwardsSurfaceDestructionNotificationToFlutterJNI() { // Setup the test. Surface fakeSurface = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -101,8 +109,9 @@ public void itForwardsSurfaceDestructionNotificationToFlutterJNI() { @Test public void itStopsRenderingToOneSurfaceBeforeRenderingToANewSurface() { // Setup the test. + Surface fakeSurface = mock(Surface.class); Surface fakeSurface2 = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -116,7 +125,8 @@ public void itStopsRenderingToOneSurfaceBeforeRenderingToANewSurface() { @Test public void itStopsRenderingToSurfaceWhenRequested() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -130,10 +140,10 @@ public void itStopsRenderingToSurfaceWhenRequested() { @Test public void iStopsRenderingToSurfaceWhenSurfaceAlreadySet() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, false); // Verify behavior under test. @@ -143,10 +153,10 @@ public void iStopsRenderingToSurfaceWhenSurfaceAlreadySet() { @Test public void itNeverStopsRenderingToSurfaceWhenRequested() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, true); // Verify behavior under test. @@ -156,13 +166,11 @@ public void itNeverStopsRenderingToSurfaceWhenRequested() { @Test public void itStopsSurfaceTextureCallbackWhenDetached() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); - flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. @@ -175,9 +183,8 @@ public void itStopsSurfaceTextureCallbackWhenDetached() { @Test public void itRegistersExistingSurfaceTexture() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); SurfaceTexture surfaceTexture = new SurfaceTexture(0); @@ -197,11 +204,8 @@ public void itRegistersExistingSurfaceTexture() { @Test public void itUnregistersTextureWhenSurfaceTextureFinalized() { // Setup the test. - FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); - when(fakeFlutterJNI.isAttached()).thenReturn(true); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -223,18 +227,15 @@ public void itUnregistersTextureWhenSurfaceTextureFinalized() { @Test public void itStopsUnregisteringTextureWhenDetached() { // Setup the test. - FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); - when(fakeFlutterJNI.isAttached()).thenReturn(false); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + engineRule.setJniIsAttached(false); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); long id = entry.id(); flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.stopRenderingToSurface(); // Execute the behavior under test. @@ -246,20 +247,19 @@ public void itStopsUnregisteringTextureWhenDetached() { verify(fakeFlutterJNI, times(0)).unregisterTexture(eq(id)); } + /** @noinspection FinalizeCalledExplicitly*/ void runFinalization(FlutterRenderer.SurfaceTextureRegistryEntry entry) { CountDownLatch latch = new CountDownLatch(1); Thread fakeFinalizer = new Thread( - new Runnable() { - public void run() { - try { - entry.finalize(); - latch.countDown(); - } catch (Throwable e) { - // do nothing - } - } - }); + () -> { + try { + entry.finalize(); + latch.countDown(); + } catch (Throwable e) { + // do nothing + } + }); fakeFinalizer.start(); try { latch.await(); @@ -271,7 +271,7 @@ public void run() { @Test public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.ViewportMetrics metrics = new FlutterRenderer.ViewportMetrics(); metrics.width = 1000; metrics.height = 1000; @@ -286,6 +286,7 @@ public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { new Rect(50, 60, 70, 80), FlutterRenderer.DisplayFeatureType.CUTOUT)); // Execute the behavior under test. + clearInvocations(fakeFlutterJNI); flutterRenderer.setViewportMetrics(metrics); // Verify behavior under test. @@ -332,16 +333,11 @@ public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { @Test public void itNotifyImageFrameListener() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); AtomicInteger invocationCount = new AtomicInteger(0); final TextureRegistry.OnFrameConsumedListener listener = - new TextureRegistry.OnFrameConsumedListener() { - @Override - public void onFrameConsumed() { - invocationCount.incrementAndGet(); - } - }; + invocationCount::incrementAndGet; FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -357,7 +353,7 @@ public void onFrameConsumed() { @Test public void itAddsListenerWhenSurfaceTextureEntryCreated() { // Setup the test. - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(fakeFlutterJNI)); + FlutterRenderer flutterRenderer = spy(engineRule.getFlutterEngine().getRenderer()); // Execute the behavior under test. FlutterRenderer.SurfaceTextureRegistryEntry entry = @@ -370,7 +366,7 @@ public void itAddsListenerWhenSurfaceTextureEntryCreated() { @Test public void itRemovesListenerWhenSurfaceTextureEntryReleased() { // Setup the test. - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(fakeFlutterJNI)); + FlutterRenderer flutterRenderer = spy(engineRule.getFlutterEngine().getRenderer()); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -384,16 +380,11 @@ public void itRemovesListenerWhenSurfaceTextureEntryReleased() { @Test public void itNotifySurfaceTextureEntryWhenMemoryPressureWarning() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); AtomicInteger invocationCount = new AtomicInteger(0); final TextureRegistry.OnTrimMemoryListener listener = - new TextureRegistry.OnTrimMemoryListener() { - @Override - public void onTrimMemory(int level) { - invocationCount.incrementAndGet(); - } - }; + level -> invocationCount.incrementAndGet(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -409,7 +400,8 @@ public void onTrimMemory(int level) { @Test public void itDoesDispatchSurfaceDestructionNotificationOnlyOnce() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -424,7 +416,8 @@ public void itDoesDispatchSurfaceDestructionNotificationOnlyOnce() { @Test public void itInvokesCreatesSurfaceWhenStartingRendering() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface)); @@ -432,7 +425,9 @@ public void itInvokesCreatesSurfaceWhenStartingRendering() { @Test public void itDoesNotInvokeCreatesSurfaceWhenResumingRendering() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + Surface fakeSurface2 = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); // The following call sequence mimics the behaviour of FlutterView when it exits from hybrid // composition mode. @@ -458,9 +453,9 @@ public void itDoesNotInvokeCreatesSurfaceWhenResumingRendering() { @Test public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); + FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -481,6 +476,7 @@ public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { // Extract the image and check its size. Image image = texture.acquireLatestImage(); + assert image != null; assertEquals(1, image.getWidth()); assertEquals(1, image.getHeight()); image.close(); @@ -500,6 +496,7 @@ public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { // Extract the image and check its size. image = texture.acquireLatestImage(); + assert image != null; assertEquals(5, image.getWidth()); assertEquals(5, image.getHeight()); image.close(); @@ -510,10 +507,10 @@ public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { } @Test - public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInflight() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInFlight() { + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); + FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -541,9 +538,9 @@ public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInflight() { @Test public void ImageReaderSurfaceProducerImageReadersAndImagesCount() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); + FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -623,9 +620,10 @@ public void ImageReaderSurfaceProducerImageReadersAndImagesCount() { @Test public void ImageReaderSurfaceProducerTrimMemoryCallback() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); + FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; + texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -687,37 +685,46 @@ public void ImageReaderSurfaceProducerTrimMemoryCallback() { // A 0x0 ImageReader is a runtime error. @Test public void ImageReaderSurfaceProducerClampsWidthAndHeightTo1() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); // Default values. - assertEquals(texture.getWidth(), 1); - assertEquals(texture.getHeight(), 1); + assertEquals(producer.getWidth(), 1); + assertEquals(producer.getHeight(), 1); // Try setting width and height to 0. - texture.setSize(0, 0); + producer.setSize(0, 0); // Ensure we can still create/get a surface without an exception being raised. - assertNotNull(texture.getSurface()); + assertNotNull(producer.getSurface()); // Expect clamp to 1. - assertEquals(texture.getWidth(), 1); - assertEquals(texture.getHeight(), 1); + assertEquals(producer.getWidth(), 1); + assertEquals(producer.getHeight(), 1); } @Test public void SurfaceTextureSurfaceProducerCreatesAConnectedTexture() { // Force creating a SurfaceTextureSurfaceProducer regardless of Android API version. - FlutterRenderer.debugForceSurfaceProducerGlTextures = true; + Surface fakeSurface = mock(Surface.class); + try { + FlutterRenderer.debugForceSurfaceProducerGlTextures = true; + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); + flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, false); + // Verify behavior under test. + assertEquals(producer.id(), 0); + verify(fakeFlutterJNI, times(1)).registerTexture(eq(producer.id()), any()); + } finally { + FlutterRenderer.debugForceSurfaceProducerGlTextures = false; + } + } - // Verify behavior under test. - assertEquals(producer.id(), 0); - verify(fakeFlutterJNI, times(1)).registerTexture(eq(producer.id()), any()); + @Test + public void CanLaunchActivityUsingFlutterEngine() { + // This is a placeholder test that will be used to test lifecycle events w/ SurfaceProducer. + scenarioRule.getScenario().moveToState(Lifecycle.State.RESUMED); } } diff --git a/shell/platform/android/test_runner/src/main/AndroidManifest.xml b/shell/platform/android/test_runner/src/main/AndroidManifest.xml index 57bdeee858695..65ca9afc77113 100644 --- a/shell/platform/android/test_runner/src/main/AndroidManifest.xml +++ b/shell/platform/android/test_runner/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + From 4dbf65732421d739751a5e618301465163ea9fc0 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 12 Jun 2024 13:55:04 -0700 Subject: [PATCH 2/7] ++ --- .../engine/renderer/FlutterEngineRule.java | 138 +++++++++--------- .../engine/renderer/FlutterRendererTest.java | 41 +++--- 2 files changed, 89 insertions(+), 90 deletions(-) diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java index d3a8beb57348d..51314b8898d58 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java @@ -1,86 +1,84 @@ - package io.flutter.embedding.engine.renderer; -import android.content.Context; -import android.content.Intent; - -import androidx.test.core.app.ApplicationProvider; - -import org.junit.rules.TestWatcher; -import org.junit.runner.Description; - import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.content.Context; +import android.content.Intent; +import androidx.test.core.app.ApplicationProvider; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.loader.FlutterLoader; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; /** * Prepares and returns a {@link FlutterEngine} and {@link Intent} primed with an engine for tests. */ public final class FlutterEngineRule extends TestWatcher { - private final static Context ctx = ApplicationProvider.getApplicationContext(); - private final static String cachedEngineId = "flutter_engine_rule_cached_engine"; - private FlutterJNI flutterJNI; - private FlutterEngine flutterEngine; - private boolean jniIsAttached = true; - - @Override - protected void starting(Description description) { - // Setup mock JNI. - flutterJNI = mock(FlutterJNI.class); - when(flutterJNI.isAttached()).thenAnswer(i -> jniIsAttached); - - // We will not try to load plugins in these tests. - FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); - when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(false); - - // Create an engine. - flutterEngine = new FlutterEngine(ctx, mockFlutterLoader, flutterJNI); - - // Place it in the engine cache. - FlutterEngineCache.getInstance().put(cachedEngineId, flutterEngine); - } - - @Override - protected void finished(Description description) { - FlutterEngineCache.getInstance().clear(); - } - - /** - * Returns a Mockito-mocked version of {@link FlutterJNI}. - * - * @return an instance that is already considered attached. - */ - FlutterJNI getFlutterJNI() { - return this.flutterJNI; - } - - /** - * Returns a pre-configured engine. - * - * @return flutter engine using the mock provided by {{@link #getFlutterJNI()}}. - */ - FlutterEngine getFlutterEngine() { - return this.flutterEngine; - } - - /** - * Sets what {@link FlutterJNI#isAttached()} returns. If not invoked, defaults to true. - * @param isAttached whether to consider JNI attached. - */ - void setJniIsAttached(boolean isAttached) { - this.jniIsAttached = isAttached; - } - - /** - * Creates an intent with {@link FlutterEngine} instance already provided. - * @return intent, i.e. to use with {@link androidx.test.ext.junit.rules.ActivityScenarioRule}. - */ - Intent makeIntent() { - return FlutterActivity.withCachedEngine(cachedEngineId).build(ctx); - } + private static final Context ctx = ApplicationProvider.getApplicationContext(); + private static final String cachedEngineId = "flutter_engine_rule_cached_engine"; + private FlutterJNI flutterJNI; + private FlutterEngine flutterEngine; + private boolean jniIsAttached = true; + + @Override + protected void starting(Description description) { + // Setup mock JNI. + flutterJNI = mock(FlutterJNI.class); + when(flutterJNI.isAttached()).thenAnswer(i -> jniIsAttached); + + // We will not try to load plugins in these tests. + FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); + when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(false); + + // Create an engine. + flutterEngine = new FlutterEngine(ctx, mockFlutterLoader, flutterJNI); + + // Place it in the engine cache. + FlutterEngineCache.getInstance().put(cachedEngineId, flutterEngine); + } + + @Override + protected void finished(Description description) { + FlutterEngineCache.getInstance().clear(); + } + + /** + * Returns a Mockito-mocked version of {@link FlutterJNI}. + * + * @return an instance that is already considered attached. + */ + FlutterJNI getFlutterJNI() { + return this.flutterJNI; + } + + /** + * Returns a pre-configured engine. + * + * @return flutter engine using the mock provided by {{@link #getFlutterJNI()}}. + */ + FlutterEngine getFlutterEngine() { + return this.flutterEngine; + } + + /** + * Sets what {@link FlutterJNI#isAttached()} returns. If not invoked, defaults to true. + * + * @param isAttached whether to consider JNI attached. + */ + void setJniIsAttached(boolean isAttached) { + this.jniIsAttached = isAttached; + } + + /** + * Creates an intent with {@link FlutterEngine} instance already provided. + * + * @return intent, i.e. to use with {@link androidx.test.ext.junit.rules.ActivityScenarioRule}. + */ + Intent makeIntent() { + return FlutterActivity.withCachedEngine(cachedEngineId).build(ctx); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index 5938180c916e3..0357293a1d303 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -15,7 +15,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.robolectric.Shadows.shadowOf; import android.graphics.Canvas; @@ -24,11 +23,9 @@ import android.media.Image; import android.os.Looper; import android.view.Surface; - import androidx.lifecycle.Lifecycle; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; - import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.view.TextureRegistry; @@ -48,7 +45,8 @@ public class FlutterRendererTest { public final FlutterEngineRule engineRule = new FlutterEngineRule(); @Rule(order = 2) - public final ActivityScenarioRule scenarioRule = new ActivityScenarioRule<>(engineRule.makeIntent()); + public final ActivityScenarioRule scenarioRule = + new ActivityScenarioRule<>(engineRule.makeIntent()); private FlutterJNI fakeFlutterJNI; @@ -247,19 +245,19 @@ public void itStopsUnregisteringTextureWhenDetached() { verify(fakeFlutterJNI, times(0)).unregisterTexture(eq(id)); } - /** @noinspection FinalizeCalledExplicitly*/ + /** @noinspection FinalizeCalledExplicitly */ void runFinalization(FlutterRenderer.SurfaceTextureRegistryEntry entry) { CountDownLatch latch = new CountDownLatch(1); Thread fakeFinalizer = new Thread( - () -> { - try { - entry.finalize(); - latch.countDown(); - } catch (Throwable e) { - // do nothing - } - }); + () -> { + try { + entry.finalize(); + latch.countDown(); + } catch (Throwable e) { + // do nothing + } + }); fakeFinalizer.start(); try { latch.await(); @@ -336,8 +334,7 @@ public void itNotifyImageFrameListener() { FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); AtomicInteger invocationCount = new AtomicInteger(0); - final TextureRegistry.OnFrameConsumedListener listener = - invocationCount::incrementAndGet; + final TextureRegistry.OnFrameConsumedListener listener = invocationCount::incrementAndGet; FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -384,7 +381,7 @@ public void itNotifySurfaceTextureEntryWhenMemoryPressureWarning() { AtomicInteger invocationCount = new AtomicInteger(0); final TextureRegistry.OnTrimMemoryListener listener = - level -> invocationCount.incrementAndGet(); + level -> invocationCount.incrementAndGet(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -455,7 +452,8 @@ public void itDoesNotInvokeCreatesSurfaceWhenResumingRendering() { public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); - FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; + FlutterRenderer.ImageReaderSurfaceProducer texture = + (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -510,7 +508,8 @@ public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInFlight() { FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); - FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; + FlutterRenderer.ImageReaderSurfaceProducer texture = + (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -540,7 +539,8 @@ public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInFlight() { public void ImageReaderSurfaceProducerImageReadersAndImagesCount() { FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); - FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; + FlutterRenderer.ImageReaderSurfaceProducer texture = + (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -622,7 +622,8 @@ public void ImageReaderSurfaceProducerImageReadersAndImagesCount() { public void ImageReaderSurfaceProducerTrimMemoryCallback() { FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); - FlutterRenderer.ImageReaderSurfaceProducer texture = (FlutterRenderer.ImageReaderSurfaceProducer) producer; + FlutterRenderer.ImageReaderSurfaceProducer texture = + (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); From 358f0c2e0360142dd846268d6236cc90164e15cd Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 13 Jun 2024 10:09:55 -0700 Subject: [PATCH 3/7] Do not cache context. --- .../flutter/embedding/engine/renderer/FlutterEngineRule.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java index 51314b8898d58..4581c66aaad0e 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java @@ -18,8 +18,8 @@ * Prepares and returns a {@link FlutterEngine} and {@link Intent} primed with an engine for tests. */ public final class FlutterEngineRule extends TestWatcher { - private static final Context ctx = ApplicationProvider.getApplicationContext(); private static final String cachedEngineId = "flutter_engine_rule_cached_engine"; + private Context ctx; private FlutterJNI flutterJNI; private FlutterEngine flutterEngine; private boolean jniIsAttached = true; @@ -35,6 +35,7 @@ protected void starting(Description description) { when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(false); // Create an engine. + ctx = = ApplicationProvider.getApplicationContext(); flutterEngine = new FlutterEngine(ctx, mockFlutterLoader, flutterJNI); // Place it in the engine cache. From 0c52336404ee925f503463eb1d9284a8f6d5d304 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 13 Jun 2024 10:29:00 -0700 Subject: [PATCH 4/7] ++ --- .../io/flutter/embedding/engine/renderer/FlutterEngineRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java index 4581c66aaad0e..318a9ba6b8d4c 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java @@ -35,7 +35,7 @@ protected void starting(Description description) { when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(false); // Create an engine. - ctx = = ApplicationProvider.getApplicationContext(); + ctx = ApplicationProvider.getApplicationContext(); flutterEngine = new FlutterEngine(ctx, mockFlutterLoader, flutterJNI); // Place it in the engine cache. From 36bf5b41b976fbb4599fc78691d8e69674a03677 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 13 Jun 2024 12:42:21 -0700 Subject: [PATCH 5/7] ++ --- .../embedding/engine/renderer/FlutterRendererTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index 0357293a1d303..df23efe6e344e 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -268,8 +268,14 @@ void runFinalization(FlutterRenderer.SurfaceTextureRegistryEntry entry) { @Test public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { + // Intentionally do not use 'engineRule' in this test, because we are testing a very narrow + // API (the side-effects of 'setViewportMetrics'). Under normal construction, the engine will + // invoke 'setViewportMetrics' a number of times automatically, making testing the side-effects + // of the method call more difficult than needed. + FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + // Setup the test. - FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.ViewportMetrics metrics = new FlutterRenderer.ViewportMetrics(); metrics.width = 1000; metrics.height = 1000; @@ -284,7 +290,6 @@ public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { new Rect(50, 60, 70, 80), FlutterRenderer.DisplayFeatureType.CUTOUT)); // Execute the behavior under test. - clearInvocations(fakeFlutterJNI); flutterRenderer.setViewportMetrics(metrics); // Verify behavior under test. From 8a2970fd998fdcf7c2d7621c28665e54212c1ba0 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 13 Jun 2024 12:43:22 -0700 Subject: [PATCH 6/7] ++ --- .../flutter/embedding/engine/renderer/FlutterRendererTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index df23efe6e344e..ed79d7b6a08f0 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -9,7 +9,6 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; From 71798526985b0d7c6a22d69a9d89e30fe24f65c1 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 13 Jun 2024 15:44:32 -0700 Subject: [PATCH 7/7] ++ --- .../flutter/embedding/engine/renderer/FlutterEngineRule.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java index 318a9ba6b8d4c..56a89554457bd 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java @@ -19,7 +19,7 @@ */ public final class FlutterEngineRule extends TestWatcher { private static final String cachedEngineId = "flutter_engine_rule_cached_engine"; - private Context ctx; + private final Context ctx = ApplicationProvider.getApplicationContext(); private FlutterJNI flutterJNI; private FlutterEngine flutterEngine; private boolean jniIsAttached = true; @@ -35,7 +35,6 @@ protected void starting(Description description) { when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(false); // Create an engine. - ctx = ApplicationProvider.getApplicationContext(); flutterEngine = new FlutterEngine(ctx, mockFlutterLoader, flutterJNI); // Place it in the engine cache.