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