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