1 package org.robolectric;
2 
3 import static com.google.common.collect.Lists.reverse;
4 
5 import com.google.common.annotations.VisibleForTesting;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.lang.reflect.Method;
9 import java.util.ArrayList;
10 import java.util.Arrays;
11 import java.util.LinkedHashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Properties;
15 import javax.annotation.Nonnull;
16 import javax.annotation.Nullable;
17 import org.robolectric.annotation.Config;
18 import org.robolectric.util.Join;
19 
20 public class ConfigMerger {
21   private final Map<String, Config> packageConfigCache = new LinkedHashMap<String, Config>() {
22     @Override
23     protected boolean removeEldestEntry(Map.Entry eldest) {
24       return size() > 10;
25     }
26   };
27 
28   /**
29    * Calculate the {@link Config} for the given test.
30    *
31    * @param testClass the class containing the test
32    * @param method the test method
33    * @param globalConfig global configuration values
34    * @return the effective configuration
35    * @since 3.2
36    */
getConfig(Class<?> testClass, Method method, Config globalConfig)37   public Config getConfig(Class<?> testClass, Method method, Config globalConfig) {
38     Config config = Config.Builder.defaults().build();
39     config = override(config, globalConfig);
40 
41     for (String packageName : reverse(packageHierarchyOf(testClass))) {
42       Config packageConfig = cachedPackageConfig(packageName);
43       config = override(config, packageConfig);
44     }
45 
46     for (Class clazz : reverse(parentClassesFor(testClass))) {
47       Config classConfig = (Config) clazz.getAnnotation(Config.class);
48       config = override(config, classConfig);
49     }
50 
51     Config methodConfig = method.getAnnotation(Config.class);
52     config = override(config, methodConfig);
53 
54     return config;
55   }
56 
57   /**
58    * Generate {@link Config} for the specified package.
59    *
60    * More specific packages, test classes, and test method configurations
61    * will override values provided here.
62    *
63    * The default implementation uses properties provided by {@link #getConfigProperties(String)}.
64    *
65    * The returned object is likely to be reused for many tests.
66    *
67    * @param packageName the name of the package, or empty string ({@code ""}) for the top level package
68    * @return {@link Config} object for the specified package
69    * @since 3.2
70    */
71   @Nullable
buildPackageConfig(String packageName)72   private Config buildPackageConfig(String packageName) {
73     return Config.Implementation.fromProperties(getConfigProperties(packageName));
74   }
75 
76   /**
77    * Return a {@link Properties} file for the given package name, or {@code null} if none is available.
78    *
79    * @since 3.2
80    */
getConfigProperties(String packageName)81   protected Properties getConfigProperties(String packageName) {
82     List<String> packageParts = new ArrayList<>(Arrays.asList(packageName.split("\\.")));
83     packageParts.add(RobolectricTestRunner.CONFIG_PROPERTIES);
84     final String resourceName = Join.join("/", packageParts);
85     try (InputStream resourceAsStream = getResourceAsStream(resourceName)) {
86       if (resourceAsStream == null) return null;
87       Properties properties = new Properties();
88       properties.load(resourceAsStream);
89       return properties;
90     } catch (IOException e) {
91       throw new RuntimeException(e);
92     }
93   }
94 
95   @Nonnull @VisibleForTesting
packageHierarchyOf(Class<?> javaClass)96   List<String> packageHierarchyOf(Class<?> javaClass) {
97     Package aPackage = javaClass.getPackage();
98     String testPackageName = aPackage == null ? "" : aPackage.getName();
99     List<String> packageHierarchy = new ArrayList<>();
100     while (!testPackageName.isEmpty()) {
101       packageHierarchy.add(testPackageName);
102       int lastDot = testPackageName.lastIndexOf('.');
103       testPackageName = lastDot > 1 ? testPackageName.substring(0, lastDot) : "";
104     }
105     packageHierarchy.add("");
106     return packageHierarchy;
107   }
108 
109   @Nonnull
parentClassesFor(Class testClass)110   private List<Class> parentClassesFor(Class testClass) {
111     List<Class> testClassHierarchy = new ArrayList<>();
112     while (testClass != null && !testClass.equals(Object.class)) {
113       testClassHierarchy.add(testClass);
114       testClass = testClass.getSuperclass();
115     }
116     return testClassHierarchy;
117   }
118 
override(Config config, Config classConfig)119   private Config override(Config config, Config classConfig) {
120     return classConfig != null ? new Config.Builder(config).overlay(classConfig).build() : config;
121   }
122 
123   @Nullable
cachedPackageConfig(String packageName)124   private Config cachedPackageConfig(String packageName) {
125     synchronized (packageConfigCache) {
126       Config config = packageConfigCache.get(packageName);
127       if (config == null && !packageConfigCache.containsKey(packageName)) {
128         config = buildPackageConfig(packageName);
129         packageConfigCache.put(packageName, config);
130       }
131       return config;
132     }
133   }
134 
135   // visible for testing
136   @SuppressWarnings("WeakerAccess")
getResourceAsStream(String resourceName)137   InputStream getResourceAsStream(String resourceName) {
138     return getClass().getClassLoader().getResourceAsStream(resourceName);
139   }
140 }
141