Add a FlutterEngineRule (JUnit TestRule) and use it in FlutterRendererTest (flutter/engine#53361)

In https://github.com/flutter/engine/pull/53280, I'm adding
lifecycle-aware methods to `SurfaceProducer`.

That means, in order to test that it WAI, we'll need to be running in a
simulated activity, and be able to switch scenario states (i.e. to
`RESUMED`). This was mentioned as well in
https://github.com/flutter/flutter/issues/133151 as being something we
want to do.

This PR adds a `FlutterEngineRule`, which allows the creation of a
"real" `FlutterEngine` and an `Intent` that can power
`AndroidScenarioRule<FlutterActivity>`. I felt bad doing all of this
work for a single `@Test`, so I also refactored the rest of the file and
cleaned things up a bit.

That said, I'm happy to revert or make changes if we liked how things
were setup before.
This commit is contained in:
Matan Lurey
2024-06-13 16:22:54 -07:00
committed by GitHub
parent 5778351976
commit 5300aafdb5
3 changed files with 185 additions and 87 deletions

View File

@@ -0,0 +1,84 @@
package io.flutter.embedding.engine.renderer;
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 static final String cachedEngineId = "flutter_engine_rule_cached_engine";
private final Context ctx = ApplicationProvider.getApplicationContext();
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);
}
}

View File

@@ -14,7 +14,6 @@ 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.robolectric.Shadows.shadowOf;
import android.graphics.Canvas;
@@ -23,12 +22,16 @@ import android.graphics.SurfaceTexture;
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 +40,14 @@ import org.robolectric.annotation.Config;
@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<FlutterActivity> scenarioRule =
new ActivityScenarioRule<>(engineRule.makeIntent());
private FlutterJNI fakeFlutterJNI;
private Surface fakeSurface;
private Surface fakeSurface2;
@Before
public void init() {
@@ -50,16 +57,14 @@ public class FlutterRendererTest {
@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 +77,7 @@ public class FlutterRendererTest {
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 +92,7 @@ public class FlutterRendererTest {
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 +106,9 @@ public class FlutterRendererTest {
@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 +122,8 @@ public class FlutterRendererTest {
@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 +137,10 @@ public class FlutterRendererTest {
@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 +150,10 @@ public class FlutterRendererTest {
@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 +163,11 @@ public class FlutterRendererTest {
@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 +180,8 @@ public class FlutterRendererTest {
@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 +201,8 @@ public class FlutterRendererTest {
@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 +224,15 @@ public class FlutterRendererTest {
@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,18 +244,17 @@ public class FlutterRendererTest {
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();
@@ -270,8 +267,14 @@ public class FlutterRendererTest {
@Test
public void itConvertsDisplayFeatureArrayToPrimitiveArrays() {
// Setup the test.
// 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.ViewportMetrics metrics = new FlutterRenderer.ViewportMetrics();
metrics.width = 1000;
metrics.height = 1000;
@@ -332,16 +335,10 @@ public class FlutterRendererTest {
@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();
}
};
final TextureRegistry.OnFrameConsumedListener listener = invocationCount::incrementAndGet;
FlutterRenderer.SurfaceTextureRegistryEntry entry =
(FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture();
@@ -357,7 +354,7 @@ public class FlutterRendererTest {
@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 +367,7 @@ public class FlutterRendererTest {
@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 +381,11 @@ public class FlutterRendererTest {
@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 +401,8 @@ public class FlutterRendererTest {
@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 +417,8 @@ public class FlutterRendererTest {
@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 +426,9 @@ public class FlutterRendererTest {
@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 +454,10 @@ public class FlutterRendererTest {
@Test
public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() {
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer();
TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer();
FlutterRenderer.ImageReaderSurfaceProducer texture =
flutterRenderer.new ImageReaderSurfaceProducer(0);
(FlutterRenderer.ImageReaderSurfaceProducer) producer;
texture.disableFenceForTest();
// Returns a null image when one hasn't been produced.
@@ -481,6 +478,7 @@ public class FlutterRendererTest {
// 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 +498,7 @@ public class FlutterRendererTest {
// 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 +509,11 @@ public class FlutterRendererTest {
}
@Test
public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInflight() {
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInFlight() {
FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer();
TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer();
FlutterRenderer.ImageReaderSurfaceProducer texture =
flutterRenderer.new ImageReaderSurfaceProducer(0);
(FlutterRenderer.ImageReaderSurfaceProducer) producer;
texture.disableFenceForTest();
// Returns a null image when one hasn't been produced.
@@ -541,9 +541,10 @@ public class FlutterRendererTest {
@Test
public void ImageReaderSurfaceProducerImageReadersAndImagesCount() {
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer();
TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer();
FlutterRenderer.ImageReaderSurfaceProducer texture =
flutterRenderer.new ImageReaderSurfaceProducer(0);
(FlutterRenderer.ImageReaderSurfaceProducer) producer;
texture.disableFenceForTest();
// Returns a null image when one hasn't been produced.
@@ -623,9 +624,11 @@ public class FlutterRendererTest {
@Test
public void ImageReaderSurfaceProducerTrimMemoryCallback() {
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer();
TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer();
FlutterRenderer.ImageReaderSurfaceProducer texture =
flutterRenderer.new ImageReaderSurfaceProducer(0);
(FlutterRenderer.ImageReaderSurfaceProducer) producer;
texture.disableFenceForTest();
// Returns a null image when one hasn't been produced.
@@ -687,37 +690,46 @@ public class FlutterRendererTest {
// 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);
}
}

View File

@@ -2,6 +2,8 @@
<application>
<activity android:name="io.flutter.embedding.android.FlutterActivity" />
<activity android:name="io.flutter.embedding.android.FlutterFragmentActivity" />
<activity android:name="io.flutter.embedding.android.FlutterFragmentActivityTest$FlutterFragmentActivityWithProvidedEngine" />