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