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.annotation.Nullable;
19 import android.app.Instrumentation;
20 import android.graphics.Bitmap;
21 import android.graphics.Point;
22 import android.renderscript.Allocation;
23 import android.renderscript.RenderScript;
24 import android.support.test.rule.ActivityTestRule;
25 import android.uirendering.cts.bitmapcomparers.BitmapComparer;
26 import android.uirendering.cts.bitmapverifiers.BitmapVerifier;
27 import android.uirendering.cts.differencevisualizers.DifferenceVisualizer;
28 import android.uirendering.cts.differencevisualizers.PassFailVisualizer;
29 import android.uirendering.cts.util.BitmapDumper;
30 import android.util.Log;
31 
32 import android.support.test.InstrumentationRegistry;
33 import org.junit.After;
34 import org.junit.Before;
35 import org.junit.Rule;
36 import org.junit.rules.TestName;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.concurrent.CountDownLatch;
41 import java.util.concurrent.TimeUnit;
42 
43 import static org.junit.Assert.assertTrue;
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     public static final boolean USE_RS = false;
54 
55     //The minimum height and width of a device
56     public static final int TEST_WIDTH = 90;
57     public static final int TEST_HEIGHT = 90;
58 
59     private int[] mHardwareArray = new int[TEST_HEIGHT * TEST_WIDTH];
60     private int[] mSoftwareArray = new int[TEST_HEIGHT * TEST_WIDTH];
61     private DifferenceVisualizer mDifferenceVisualizer;
62     private RenderScript mRenderScript;
63     private TestCaseBuilder mTestCaseBuilder;
64 
65     @Rule
66     public ActivityTestRule<DrawActivity> mActivityRule = new ActivityTestRule<>(
67             DrawActivity.class);
68 
69     @Rule
70     public TestName name = new TestName();
71 
72     /**
73      * The default constructor creates the package name and sets the DrawActivity as the class that
74      * we would use.
75      */
ActivityTestBase()76     public ActivityTestBase() {
77         mDifferenceVisualizer = new PassFailVisualizer();
78 
79         // Create a location for the files to be held, if it doesn't exist already
80         BitmapDumper.createSubDirectory(this.getClass().getSimpleName());
81 
82         // If we have a test currently, let's remove the older files if they exist
83         if (getName() != null) {
84             BitmapDumper.deleteFileInClassFolder(this.getClass().getSimpleName(), getName());
85         }
86     }
87 
getActivity()88     protected DrawActivity getActivity() {
89         return mActivityRule.getActivity();
90     }
91 
getName()92     protected String getName() {
93         return name.getMethodName();
94     }
95 
getInstrumentation()96     protected Instrumentation getInstrumentation() {
97         return InstrumentationRegistry.getInstrumentation();
98     }
99 
100     @Before
setUp()101     public void setUp() {
102         mDifferenceVisualizer = new PassFailVisualizer();
103         if (USE_RS) {
104             mRenderScript = RenderScript.create(getActivity().getApplicationContext());
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(Point testOffset)127     public Bitmap takeScreenshot(Point testOffset) {
128         getInstrumentation().waitForIdleSync();
129         Bitmap source = getInstrumentation().getUiAutomation().takeScreenshot();
130         return Bitmap.createBitmap(source, testOffset.x, testOffset.y, TEST_WIDTH, TEST_HEIGHT);
131     }
132 
runRenderSpec(TestCase testCase)133     protected Point runRenderSpec(TestCase testCase) {
134         Point testOffset = getActivity().enqueueRenderSpecAndWait(
135                 testCase.layoutID, testCase.canvasClient,
136                 null, testCase.viewInitializer, testCase.useHardware);
137         testCase.wasTestRan = true;
138         if (testCase.readyFence != null) {
139             try {
140                 testCase.readyFence.await(5, TimeUnit.SECONDS);
141             } catch (InterruptedException e) {
142                 throw new RuntimeException("readyFence didn't signal within 5 seconds");
143             }
144         }
145         return testOffset;
146     }
147 
148     /**
149      * Used to execute a specific part of a test and get the resultant bitmap
150      */
captureRenderSpec(TestCase testCase)151     protected Bitmap captureRenderSpec(TestCase testCase) {
152         Point testOffset = runRenderSpec(testCase);
153         return takeScreenshot(testOffset);
154     }
155 
156     /**
157      * Compares the two bitmaps saved using the given test. If they fail, the files are saved using
158      * the test name.
159      */
assertBitmapsAreSimilar(Bitmap bitmap1, Bitmap bitmap2, BitmapComparer comparer, String debugMessage)160     protected void assertBitmapsAreSimilar(Bitmap bitmap1, Bitmap bitmap2,
161             BitmapComparer comparer, String debugMessage) {
162         boolean success;
163 
164         if (USE_RS && comparer.supportsRenderScript()) {
165             Allocation idealAllocation = Allocation.createFromBitmap(mRenderScript, bitmap1,
166                     Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
167             Allocation givenAllocation = Allocation.createFromBitmap(mRenderScript, bitmap2,
168                     Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
169             success = comparer.verifySameRS(getActivity().getResources(), idealAllocation,
170                     givenAllocation, 0, TEST_WIDTH, TEST_WIDTH, TEST_HEIGHT, mRenderScript);
171         } else {
172             bitmap1.getPixels(mSoftwareArray, 0, TEST_WIDTH, 0, 0, TEST_WIDTH, TEST_HEIGHT);
173             bitmap2.getPixels(mHardwareArray, 0, TEST_WIDTH, 0, 0, TEST_WIDTH, TEST_HEIGHT);
174             success = comparer.verifySame(mSoftwareArray, mHardwareArray, 0, TEST_WIDTH, TEST_WIDTH,
175                     TEST_HEIGHT);
176         }
177 
178         if (!success) {
179             BitmapDumper.dumpBitmaps(bitmap1, bitmap2, getName(), this.getClass().getSimpleName(),
180                     mDifferenceVisualizer);
181         }
182 
183         assertTrue(debugMessage, success);
184     }
185 
186     /**
187      * Tests to see if a bitmap passes a verifier's test. If it doesn't the bitmap is saved to the
188      * sdcard.
189      */
assertBitmapIsVerified(Bitmap bitmap, BitmapVerifier bitmapVerifier, String debugMessage)190     protected void assertBitmapIsVerified(Bitmap bitmap, BitmapVerifier bitmapVerifier,
191             String debugMessage) {
192         bitmap.getPixels(mSoftwareArray, 0, TEST_WIDTH, 0, 0,
193                 TEST_WIDTH, TEST_HEIGHT);
194         boolean success = bitmapVerifier.verify(mSoftwareArray, 0, TEST_WIDTH, TEST_WIDTH, TEST_HEIGHT);
195         if (!success) {
196             Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, TEST_WIDTH, TEST_HEIGHT);
197             BitmapDumper.dumpBitmap(croppedBitmap, getName(), this.getClass().getSimpleName());
198             BitmapDumper.dumpBitmap(bitmapVerifier.getDifferenceBitmap(), getName() + "_verifier",
199                     this.getClass().getSimpleName());
200         }
201         assertTrue(debugMessage, success);
202     }
203 
createTest()204     protected TestCaseBuilder createTest() {
205         mTestCaseBuilder = new TestCaseBuilder();
206         return mTestCaseBuilder;
207     }
208 
209     /**
210      * Defines a group of CanvasClients, XML layouts, and WebView html files for testing.
211      */
212     protected class TestCaseBuilder {
213         private List<TestCase> mTestCases;
214 
TestCaseBuilder()215         private TestCaseBuilder() {
216             mTestCases = new ArrayList<>();
217         }
218 
219         /**
220          * Runs a test where the first test case is considered the "ideal" image and from there,
221          * every test case is tested against it.
222          */
runWithComparer(BitmapComparer bitmapComparer)223         public void runWithComparer(BitmapComparer bitmapComparer) {
224             if (mTestCases.size() == 0) {
225                 throw new IllegalStateException("Need at least one test to run");
226             }
227 
228             Bitmap idealBitmap = captureRenderSpec(mTestCases.remove(0));
229 
230             for (TestCase testCase : mTestCases) {
231                 Bitmap testCaseBitmap = captureRenderSpec(testCase);
232                 assertBitmapsAreSimilar(idealBitmap, testCaseBitmap, bitmapComparer,
233                         testCase.getDebugString());
234             }
235         }
236 
237         /**
238          * Runs a test where each testcase is independent of the others and each is checked against
239          * the verifier given.
240          */
runWithVerifier(BitmapVerifier bitmapVerifier)241         public void runWithVerifier(BitmapVerifier bitmapVerifier) {
242             if (mTestCases.size() == 0) {
243                 throw new IllegalStateException("Need at least one test to run");
244             }
245 
246             for (TestCase testCase : mTestCases) {
247                 Bitmap testCaseBitmap = captureRenderSpec(testCase);
248                 assertBitmapIsVerified(testCaseBitmap, bitmapVerifier, testCase.getDebugString());
249             }
250         }
251 
252         private static final int VERIFY_ANIMATION_LOOP_COUNT = 20;
253         private static final int VERIFY_ANIMATION_SLEEP_MS = 100;
254 
255         /**
256          * Runs a test where each testcase is independent of the others and each is checked against
257          * the verifier given in a loop.
258          *
259          * A screenshot is captured several times in a loop, to ensure that valid output is produced
260          * at many different times during the animation.
261          */
runWithAnimationVerifier(BitmapVerifier bitmapVerifier)262         public void runWithAnimationVerifier(BitmapVerifier bitmapVerifier) {
263             if (mTestCases.size() == 0) {
264                 throw new IllegalStateException("Need at least one test to run");
265             }
266 
267             for (TestCase testCase : mTestCases) {
268                 Point testOffset = runRenderSpec(testCase);
269 
270                 for (int i = 0; i < VERIFY_ANIMATION_LOOP_COUNT; i++) {
271                     try {
272                         Thread.sleep(VERIFY_ANIMATION_SLEEP_MS);
273                     } catch (InterruptedException e) {
274                         e.printStackTrace();
275                     }
276                     Bitmap testCaseBitmap = takeScreenshot(testOffset);
277                     assertBitmapIsVerified(testCaseBitmap, bitmapVerifier,
278                             testCase.getDebugString());
279                 }
280             }
281         }
282 
283         /**
284          * Runs a test where each testcase is run without verification. Should only be used
285          * where custom CanvasClients, Views, or ViewInitializers do their own internal
286          * test assertions.
287          */
runWithoutVerification()288         public void runWithoutVerification() {
289             runWithVerifier(new BitmapVerifier() {
290                 @Override
291                 public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
292                     return true;
293                 }
294             });
295         }
296 
addLayout(int layoutId, @Nullable ViewInitializer viewInitializer)297         public TestCaseBuilder addLayout(int layoutId, @Nullable ViewInitializer viewInitializer) {
298             return addLayout(layoutId, viewInitializer, false)
299                     .addLayout(layoutId, viewInitializer, true);
300         }
301 
addLayout(int layoutId, @Nullable ViewInitializer viewInitializer, boolean useHardware)302         public TestCaseBuilder addLayout(int layoutId, @Nullable ViewInitializer viewInitializer,
303                                          boolean useHardware) {
304             mTestCases.add(new TestCase(layoutId, viewInitializer, useHardware));
305             return this;
306         }
307 
addLayout(int layoutId, @Nullable ViewInitializer viewInitializer, boolean useHardware, CountDownLatch readyFence)308         public TestCaseBuilder addLayout(int layoutId, @Nullable ViewInitializer viewInitializer,
309                 boolean useHardware, CountDownLatch readyFence) {
310             TestCase test = new TestCase(layoutId, viewInitializer, useHardware);
311             test.readyFence = readyFence;
312             mTestCases.add(test);
313             return this;
314         }
315 
addCanvasClient(CanvasClient canvasClient)316         public TestCaseBuilder addCanvasClient(CanvasClient canvasClient) {
317             return addCanvasClient(null, canvasClient);
318         }
319 
addCanvasClient(CanvasClient canvasClient, boolean useHardware)320         public TestCaseBuilder addCanvasClient(CanvasClient canvasClient, boolean useHardware) {
321             return addCanvasClient(null, canvasClient, useHardware);
322         }
323 
addCanvasClient(String debugString, CanvasClient canvasClient)324         public TestCaseBuilder addCanvasClient(String debugString, CanvasClient canvasClient) {
325             return addCanvasClient(debugString, canvasClient, false)
326                     .addCanvasClient(debugString, canvasClient, true);
327         }
328 
addCanvasClient(String debugString, CanvasClient canvasClient, boolean useHardware)329         public TestCaseBuilder addCanvasClient(String debugString,
330                     CanvasClient canvasClient, boolean useHardware) {
331             mTestCases.add(new TestCase(canvasClient, debugString, useHardware));
332             return this;
333         }
334 
getTestCases()335         private List<TestCase> getTestCases() {
336             return mTestCases;
337         }
338     }
339 
340     private class TestCase {
341         public int layoutID;
342         public ViewInitializer viewInitializer;
343         /** After launching the test case this fence is used to signal when
344          * to proceed with capture & verification. If this is null the test
345          * proceeds immediately to verification */
346         @Nullable
347         public CountDownLatch readyFence;
348 
349         public CanvasClient canvasClient;
350         public String canvasClientDebugString;
351 
352         public boolean useHardware;
353         public boolean wasTestRan = false;
354 
TestCase(int layoutId, ViewInitializer viewInitializer, boolean useHardware)355         public TestCase(int layoutId, ViewInitializer viewInitializer, boolean useHardware) {
356             this.layoutID = layoutId;
357             this.viewInitializer = viewInitializer;
358             this.useHardware = useHardware;
359         }
360 
TestCase(CanvasClient client, String debugString, boolean useHardware)361         public TestCase(CanvasClient client, String debugString, boolean useHardware) {
362             this.canvasClient = client;
363             this.canvasClientDebugString = debugString;
364             this.useHardware = useHardware;
365         }
366 
getDebugString()367         public String getDebugString() {
368             String debug = "";
369             if (canvasClient != null) {
370                 debug += "CanvasClient : ";
371                 if (canvasClientDebugString != null) {
372                     debug += canvasClientDebugString;
373                 } else {
374                     debug += "no debug string given";
375                 }
376             } else {
377                 debug += "Layout resource : " +
378                         getActivity().getResources().getResourceName(layoutID);
379             }
380             debug += "\nTest ran in " + (useHardware ? "hardware" : "software") + "\n";
381             return debug;
382         }
383     }
384 }
385