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