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