• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 static org.junit.Assert.assertNotNull;
20 import static org.junit.Assert.fail;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.view.Choreographer;
25 
26 import com.android.ide.common.rendering.api.ILayoutLog;
27 import com.android.ide.common.rendering.api.RenderSession;
28 import com.android.ide.common.rendering.api.Result;
29 import com.android.ide.common.rendering.api.SessionParams;
30 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
31 import com.android.ide.common.resources.ResourceRepository;
32 import com.android.internal.lang.System_Delegate;
33 import com.android.layoutlib.bridge.Bridge;
34 import com.android.layoutlib.bridge.android.RenderParamsFlags;
35 import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
36 import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
37 import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
38 import com.android.layoutlib.bridge.intensive.util.ImageUtils;
39 import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader;
40 import com.android.layoutlib.bridge.intensive.util.SessionParamsBuilder;
41 import com.android.layoutlib.bridge.intensive.util.TestAssetRepository;
42 import com.android.resources.aar.AarSourceResourceRepository;
43 import com.android.resources.aar.FrameworkResourceRepository;
44 import com.android.utils.ILogger;
45 
46 import com.google.android.collect.Lists;
47 
48 import org.junit.AfterClass;
49 import org.junit.Before;
50 import org.junit.BeforeClass;
51 import org.junit.Rule;
52 import org.junit.rules.TestWatcher;
53 import org.junit.runner.Description;
54 
55 import java.awt.image.BufferedImage;
56 import java.io.File;
57 import java.io.FileNotFoundException;
58 import java.io.IOException;
59 import java.net.URL;
60 import java.util.ArrayList;
61 import java.util.Arrays;
62 import java.util.Collections;
63 import java.util.concurrent.TimeUnit;
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  */
88 public class RenderTestBase {
89 
90     /**
91      * Listener for render process.
92      */
93     public interface RenderSessionListener {
94 
95         /**
96          * Called before session is disposed after rendering.
97          */
beforeDisposed(RenderSession session)98         void beforeDisposed(RenderSession session);
99     }
100 
101     private static final String NATIVE_LIB_PATH_PROPERTY = "native.lib.path";
102     private static final String FONT_DIR_PROPERTY = "font.dir";
103     private static final String ICU_DATA_PATH_PROPERTY = "icu.data.path";
104     private static final String HYPHEN_DATA_DIR_PROPERTY = "hyphen.data.dir";
105     private static final String KEYBOARD_DIR_PROPERTY = "keyboard.dir";
106     private static final String PLATFORM_DIR_PROPERTY = "platform.dir";
107     private static final String RESOURCE_DIR_PROPERTY = "test_res.dir";
108 
109     private static final String NATIVE_LIB_DIR_PATH;
110     private static final String FONT_DIR;
111     private static final String ICU_DATA_PATH;
112     private static final String HYPHEN_DATA_DIR;
113     private static final String KEYBOARD_DIR;
114     protected static final String PLATFORM_DIR;
115     private static final String TEST_RES_DIR;
116     /** Location of the app to test inside {@link #TEST_RES_DIR} */
117     protected static final String APP_TEST_DIR = "testApp/MyApplication";
118     /** Location of the app's res dir inside {@link #TEST_RES_DIR} */
119     private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res";
120     /** Location of the app's asset dir inside {@link #TEST_RES_DIR} */
121     private static final String APP_TEST_ASSET = APP_TEST_DIR + "/src/main/assets/";
122     private static final String APP_CLASSES_LOCATION =
123             APP_TEST_DIR + "/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/";
124     protected static Bridge sBridge;
125     /** List of log messages generated by a render call. It can be used to find specific errors */
126     protected static ArrayList<String> sRenderMessages = Lists.newArrayList();
127     private static ILayoutLog sLayoutLibLog;
128     private static FrameworkResourceRepository sFrameworkRepo;
129     private static ResourceRepository sProjectResources;
130     private static ILogger sLogger;
131 
132     static {
133         // Test that System Properties are properly set.
134         PLATFORM_DIR = getPlatformDir();
135         if (PLATFORM_DIR == null) {
136             fail(String.format("System Property %1$s not properly set. The value is %2$s",
137                     PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY)));
138         }
139 
140         NATIVE_LIB_DIR_PATH = getNativeLibDirPath();
141         FONT_DIR = getFontDir();
142         ICU_DATA_PATH = getIcuDataPath();
143         HYPHEN_DATA_DIR = getHyphenDataDir();
144         KEYBOARD_DIR = getKeyboardDir();
145 
146         TEST_RES_DIR = getTestResDir();
147         if (TEST_RES_DIR == null) {
148             fail(String.format("System property %1$s.dir not properly set. The value is %2$s",
149                     RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY)));
150         }
151     }
152 
153     @Rule
154     public TestWatcher sRenderMessageWatcher = new TestWatcher() {
155         @Override
156         protected void succeeded(Description description) {
157             // We only check error messages if the rest of the test case was successful.
158             if (!sRenderMessages.isEmpty()) {
159                 fail(description.getMethodName() + " render error message: " +
160                         sRenderMessages.get(0));
161             }
162         }
163     };
164 
165     @Rule
166     public TestWatcher sMemoryLeakChecker = new TestWatcher() {
167         @Override
168         protected void succeeded(Description description) {
169             for (int i = Choreographer.CALLBACK_INPUT; i <= Choreographer.CALLBACK_COMMIT; ++i) {
170                 if (Choreographer.getInstance().mCallbackQueues[i].mHead != null) {
171                     fail("Memory leak: leftover frame callbacks are detected in Choreographer");
172                 }
173             }
174         }
175     };
176 
177     protected ClassLoader mDefaultClassLoader;
178 
getNativeLibDirPath()179     private static String getNativeLibDirPath() {
180         String nativeLibDirPath = System.getProperty(NATIVE_LIB_PATH_PROPERTY);
181         if (nativeLibDirPath != null) {
182             File nativeLibDir = new File(nativeLibDirPath);
183             if (nativeLibDir.isDirectory()) {
184                 nativeLibDirPath = nativeLibDir.getAbsolutePath();
185             } else {
186                 nativeLibDirPath = null;
187             }
188         }
189         if (nativeLibDirPath == null) {
190             nativeLibDirPath = PLATFORM_DIR + "/../../../../../lib64/";
191         }
192         return nativeLibDirPath;
193     }
194 
getFontDir()195     private static String getFontDir() {
196         String fontDir = System.getProperty(FONT_DIR_PROPERTY);
197         if (fontDir == null) {
198             // The fonts are built into out/host/common/obj/PACKAGING/fonts_intermediates
199             // as specified in build/make/core/layoutlib_data.mk, and PLATFORM_DIR is
200             // out/host/[arch]/sdk/sdk*/android-sdk*/platforms/android*
201             fontDir = PLATFORM_DIR +
202                     "/../../../../../../common/obj/PACKAGING/fonts_intermediates";
203         }
204         return fontDir;
205     }
206 
getIcuDataPath()207     private static String getIcuDataPath() {
208         String icuDataPath = System.getProperty(ICU_DATA_PATH_PROPERTY);
209         if (icuDataPath == null) {
210             icuDataPath = PLATFORM_DIR + "/../../../../../com.android.i18n/etc/icu/icudt71l.dat";
211         }
212         return icuDataPath;
213     }
214 
getHyphenDataDir()215     private static String getHyphenDataDir() {
216         String hyphenDataDir = System.getProperty(HYPHEN_DATA_DIR_PROPERTY);
217         if (hyphenDataDir == null) {
218             hyphenDataDir =
219                     PLATFORM_DIR + "/../../../../../../common/obj/PACKAGING/hyphen_intermediates";
220         }
221         return hyphenDataDir;
222     }
223 
getKeyboardDir()224     private static String getKeyboardDir() {
225         String keyboardDir = System.getProperty(KEYBOARD_DIR_PROPERTY);
226         if (keyboardDir == null) {
227             // The keyboard files are built into
228             // out/host/common/obj/PACKAGING/keyboards_intermediates
229             // as specified in build/make/core/layoutlib_data.mk, and PLATFORM_DIR is
230             // out/host/[arch]/sdk/sdk*/android-sdk*/platforms/android*
231             keyboardDir = PLATFORM_DIR +
232                     "/../../../../../../common/obj/PACKAGING/keyboards_intermediates";
233         }
234         return keyboardDir;
235     }
236 
getPlatformDir()237     private static String getPlatformDir() {
238         String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY);
239         if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) {
240             return platformDir;
241         }
242         // System Property not set. Try to find the directory in the build directory.
243         String androidHostOut = System.getenv("ANDROID_HOST_OUT");
244         if (androidHostOut != null) {
245             platformDir = getPlatformDirFromHostOut(new File(androidHostOut));
246             if (platformDir != null) {
247                 return platformDir;
248             }
249         }
250         String workingDirString = System.getProperty("user.dir");
251         File workingDir = new File(workingDirString);
252         // Test if workingDir is android checkout root.
253         platformDir = getPlatformDirFromRoot(workingDir);
254         if (platformDir != null) {
255             return platformDir;
256         }
257 
258         // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge.
259         File currentDir = workingDir;
260         if (currentDir.getName().equalsIgnoreCase("bridge")) {
261             currentDir = currentDir.getParentFile();
262         }
263 
264         // Find frameworks/layoutlib
265         while (currentDir != null && !"layoutlib".equals(currentDir.getName())) {
266             currentDir = currentDir.getParentFile();
267         }
268 
269         if (currentDir == null ||
270                 currentDir.getParentFile() == null ||
271                 !"frameworks".equals(currentDir.getParentFile().getName())) {
272             return null;
273         }
274 
275         // Test if currentDir is  platform/frameworks/layoutlib. That is, root should be
276         // workingDir/../../ (2 levels up)
277         for (int i = 0; i < 2; i++) {
278             if (currentDir != null) {
279                 currentDir = currentDir.getParentFile();
280             }
281         }
282         return currentDir == null ? null : getPlatformDirFromRoot(currentDir);
283     }
284 
getPlatformDirFromRoot(File root)285     private static String getPlatformDirFromRoot(File root) {
286         if (!root.isDirectory()) {
287             return null;
288         }
289         File out = new File(root, "out");
290         if (!out.isDirectory()) {
291             return null;
292         }
293         File host = new File(out, "host");
294         if (!host.isDirectory()) {
295             return null;
296         }
297         File[] hosts = host.listFiles(path -> path.isDirectory() &&
298                 (path.getName().startsWith("linux-") ||
299                         path.getName().startsWith("darwin-")));
300         assert hosts != null;
301         for (File hostOut : hosts) {
302             String platformDir = getPlatformDirFromHostOut(hostOut);
303             if (platformDir != null) {
304                 return platformDir;
305             }
306         }
307 
308         return null;
309     }
310 
getPlatformDirFromHostOut(File out)311     private static String getPlatformDirFromHostOut(File out) {
312         if (!out.isDirectory()) {
313             return null;
314         }
315         File sdkDir = new File(out, "sdk");
316         if (!sdkDir.isDirectory()) {
317             return null;
318         }
319         File[] sdkDirs = sdkDir.listFiles(path -> {
320             // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7)
321             return path.isDirectory() && path.getName().startsWith("sdk");
322         });
323         assert sdkDirs != null;
324         for (File dir : sdkDirs) {
325             String platformDir = getPlatformDirFromHostOutSdkSdk(dir);
326             if (platformDir != null) {
327                 return platformDir;
328             }
329         }
330         return null;
331     }
332 
getPlatformDirFromHostOutSdkSdk(File sdkDir)333     private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) {
334         File[] possibleSdks = sdkDir.listFiles(
335                 path -> path.isDirectory() && path.getName().contains("android-sdk"));
336         assert possibleSdks != null;
337         for (File possibleSdk : possibleSdks) {
338             File platformsDir = new File(possibleSdk, "platforms");
339             File[] platforms = platformsDir.listFiles(
340                     path -> path.isDirectory() && path.getName().startsWith("android-"));
341             if (platforms == null || platforms.length == 0) {
342                 continue;
343             }
344             Arrays.sort(platforms, (o1, o2) -> {
345                 final int MAX_VALUE = 1000;
346                 String suffix1 = o1.getName().substring("android-".length());
347                 String suffix2 = o2.getName().substring("android-".length());
348                 int suff1, suff2;
349                 try {
350                     suff1 = Integer.parseInt(suffix1);
351                 } catch (NumberFormatException e) {
352                     suff1 = MAX_VALUE;
353                 }
354                 try {
355                     suff2 = Integer.parseInt(suffix2);
356                 } catch (NumberFormatException e) {
357                     suff2 = MAX_VALUE;
358                 }
359                 if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) {
360                     return suff2 - suff1;
361                 }
362                 return suffix2.compareTo(suffix1);
363             });
364             return platforms[0].getAbsolutePath();
365         }
366         return null;
367     }
368 
getTestResDir()369     private static String getTestResDir() {
370         String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY);
371         if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) {
372             return resourceDir;
373         }
374         // TEST_RES_DIR not explicitly set. Fallback to the class's source location.
375         try {
376             URL location = RenderTestBase.class.getProtectionDomain().getCodeSource().getLocation();
377             return new File(location.getPath()).exists() ? location.getPath() : null;
378         } catch (NullPointerException e) {
379             // Prevent a lot of null checks by just catching the exception.
380             return null;
381         }
382     }
383 
384     /**
385      * Initialize the bridge and the resource maps.
386      */
387     @BeforeClass
beforeClass()388     public static void beforeClass() {
389         File data_dir = new File(PLATFORM_DIR, "data");
390         File res = new File(data_dir, "res");
391         sFrameworkRepo =
392                 FrameworkResourceRepository.create(
393                         res.getAbsoluteFile().toPath(), Collections.emptySet(), null, false);
394 
395         File projectRes = new File(TEST_RES_DIR + "/" + APP_TEST_RES);
396         sProjectResources =
397                 AarSourceResourceRepository.create(
398                         projectRes.getAbsoluteFile().toPath(), "Application");
399 
400         File fontLocation = new File(FONT_DIR);
401         File buildProp = new File(PLATFORM_DIR, "build.prop");
402         File attrs = new File(res, "values" + File.separator + "attrs.xml");
403 
404         String[] keyboardPaths = new String[] { KEYBOARD_DIR + "/Generic.kcm" };
405         sBridge = new Bridge();
406         sBridge.init(
407                 ConfigGenerator.loadProperties(buildProp),
408                 fontLocation,
409                 NATIVE_LIB_DIR_PATH,
410                 ICU_DATA_PATH,
411                 HYPHEN_DATA_DIR,
412                 keyboardPaths,
413                 ConfigGenerator.getEnumMap(attrs),
414                 getLayoutLog());
415         Bridge.getLock().lock();
416         try {
417             Bridge.setLog(getLayoutLog());
418         } finally {
419             Bridge.getLock().unlock();
420         }
421     }
422 
423     @AfterClass
tearDown()424     public static void tearDown() {
425         sLayoutLibLog = null;
426         sFrameworkRepo = null;
427         sProjectResources = null;
428         sLogger = null;
429         sBridge = null;
430     }
431 
432     @NonNull
render(com.android.ide.common.rendering.api.Bridge bridge, SessionParams params, long frameTimeNanos)433     protected static RenderResult render(com.android.ide.common.rendering.api.Bridge bridge,
434             SessionParams params,
435             long frameTimeNanos) {
436         return render(bridge, params, frameTimeNanos, null);
437     }
438 
439     @NonNull
render(com.android.ide.common.rendering.api.Bridge bridge, SessionParams params, long frameTimeNanos, @Nullable RenderSessionListener listener)440     protected static RenderResult render(com.android.ide.common.rendering.api.Bridge bridge,
441             SessionParams params,
442             long frameTimeNanos,
443             @Nullable RenderSessionListener listener) {
444         // TODO: Set up action bar handler properly to test menu rendering.
445         // Create session params.
446         System_Delegate.setBootTimeNanos(TimeUnit.MILLISECONDS.toNanos(871732800000L));
447         System_Delegate.setNanosTime(TimeUnit.MILLISECONDS.toNanos(871732800000L));
448         RenderSession session = bridge.createSession(params);
449 
450         try {
451             if (frameTimeNanos != -1) {
452                 session.setElapsedFrameTimeNanos(frameTimeNanos);
453             }
454 
455             if (!session.getResult().isSuccess()) {
456                 getLogger().error(session.getResult().getException(),
457                         session.getResult().getErrorMessage());
458             }
459             else {
460                 // Render the session with a timeout of 50s.
461                 Result renderResult = session.render(50000);
462                 if (!renderResult.isSuccess()) {
463                     getLogger().error(session.getResult().getException(),
464                             session.getResult().getErrorMessage());
465                 }
466             }
467             if (listener != null) {
468                 listener.beforeDisposed(session);
469             }
470 
471             return RenderResult.getFromSession(session);
472         } finally {
473             session.dispose();
474         }
475     }
476 
477     /**
478      * Compares the golden image with the passed image
479      */
verify(@onNull String goldenImageName, @NonNull BufferedImage image)480     protected static void verify(@NonNull String goldenImageName, @NonNull BufferedImage image) {
481         try {
482             String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenImageName;
483             ImageUtils.requireSimilar(goldenImagePath, image);
484         } catch (IOException e) {
485             getLogger().error(e, e.getMessage());
486         }
487     }
488 
489     /**
490      * Create a new rendering session and test that rendering the given layout doesn't throw any
491      * exceptions and matches the provided image.
492      * <p>
493      * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates
494      * how far in the future is.
495      */
496     @Nullable
renderAndVerify(SessionParams params, String goldenFileName, long frameTimeNanos)497     protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName,
498             long frameTimeNanos) throws ClassNotFoundException {
499         RenderResult result = RenderTestBase.render(sBridge, params, frameTimeNanos);
500         assertNotNull(result.getImage());
501         verify(goldenFileName, result.getImage());
502 
503         return result;
504     }
505 
506     /**
507      * Create a new rendering session and test that rendering the given layout doesn't throw any
508      * exceptions and matches the provided image.
509      */
510     @Nullable
renderAndVerify(SessionParams params, String goldenFileName)511     protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName)
512             throws ClassNotFoundException {
513         return RenderTestBase.renderAndVerify(params, goldenFileName, TimeUnit.SECONDS.toNanos(2));
514     }
515 
getLayoutLog()516     protected static ILayoutLog getLayoutLog() {
517         if (sLayoutLibLog == null) {
518             sLayoutLibLog = new ILayoutLog() {
519                 @Override
520                 public void warning(@Nullable String tag, @NonNull String message, @Nullable Object viewCookie,
521                         @Nullable Object data) {
522                     System.out.println("Warning " + tag + ": " + message);
523                     failWithMsg(message);
524                 }
525 
526                 @Override
527                 public void fidelityWarning(@Nullable String tag, String message,
528                         Throwable throwable, Object cookie, Object data) {
529 
530                     System.out.println("FidelityWarning " + tag + ": " + message);
531                     if (throwable != null) {
532                         throwable.printStackTrace();
533                     }
534                     failWithMsg(message == null ? "" : message);
535                 }
536 
537                 @Override
538                 public void error(@Nullable String tag, @NonNull String message, @Nullable Object viewCookie,
539                         @Nullable Object data) {
540                     System.out.println("Error " + tag + ": " + message);
541                     failWithMsg(message);
542                 }
543 
544                 @Override
545                 public void error(@Nullable String tag, @NonNull String message, @Nullable Throwable throwable,
546                         @Nullable Object viewCookie, @Nullable Object data) {
547                     System.out.println("Error " + tag + ": " + message);
548                     if (throwable != null) {
549                         throwable.printStackTrace();
550                     }
551                     failWithMsg(message);
552                 }
553 
554                 @Override
555                 public void logAndroidFramework(int priority, String tag, String message) {
556                     System.out.println("Android framework message " + tag + ": " + message);
557                 }
558             };
559         }
560         return sLayoutLibLog;
561     }
562 
ignoreAllLogging()563     protected static void ignoreAllLogging() {
564         sLayoutLibLog = new ILayoutLog() {};
565         sLogger = new ILogger() {
566             @Override
567             public void error(Throwable t, String msgFormat, Object... args) {
568             }
569 
570             @Override
571             public void warning(String msgFormat, Object... args) {
572             }
573 
574             @Override
575             public void info(String msgFormat, Object... args) {
576             }
577 
578             @Override
579             public void verbose(String msgFormat, Object... args) {
580             }
581         };
582     }
583 
getLogger()584     protected static ILogger getLogger() {
585         if (sLogger == null) {
586             sLogger = new ILogger() {
587                 @Override
588                 public void error(Throwable t, @Nullable String msgFormat, Object... args) {
589                     if (t != null) {
590                         t.printStackTrace();
591                     }
592                     failWithMsg(msgFormat == null ? "" : msgFormat, args);
593                 }
594 
595                 @Override
596                 public void warning(@NonNull String msgFormat, Object... args) {
597                     failWithMsg(msgFormat, args);
598                 }
599 
600                 @Override
601                 public void info(@NonNull String msgFormat, Object... args) {
602                     // pass.
603                 }
604 
605                 @Override
606                 public void verbose(@NonNull String msgFormat, Object... args) {
607                     // pass.
608                 }
609             };
610         }
611         return sLogger;
612     }
613 
failWithMsg(@onNull String msgFormat, Object... args)614     private static void failWithMsg(@NonNull String msgFormat, Object... args) {
615         sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args));
616     }
617 
618     @Before
beforeTestCase()619     public void beforeTestCase() {
620         // Default class loader with access to the app classes
621         mDefaultClassLoader = new ModuleClassLoader(APP_CLASSES_LOCATION, getClass().getClassLoader());
622         sRenderMessages.clear();
623     }
624 
625     @NonNull
createParserFromPath(String layoutPath)626     protected LayoutPullParser createParserFromPath(String layoutPath)
627             throws FileNotFoundException {
628         return LayoutPullParser.createFromPath(APP_TEST_RES + "/layout/" + layoutPath);
629     }
630 
631     /**
632      * Create a new rendering session and test that rendering the given layout on nexus 5
633      * doesn't throw any exceptions and matches the provided image.
634      */
635     @Nullable
renderAndVerify(String layoutFileName, String goldenFileName, boolean decoration)636     protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName,
637             boolean decoration)
638             throws ClassNotFoundException, FileNotFoundException {
639         return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5, decoration);
640     }
641 
642     /**
643      * Create a new rendering session and test that rendering the given layout on given device
644      * doesn't throw any exceptions and matches the provided image.
645      */
646     @Nullable
renderAndVerify(String layoutFileName, String goldenFileName, ConfigGenerator deviceConfig, boolean decoration)647     protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName,
648             ConfigGenerator deviceConfig, boolean decoration) throws ClassNotFoundException,
649             FileNotFoundException {
650         SessionParams params = createSessionParams(layoutFileName, deviceConfig);
651         if (!decoration) {
652             params.setForceNoDecor();
653         }
654         return renderAndVerify(params, goldenFileName);
655     }
656 
createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)657     protected SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)
658             throws ClassNotFoundException, FileNotFoundException {
659         // Create the layout pull parser.
660         LayoutPullParser parser = createParserFromPath(layoutFileName);
661         // Create LayoutLibCallback.
662         LayoutLibTestCallback layoutLibCallback =
663                 new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
664         layoutLibCallback.initResources();
665         // TODO: Set up action bar handler properly to test menu rendering.
666         // Create session params.
667         return getSessionParamsBuilder()
668                 .setParser(parser)
669                 .setConfigGenerator(deviceConfig)
670                 .setCallback(layoutLibCallback)
671                 .build();
672     }
673 
674     /**
675      * Returns a pre-configured {@link SessionParamsBuilder} for target API 22, Normal rendering
676      * mode, AppTheme as theme and Nexus 5.
677      */
678     @NonNull
getSessionParamsBuilder()679     protected SessionParamsBuilder getSessionParamsBuilder() {
680         return new SessionParamsBuilder()
681                 .setLayoutLog(getLayoutLog())
682                 .setFrameworkResources(sFrameworkRepo)
683                 .setConfigGenerator(ConfigGenerator.NEXUS_5)
684                 .setProjectResources(sProjectResources)
685                 .setTheme("AppTheme", true)
686                 .setRenderingMode(RenderingMode.NORMAL)
687                 .setTargetSdk(28)
688                 .setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true)
689                 .setAssetRepository(new TestAssetRepository(TEST_RES_DIR + "/" + APP_TEST_ASSET));
690     }
691 }
692