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 
17 package com.android.layoutlib.bridge.intensive;
18 
19 import com.android.ide.common.rendering.api.LayoutLog;
20 import com.android.ide.common.rendering.api.RenderSession;
21 import com.android.ide.common.rendering.api.Result;
22 import com.android.ide.common.rendering.api.SessionParams;
23 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
24 import com.android.ide.common.resources.deprecated.FrameworkResources;
25 import com.android.ide.common.resources.deprecated.ResourceItem;
26 import com.android.ide.common.resources.deprecated.ResourceRepository;
27 import com.android.io.FolderWrapper;
28 import com.android.layoutlib.bridge.Bridge;
29 import com.android.layoutlib.bridge.android.RenderParamsFlags;
30 import com.android.layoutlib.bridge.impl.DelegateManager;
31 import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
32 import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
33 import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
34 import com.android.layoutlib.bridge.intensive.util.ImageUtils;
35 import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader;
36 import com.android.layoutlib.bridge.intensive.util.SessionParamsBuilder;
37 import com.android.layoutlib.bridge.intensive.util.TestAssetRepository;
38 import com.android.layoutlib.bridge.intensive.util.TestUtils;
39 import com.android.tools.layoutlib.java.System_Delegate;
40 import com.android.utils.ILogger;
41 
42 import org.junit.AfterClass;
43 import org.junit.Before;
44 import org.junit.BeforeClass;
45 import org.junit.Rule;
46 import org.junit.rules.TestWatcher;
47 import org.junit.runner.Description;
48 
49 import android.annotation.NonNull;
50 import android.annotation.Nullable;
51 
52 import java.awt.image.BufferedImage;
53 import java.io.File;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 import java.net.URL;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.concurrent.TimeUnit;
60 
61 import com.google.android.collect.Lists;
62 import com.google.common.collect.ImmutableMap;
63 
64 import static org.junit.Assert.assertNotNull;
65 import 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  */
90 public 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/javac/debug/compileDebugJavaWithJavac/classes/";
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 
getPlatformDir()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 
getPlatformDirFromRoot(File root)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 
getPlatformDirFromHostOut(File out)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 
getPlatformDirFromHostOutSdkSdk(File sdkDir)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 
getTestResDir()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
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, null,
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
tearDown()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
render(com.android.ide.common.rendering.api.Bridge bridge, SessionParams params, long frameTimeNanos)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      */
verify(@onNull String goldenImageName, @NonNull BufferedImage image)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
renderAndVerify(SessionParams params, String goldenFileName, long frameTimeNanos)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
renderAndVerify(SessionParams params, String goldenFileName)407     protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName)
408             throws ClassNotFoundException {
409         return RenderTestBase.renderAndVerify(params, goldenFileName, -1);
410     }
411 
getLayoutLog()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(@Nullable String tag, String message,
423                         Throwable throwable, Object cookie, Object data) {
424 
425                     System.out.println("FidelityWarning " + tag + ": " + message);
426                     if (throwable != null) {
427                         throwable.printStackTrace();
428                     }
429                     failWithMsg(message == null ? "" : message);
430                 }
431 
432                 @Override
433                 public void error(String tag, String message, Object data) {
434                     System.out.println("Error " + tag + ": " + message);
435                     failWithMsg(message);
436                 }
437 
438                 @Override
439                 public void error(String tag, String message, Throwable throwable, Object data) {
440                     System.out.println("Error " + tag + ": " + message);
441                     if (throwable != null) {
442                         throwable.printStackTrace();
443                     }
444                     failWithMsg(message);
445                 }
446             };
447         }
448         return sLayoutLibLog;
449     }
450 
ignoreAllLogging()451     protected static void ignoreAllLogging() {
452         sLayoutLibLog = new LayoutLog();
453         sLogger = new ILogger() {
454             @Override
455             public void error(Throwable t, String msgFormat, Object... args) {
456             }
457 
458             @Override
459             public void warning(String msgFormat, Object... args) {
460             }
461 
462             @Override
463             public void info(String msgFormat, Object... args) {
464             }
465 
466             @Override
467             public void verbose(String msgFormat, Object... args) {
468             }
469         };
470     }
471 
getLogger()472     protected static ILogger getLogger() {
473         if (sLogger == null) {
474             sLogger = new ILogger() {
475                 @Override
476                 public void error(Throwable t, @Nullable String msgFormat, Object... args) {
477                     if (t != null) {
478                         t.printStackTrace();
479                     }
480                     failWithMsg(msgFormat == null ? "" : msgFormat, args);
481                 }
482 
483                 @Override
484                 public void warning(@NonNull String msgFormat, Object... args) {
485                     failWithMsg(msgFormat, args);
486                 }
487 
488                 @Override
489                 public void info(@NonNull String msgFormat, Object... args) {
490                     // pass.
491                 }
492 
493                 @Override
494                 public void verbose(@NonNull String msgFormat, Object... args) {
495                     // pass.
496                 }
497             };
498         }
499         return sLogger;
500     }
501 
failWithMsg(@onNull String msgFormat, Object... args)502     private static void failWithMsg(@NonNull String msgFormat, Object... args) {
503         sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args));
504     }
505 
506     @Before
beforeTestCase()507     public void beforeTestCase() {
508         // Default class loader with access to the app classes
509         mDefaultClassLoader = new ModuleClassLoader(APP_CLASSES_LOCATION, getClass().getClassLoader());
510         sRenderMessages.clear();
511     }
512 
513     @NonNull
createParserFromPath(String layoutPath)514     protected LayoutPullParser createParserFromPath(String layoutPath)
515             throws FileNotFoundException {
516         return LayoutPullParser.createFromPath(APP_TEST_RES + "/layout/" + layoutPath);
517     }
518 
519     /**
520      * Create a new rendering session and test that rendering the given layout on nexus 5
521      * doesn't throw any exceptions and matches the provided image.
522      */
523     @Nullable
renderAndVerify(String layoutFileName, String goldenFileName, boolean decoration)524     protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName,
525             boolean decoration)
526             throws ClassNotFoundException, FileNotFoundException {
527         return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5, decoration);
528     }
529 
530     /**
531      * Create a new rendering session and test that rendering the given layout on given device
532      * doesn't throw any exceptions and matches the provided image.
533      */
534     @Nullable
renderAndVerify(String layoutFileName, String goldenFileName, ConfigGenerator deviceConfig, boolean decoration)535     protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName,
536             ConfigGenerator deviceConfig, boolean decoration) throws ClassNotFoundException,
537             FileNotFoundException {
538         SessionParams params = createSessionParams(layoutFileName, deviceConfig);
539         if (!decoration) {
540             params.setForceNoDecor();
541         }
542         return renderAndVerify(params, goldenFileName);
543     }
544 
createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)545     protected SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)
546             throws ClassNotFoundException, FileNotFoundException {
547         // Create the layout pull parser.
548         LayoutPullParser parser = createParserFromPath(layoutFileName);
549         // Create LayoutLibCallback.
550         LayoutLibTestCallback layoutLibCallback =
551                 new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
552         layoutLibCallback.initResources();
553         // TODO: Set up action bar handler properly to test menu rendering.
554         // Create session params.
555         return getSessionParamsBuilder()
556                 .setParser(parser)
557                 .setConfigGenerator(deviceConfig)
558                 .setCallback(layoutLibCallback)
559                 .build();
560     }
561 
562     /**
563      * Returns a pre-configured {@link SessionParamsBuilder} for target API 22, Normal rendering
564      * mode, AppTheme as theme and Nexus 5.
565      */
566     @NonNull
getSessionParamsBuilder()567     protected SessionParamsBuilder getSessionParamsBuilder() {
568         return new SessionParamsBuilder()
569                 .setLayoutLog(getLayoutLog())
570                 .setFrameworkResources(sFrameworkRepo)
571                 .setConfigGenerator(ConfigGenerator.NEXUS_5)
572                 .setProjectResources(sProjectResources)
573                 .setTheme("AppTheme", true)
574                 .setRenderingMode(RenderingMode.NORMAL)
575                 .setTargetSdk(28)
576                 .setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true)
577                 .setAssetRepository(new TestAssetRepository(TEST_RES_DIR + "/" + APP_TEST_ASSET));
578     }
579 }
580