1 /* 2 * Copyright (C) 2022 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 17 package com.android.layoutlib.bridge.intensive; 18 19 import static org.junit.Assert.assertNotNull; 20 import static org.junit.Assert.fail; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.view.Choreographer; 25 26 import com.android.ide.common.rendering.api.ILayoutLog; 27 import com.android.ide.common.rendering.api.RenderSession; 28 import com.android.ide.common.rendering.api.Result; 29 import com.android.ide.common.rendering.api.SessionParams; 30 import com.android.ide.common.rendering.api.SessionParams.RenderingMode; 31 import com.android.ide.common.resources.ResourceRepository; 32 import com.android.internal.lang.System_Delegate; 33 import com.android.layoutlib.bridge.Bridge; 34 import com.android.layoutlib.bridge.android.RenderParamsFlags; 35 import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator; 36 import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback; 37 import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser; 38 import com.android.layoutlib.bridge.intensive.util.ImageUtils; 39 import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader; 40 import com.android.layoutlib.bridge.intensive.util.SessionParamsBuilder; 41 import com.android.layoutlib.bridge.intensive.util.TestAssetRepository; 42 import com.android.resources.aar.AarSourceResourceRepository; 43 import com.android.resources.aar.FrameworkResourceRepository; 44 import com.android.utils.ILogger; 45 46 import com.google.android.collect.Lists; 47 48 import org.junit.AfterClass; 49 import org.junit.Before; 50 import org.junit.BeforeClass; 51 import org.junit.Rule; 52 import org.junit.rules.TestWatcher; 53 import org.junit.runner.Description; 54 55 import java.awt.image.BufferedImage; 56 import java.io.File; 57 import java.io.FileNotFoundException; 58 import java.io.IOException; 59 import java.net.URL; 60 import java.util.ArrayList; 61 import java.util.Arrays; 62 import java.util.Collections; 63 import java.util.concurrent.TimeUnit; 64 65 /** 66 * Base class for render tests. The render tests load all the framework resources and a project 67 * checked in this test's resources. The main dependencies 68 * are: 69 * 1. Fonts directory. 70 * 2. Framework Resources. 71 * 3. App resources. 72 * 4. build.prop file 73 * <p> 74 * These are configured by two variables set in the system properties. 75 * <p> 76 * 1. platform.dir: This is the directory for the current platform in the built SDK 77 * (.../sdk/platforms/android-<version>). 78 * <p> 79 * The fonts are platform.dir/data/fonts. 80 * The Framework resources are platform.dir/data/res. 81 * build.prop is at platform.dir/build.prop. 82 * <p> 83 * 2. test_res.dir: This is the directory for the resources of the test. If not specified, this 84 * falls back to getClass().getProtectionDomain().getCodeSource().getLocation() 85 * <p> 86 * The app resources are at: test_res.dir/testApp/MyApplication/app/src/main/res 87 */ 88 public class RenderTestBase { 89 90 /** 91 * Listener for render process. 92 */ 93 public interface RenderSessionListener { 94 95 /** 96 * Called before session is disposed after rendering. 97 */ beforeDisposed(RenderSession session)98 void beforeDisposed(RenderSession session); 99 } 100 101 private static final String NATIVE_LIB_PATH_PROPERTY = "native.lib.path"; 102 private static final String FONT_DIR_PROPERTY = "font.dir"; 103 private static final String ICU_DATA_PATH_PROPERTY = "icu.data.path"; 104 private static final String HYPHEN_DATA_DIR_PROPERTY = "hyphen.data.dir"; 105 private static final String KEYBOARD_DIR_PROPERTY = "keyboard.dir"; 106 private static final String PLATFORM_DIR_PROPERTY = "platform.dir"; 107 private static final String RESOURCE_DIR_PROPERTY = "test_res.dir"; 108 109 private static final String NATIVE_LIB_DIR_PATH; 110 private static final String FONT_DIR; 111 private static final String ICU_DATA_PATH; 112 private static final String HYPHEN_DATA_DIR; 113 private static final String KEYBOARD_DIR; 114 protected static final String PLATFORM_DIR; 115 private static final String TEST_RES_DIR; 116 /** Location of the app to test inside {@link #TEST_RES_DIR} */ 117 protected static final String APP_TEST_DIR = "testApp/MyApplication"; 118 /** Location of the app's res dir inside {@link #TEST_RES_DIR} */ 119 private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res"; 120 /** Location of the app's asset dir inside {@link #TEST_RES_DIR} */ 121 private static final String APP_TEST_ASSET = APP_TEST_DIR + "/src/main/assets/"; 122 private static final String APP_CLASSES_LOCATION = 123 APP_TEST_DIR + "/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/"; 124 protected static Bridge sBridge; 125 /** List of log messages generated by a render call. It can be used to find specific errors */ 126 protected static ArrayList<String> sRenderMessages = Lists.newArrayList(); 127 private static ILayoutLog sLayoutLibLog; 128 private static FrameworkResourceRepository sFrameworkRepo; 129 private static ResourceRepository sProjectResources; 130 private static ILogger sLogger; 131 132 static { 133 // Test that System Properties are properly set. 134 PLATFORM_DIR = getPlatformDir(); 135 if (PLATFORM_DIR == null) { 136 fail(String.format("System Property %1$s not properly set. The value is %2$s", 137 PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY))); 138 } 139 140 NATIVE_LIB_DIR_PATH = getNativeLibDirPath(); 141 FONT_DIR = getFontDir(); 142 ICU_DATA_PATH = getIcuDataPath(); 143 HYPHEN_DATA_DIR = getHyphenDataDir(); 144 KEYBOARD_DIR = getKeyboardDir(); 145 146 TEST_RES_DIR = getTestResDir(); 147 if (TEST_RES_DIR == null) { 148 fail(String.format("System property %1$s.dir not properly set. The value is %2$s", 149 RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY))); 150 } 151 } 152 153 @Rule 154 public TestWatcher sRenderMessageWatcher = new TestWatcher() { 155 @Override 156 protected void succeeded(Description description) { 157 // We only check error messages if the rest of the test case was successful. 158 if (!sRenderMessages.isEmpty()) { 159 fail(description.getMethodName() + " render error message: " + 160 sRenderMessages.get(0)); 161 } 162 } 163 }; 164 165 @Rule 166 public TestWatcher sMemoryLeakChecker = new TestWatcher() { 167 @Override 168 protected void succeeded(Description description) { 169 for (int i = Choreographer.CALLBACK_INPUT; i <= Choreographer.CALLBACK_COMMIT; ++i) { 170 if (Choreographer.getInstance().mCallbackQueues[i].mHead != null) { 171 fail("Memory leak: leftover frame callbacks are detected in Choreographer"); 172 } 173 } 174 } 175 }; 176 177 protected ClassLoader mDefaultClassLoader; 178 getNativeLibDirPath()179 private static String getNativeLibDirPath() { 180 String nativeLibDirPath = System.getProperty(NATIVE_LIB_PATH_PROPERTY); 181 if (nativeLibDirPath != null) { 182 File nativeLibDir = new File(nativeLibDirPath); 183 if (nativeLibDir.isDirectory()) { 184 nativeLibDirPath = nativeLibDir.getAbsolutePath(); 185 } else { 186 nativeLibDirPath = null; 187 } 188 } 189 if (nativeLibDirPath == null) { 190 nativeLibDirPath = PLATFORM_DIR + "/../../../../../lib64/"; 191 } 192 return nativeLibDirPath; 193 } 194 getFontDir()195 private static String getFontDir() { 196 String fontDir = System.getProperty(FONT_DIR_PROPERTY); 197 if (fontDir == null) { 198 // The fonts are built into out/host/common/obj/PACKAGING/fonts_intermediates 199 // as specified in build/make/core/layoutlib_data.mk, and PLATFORM_DIR is 200 // out/host/[arch]/sdk/sdk*/android-sdk*/platforms/android* 201 fontDir = PLATFORM_DIR + 202 "/../../../../../../common/obj/PACKAGING/fonts_intermediates"; 203 } 204 return fontDir; 205 } 206 getIcuDataPath()207 private static String getIcuDataPath() { 208 String icuDataPath = System.getProperty(ICU_DATA_PATH_PROPERTY); 209 if (icuDataPath == null) { 210 icuDataPath = PLATFORM_DIR + "/../../../../../com.android.i18n/etc/icu/icudt71l.dat"; 211 } 212 return icuDataPath; 213 } 214 getHyphenDataDir()215 private static String getHyphenDataDir() { 216 String hyphenDataDir = System.getProperty(HYPHEN_DATA_DIR_PROPERTY); 217 if (hyphenDataDir == null) { 218 hyphenDataDir = 219 PLATFORM_DIR + "/../../../../../../common/obj/PACKAGING/hyphen_intermediates"; 220 } 221 return hyphenDataDir; 222 } 223 getKeyboardDir()224 private static String getKeyboardDir() { 225 String keyboardDir = System.getProperty(KEYBOARD_DIR_PROPERTY); 226 if (keyboardDir == null) { 227 // The keyboard files are built into 228 // out/host/common/obj/PACKAGING/keyboards_intermediates 229 // as specified in build/make/core/layoutlib_data.mk, and PLATFORM_DIR is 230 // out/host/[arch]/sdk/sdk*/android-sdk*/platforms/android* 231 keyboardDir = PLATFORM_DIR + 232 "/../../../../../../common/obj/PACKAGING/keyboards_intermediates"; 233 } 234 return keyboardDir; 235 } 236 getPlatformDir()237 private static String getPlatformDir() { 238 String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY); 239 if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) { 240 return platformDir; 241 } 242 // System Property not set. Try to find the directory in the build directory. 243 String androidHostOut = System.getenv("ANDROID_HOST_OUT"); 244 if (androidHostOut != null) { 245 platformDir = getPlatformDirFromHostOut(new File(androidHostOut)); 246 if (platformDir != null) { 247 return platformDir; 248 } 249 } 250 String workingDirString = System.getProperty("user.dir"); 251 File workingDir = new File(workingDirString); 252 // Test if workingDir is android checkout root. 253 platformDir = getPlatformDirFromRoot(workingDir); 254 if (platformDir != null) { 255 return platformDir; 256 } 257 258 // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge. 259 File currentDir = workingDir; 260 if (currentDir.getName().equalsIgnoreCase("bridge")) { 261 currentDir = currentDir.getParentFile(); 262 } 263 264 // Find frameworks/layoutlib 265 while (currentDir != null && !"layoutlib".equals(currentDir.getName())) { 266 currentDir = currentDir.getParentFile(); 267 } 268 269 if (currentDir == null || 270 currentDir.getParentFile() == null || 271 !"frameworks".equals(currentDir.getParentFile().getName())) { 272 return null; 273 } 274 275 // Test if currentDir is platform/frameworks/layoutlib. That is, root should be 276 // workingDir/../../ (2 levels up) 277 for (int i = 0; i < 2; i++) { 278 if (currentDir != null) { 279 currentDir = currentDir.getParentFile(); 280 } 281 } 282 return currentDir == null ? null : getPlatformDirFromRoot(currentDir); 283 } 284 getPlatformDirFromRoot(File root)285 private static String getPlatformDirFromRoot(File root) { 286 if (!root.isDirectory()) { 287 return null; 288 } 289 File out = new File(root, "out"); 290 if (!out.isDirectory()) { 291 return null; 292 } 293 File host = new File(out, "host"); 294 if (!host.isDirectory()) { 295 return null; 296 } 297 File[] hosts = host.listFiles(path -> path.isDirectory() && 298 (path.getName().startsWith("linux-") || 299 path.getName().startsWith("darwin-"))); 300 assert hosts != null; 301 for (File hostOut : hosts) { 302 String platformDir = getPlatformDirFromHostOut(hostOut); 303 if (platformDir != null) { 304 return platformDir; 305 } 306 } 307 308 return null; 309 } 310 getPlatformDirFromHostOut(File out)311 private static String getPlatformDirFromHostOut(File out) { 312 if (!out.isDirectory()) { 313 return null; 314 } 315 File sdkDir = new File(out, "sdk"); 316 if (!sdkDir.isDirectory()) { 317 return null; 318 } 319 File[] sdkDirs = sdkDir.listFiles(path -> { 320 // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7) 321 return path.isDirectory() && path.getName().startsWith("sdk"); 322 }); 323 assert sdkDirs != null; 324 for (File dir : sdkDirs) { 325 String platformDir = getPlatformDirFromHostOutSdkSdk(dir); 326 if (platformDir != null) { 327 return platformDir; 328 } 329 } 330 return null; 331 } 332 getPlatformDirFromHostOutSdkSdk(File sdkDir)333 private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) { 334 File[] possibleSdks = sdkDir.listFiles( 335 path -> path.isDirectory() && path.getName().contains("android-sdk")); 336 assert possibleSdks != null; 337 for (File possibleSdk : possibleSdks) { 338 File platformsDir = new File(possibleSdk, "platforms"); 339 File[] platforms = platformsDir.listFiles( 340 path -> path.isDirectory() && path.getName().startsWith("android-")); 341 if (platforms == null || platforms.length == 0) { 342 continue; 343 } 344 Arrays.sort(platforms, (o1, o2) -> { 345 final int MAX_VALUE = 1000; 346 String suffix1 = o1.getName().substring("android-".length()); 347 String suffix2 = o2.getName().substring("android-".length()); 348 int suff1, suff2; 349 try { 350 suff1 = Integer.parseInt(suffix1); 351 } catch (NumberFormatException e) { 352 suff1 = MAX_VALUE; 353 } 354 try { 355 suff2 = Integer.parseInt(suffix2); 356 } catch (NumberFormatException e) { 357 suff2 = MAX_VALUE; 358 } 359 if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) { 360 return suff2 - suff1; 361 } 362 return suffix2.compareTo(suffix1); 363 }); 364 return platforms[0].getAbsolutePath(); 365 } 366 return null; 367 } 368 getTestResDir()369 private static String getTestResDir() { 370 String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY); 371 if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) { 372 return resourceDir; 373 } 374 // TEST_RES_DIR not explicitly set. Fallback to the class's source location. 375 try { 376 URL location = RenderTestBase.class.getProtectionDomain().getCodeSource().getLocation(); 377 return new File(location.getPath()).exists() ? location.getPath() : null; 378 } catch (NullPointerException e) { 379 // Prevent a lot of null checks by just catching the exception. 380 return null; 381 } 382 } 383 384 /** 385 * Initialize the bridge and the resource maps. 386 */ 387 @BeforeClass beforeClass()388 public static void beforeClass() { 389 File data_dir = new File(PLATFORM_DIR, "data"); 390 File res = new File(data_dir, "res"); 391 sFrameworkRepo = 392 FrameworkResourceRepository.create( 393 res.getAbsoluteFile().toPath(), Collections.emptySet(), null, false); 394 395 File projectRes = new File(TEST_RES_DIR + "/" + APP_TEST_RES); 396 sProjectResources = 397 AarSourceResourceRepository.create( 398 projectRes.getAbsoluteFile().toPath(), "Application"); 399 400 File fontLocation = new File(FONT_DIR); 401 File buildProp = new File(PLATFORM_DIR, "build.prop"); 402 File attrs = new File(res, "values" + File.separator + "attrs.xml"); 403 404 String[] keyboardPaths = new String[] { KEYBOARD_DIR + "/Generic.kcm" }; 405 sBridge = new Bridge(); 406 sBridge.init( 407 ConfigGenerator.loadProperties(buildProp), 408 fontLocation, 409 NATIVE_LIB_DIR_PATH, 410 ICU_DATA_PATH, 411 HYPHEN_DATA_DIR, 412 keyboardPaths, 413 ConfigGenerator.getEnumMap(attrs), 414 getLayoutLog()); 415 Bridge.getLock().lock(); 416 try { 417 Bridge.setLog(getLayoutLog()); 418 } finally { 419 Bridge.getLock().unlock(); 420 } 421 } 422 423 @AfterClass tearDown()424 public static void tearDown() { 425 sLayoutLibLog = null; 426 sFrameworkRepo = null; 427 sProjectResources = null; 428 sLogger = null; 429 sBridge = null; 430 } 431 432 @NonNull render(com.android.ide.common.rendering.api.Bridge bridge, SessionParams params, long frameTimeNanos)433 protected static RenderResult render(com.android.ide.common.rendering.api.Bridge bridge, 434 SessionParams params, 435 long frameTimeNanos) { 436 return render(bridge, params, frameTimeNanos, null); 437 } 438 439 @NonNull render(com.android.ide.common.rendering.api.Bridge bridge, SessionParams params, long frameTimeNanos, @Nullable RenderSessionListener listener)440 protected static RenderResult render(com.android.ide.common.rendering.api.Bridge bridge, 441 SessionParams params, 442 long frameTimeNanos, 443 @Nullable RenderSessionListener listener) { 444 // TODO: Set up action bar handler properly to test menu rendering. 445 // Create session params. 446 System_Delegate.setBootTimeNanos(TimeUnit.MILLISECONDS.toNanos(871732800000L)); 447 System_Delegate.setNanosTime(TimeUnit.MILLISECONDS.toNanos(871732800000L)); 448 RenderSession session = bridge.createSession(params); 449 450 try { 451 if (frameTimeNanos != -1) { 452 session.setElapsedFrameTimeNanos(frameTimeNanos); 453 } 454 455 if (!session.getResult().isSuccess()) { 456 getLogger().error(session.getResult().getException(), 457 session.getResult().getErrorMessage()); 458 } 459 else { 460 // Render the session with a timeout of 50s. 461 Result renderResult = session.render(50000); 462 if (!renderResult.isSuccess()) { 463 getLogger().error(session.getResult().getException(), 464 session.getResult().getErrorMessage()); 465 } 466 } 467 if (listener != null) { 468 listener.beforeDisposed(session); 469 } 470 471 return RenderResult.getFromSession(session); 472 } finally { 473 session.dispose(); 474 } 475 } 476 477 /** 478 * Compares the golden image with the passed image 479 */ verify(@onNull String goldenImageName, @NonNull BufferedImage image)480 protected static void verify(@NonNull String goldenImageName, @NonNull BufferedImage image) { 481 try { 482 String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenImageName; 483 ImageUtils.requireSimilar(goldenImagePath, image); 484 } catch (IOException e) { 485 getLogger().error(e, e.getMessage()); 486 } 487 } 488 489 /** 490 * Create a new rendering session and test that rendering the given layout doesn't throw any 491 * exceptions and matches the provided image. 492 * <p> 493 * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates 494 * how far in the future is. 495 */ 496 @Nullable renderAndVerify(SessionParams params, String goldenFileName, long frameTimeNanos)497 protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName, 498 long frameTimeNanos) throws ClassNotFoundException { 499 RenderResult result = RenderTestBase.render(sBridge, params, frameTimeNanos); 500 assertNotNull(result.getImage()); 501 verify(goldenFileName, result.getImage()); 502 503 return result; 504 } 505 506 /** 507 * Create a new rendering session and test that rendering the given layout doesn't throw any 508 * exceptions and matches the provided image. 509 */ 510 @Nullable renderAndVerify(SessionParams params, String goldenFileName)511 protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName) 512 throws ClassNotFoundException { 513 return RenderTestBase.renderAndVerify(params, goldenFileName, TimeUnit.SECONDS.toNanos(2)); 514 } 515 getLayoutLog()516 protected static ILayoutLog getLayoutLog() { 517 if (sLayoutLibLog == null) { 518 sLayoutLibLog = new ILayoutLog() { 519 @Override 520 public void warning(@Nullable String tag, @NonNull String message, @Nullable Object viewCookie, 521 @Nullable Object data) { 522 System.out.println("Warning " + tag + ": " + message); 523 failWithMsg(message); 524 } 525 526 @Override 527 public void fidelityWarning(@Nullable String tag, String message, 528 Throwable throwable, Object cookie, Object data) { 529 530 System.out.println("FidelityWarning " + tag + ": " + message); 531 if (throwable != null) { 532 throwable.printStackTrace(); 533 } 534 failWithMsg(message == null ? "" : message); 535 } 536 537 @Override 538 public void error(@Nullable String tag, @NonNull String message, @Nullable Object viewCookie, 539 @Nullable Object data) { 540 System.out.println("Error " + tag + ": " + message); 541 failWithMsg(message); 542 } 543 544 @Override 545 public void error(@Nullable String tag, @NonNull String message, @Nullable Throwable throwable, 546 @Nullable Object viewCookie, @Nullable Object data) { 547 System.out.println("Error " + tag + ": " + message); 548 if (throwable != null) { 549 throwable.printStackTrace(); 550 } 551 failWithMsg(message); 552 } 553 554 @Override 555 public void logAndroidFramework(int priority, String tag, String message) { 556 System.out.println("Android framework message " + tag + ": " + message); 557 } 558 }; 559 } 560 return sLayoutLibLog; 561 } 562 ignoreAllLogging()563 protected static void ignoreAllLogging() { 564 sLayoutLibLog = new ILayoutLog() {}; 565 sLogger = new ILogger() { 566 @Override 567 public void error(Throwable t, String msgFormat, Object... args) { 568 } 569 570 @Override 571 public void warning(String msgFormat, Object... args) { 572 } 573 574 @Override 575 public void info(String msgFormat, Object... args) { 576 } 577 578 @Override 579 public void verbose(String msgFormat, Object... args) { 580 } 581 }; 582 } 583 getLogger()584 protected static ILogger getLogger() { 585 if (sLogger == null) { 586 sLogger = new ILogger() { 587 @Override 588 public void error(Throwable t, @Nullable String msgFormat, Object... args) { 589 if (t != null) { 590 t.printStackTrace(); 591 } 592 failWithMsg(msgFormat == null ? "" : msgFormat, args); 593 } 594 595 @Override 596 public void warning(@NonNull String msgFormat, Object... args) { 597 failWithMsg(msgFormat, args); 598 } 599 600 @Override 601 public void info(@NonNull String msgFormat, Object... args) { 602 // pass. 603 } 604 605 @Override 606 public void verbose(@NonNull String msgFormat, Object... args) { 607 // pass. 608 } 609 }; 610 } 611 return sLogger; 612 } 613 failWithMsg(@onNull String msgFormat, Object... args)614 private static void failWithMsg(@NonNull String msgFormat, Object... args) { 615 sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args)); 616 } 617 618 @Before beforeTestCase()619 public void beforeTestCase() { 620 // Default class loader with access to the app classes 621 mDefaultClassLoader = new ModuleClassLoader(APP_CLASSES_LOCATION, getClass().getClassLoader()); 622 sRenderMessages.clear(); 623 } 624 625 @NonNull createParserFromPath(String layoutPath)626 protected LayoutPullParser createParserFromPath(String layoutPath) 627 throws FileNotFoundException { 628 return LayoutPullParser.createFromPath(APP_TEST_RES + "/layout/" + layoutPath); 629 } 630 631 /** 632 * Create a new rendering session and test that rendering the given layout on nexus 5 633 * doesn't throw any exceptions and matches the provided image. 634 */ 635 @Nullable renderAndVerify(String layoutFileName, String goldenFileName, boolean decoration)636 protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName, 637 boolean decoration) 638 throws ClassNotFoundException, FileNotFoundException { 639 return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5, decoration); 640 } 641 642 /** 643 * Create a new rendering session and test that rendering the given layout on given device 644 * doesn't throw any exceptions and matches the provided image. 645 */ 646 @Nullable renderAndVerify(String layoutFileName, String goldenFileName, ConfigGenerator deviceConfig, boolean decoration)647 protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName, 648 ConfigGenerator deviceConfig, boolean decoration) throws ClassNotFoundException, 649 FileNotFoundException { 650 SessionParams params = createSessionParams(layoutFileName, deviceConfig); 651 if (!decoration) { 652 params.setForceNoDecor(); 653 } 654 return renderAndVerify(params, goldenFileName); 655 } 656 createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)657 protected SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig) 658 throws ClassNotFoundException, FileNotFoundException { 659 // Create the layout pull parser. 660 LayoutPullParser parser = createParserFromPath(layoutFileName); 661 // Create LayoutLibCallback. 662 LayoutLibTestCallback layoutLibCallback = 663 new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); 664 layoutLibCallback.initResources(); 665 // TODO: Set up action bar handler properly to test menu rendering. 666 // Create session params. 667 return getSessionParamsBuilder() 668 .setParser(parser) 669 .setConfigGenerator(deviceConfig) 670 .setCallback(layoutLibCallback) 671 .build(); 672 } 673 674 /** 675 * Returns a pre-configured {@link SessionParamsBuilder} for target API 22, Normal rendering 676 * mode, AppTheme as theme and Nexus 5. 677 */ 678 @NonNull getSessionParamsBuilder()679 protected SessionParamsBuilder getSessionParamsBuilder() { 680 return new SessionParamsBuilder() 681 .setLayoutLog(getLayoutLog()) 682 .setFrameworkResources(sFrameworkRepo) 683 .setConfigGenerator(ConfigGenerator.NEXUS_5) 684 .setProjectResources(sProjectResources) 685 .setTheme("AppTheme", true) 686 .setRenderingMode(RenderingMode.NORMAL) 687 .setTargetSdk(28) 688 .setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true) 689 .setAssetRepository(new TestAssetRepository(TEST_RES_DIR + "/" + APP_TEST_ASSET)); 690 } 691 } 692