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