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