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