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