2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.layoutlib.bridge.intensive;
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.FrameworkResources;
25 import com.android.ide.common.resources.ResourceItem;
26 import com.android.ide.common.resources.ResourceRepository;
27 import com.android.ide.common.resources.ResourceResolver;
28 import com.android.ide.common.resources.configuration.FolderConfiguration;
29 import com.android.io.FolderWrapper;
30 import com.android.layoutlib.bridge.Bridge;
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.resources.Density;
35 import com.android.resources.Navigation;
36 import com.android.utils.ILogger;
38 import org.junit.Before;
39 import org.junit.Test;
41 import android.annotation.NonNull;
44 import java.io.FileFilter;
45 import java.io.IOException;
47 import java.util.Arrays;
48 import java.util.Comparator;
50 import static org.junit.Assert.fail;
53 * This is a set of tests that loads all the framework resources and a project checked in this
54 * test's resources. The main dependencies
57 * 2. Framework Resources.
61 * These are configured by two variables set in the system properties.
63 * 1. platform.dir: This is the directory for the current platform in the built SDK
64 * (.../sdk/platforms/android-<version>).
66 * The fonts are platform.dir/data/fonts.
67 * The Framework resources are platform.dir/data/res.
68 * build.prop is at platform.dir/build.prop.
70 * 2. test_res.dir: This is the directory for the resources of the test. If not specified, this
71 * falls back to getClass().getProtectionDomain().getCodeSource().getLocation()
73 * The app resources are at: test_res.dir/testApp/MyApplication/app/src/main/res
77 private static final String PLATFORM_DIR_PROPERTY = "platform.dir";
78 private static final String RESOURCE_DIR_PROPERTY = "test_res.dir";
80 private static final String PLATFORM_DIR;
81 private static final String TEST_RES_DIR;
82 /** Location of the app to test inside {@link #TEST_RES_DIR}*/
83 private static final String APP_TEST_DIR = "/testApp/MyApplication";
84 /** Location of the app's res dir inside {@link #TEST_RES_DIR}*/
85 private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res";
87 private LayoutLog mLayoutLibLog;
88 private FrameworkResources mFrameworkRepo;
89 private ResourceRepository mProjectResources;
90 private ILogger mLogger;
91 private Bridge mBridge;
94 // Test that System Properties are properly set.
95 PLATFORM_DIR = getPlatformDir();
96 if (PLATFORM_DIR == null) {
97 fail(String.format("System Property %1$s not properly set. The value is %2$s",
98 PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY)));
101 TEST_RES_DIR = getTestResDir();
102 if (TEST_RES_DIR == null) {
103 fail(String.format("System property %1$s.dir not properly set. The value is %2$s",
104 RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY)));
108 private static String getPlatformDir() {
109 String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY);
110 if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) {
113 // System Property not set. Try to find the directory in the build directory.
114 String androidHostOut = System.getenv("ANDROID_HOST_OUT");
115 if (androidHostOut != null) {
116 platformDir = getPlatformDirFromHostOut(new File(androidHostOut));
117 if (platformDir != null) {
121 String workingDirString = System.getProperty("user.dir");
122 File workingDir = new File(workingDirString);
123 // Test if workingDir is android checkout root.
124 platformDir = getPlatformDirFromRoot(workingDir);
125 if (platformDir != null) {
129 // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge.
130 File currentDir = workingDir;
131 if (currentDir.getName().equalsIgnoreCase("bridge")) {
132 currentDir = currentDir.getParentFile();
134 // Test if currentDir is platform/frameworks/base/tools/layoutlib. That is, root should be
135 // workingDir/../../../../ (4 levels up)
136 for (int i = 0; i < 4; i++) {
137 if (currentDir != null) {
138 currentDir = currentDir.getParentFile();
141 return currentDir == null ? null : getPlatformDirFromRoot(currentDir);
144 private static String getPlatformDirFromRoot(File root) {
145 if (!root.isDirectory()) {
148 File out = new File(root, "out");
149 if (!out.isDirectory()) {
152 File host = new File(out, "host");
153 if (!host.isDirectory()) {
156 File[] hosts = host.listFiles(new FileFilter() {
158 public boolean accept(File path) {
159 return path.isDirectory() && (path.getName().startsWith("linux-") || path.getName()
160 .startsWith("darwin-"));
163 for (File hostOut : hosts) {
164 String platformDir = getPlatformDirFromHostOut(hostOut);
165 if (platformDir != null) {
172 private static String getPlatformDirFromHostOut(File out) {
173 if (!out.isDirectory()) {
176 File sdkDir = new File(out, "sdk");
177 if (!sdkDir.isDirectory()) {
180 File[] sdkDirs = sdkDir.listFiles(new FileFilter() {
182 public boolean accept(File path) {
183 // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7)
184 return path.isDirectory() && path.getName().startsWith("sdk");
187 for (File dir : sdkDirs) {
188 String platformDir = getPlatformDirFromHostOutSdkSdk(dir);
189 if (platformDir != null) {
196 private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) {
197 File[] possibleSdks = sdkDir.listFiles(new FileFilter() {
199 public boolean accept(File path) {
200 return path.isDirectory() && path.getName().contains("android-sdk");
203 for (File possibleSdk : possibleSdks) {
204 File platformsDir = new File(possibleSdk, "platforms");
205 File[] platforms = platformsDir.listFiles(new FileFilter() {
207 public boolean accept(File path) {
208 return path.isDirectory() && path.getName().startsWith("android-");
211 if (platforms == null || platforms.length == 0) {
214 Arrays.sort(platforms, new Comparator<File>() {
215 // Codenames before ints. Higher APIs precede lower.
217 public int compare(File o1, File o2) {
218 final int MAX_VALUE = 1000;
219 String suffix1 = o1.getName().substring("android-".length());
220 String suffix2 = o2.getName().substring("android-".length());
223 suff1 = Integer.parseInt(suffix1);
224 } catch (NumberFormatException e) {
228 suff2 = Integer.parseInt(suffix2);
229 } catch (NumberFormatException e) {
232 if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) {
233 return suff2 - suff1;
235 return suffix2.compareTo(suffix1);
238 return platforms[0].getAbsolutePath();
243 private static String getTestResDir() {
244 String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY);
245 if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) {
248 // TEST_RES_DIR not explicitly set. Fallback to the class's source location.
250 URL location = Main.class.getProtectionDomain().getCodeSource().getLocation();
251 return new File(location.getPath()).exists() ? location.getPath() : null;
252 } catch (NullPointerException e) {
253 // Prevent a lot of null checks by just catching the exception.
258 * Initialize the bridge and the resource maps.
261 public void setUp() {
262 File data_dir = new File(PLATFORM_DIR, "data");
263 File res = new File(data_dir, "res");
264 mFrameworkRepo = new FrameworkResources(new FolderWrapper(res));
265 mFrameworkRepo.loadResources();
266 mFrameworkRepo.loadPublicResources(getLogger());
269 new ResourceRepository(new FolderWrapper(TEST_RES_DIR + APP_TEST_RES), false) {
272 protected ResourceItem createResourceItem(@NonNull String name) {
273 return new ResourceItem(name);
276 mProjectResources.loadResources();
278 File fontLocation = new File(data_dir, "fonts");
279 File buildProp = new File(PLATFORM_DIR, "build.prop");
280 File attrs = new File(res, "values" + File.separator + "attrs.xml");
281 mBridge = new Bridge();
282 mBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation,
283 ConfigGenerator.getEnumMap(attrs), getLayoutLog());
286 /** Text activity.xml */
288 public void testActivity() throws ClassNotFoundException {
289 renderAndVerify("activity.xml", "activity.png");
293 /** Test allwidgets.xml */
295 public void testAllWidgets() throws ClassNotFoundException {
296 renderAndVerify("allwidgets.xml", "allwidgets.png");
299 /** Test expand_layout.xml */
301 public void testExpand() throws ClassNotFoundException {
302 // Create the layout pull parser.
303 LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" +
304 "expand_vert_layout.xml");
305 // Create LayoutLibCallback.
306 LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger());
307 layoutLibCallback.initResources();
309 ConfigGenerator customConfigGenerator = new ConfigGenerator()
312 .setDensity(Density.XHIGH)
313 .setNavigation(Navigation.NONAV);
315 SessionParams params = getSessionParams(parser, customConfigGenerator,
316 layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", RenderingMode.V_SCROLL,
319 renderAndVerify(params, "expand_vert_layout.png");
321 customConfigGenerator = new ConfigGenerator()
323 .setScreenHeight(300)
324 .setDensity(Density.XHIGH)
325 .setNavigation(Navigation.NONAV);
326 parser = new LayoutPullParser(APP_TEST_RES + "/layout/" +
327 "expand_horz_layout.xml");
328 params = getSessionParams(parser, customConfigGenerator,
329 layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", RenderingMode
332 renderAndVerify(params, "expand_horz_layout.png");
336 * Create a new rendering session and test that rendering given layout on nexus 5
337 * doesn't throw any exceptions and matches the provided image.
339 private void renderAndVerify(SessionParams params, String goldenFileName)
340 throws ClassNotFoundException {
341 // TODO: Set up action bar handler properly to test menu rendering.
342 // Create session params.
343 RenderSession session = mBridge.createSession(params);
344 if (!session.getResult().isSuccess()) {
345 getLogger().error(session.getResult().getException(),
346 session.getResult().getErrorMessage());
348 // Render the session with a timeout of 50s.
349 Result renderResult = session.render(50000);
350 if (!renderResult.isSuccess()) {
351 getLogger().error(session.getResult().getException(),
352 session.getResult().getErrorMessage());
355 String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenFileName;
356 ImageUtils.requireSimilar(goldenImagePath, session.getImage());
357 } catch (IOException e) {
358 getLogger().error(e, e.getMessage());
363 * Create a new rendering session and test that rendering given layout on nexus 5
364 * doesn't throw any exceptions and matches the provided image.
366 private void renderAndVerify(String layoutFileName, String goldenFileName)
367 throws ClassNotFoundException {
368 // Create the layout pull parser.
369 LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + layoutFileName);
370 // Create LayoutLibCallback.
371 LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger());
372 layoutLibCallback.initResources();
373 // TODO: Set up action bar handler properly to test menu rendering.
374 // Create session params.
375 SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
376 layoutLibCallback, "Theme.Material.Light.DarkActionBar", RenderingMode.NORMAL, 22);
377 renderAndVerify(params, goldenFileName);
381 * Uses Theme.Material and Target sdk version as 22.
383 private SessionParams getSessionParams(LayoutPullParser layoutParser,
384 ConfigGenerator configGenerator, LayoutLibTestCallback layoutLibCallback,
385 String themeName, RenderingMode renderingMode, int targetSdk) {
386 FolderConfiguration config = configGenerator.getFolderConfig();
387 ResourceResolver resourceResolver =
388 ResourceResolver.create(mProjectResources.getConfiguredResources(config),
389 mFrameworkRepo.getConfiguredResources(config),
392 return new SessionParams(
395 null /*used for caching*/,
396 configGenerator.getHardwareConfig(),
404 private LayoutLog getLayoutLog() {
405 if (mLayoutLibLog == null) {
406 mLayoutLibLog = new LayoutLog() {
408 public void warning(String tag, String message, Object data) {
409 System.out.println("Warning " + tag + ": " + message);
410 failWithMsg(message);
414 public void fidelityWarning(String tag, String message, Throwable throwable,
416 System.out.println("FidelityWarning " + tag + ": " + message);
417 if (throwable != null) {
418 throwable.printStackTrace();
420 failWithMsg(message);
424 public void error(String tag, String message, Object data) {
425 System.out.println("Error " + tag + ": " + message);
426 failWithMsg(message);
430 public void error(String tag, String message, Throwable throwable, Object data) {
431 System.out.println("Error " + tag + ": " + message);
432 if (throwable != null) {
433 throwable.printStackTrace();
435 failWithMsg(message);
439 return mLayoutLibLog;
442 private ILogger getLogger() {
443 if (mLogger == null) {
444 mLogger = new ILogger() {
446 public void error(Throwable t, String msgFormat, Object... args) {
450 failWithMsg(msgFormat, args);
454 public void warning(@NonNull String msgFormat, Object... args) {
455 failWithMsg(msgFormat, args);
459 public void info(@NonNull String msgFormat, Object... args) {
464 public void verbose(@NonNull String msgFormat, Object... args) {
472 private static void failWithMsg(@NonNull String msgFormat, Object... args) {
473 fail(args == null ? "" : String.format(msgFormat, args));