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