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