ConfigMerger.java revision 7d074bb6c8e8c5e3f73faec0f35a6cf129227eba
1package org.robolectric;
2
3import com.google.common.annotations.VisibleForTesting;
4import javax.annotation.Nonnull;
5import javax.annotation.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 ""}) 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} 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  @Nonnull @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  @Nonnull
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