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