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