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