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