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