ConfigMerger.java revision a7f2db77a1a5457867de967e0939b854ac2ab27a
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    String testPackageName = javaClass.getPackage().getName();
95    List<String> packageHierarchy = new ArrayList<>();
96    while (!testPackageName.isEmpty()) {
97      packageHierarchy.add(testPackageName);
98      int lastDot = testPackageName.lastIndexOf('.');
99      testPackageName = lastDot > 1 ? testPackageName.substring(0, lastDot) : "";
100    }
101    packageHierarchy.add("");
102    return packageHierarchy;
103  }
104
105  @NotNull
106  private List<Class> parentClassesFor(Class testClass) {
107    List<Class> testClassHierarchy = new ArrayList<>();
108    while (testClass != null && !testClass.equals(Object.class)) {
109      testClassHierarchy.add(testClass);
110      testClass = testClass.getSuperclass();
111    }
112    return testClassHierarchy;
113  }
114
115  private Config override(Config config, Config classConfig) {
116    return classConfig != null ? new Config.Builder(config).overlay(classConfig).build() : config;
117  }
118
119  @Nullable
120  private Config cachedPackageConfig(String packageName) {
121    synchronized (packageConfigCache) {
122      Config config = packageConfigCache.get(packageName);
123      if (config == null && !packageConfigCache.containsKey(packageName)) {
124        config = buildPackageConfig(packageName);
125        packageConfigCache.put(packageName, config);
126      }
127      return config;
128    }
129  }
130
131  // visible for testing
132  @SuppressWarnings("WeakerAccess")
133  InputStream getResourceAsStream(String resourceName) {
134    return getClass().getClassLoader().getResourceAsStream(resourceName);
135  }
136}
137