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 17package com.android.layoutlib.bridge.intensive; 18 19import com.android.ide.common.rendering.api.LayoutLog; 20import com.android.ide.common.rendering.api.RenderSession; 21import com.android.ide.common.rendering.api.Result; 22import com.android.ide.common.rendering.api.SessionParams; 23import com.android.ide.common.rendering.api.SessionParams.RenderingMode; 24import com.android.ide.common.resources.deprecated.FrameworkResources; 25import com.android.ide.common.resources.deprecated.ResourceItem; 26import com.android.ide.common.resources.deprecated.ResourceRepository; 27import com.android.io.FolderWrapper; 28import com.android.layoutlib.bridge.Bridge; 29import com.android.layoutlib.bridge.android.RenderParamsFlags; 30import com.android.layoutlib.bridge.impl.DelegateManager; 31import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator; 32import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback; 33import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser; 34import com.android.layoutlib.bridge.intensive.util.ImageUtils; 35import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader; 36import com.android.layoutlib.bridge.intensive.util.SessionParamsBuilder; 37import com.android.layoutlib.bridge.intensive.util.TestAssetRepository; 38import com.android.layoutlib.bridge.intensive.util.TestUtils; 39import com.android.tools.layoutlib.java.System_Delegate; 40import com.android.utils.ILogger; 41 42import org.junit.AfterClass; 43import org.junit.Before; 44import org.junit.BeforeClass; 45import org.junit.Rule; 46import org.junit.rules.TestWatcher; 47import org.junit.runner.Description; 48 49import android.annotation.NonNull; 50import android.annotation.Nullable; 51 52import java.awt.image.BufferedImage; 53import java.io.File; 54import java.io.FileNotFoundException; 55import java.io.IOException; 56import java.net.URL; 57import java.util.ArrayList; 58import java.util.Arrays; 59import java.util.concurrent.TimeUnit; 60 61import com.google.android.collect.Lists; 62import com.google.common.collect.ImmutableMap; 63 64import static org.junit.Assert.assertNotNull; 65import 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 */ 90public 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/classes/debug/"; 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 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 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 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 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 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 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, 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 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 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 */ 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 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 407 protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName) 408 throws ClassNotFoundException { 409 return RenderTestBase.renderAndVerify(params, goldenFileName, -1); 410 } 411 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(String tag, String message, Throwable throwable, 423 Object viewCookie, Object data) { 424 System.out.println("FidelityWarning " + tag + ": " + message); 425 if (throwable != null) { 426 throwable.printStackTrace(); 427 } 428 failWithMsg(message == null ? "" : message); 429 } 430 431 @Override 432 public void error(String tag, String message, Object data) { 433 System.out.println("Error " + tag + ": " + message); 434 failWithMsg(message); 435 } 436 437 @Override 438 public void error(String tag, String message, Throwable throwable, Object data) { 439 System.out.println("Error " + tag + ": " + message); 440 if (throwable != null) { 441 throwable.printStackTrace(); 442 } 443 failWithMsg(message); 444 } 445 }; 446 } 447 return sLayoutLibLog; 448 } 449 450 protected static void ignoreAllLogging() { 451 sLayoutLibLog = new LayoutLog(); 452 sLogger = new ILogger() { 453 @Override 454 public void error(Throwable t, String msgFormat, Object... args) { 455 } 456 457 @Override 458 public void warning(String msgFormat, Object... args) { 459 } 460 461 @Override 462 public void info(String msgFormat, Object... args) { 463 } 464 465 @Override 466 public void verbose(String msgFormat, Object... args) { 467 } 468 }; 469 } 470 471 protected static ILogger getLogger() { 472 if (sLogger == null) { 473 sLogger = new ILogger() { 474 @Override 475 public void error(Throwable t, @Nullable String msgFormat, Object... args) { 476 if (t != null) { 477 t.printStackTrace(); 478 } 479 failWithMsg(msgFormat == null ? "" : msgFormat, args); 480 } 481 482 @Override 483 public void warning(@NonNull String msgFormat, Object... args) { 484 failWithMsg(msgFormat, args); 485 } 486 487 @Override 488 public void info(@NonNull String msgFormat, Object... args) { 489 // pass. 490 } 491 492 @Override 493 public void verbose(@NonNull String msgFormat, Object... args) { 494 // pass. 495 } 496 }; 497 } 498 return sLogger; 499 } 500 501 private static void failWithMsg(@NonNull String msgFormat, Object... args) { 502 sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args)); 503 } 504 505 @Before 506 public void beforeTestCase() { 507 // Default class loader with access to the app classes 508 mDefaultClassLoader = new ModuleClassLoader(APP_CLASSES_LOCATION, getClass().getClassLoader()); 509 sRenderMessages.clear(); 510 } 511 512 @NonNull 513 protected LayoutPullParser createParserFromPath(String layoutPath) 514 throws FileNotFoundException { 515 return LayoutPullParser.createFromPath(APP_TEST_RES + "/layout/" + layoutPath); 516 } 517 518 /** 519 * Create a new rendering session and test that rendering the given layout on nexus 5 520 * doesn't throw any exceptions and matches the provided image. 521 */ 522 @Nullable 523 protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName) 524 throws ClassNotFoundException, FileNotFoundException { 525 return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5); 526 } 527 528 /** 529 * Create a new rendering session and test that rendering the given layout on given device 530 * doesn't throw any exceptions and matches the provided image. 531 */ 532 @Nullable 533 protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName, 534 ConfigGenerator deviceConfig) throws ClassNotFoundException, FileNotFoundException { 535 SessionParams params = createSessionParams(layoutFileName, deviceConfig); 536 return renderAndVerify(params, goldenFileName); 537 } 538 539 protected SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig) 540 throws ClassNotFoundException, FileNotFoundException { 541 // Create the layout pull parser. 542 LayoutPullParser parser = createParserFromPath(layoutFileName); 543 // Create LayoutLibCallback. 544 LayoutLibTestCallback layoutLibCallback = 545 new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); 546 layoutLibCallback.initResources(); 547 // TODO: Set up action bar handler properly to test menu rendering. 548 // Create session params. 549 return getSessionParamsBuilder() 550 .setParser(parser) 551 .setConfigGenerator(deviceConfig) 552 .setCallback(layoutLibCallback) 553 .build(); 554 } 555 556 /** 557 * Returns a pre-configured {@link SessionParamsBuilder} for target API 22, Normal rendering 558 * mode, AppTheme as theme and Nexus 5. 559 */ 560 @NonNull 561 protected SessionParamsBuilder getSessionParamsBuilder() { 562 return new SessionParamsBuilder() 563 .setLayoutLog(getLayoutLog()) 564 .setFrameworkResources(sFrameworkRepo) 565 .setConfigGenerator(ConfigGenerator.NEXUS_5) 566 .setProjectResources(sProjectResources) 567 .setTheme("AppTheme", true) 568 .setRenderingMode(RenderingMode.NORMAL) 569 .setTargetSdk(22) 570 .setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true) 571 .setAssetRepository(new TestAssetRepository(TEST_RES_DIR + "/" + APP_TEST_ASSET)); 572 } 573} 574