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