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