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