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