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