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