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