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