1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.uirendering.cts.testinfrastructure;
17 
18 import android.app.Instrumentation;
19 import android.content.Intent;
20 import android.graphics.Bitmap;
21 import android.graphics.Bitmap.Config;
22 import android.graphics.Point;
23 import android.graphics.Rect;
24 import android.uirendering.cts.bitmapcomparers.BitmapComparer;
25 import android.uirendering.cts.bitmapverifiers.BitmapVerifier;
26 import android.uirendering.cts.util.BitmapAsserter;
27 import android.util.Log;
28 import android.view.AttachedSurfaceControl;
29 import android.view.PixelCopy;
30 import android.view.SurfaceControl;
31 
32 import androidx.annotation.Nullable;
33 import androidx.test.InstrumentationRegistry;
34 
35 import com.android.compatibility.common.util.SynchronousPixelCopy;
36 
37 import org.junit.After;
38 import org.junit.AfterClass;
39 import org.junit.Assert;
40 import org.junit.Rule;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.concurrent.CountDownLatch;
45 import java.util.concurrent.TimeUnit;
46 
47 /**
48  * This class contains the basis for the graphics hardware test classes. Contained within this class
49  * are several methods that help with the execution of tests, and should be extended to gain the
50  * functionality built in.
51  */
52 public abstract class ActivityTestBase {
53     public static final String TAG = "ActivityTestBase";
54     public static final boolean DEBUG = false;
55 
56     //The minimum height and width of a device
57     public static final int TEST_WIDTH = 90;
58     public static final int TEST_HEIGHT = 90;
59 
60     private TestCaseBuilder mTestCaseBuilder;
61     private Screenshotter mScreenshotter;
62 
63     private static DrawActivity sActivity;
64 
65     @Rule
66     public Tracer name = new Tracer();
67 
getName()68     protected String getName() {
69         return name.getMethodName();
70     }
71 
getInstrumentation()72     protected Instrumentation getInstrumentation() {
73         return InstrumentationRegistry.getInstrumentation();
74     }
75 
getActivity()76     protected DrawActivity getActivity() {
77         if (sActivity == null) {
78             Instrumentation instrumentation = getInstrumentation();
79             instrumentation.setInTouchMode(true);
80             Intent intent = new Intent(Intent.ACTION_MAIN);
81             intent.setClass(instrumentation.getTargetContext(), DrawActivity.class);
82             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
83             intent.putExtra(DrawActivity.EXTRA_WIDE_COLOR_GAMUT, isWideColorGamut());
84             intent.putExtra(DrawActivity.EXTRA_USE_FORCE_DARK, useForceDark());
85             sActivity = (DrawActivity) instrumentation.startActivitySync(intent);
86         }
87         return sActivity;
88     }
89 
isWideColorGamut()90     protected boolean isWideColorGamut() {
91         return false;
92     }
93 
useForceDark()94     protected boolean useForceDark() {
95         return false;
96     }
97 
98     @AfterClass
tearDownClass()99     public static void tearDownClass() {
100         if (sActivity != null) {
101             // All tests are finished, tear down the activity
102             sActivity.allTestsFinished();
103             sActivity = null;
104         }
105     }
106 
107     @After
tearDown()108     public void tearDown() {
109         if (mTestCaseBuilder != null) {
110             List<TestCase> testCases = mTestCaseBuilder.getTestCases();
111 
112             if (testCases.size() == 0) {
113                 throw new IllegalStateException("Must have at least one test case");
114             }
115 
116             for (TestCase testCase : testCases) {
117                 if (!testCase.wasTestRan) {
118                     Log.w(TAG, getName() + " not all of the tests ran");
119                     break;
120                 }
121             }
122             mTestCaseBuilder = null;
123         }
124     }
125 
unsafeAwait(CountDownLatch latch)126     private void unsafeAwait(CountDownLatch latch) {
127         try {
128             latch.await(5, TimeUnit.SECONDS);
129         } catch (InterruptedException e) {
130             throw new RuntimeException("readyFence didn't signal within 5 seconds");
131         }
132     }
133 
134     // waitForRedraw checks that HWUI finished drawing but SurfaceFlinger may be backpressured, so
135     // synchronizing by applying no-op transactions with UI draws to guarantee that we wait until
136     // SurfaceFlinger's frontend has latched the UI frame.
waitForScreenshottable()137     protected void waitForScreenshottable() {
138         CountDownLatch latch = new CountDownLatch(1);
139         getActivity().runOnUiThread(() -> {
140             AttachedSurfaceControl rootSurfaceControl =
141                     getActivity().getWindow().getRootSurfaceControl();
142 
143             SurfaceControl stub = new SurfaceControl.Builder().setName("test").build();
144             rootSurfaceControl.applyTransactionOnDraw(
145                     rootSurfaceControl.buildReparentTransaction(stub));
146             rootSurfaceControl.applyTransactionOnDraw(
147                     new SurfaceControl.Transaction().reparent(stub, null)
148                             .addTransactionCommittedListener(Runnable::run, latch::countDown));
149         });
150         getActivity().waitForRedraw();
151         unsafeAwait(latch);
152     }
153 
takeScreenshot(TestPositionInfo testPositionInfo)154     private Bitmap takeScreenshot(TestPositionInfo testPositionInfo) {
155         if (mScreenshotter == null) {
156             SynchronousPixelCopy copy = new SynchronousPixelCopy();
157             Bitmap dest = Bitmap.createBitmap(
158                     TEST_WIDTH, TEST_HEIGHT,
159                     getActivity().getWindow().isWideColorGamut()
160                             ? Config.RGBA_F16 : Config.ARGB_8888);
161             Rect srcRect = new Rect(0, 0, TEST_WIDTH, TEST_HEIGHT);
162             srcRect.offset(testPositionInfo.surfaceOffset.x, testPositionInfo.surfaceOffset.y);
163             Log.d(TAG, "capturing screenshot of " + srcRect.toShortString());
164             int copyResult = copy.request(getActivity().getWindow(), srcRect, dest);
165             Assert.assertEquals(PixelCopy.SUCCESS, copyResult);
166             return dest;
167         } else {
168             return mScreenshotter.takeScreenshot(testPositionInfo);
169         }
170     }
171 
runRenderSpec(TestCase testCase)172     private TestPositionInfo runRenderSpec(TestCase testCase) {
173         TestPositionInfo testPositionInfo = getActivity().enqueueRenderSpecAndWait(
174                 testCase.layoutID, testCase.canvasClient,
175                 testCase.viewInitializer, testCase.useHardware, testCase.usePicture);
176         testCase.wasTestRan = true;
177         if (testCase.readyFence != null) {
178             unsafeAwait(testCase.readyFence);
179             // The fence setup may have (and probably did) changed things that we need to
180             // wait have been drawn. So force an invalidate() and wait for it to finish
181             getActivity().waitForRedraw();
182         }
183 
184         if (mScreenshotter != null) {
185             // If we have a screenshotter then we're (probably) using SurfaceFlinger to
186             // capture a screenshot, so we wait until we know SurfaceFlinger latched the most
187             // recent content
188             waitForScreenshottable();
189         }
190         return testPositionInfo;
191     }
192 
193     /**
194      * Used to execute a specific part of a test and get the resultant bitmap
195      */
captureRenderSpec(TestCase testCase)196     private Bitmap captureRenderSpec(TestCase testCase) {
197         return takeScreenshot(runRenderSpec(testCase));
198     }
199 
createTest()200     protected TestCaseBuilder createTest() {
201         mTestCaseBuilder = new TestCaseBuilder();
202         mScreenshotter = null;
203         return mTestCaseBuilder;
204     }
205 
206     public static class TestPositionInfo {
207         /**
208          * Position of capture area in surface space - use this offset for e.g.
209          * PixelCopy from a window's surface.
210          */
211         public final Point surfaceOffset;
212 
TestPositionInfo(Point surfaceOffset)213         public TestPositionInfo(Point surfaceOffset) {
214             this.surfaceOffset = surfaceOffset;
215         }
216     }
217 
218     public interface Screenshotter {
takeScreenshot(TestPositionInfo params)219         Bitmap takeScreenshot(TestPositionInfo params);
220     }
221 
222     /**
223      * Defines a group of CanvasClients, XML layouts, and WebView html files for testing.
224      */
225     protected class TestCaseBuilder {
226         private List<TestCase> mTestCases;
227 
TestCaseBuilder()228         private TestCaseBuilder() {
229             mTestCases = new ArrayList<>();
230         }
231 
232         /**
233          * Runs a test where the first test case is considered the "ideal" image and from there,
234          * every test case is tested against it.
235          */
runWithComparer(BitmapComparer bitmapComparer)236         public void runWithComparer(BitmapComparer bitmapComparer) {
237             if (mTestCases.size() == 0) {
238                 throw new IllegalStateException("Need at least one test to run");
239             }
240 
241             Bitmap idealBitmap = captureRenderSpec(mTestCases.remove(0));
242 
243             try {
244                 for (TestCase testCase : mTestCases) {
245                     Bitmap testCaseBitmap = captureRenderSpec(testCase);
246                     BitmapAsserter.assertBitmapsAreSimilar(idealBitmap, testCaseBitmap,
247                             bitmapComparer, testCase.getDebugString());
248                 }
249             } finally {
250                 getActivity().reset();
251             }
252         }
253 
254         /**
255          * Runs a test where each testcase is independent of the others and each is checked against
256          * the verifier given.
257          */
runWithVerifier(BitmapVerifier bitmapVerifier)258         public void runWithVerifier(BitmapVerifier bitmapVerifier) {
259             if (mTestCases.size() == 0) {
260                 throw new IllegalStateException("Need at least one test to run");
261             }
262 
263             try {
264                 for (TestCase testCase : mTestCases) {
265                     Bitmap testCaseBitmap = captureRenderSpec(testCase);
266                     BitmapAsserter.assertBitmapIsVerified(testCaseBitmap, bitmapVerifier,
267                             testCase.getDebugString());
268                 }
269             } finally {
270                 getActivity().reset();
271             }
272         }
273 
274         private static final int VERIFY_ANIMATION_LOOP_COUNT = 20;
275         private static final int VERIFY_ANIMATION_SLEEP_MS = 100;
276 
277         /**
278          * Runs a test where each testcase is independent of the others and each is checked against
279          * the verifier given in a loop.
280          *
281          * A screenshot is captured several times in a loop, to ensure that valid output is produced
282          * at many different times during the animation.
283          */
runWithAnimationVerifier(BitmapVerifier bitmapVerifier)284         public void runWithAnimationVerifier(BitmapVerifier bitmapVerifier) {
285             if (mTestCases.size() == 0) {
286                 throw new IllegalStateException("Need at least one test to run");
287             }
288 
289             try {
290                 for (TestCase testCase : mTestCases) {
291                     TestPositionInfo testPositionInfo = runRenderSpec(testCase);
292 
293                     for (int i = 0; i < VERIFY_ANIMATION_LOOP_COUNT; i++) {
294                         try {
295                             Thread.sleep(VERIFY_ANIMATION_SLEEP_MS);
296                         } catch (InterruptedException e) {
297                             e.printStackTrace();
298                         }
299                         Bitmap testCaseBitmap = takeScreenshot(testPositionInfo);
300                         BitmapAsserter.assertBitmapIsVerified(testCaseBitmap, bitmapVerifier,
301                                 testCase.getDebugString());
302                     }
303                 }
304             } finally {
305                 getActivity().reset();
306             }
307         }
308 
309         /**
310          * Runs a test where each testcase is run without verification. Should only be used
311          * where custom CanvasClients, Views, or ViewInitializers do their own internal
312          * test assertions.
313          */
runWithoutVerification()314         public void runWithoutVerification() {
315             runWithVerifier(new BitmapVerifier() {
316                 @Override
317                 public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
318                     return true;
319                 }
320             });
321         }
322 
withScreenshotter(Screenshotter screenshotter)323         public TestCaseBuilder withScreenshotter(Screenshotter screenshotter) {
324             Assert.assertNull("Screenshotter is already set!", mScreenshotter);
325             mScreenshotter = screenshotter;
326             return this;
327         }
328 
addLayout(int layoutId, @Nullable ViewInitializer viewInitializer)329         public TestCaseBuilder addLayout(int layoutId, @Nullable ViewInitializer viewInitializer) {
330             return addLayout(layoutId, viewInitializer, false)
331                     .addLayout(layoutId, viewInitializer, true);
332         }
333 
addLayout(int layoutId, @Nullable ViewInitializer viewInitializer, boolean useHardware)334         public TestCaseBuilder addLayout(int layoutId, @Nullable ViewInitializer viewInitializer,
335                 boolean useHardware) {
336             mTestCases.add(new TestCase(layoutId, viewInitializer, useHardware));
337             return this;
338         }
339 
addLayout(int layoutId, @Nullable ViewInitializer viewInitializer, boolean useHardware, CountDownLatch readyFence)340         public TestCaseBuilder addLayout(int layoutId, @Nullable ViewInitializer viewInitializer,
341                 boolean useHardware, CountDownLatch readyFence) {
342             TestCase test = new TestCase(layoutId, viewInitializer, useHardware);
343             test.readyFence = readyFence;
344             mTestCases.add(test);
345             return this;
346         }
347 
addCanvasClient(CanvasClient canvasClient)348         public TestCaseBuilder addCanvasClient(CanvasClient canvasClient) {
349             return addCanvasClient(null, canvasClient);
350         }
351 
addCanvasClient(CanvasClient canvasClient, boolean useHardware)352         public TestCaseBuilder addCanvasClient(CanvasClient canvasClient, boolean useHardware) {
353             return addCanvasClient(null, canvasClient, useHardware);
354         }
355 
addCanvasClient(String debugString, CanvasClient canvasClient)356         public TestCaseBuilder addCanvasClient(String debugString, CanvasClient canvasClient) {
357             return addCanvasClient(debugString, canvasClient, false)
358                     .addCanvasClient(debugString, canvasClient, true);
359         }
360 
addCanvasClient(String debugString, CanvasClient canvasClient, boolean useHardware)361         public TestCaseBuilder addCanvasClient(String debugString,
362                 CanvasClient canvasClient, boolean useHardware) {
363             return addCanvasClientInternal(debugString, canvasClient, useHardware, false)
364                     .addCanvasClientInternal(debugString, canvasClient, useHardware, true);
365         }
366 
addCanvasClientWithoutUsingPicture(CanvasClient canvasClient)367         public TestCaseBuilder addCanvasClientWithoutUsingPicture(CanvasClient canvasClient) {
368             return addCanvasClientWithoutUsingPicture(null, canvasClient);
369         }
370 
addCanvasClientWithoutUsingPicture(String debugString, CanvasClient canvasClient)371         public TestCaseBuilder addCanvasClientWithoutUsingPicture(String debugString,
372                 CanvasClient canvasClient) {
373             return addCanvasClientInternal(debugString, canvasClient, false, false)
374                     .addCanvasClientInternal(debugString, canvasClient, true, false);
375         }
376 
addCanvasClientWithoutUsingPicture(CanvasClient canvasClient, boolean useHardware)377         public TestCaseBuilder addCanvasClientWithoutUsingPicture(CanvasClient canvasClient,
378                 boolean useHardware) {
379             return addCanvasClientInternal(null, canvasClient, useHardware, false);
380         }
381 
addCanvasClientInternal(String debugString, CanvasClient canvasClient, boolean useHardware, boolean usePicture)382         private TestCaseBuilder addCanvasClientInternal(String debugString,
383                 CanvasClient canvasClient, boolean useHardware, boolean usePicture) {
384             mTestCases.add(new TestCase(canvasClient, debugString, useHardware, usePicture));
385             return this;
386         }
387 
getTestCases()388         private List<TestCase> getTestCases() {
389             return mTestCases;
390         }
391     }
392 
393     private class TestCase {
394         public int layoutID;
395         public ViewInitializer viewInitializer;
396         /**
397          * After launching the test case this fence is used to signal when
398          * to proceed with capture & verification. If this is null the test
399          * proceeds immediately to verification
400          */
401         @Nullable
402         public CountDownLatch readyFence;
403 
404         public CanvasClient canvasClient;
405         public String canvasClientDebugString;
406 
407         public boolean useHardware;
408         public boolean usePicture = false;
409         public boolean wasTestRan = false;
410 
TestCase(int layoutId, ViewInitializer viewInitializer, boolean useHardware)411         public TestCase(int layoutId, ViewInitializer viewInitializer, boolean useHardware) {
412             this.layoutID = layoutId;
413             this.viewInitializer = viewInitializer;
414             this.useHardware = useHardware;
415         }
416 
TestCase(CanvasClient client, String debugString, boolean useHardware, boolean usePicture)417         public TestCase(CanvasClient client, String debugString, boolean useHardware,
418                 boolean usePicture) {
419             this.canvasClient = client;
420             this.canvasClientDebugString = debugString;
421             this.useHardware = useHardware;
422             this.usePicture = usePicture;
423         }
424 
getDebugString()425         public String getDebugString() {
426             String debug = "";
427             if (canvasClient != null) {
428                 debug += "CanvasClient : ";
429                 if (canvasClientDebugString != null) {
430                     debug += canvasClientDebugString;
431                 } else {
432                     debug += "no debug string given";
433                 }
434             } else {
435                 debug += "Layout resource : " +
436                         getActivity().getResources().getResourceName(layoutID);
437             }
438             debug += "\nTest ran in " + (useHardware ? "hardware" : "software") +
439                     (usePicture ? " with picture" : " without picture") + "\n";
440             return debug;
441         }
442     }
443 }
444