Config.java revision 793ee1db287b053127b6e60891c3dbfd1ce4bc54
1package org.robolectric.annotation;
2
3import android.app.Application;
4
5import javax.annotation.Nonnull;
6
7import java.lang.annotation.Annotation;
8import java.lang.annotation.Documented;
9import java.lang.annotation.ElementType;
10import java.lang.annotation.Inherited;
11import java.lang.annotation.Retention;
12import java.lang.annotation.RetentionPolicy;
13import java.lang.annotation.Target;
14import java.util.Arrays;
15import java.util.HashSet;
16import java.util.Properties;
17import java.util.Set;
18
19/**
20 * Configuration settings that can be used on a per-class or per-test basis.
21 */
22@Documented
23@Inherited
24@Retention(RetentionPolicy.RUNTIME)
25@Target({ElementType.TYPE, ElementType.METHOD})
26public @interface Config {
27  /**
28   * TODO(vnayar): Create named constants for default values instead of magic numbers.
29   * Array named contants must be avoided in order to dodge a JDK 1.7 bug.
30   *   error: annotation Config is missing value for the attribute <clinit>
31   * See <a href="https://bugs.openjdk.java.net/browse/JDK-8013485">JDK-8013485</a>.
32   */
33  String NONE = "--none";
34  String DEFAULT_VALUE_STRING = "--default";
35  int DEFAULT_VALUE_INT = -1;
36
37  String DEFAULT_MANIFEST_NAME = "AndroidManifest.xml";
38  Class<? extends Application> DEFAULT_APPLICATION = DefaultApplication.class;
39  String DEFAULT_PACKAGE_NAME = "";
40  String DEFAULT_ABI_SPLIT = "";
41  String DEFAULT_QUALIFIERS = "";
42  String DEFAULT_RES_FOLDER = "res";
43  String DEFAULT_ASSET_FOLDER = "assets";
44  String DEFAULT_BUILD_FOLDER = "build";
45
46  int ALL_SDKS = -2;
47  int TARGET_SDK = -3;
48  int OLDEST_SDK = -4;
49  int NEWEST_SDK = -5;
50
51  /**
52   * The Android SDK level to emulate. This value will also be set as Build.VERSION.SDK_INT.
53   */
54  int[] sdk() default {};  // DEFAULT_SDK
55
56  /**
57   * The minimum Android SDK level to emulate when running tests on multiple API versions.
58   */
59  int minSdk() default -1;
60
61  /**
62   * The maximum Android SDK level to emulate when running tests on multiple API versions.
63   */
64  int maxSdk() default -1;
65
66  /**
67   * The Android manifest file to load; Robolectric will look relative to the current directory.
68   * Resources and assets will be loaded relative to the manifest.
69   *
70   * If not specified, Robolectric defaults to {@code AndroidManifest.xml}.
71   *
72   * If your project has no manifest or resources, use {@link Config#NONE}.
73   *
74   * @return The Android manifest file to load.
75   */
76  String manifest() default DEFAULT_VALUE_STRING;
77
78  /**
79   * Reference to the BuildConfig class created by the Gradle build system.
80   *
81   * @return Reference to BuildConfig class.
82   */
83  Class<?> constants() default Void.class;  // DEFAULT_CONSTANTS
84
85  /**
86   * The {@link android.app.Application} class to use in the test, this takes precedence over any application
87   * specified in the AndroidManifest.xml.
88   *
89   * @return The {@link android.app.Application} class to use in the test.
90   */
91  Class<? extends Application> application() default DefaultApplication.class;  // DEFAULT_APPLICATION
92
93  /**
94   * Java package name where the "R.class" file is located. This only needs to be specified if you define
95   * an {@code applicationId} associated with {@code productFlavors} or specify {@code applicationIdSuffix}
96   * in your build.gradle.
97   *
98   * If not specified, Robolectric defaults to the {@code applicationId}.
99   *
100   * @return The java package name for R.class.
101   */
102  String packageName() default DEFAULT_PACKAGE_NAME;
103
104  /**
105   * The ABI split to use when locating resources and AndroidManifest.xml
106   *
107   * You do not typically have to set this, unless you are utilizing the ABI split feature.
108   *
109   * @return The ABI split to test with
110   */
111  String abiSplit() default DEFAULT_ABI_SPLIT;
112
113  /**
114   * Qualifiers for the resource resolution, such as "fr-normal-port-hdpi".
115   *
116   * @return Qualifiers used for resource resolution.
117   */
118  String qualifiers() default DEFAULT_QUALIFIERS;
119
120  /**
121   * The directory from which to load resources.  This should be relative to the directory containing AndroidManifest.xml.
122   *
123   * If not specified, Robolectric defaults to {@code res}.
124   *
125   * @return Android resource directory.
126   */
127  String resourceDir() default DEFAULT_RES_FOLDER;
128
129  /**
130   * The directory from which to load assets. This should be relative to the directory containing AndroidManifest.xml.
131   *
132   * If not specified, Robolectric defaults to {@code assets}.
133   *
134   * @return Android asset directory.
135   */
136  String assetDir() default DEFAULT_ASSET_FOLDER;
137
138  /**
139   * The directory where application files are created during the application build process.
140   *
141   * If not specified, Robolectric defaults to {@code build}.
142   *
143   * @return Android build directory.
144   */
145  String buildDir() default DEFAULT_BUILD_FOLDER;
146
147  /**
148   * A list of shadow classes to enable, in addition to those that are already present.
149   *
150   * @return A list of additional shadow classes to enable.
151   */
152  Class<?>[] shadows() default {};  // DEFAULT_SHADOWS
153
154  /**
155   * A list of instrumented packages, in addition to those that are already instrumented.
156   *
157   * @return A list of additional instrumented packages.
158   */
159  String[] instrumentedPackages() default {};  // DEFAULT_INSTRUMENTED_PACKAGES
160
161  /**
162   * A list of folders containing Android Libraries on which this project depends.
163   *
164   * @return A list of Android Libraries.
165   */
166  String[] libraries() default {};  // DEFAULT_LIBRARIES;
167
168  class Implementation implements Config {
169    private final int[] sdk;
170    private final int minSdk;
171    private final int maxSdk;
172    private final String manifest;
173    private final String qualifiers;
174    private final String resourceDir;
175    private final String assetDir;
176    private final String buildDir;
177    private final String packageName;
178    private final String abiSplit;
179    private final Class<?> constants;
180    private final Class<?>[] shadows;
181    private final String[] instrumentedPackages;
182    private final Class<? extends Application> application;
183    private final String[] libraries;
184
185    public static Config fromProperties(Properties properties) {
186      if (properties == null || properties.size() == 0) return null;
187      return new Implementation(
188          parseSdkArrayProperty(properties.getProperty("sdk", "")),
189          parseSdkInt(properties.getProperty("minSdk", "-1")),
190          parseSdkInt(properties.getProperty("maxSdk", "-1")),
191          properties.getProperty("manifest", DEFAULT_VALUE_STRING),
192          properties.getProperty("qualifiers", DEFAULT_QUALIFIERS),
193          properties.getProperty("packageName", DEFAULT_PACKAGE_NAME),
194          properties.getProperty("abiSplit", DEFAULT_ABI_SPLIT),
195          properties.getProperty("resourceDir", DEFAULT_RES_FOLDER),
196          properties.getProperty("assetDir", DEFAULT_ASSET_FOLDER),
197          properties.getProperty("buildDir", DEFAULT_BUILD_FOLDER),
198          parseClasses(properties.getProperty("shadows", "")),
199          parseStringArrayProperty(properties.getProperty("instrumentedPackages", "")),
200          parseApplication(properties.getProperty("application", DEFAULT_APPLICATION.getCanonicalName())),
201          parseStringArrayProperty(properties.getProperty("libraries", "")),
202          parseClass(properties.getProperty("constants", ""))
203      );
204    }
205
206    private static Class<?> parseClass(String className) {
207      if (className.isEmpty()) return null;
208      try {
209        return Implementation.class.getClassLoader().loadClass(className);
210      } catch (ClassNotFoundException e) {
211        throw new RuntimeException("Could not load class: " + className);
212      }
213    }
214
215    private static Class<?>[] parseClasses(String input) {
216      if (input.isEmpty()) return new Class[0];
217      final String[] classNames = input.split("[, ]+");
218      final Class[] classes = new Class[classNames.length];
219      for (int i = 0; i < classNames.length; i++) {
220        classes[i] = parseClass(classNames[i]);
221      }
222      return classes;
223    }
224
225    @SuppressWarnings("unchecked")
226    private static <T extends Application> Class<T> parseApplication(String className) {
227      return (Class<T>) parseClass(className);
228    }
229
230    private static String[] parseStringArrayProperty(String property) {
231      if (property.isEmpty()) return new String[0];
232      return property.split("[, ]+");
233    }
234
235    private static int[] parseSdkArrayProperty(String property) {
236      String[] parts = parseStringArrayProperty(property);
237      int[] result = new int[parts.length];
238      for (int i = 0; i < parts.length; i++) {
239        result[i] = parseSdkInt(parts[i]);
240      }
241
242      return result;
243    }
244
245    private static int parseSdkInt(String part) {
246      String spec = part.trim();
247      switch (spec) {
248        case "ALL_SDKS":
249          return Config.ALL_SDKS;
250        case "TARGET_SDK":
251          return Config.TARGET_SDK;
252        case "OLDEST_SDK":
253          return Config.OLDEST_SDK;
254        case "NEWEST_SDK":
255          return Config.NEWEST_SDK;
256        default:
257          return Integer.parseInt(spec);
258      }
259    }
260
261    private static void validate(Config config) {
262      //noinspection ConstantConditions
263      if (config.sdk() != null && config.sdk().length > 0 &&
264          (config.minSdk() != DEFAULT_VALUE_INT || config.maxSdk() != DEFAULT_VALUE_INT)) {
265        throw new IllegalArgumentException("sdk and minSdk/maxSdk may not be specified together" +
266            " (sdk=" + Arrays.toString(config.sdk()) + ", minSdk=" + config.minSdk() + ", maxSdk=" + config.maxSdk() + ")");
267      }
268
269      if (config.minSdk() > DEFAULT_VALUE_INT && config.maxSdk() > DEFAULT_VALUE_INT && config.minSdk() > config.maxSdk()) {
270        throw new IllegalArgumentException("minSdk may not be larger than maxSdk" +
271            " (minSdk=" + config.minSdk() + ", maxSdk=" + config.maxSdk() + ")");
272      }
273    }
274
275    public Implementation(int[] sdk, int minSdk, int maxSdk, String manifest, String qualifiers, String packageName, String abiSplit, String resourceDir, String assetDir, String buildDir, Class<?>[] shadows, String[] instrumentedPackages, Class<? extends Application> application, String[] libraries, Class<?> constants) {
276      this.sdk = sdk;
277      this.minSdk = minSdk;
278      this.maxSdk = maxSdk;
279      this.manifest = manifest;
280      this.qualifiers = qualifiers;
281      this.packageName = packageName;
282      this.abiSplit = abiSplit;
283      this.resourceDir = resourceDir;
284      this.assetDir = assetDir;
285      this.buildDir = buildDir;
286      this.shadows = shadows;
287      this.instrumentedPackages = instrumentedPackages;
288      this.application = application;
289      this.libraries = libraries;
290      this.constants = constants;
291
292      validate(this);
293    }
294
295    @Override
296    public int[] sdk() {
297      return sdk;
298    }
299
300    @Override
301    public int minSdk() {
302      return minSdk;
303    }
304
305    @Override
306    public int maxSdk() {
307      return maxSdk;
308    }
309
310    @Override
311    public String manifest() {
312      return manifest;
313    }
314
315    @Override
316    public Class<?> constants() {
317      return constants;
318    }
319
320    @Override
321    public Class<? extends Application> application() {
322      return application;
323    }
324
325    @Override
326    public String qualifiers() {
327      return qualifiers;
328    }
329
330    @Override
331    public String packageName() {
332      return packageName;
333    }
334
335    @Override
336    public String abiSplit() {
337      return abiSplit;
338    }
339
340    @Override
341    public String resourceDir() {
342      return resourceDir;
343    }
344
345    @Override
346    public String assetDir() {
347      return assetDir;
348    }
349
350    @Override
351    public String buildDir() {
352      return buildDir;
353    }
354
355    @Override
356    public Class<?>[] shadows() {
357      return shadows;
358    }
359
360    @Override
361    public String[] instrumentedPackages() {
362      return instrumentedPackages;
363    }
364
365    @Override
366    public String[] libraries() {
367      return libraries;
368    }
369
370    @Nonnull @Override
371    public Class<? extends Annotation> annotationType() {
372      return Config.class;
373    }
374  }
375
376  class Builder {
377    protected int[] sdk = new int[0];
378    protected int minSdk = -1;
379    protected int maxSdk = -1;
380    protected String manifest = Config.DEFAULT_VALUE_STRING;
381    protected String qualifiers = Config.DEFAULT_QUALIFIERS;
382    protected String packageName = Config.DEFAULT_PACKAGE_NAME;
383    protected String abiSplit = Config.DEFAULT_ABI_SPLIT;
384    protected String resourceDir = Config.DEFAULT_RES_FOLDER;
385    protected String assetDir = Config.DEFAULT_ASSET_FOLDER;
386    protected String buildDir = Config.DEFAULT_BUILD_FOLDER;
387    protected Class<?>[] shadows = new Class[0];
388    protected String[] instrumentedPackages = new String[0];
389    protected Class<? extends Application> application = DEFAULT_APPLICATION;
390    protected String[] libraries = new String[0];
391    protected Class<?> constants = Void.class;
392
393    public Builder() {
394    }
395
396    public Builder(Config config) {
397      sdk = config.sdk();
398      minSdk = config.minSdk();
399      maxSdk = config.maxSdk();
400      manifest = config.manifest();
401      qualifiers = config.qualifiers();
402      packageName = config.packageName();
403      abiSplit = config.abiSplit();
404      resourceDir = config.resourceDir();
405      assetDir = config.assetDir();
406      buildDir = config.buildDir();
407      shadows = config.shadows();
408      instrumentedPackages = config.instrumentedPackages();
409      application = config.application();
410      libraries = config.libraries();
411      constants = config.constants();
412    }
413
414    public Builder setSdk(int... sdk) {
415      this.sdk = sdk;
416      return this;
417    }
418
419    public Builder setMinSdk(int minSdk) {
420      this.minSdk = minSdk;
421      return this;
422    }
423
424    public Builder setMaxSdk(int maxSdk) {
425      this.maxSdk = maxSdk;
426      return this;
427    }
428
429    public Builder setManifest(String manifest) {
430      this.manifest = manifest;
431      return this;
432    }
433
434    public Builder setQualifiers(String qualifiers) {
435      this.qualifiers = qualifiers;
436      return this;
437    }
438
439    public Builder setPackageName(String packageName) {
440      this.packageName = packageName;
441      return this;
442    }
443
444    public Builder setAbiSplit(String abiSplit) {
445      this.abiSplit = abiSplit;
446      return this;
447    }
448
449    public Builder setResourceDir(String resourceDir) {
450      this.resourceDir = resourceDir;
451      return this;
452    }
453
454    public Builder setAssetDir(String assetDir) {
455      this.assetDir = assetDir;
456      return this;
457    }
458
459    public Builder setBuildDir(String buildDir) {
460      this.buildDir = buildDir;
461      return this;
462    }
463
464    public Builder setShadows(Class<?>[] shadows) {
465      this.shadows = shadows;
466      return this;
467    }
468
469    public Builder setInstrumentedPackages(String[] instrumentedPackages) {
470      this.instrumentedPackages = instrumentedPackages;
471      return this;
472    }
473
474    public Builder setApplication(Class<? extends Application> application) {
475      this.application = application;
476      return this;
477    }
478
479    public Builder setLibraries(String[] libraries) {
480      this.libraries = libraries;
481      return this;
482    }
483
484    public Builder setConstants(Class<?> constants) {
485      this.constants = constants;
486      return this;
487    }
488
489    /**
490     * This returns actual default values where they exist, in the sense that we could use
491     * the values, rather than markers like {@code -1} or {@code --default}.
492     */
493    public static Builder defaults() {
494      return new Builder()
495          .setManifest(DEFAULT_MANIFEST_NAME)
496          .setResourceDir(DEFAULT_RES_FOLDER)
497          .setAssetDir(DEFAULT_ASSET_FOLDER);
498    }
499
500    public Builder overlay(Config overlayConfig) {
501      int[] overlaySdk = overlayConfig.sdk();
502      int overlayMinSdk = overlayConfig.minSdk();
503      int overlayMaxSdk = overlayConfig.maxSdk();
504
505      //noinspection ConstantConditions
506      if (overlaySdk != null && overlaySdk.length > 0) {
507        this.sdk = overlaySdk;
508        this.minSdk = overlayMinSdk;
509        this.maxSdk = overlayMaxSdk;
510      } else {
511        if (overlayMinSdk != DEFAULT_VALUE_INT || overlayMaxSdk != DEFAULT_VALUE_INT) {
512          this.sdk = new int[0];
513        } else {
514          this.sdk = pickSdk(this.sdk, overlaySdk, new int[0]);
515        }
516        this.minSdk = pick(this.minSdk, overlayMinSdk, DEFAULT_VALUE_INT);
517        this.maxSdk = pick(this.maxSdk, overlayMaxSdk, DEFAULT_VALUE_INT);
518      }
519      this.manifest = pick(this.manifest, overlayConfig.manifest(), DEFAULT_VALUE_STRING);
520      this.qualifiers = pick(this.qualifiers, overlayConfig.qualifiers(), "");
521      this.packageName = pick(this.packageName, overlayConfig.packageName(), "");
522      this.abiSplit = pick(this.abiSplit, overlayConfig.abiSplit(), "");
523      this.resourceDir = pick(this.resourceDir, overlayConfig.resourceDir(), Config.DEFAULT_RES_FOLDER);
524      this.assetDir = pick(this.assetDir, overlayConfig.assetDir(), Config.DEFAULT_ASSET_FOLDER);
525      this.buildDir = pick(this.buildDir, overlayConfig.buildDir(), Config.DEFAULT_BUILD_FOLDER);
526      this.constants = pick(this.constants, overlayConfig.constants(), Void.class);
527
528      Set<Class<?>> shadows = new HashSet<>();
529      shadows.addAll(Arrays.asList(this.shadows));
530      shadows.addAll(Arrays.asList(overlayConfig.shadows()));
531      this.shadows = shadows.toArray(new Class[shadows.size()]);
532
533      Set<String> instrumentedPackages = new HashSet<>();
534      instrumentedPackages.addAll(Arrays.asList(this.instrumentedPackages));
535      instrumentedPackages.addAll(Arrays.asList(overlayConfig.instrumentedPackages()));
536      this.instrumentedPackages = instrumentedPackages.toArray(new String[instrumentedPackages.size()]);
537
538      this.application = pick(this.application, overlayConfig.application(), DEFAULT_APPLICATION);
539
540      Set<String> libraries = new HashSet<>();
541      libraries.addAll(Arrays.asList(this.libraries));
542      libraries.addAll(Arrays.asList(overlayConfig.libraries()));
543      this.libraries = libraries.toArray(new String[libraries.size()]);
544
545      return this;
546    }
547
548    private <T> T pick(T baseValue, T overlayValue, T nullValue) {
549      return overlayValue != null ? (overlayValue.equals(nullValue) ? baseValue : overlayValue) : null;
550    }
551
552    private int[] pickSdk(int[] baseValue, int[] overlayValue, int[] nullValue) {
553      return Arrays.equals(overlayValue, nullValue) ? baseValue : overlayValue;
554    }
555
556    public Implementation build() {
557      return new Implementation(sdk, minSdk, maxSdk, manifest, qualifiers, packageName, abiSplit, resourceDir, assetDir, buildDir, shadows, instrumentedPackages, application, libraries, constants);
558    }
559
560    public static boolean isDefaultApplication(Class<? extends Application> clazz) {
561      return clazz == null || clazz.getCanonicalName().equals(DEFAULT_APPLICATION.getCanonicalName());
562    }
563  }
564}
565