diff options
author | Rex Hoffman <rexhoffman@google.com> | 2024-04-03 13:47:24 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2024-04-03 13:47:24 +0000 |
commit | c1e9dcc09586aac4d2adb38e90d396a7387092fd (patch) | |
tree | d5db24dce82a9a30cb3f57e1407af3a4ded60a89 | |
parent | 8a88d54f8fc094d3d6b6758193712e0673291b9b (diff) | |
parent | e1d1549a07a4521c8cb1ba48b15fe28948f4c07d (diff) | |
download | robolectric-c1e9dcc09586aac4d2adb38e90d396a7387092fd.tar.gz |
Merge changes I6ae79b35,If1ded0da into main
* changes:
Support for Robolectric's validator
Merge remote-tracking branch 'aosp/upstream-google' into merge
87 files changed, 1860 insertions, 788 deletions
diff --git a/.github/workflows/copybara_build_and_test.yml b/.github/workflows/copybara_build_and_test.yml index 55a995360..f780ecf94 100644 --- a/.github/workflows/copybara_build_and_test.yml +++ b/.github/workflows/copybara_build_and_test.yml @@ -40,6 +40,5 @@ jobs: -Drobolectric.alwaysIncludeVariantMarkersInTestName=true \ -Drobolectric.enabledSdks=34 \ -Dorg.gradle.workers.max=2 \ - -Drobolectric.usePreinstrumentedJars=false \ -x :integration_tests:nativegraphics:test \ ) diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml index f8d065d2b..a3661d768 100644 --- a/.github/workflows/gradle_tasks_validation.yml +++ b/.github/workflows/gradle_tasks_validation.yml @@ -58,9 +58,6 @@ jobs: steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - name: Set up JDK uses: actions/setup-java@v4 @@ -75,3 +72,20 @@ jobs: - name: Run :preinstrumented:instrumentAll with SDK 33 run: PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll + + run_publishToMavenLocal: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: 17 + + - uses: gradle/actions/setup-gradle@v3 + + - name: Publish to Maven local + run: ./gradlew publishToMavenLocal --no-watch-fs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 66ca84bb9..00e9e46b2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,7 +68,6 @@ jobs: --no-watch-fs \ -Drobolectric.enabledSdks=${{ matrix.api-versions }} \ -Drobolectric.alwaysIncludeVariantMarkersInTestName=true \ - -Drobolectric.usePreinstrumentedJars=false \ -Dorg.gradle.workers.max=2 \ -x :integration_tests:nativegraphics:test \ -x :integration_tests:roborazzi:test diff --git a/.github/workflows/validate_commit_message.yml b/.github/workflows/validate_commit_message.yml index 57124e8b2..f90be1f69 100644 --- a/.github/workflows/validate_commit_message.yml +++ b/.github/workflows/validate_commit_message.yml @@ -25,7 +25,7 @@ jobs: # Check that the commit title isn't excessively long. commit_title="$(git log -1 --pretty=format:'%s')" if [ "${#commit_title}" -gt 120 ]; then - echo "Error: The title of commit is too long" + echo "Error: the title of the commit is too long (max: 120 characters)" exit 1 fi @@ -41,6 +41,14 @@ jobs: # Check that the commit has a body commit_body="$(git log -1 --pretty=format:'%b' | grep -v 'PiperOrigin-RevId')" if [ -z "$commit_body" ]; then - echo "Error: The commit message should have a descriptive body" + echo "Error: the commit should have a descriptive body" exit 1 fi + + while read -r line; do + if [ "${#line}" -gt 120 ] && [[ ! "$line" =~ (https?://|www\.) ]]; then + echo "Error: the following line of the commit body is too long (max: 120 characters):" + echo "> $line" + exit 1 + fi + done <<< $commit_body diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..1654ebbeb --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,213 @@ +# Robolectric architecture + +Robolectric is a unit testing framework that allows Android code to be tested on +the JVM without the need for an emulator or device. This allows tests to run +very quickly in a more hermetic environment. Robolectric has a complex +architecture and makes use of many advanced features of the JVM such as bytecode +instrumentation and custom ClassLoaders. This document provides a high level +overview of the architecture of Robolectric. + +# Android framework Jars and instrumentation + +At the heart of Robolectric are the Android framework Jars and the bytecode +instrumentation. The Android framework Jars are a collection of Jar files that +are built directly from Android platform sources. There is a single Jar file for +each version of Android. These Jar files can be built by checking out an AOSP +repo and building the +[robolectric-host-android\_all](https://cs.android.com/android/platform/superproject/main/+/main:external/robolectric/Android.bp;l=99) +target. Unlike the android.jar (stubs jar) files managed by Android Studio, +which only contain public method signatures, the Robolectric android-all Jars +contain the implementation of the Android Java framework. This gives Robolectric +the ability to use as much real Android code as possible. A new android-all jar +is uploaded to MavenCentral for each Android release. You can see the current +android-all jars +[here](https://repo1.maven.org/maven2/org/robolectric/android-all/). + +However, the pristine android-all jars are not the ones used during tests. +Instead, Robolectric modifies the pristine android-all jars using bytecode +instrumentation (see +[ClassInstrumentor](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java)). +It performs several modifications: + +1. All Android methods, including constructors and static initializers, are + modified to support `shadowing`. This allows any method call to the Android + framework to be intercepted by Robolectric and delegated to a shadow method. + At a high level, this is done by iterating over each Android method and + converting it into two methods: the original method (but renamed), and the + `invokedynamic delegator` which can optionally invoke shadow methods if they + are available. + +1. Android constructors are specially modified to create shadow objects, if a + shadow class is bound to the Android class being instantiated. + +1. Because the Android version of Java core classes (libcore) contain subtle + differences to the JDKs, certain problematic method calls have to be + intercepted and rewritten. See + [AndroidInterceptors](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java). + +1. Native methods undergo special instrumentation. Currently native methods are + converted to no-op non-native methods that are shadowable by default. + However, there is now a native variant of each method also created. There is + more details about native code in a section below. + +1. The `final` keyword is stripped from classes and methods. + +1. Some bespoke pieces of instrumentation, such as supporting + [SparseArray.set](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java#L201). + +This instrumentation is typically performed when a new release of Robolectric is +made. These pre-instrumented Android-all jars are published on MavenCentral. See +the +[android-all-instrumented](https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/) +path. They are lazily downloaded and during tests runtime using +[MavenArtifactFetcher](https://github.com/robolectric/robolectric/blob/master/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java). + +Although Robolectric supports shadowing for Android framework classes, it is +also possible for users to perform Robolectric instrumentation for any package +(with the exception of built in Java packages). This enables shadowing of +arbitrary third-party code. + +# Shadows + +By default when an Android method is invoked during a Robolectric test, the real +Android framework code is invoked. This is because a lot of Android framework +classes are pure Java code (e.g the +[Intent](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/Intent.java) +class or the +[org.json](https://cs.android.com/android/platform/superproject/main/+/main:libcore/json/src/main/java/org/json/) +package) and that code can run on the JVM without any modifications needed. + +However, there are cases where Robolectric needs to intercept and replace +Android method calls. This most commonly occurs when Android system service or +native methods are invoked. To do this, Robolectric uses a system called Shadow +classes. + +Shadow classes are Java classes that contain the replacement code of Android +methods when they are invoked. Each shadow class is bound to specific Android +classes and methods through annotations. There are currently hundreds of shadow +classes that can be found +[here](https://github.com/robolectric/robolectric/tree/master/shadows/framework/src/main/java/org/robolectric/shadows). + +Shadow classes may optionally contain public apis APIs that can customize the +behavior of the methods they are shadowing. + +Robolectric allows tests to specify custom shadows as well to provide user +defined implementation for Android classes. + +## Shadow Packages and the Robolectric Annotation Processor + +There are two categories of shadows: Robolectric’s built-in shadows that are +aggregated using the [Robolectric Annotation Processor +(RAP)](https://github.com/robolectric/robolectric/blob/master/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java), +and custom shadows that are commonly specified using `@Config(shadows = …)`. RAP +is configured to process all of the shadow files that exist in Robolectric’s +code. The main shadow package is [framework +shadows](https://github.com/robolectric/robolectric/tree/master/shadows/framework), +which contain shadows for the Android framework. There are other shadow packages +in Robolectric's code, such as [httpclient +shadows](https://github.com/robolectric/robolectric/tree/master/shadows/httpclient), +but all of them outside of framework shadows are deprecated. When Robolectric is +built, each shadow package is processed by RAP and a +[ShadowProvider](https://github.com/robolectric/robolectric/blob/master/shadowapi/src/main/java/org/robolectric/internal/ShadowProvider.java) +file is generated. For example, to see the ShadowProvider for the framework +shadows, you can run: + +```sh +./gradlew :shadows:framework:assemble +cat ./shadows/framework/build/generated/src/apt/main/org/robolectric/Shadows.java +``` + +In this file you will see the class `public class Shadows implements +ShadowProvider`. + +During runtime, Robolectric will use ServiceLoader to detect all shadow packages +that implement ShadowProvider and the shadow classes contained in them. + +# Sandbox and ClassLoader + +Before a Robolectric test is executed, a +[Sandbox](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java) +must be initialized. A Sandbox consists of some high-level structures that are +necessary to run a Robolectric test. It primarily contains a +[SandboxClassLoader](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java), +which is a custom ClassLoader that is bound to a specific instrumented +Android-all jar. Sandboxes also contain the ExecutorService that serves as the +main thread (UI thread) as well as high-level instrumentation configuration. The +SandboxClassLoader is installed as the default ClassLoader for the test method. +When any Android class is requested, SandboxClassLoader will attempt to load the +Android class from the instrumented Android-all Jar first. The primary goal of +SandboxClassLoader is to ensure that classes from the android.jar stubs jar are +not inadvertently loaded. When classes from the android.jar stubs jar are +loaded, attempting to invoke any method on them will result in a +`RuntimeException(“Stub!”)` error. Typically the Android stubs jar is on the +class path during a Robolectric test, but it is important not to load classes +from the stubs jar. + +# Invokedynamic Delegators and ShadowWrangler + +This section provides more detail for `invokedynamic delegators` that were +referenced in the instrumentation section. For an overview of the +`invokedynamic` JVM instructions, you can search for articles or watch [YouTube +videos such as this](https://www.youtube.com/watch?v=KhiECfzyVt0). + +To reiterate, for any Android method, Robolectric’s instrumentation adds an +`invokedynamic delegator` that is responsible for determining at runtime to +either invoke the real Android framework code or a shadow method. The first time +an Android method is invoked in a Sandbox, it will result in a call to one of +the bootstrap methods in +[InvokeDynamicSupport](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java). +This will subsequently invoke the +[ShadowWrangler.findShadowMethodHandle](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java#L197) +to determine if a shadow method exists for the method that is being invoked. If +a shadow method is available a MethodHandle to it will be returned. Otherwise a +MethodHandle for the original framework code will be returned. + +# Test lifecycle + +There is a lot of work done by Robolectric before and after a test is run. +Besides the Sandbox and ClassLoader initialization mentioned above, there is +also extensive Android environment initialization that occurs before each test. +The high-level class for this is +[AndroidTestEnvironment](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java). +This involves: + +* Initializing up the Looper mode (i.e. the scheduler) +* Initializing system and app resources +* Initializing the application context and system context +* Loading the Android manifest for the test +* Creating the Application object used for the test +* Initializing the [display configuration](https://robolectric.org/device-configuration/) +* Setting up the ActivityThread +* Creating app directories + +It is possible for users to extend the test environment setup using +[TestEnvironmentLifecyclePlugin](https://github.com/robolectric/robolectric/blob/master/pluginapi/src/main/java/org/robolectric/pluginapi/TestEnvironmentLifecyclePlugin.java). + +Similarly, after each test, many Android classes are reset during +[RobolectricTestRunner.finallyAfterTest](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java#L301). +This will iterate over all shadows and invoke their static `@Resetter` methods. + +# Plugin System + +Many parts of Robolectric can be customized using a plugin system based on +Java’s +[ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). +This extensibility is useful when running Robolectric in more constrained +environments. For example, by default, most of the Robolectric classes are +designed to work in a Gradle/Android Studio environment. However, there are +companies (such as Google) that use alternate build systems (such as Bazel), and +it can be helpful to have the ability to customize the behavior of some core +modules. + +The +[pluginapi](https://github.com/robolectric/robolectric/tree/master/pluginapi/src) +subproject contains many extension points of Robolectric. However, virtually any +class that is loaded by Robolectric’s +[Injector](https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/Injector.java) +has the ability to use +[PluginFinder](https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/PluginFinder.java), +which means it can be extended at runtime. + +Typically ServiceLoaders plugins can be easily written using the +[AutoService](https://github.com/google/auto/tree/main/service) project. + diff --git a/Android.bp b/Android.bp index 0f1cfd421..4e5cb23b2 100644 --- a/Android.bp +++ b/Android.bp @@ -61,6 +61,17 @@ robolectric_build_props { } java_genrule { + name: "robolectric_props_jar", + host_supported: true, + tools: ["soong_zip"], + srcs: [":robolectric_build_props"], + out: ["robolectric_props.jar"], + cmd: "cp $(location :robolectric_build_props) . && $(location soong_zip) " + + "-o $(location robolectric_props.jar) " + + "-f ./build.prop ", +} + +java_genrule { name: "robolectric_framework_res", host_supported: true, tools: ["zip2zip"], @@ -95,12 +106,15 @@ java_device_for_host { ], } -java_library_host { +java_library { name: "robolectric-host-android_all", + host_supported: true, + device_supported: false, static_libs: [ "robolectric_android-all-device-deps", "robolectric_tzdata", "robolectric_framework_res", + "robolectric_props_jar", ], // WARNING: DO NOT ADD NEW DEPENDENCIES ON THIS MODULE OR ITS DIST JAR // This dist jar is an internal implementation detail. For external Gradle builds (outside @@ -118,11 +132,6 @@ java_library_host { ], dest: "android-all-robolectric.jar", }, - - java_resources: [ - // Copy the build.prop - ":robolectric_build_props", - ], visibility: [ ":__subpackages__", "//external/robolectric-shadows:__subpackages__", @@ -47,6 +47,9 @@ testImplementation "org.robolectric:robolectric:4.11.1" Robolectric is built using Gradle. Both IntelliJ and Android Studio can import the top-level `build.gradle` file and will automatically generate their project files from it. +To get a high-level overview of Robolectric's architecture, check out +[ARCHITECTURE.md](ARCHITECTURE.md). + ### Prerequisites See [Building Robolectric](http://robolectric.org/building-robolectric/) for more details about setting up a build environment for Robolectric. diff --git a/annotations/build.gradle b/annotations/build.gradle index d8bd113c5..c26efdb25 100644 --- a/annotations/build.gradle +++ b/annotations/build.gradle @@ -6,5 +6,11 @@ apply plugin: DeployedRoboJavaModulePlugin dependencies { compileOnly libs.findbugs.jsr305 + compileOnly libs.javax.annotation.api compileOnly AndroidSdk.MAX_SDK.coordinates + testImplementation libs.truth + testImplementation libs.junit4 + testImplementation libs.javax.annotation.api + testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK + testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports } diff --git a/annotations/src/main/java/org/robolectric/annotation/InDevelopment.java b/annotations/src/main/java/org/robolectric/annotation/InDevelopment.java new file mode 100644 index 000000000..beacbe92a --- /dev/null +++ b/annotations/src/main/java/org/robolectric/annotation/InDevelopment.java @@ -0,0 +1,18 @@ +package org.robolectric.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * InDevelopment applies to @Implementation methods and @Implements classes that are affected by + * changes in unreleased versions of Android. <br> + * Only unreleased versions of android, as defined in the AndroidVersions honor this annotation + * during validation. + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface InDevelopment {} diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java b/annotations/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java index 316365b39..316365b39 100644 --- a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java +++ b/annotations/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java b/annotations/src/main/java/org/robolectric/versioning/AndroidVersions.java index e5431eb97..71cc48042 100644 --- a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java +++ b/annotations/src/main/java/org/robolectric/versioning/AndroidVersions.java @@ -19,7 +19,7 @@ package org.robolectric.versioning; import static java.util.Arrays.asList; import java.io.IOException; -import java.lang.reflect.Field; +import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.AbstractMap; @@ -29,12 +29,11 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import javax.annotation.Nullable; -import org.robolectric.util.Logger; -import org.robolectric.util.ReflectionHelpers; /** * Android versioning is complicated.<br> @@ -63,17 +62,13 @@ public final class AndroidVersions { * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt may * still be that of the prior release. */ - public int getSdkInt() { - return ReflectionHelpers.getStaticField(this.getClass(), "SDK_INT"); - } + public abstract int getSdkInt(); /** * single character short code for the release, multiple characters for minor releases (only * minor version numbers increment - usually within the same year). */ - public String getShortCode() { - return ReflectionHelpers.getStaticField(this.getClass(), "SHORT_CODE"); - } + public abstract String getShortCode(); /** * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt will @@ -81,26 +76,22 @@ public final class AndroidVersions { * including most modern build tools; bazle, soong all are full build systems - and as such * organizations using them have no concerns. */ - public boolean isReleased() { - return ReflectionHelpers.getStaticField(this.getClass(), "RELEASED"); - } + public abstract boolean isReleased(); /** major.minor version number as String. */ - public String getVersion() { - return ReflectionHelpers.getStaticField(this.getClass(), "VERSION"); - } + public abstract String getVersion(); /** * Implements comparable. * * @param other the object to be compared. * @return 1 if this is greater than other, 0 if equal, -1 if less - * @throws RuntimeException if other is not an instance of AndroidRelease. + * @throws IllegalStateException if other is not an instance of AndroidRelease. */ @Override public int compareTo(AndroidRelease other) { if (other == null) { - throw new RuntimeException( + throw new IllegalStateException( "Only " + AndroidVersions.class.getName() + " should define Releases, illegal class " @@ -123,21 +114,80 @@ public final class AndroidVersions { } } + /** A released version of Android */ + public abstract static class AndroidReleased extends AndroidRelease { + @Override + public boolean isReleased() { + return true; + } + } + + /** An in-development version of Android */ + public abstract static class AndroidUnreleased extends AndroidRelease { + @Override + public boolean isReleased() { + return false; + } + } + + /** + * Version: -1 <br> + * ShortCode: "" <br> + * SDK API Level: "" <br> + * release: false <br> + */ + public static final class Unbound extends AndroidUnreleased { + + public static final int SDK_INT = -1; + + public static final String SHORT_CODE = "_"; + + public static final String VERSION = "_"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } + } + /** * Version: 4.1 <br> * ShortCode: J <br> * SDK API Level: 16 <br> * release: true <br> */ - public static final class J extends AndroidRelease { + public static final class J extends AndroidReleased { public static final int SDK_INT = 16; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "J"; public static final String VERSION = "4.1"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -146,15 +196,28 @@ public final class AndroidVersions { * SDK API Level: 17 <br> * release: true <br> */ - public static final class JMR1 extends AndroidRelease { + public static final class JMR1 extends AndroidReleased { public static final int SDK_INT = 17; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "JMR1"; public static final String VERSION = "4.2"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -163,15 +226,28 @@ public final class AndroidVersions { * SDK API Level: 18 <br> * release: true <br> */ - public static final class JMR2 extends AndroidRelease { + public static final class JMR2 extends AndroidReleased { public static final int SDK_INT = 18; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "JMR2"; public static final String VERSION = "4.3"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -180,15 +256,28 @@ public final class AndroidVersions { * SDK API Level: 19 <br> * release: true <br> */ - public static final class K extends AndroidRelease { + public static final class K extends AndroidReleased { public static final int SDK_INT = 19; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "K"; public static final String VERSION = "4.4"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } // Skipping K Watch release, which was 20. @@ -199,15 +288,28 @@ public final class AndroidVersions { * SDK API Level: 21 <br> * release: true <br> */ - public static final class L extends AndroidRelease { + public static final class L extends AndroidReleased { public static final int SDK_INT = 21; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "L"; public static final String VERSION = "5.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -216,15 +318,28 @@ public final class AndroidVersions { * SDK API Level: 22 <br> * release: true <br> */ - public static final class LMR1 extends AndroidRelease { + public static final class LMR1 extends AndroidReleased { public static final int SDK_INT = 22; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "LMR1"; public static final String VERSION = "5.1"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -233,15 +348,28 @@ public final class AndroidVersions { * SDK API Level: 23 <br> * release: true <br> */ - public static final class M extends AndroidRelease { + public static final class M extends AndroidReleased { public static final int SDK_INT = 23; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "M"; public static final String VERSION = "6.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -250,15 +378,28 @@ public final class AndroidVersions { * SDK API Level: 24 <br> * release: true <br> */ - public static final class N extends AndroidRelease { + public static final class N extends AndroidReleased { public static final int SDK_INT = 24; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "N"; public static final String VERSION = "7.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -267,15 +408,28 @@ public final class AndroidVersions { * SDK Framework: 25 <br> * release: true <br> */ - public static final class NMR1 extends AndroidRelease { + public static final class NMR1 extends AndroidReleased { public static final int SDK_INT = 25; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "NMR1"; - private static final String VERSION = "7.1"; + public static final String VERSION = "7.1"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -284,15 +438,28 @@ public final class AndroidVersions { * SDK API Level: 26 <br> * release: true <br> */ - public static final class O extends AndroidRelease { + public static final class O extends AndroidReleased { public static final int SDK_INT = 26; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "O"; public static final String VERSION = "8.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -301,15 +468,28 @@ public final class AndroidVersions { * SDK API Level: 27 <br> * release: true <br> */ - public static final class OMR1 extends AndroidRelease { + public static final class OMR1 extends AndroidReleased { public static final int SDK_INT = 27; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "OMR1"; public static final String VERSION = "8.1"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -318,15 +498,28 @@ public final class AndroidVersions { * SDK API Level: 28 <br> * release: true <br> */ - public static final class P extends AndroidRelease { + public static final class P extends AndroidReleased { public static final int SDK_INT = 28; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "P"; public static final String VERSION = "9.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -335,15 +528,28 @@ public final class AndroidVersions { * SDK API Level: 29 <br> * release: true <br> */ - public static final class Q extends AndroidRelease { + public static final class Q extends AndroidReleased { public static final int SDK_INT = 29; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "Q"; public static final String VERSION = "10.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -352,15 +558,28 @@ public final class AndroidVersions { * SDK API Level: 30 <br> * release: true <br> */ - public static final class R extends AndroidRelease { + public static final class R extends AndroidReleased { public static final int SDK_INT = 30; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "R"; public static final String VERSION = "11.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -369,15 +588,28 @@ public final class AndroidVersions { * SDK API Level: 31 <br> * release: true <br> */ - public static final class S extends AndroidRelease { + public static final class S extends AndroidReleased { public static final int SDK_INT = 31; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "S"; public static final String VERSION = "12.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -387,15 +619,28 @@ public final class AndroidVersions { * release: true <br> */ @SuppressWarnings("UPPER_SNAKE_CASE") - public static final class Sv2 extends AndroidRelease { + public static final class Sv2 extends AndroidReleased { public static final int SDK_INT = 32; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "Sv2"; public static final String VERSION = "12.1"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -404,15 +649,28 @@ public final class AndroidVersions { * SDK API Level: 33 <br> * release: true <br> */ - public static final class T extends AndroidRelease { + public static final class T extends AndroidReleased { public static final int SDK_INT = 33; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "T"; public static final String VERSION = "13.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -421,15 +679,28 @@ public final class AndroidVersions { * SDK API Level: 34 <br> * release: false <br> */ - public static final class U extends AndroidRelease { + public static final class U extends AndroidReleased { public static final int SDK_INT = 34; - public static final boolean RELEASED = true; - public static final String SHORT_CODE = "U"; public static final String VERSION = "14.0"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** @@ -438,15 +709,28 @@ public final class AndroidVersions { * SDK API Level: 34+ <br> * release: false <br> */ - public static final class V extends AndroidRelease { + public static final class V extends AndroidUnreleased { public static final int SDK_INT = 35; - public static final boolean RELEASED = false; - public static final String SHORT_CODE = "V"; public static final String VERSION = "15"; + + @Override + public int getSdkInt() { + return SDK_INT; + } + + @Override + public String getShortCode() { + return SHORT_CODE; + } + + @Override + public String getVersion() { + return VERSION; + } } /** The current release this process is running on. */ @@ -574,7 +858,7 @@ public final class AndroidVersions { } } if (errors.length() > 0) { - throw new RuntimeException( + throw new IllegalStateException( errors .append("Please check the AndroidReleases defined ") .append("in ") @@ -587,18 +871,13 @@ public final class AndroidVersions { public AndroidRelease computeCurrentSdk( int reportedVersion, String releaseName, String codename, List<String> activeCodeNames) { - Logger.info("Reported Version: " + reportedVersion); - Logger.info("Release Name: " + releaseName); - Logger.info("Code Name: " + codename); - Logger.info("Active Code Names: " + String.join(",", activeCodeNames)); - AndroidRelease current = null; // Special case "REL", which means the build is not a pre-release build. - if ("REL".equals(codename)) { + if (Objects.equals(codename, "REL")) { // the first letter of the code name equal to the release number. current = sdkIntToAllReleases.get(reportedVersion); if (current != null && !current.isReleased()) { - throw new RuntimeException( + throw new IllegalStateException( "The current sdk " + current.getShortCode() + " has been released. Please update the contents of " @@ -657,7 +936,7 @@ public final class AndroidVersions { .append("contents of current sdk jar to the released version.\n"); } if (detectedProblems.length() > 0) { - throw new RuntimeException(detectedProblems.toString()); + throw new IllegalStateException(detectedProblems.toString()); } } } @@ -670,7 +949,7 @@ public final class AndroidVersions { * the shortCode, sdkInt, and release information. * * <p>All errors are stored and can be reported at once by asking the SdkInformation to throw a - * runtime exception after it has been populated. + * IllegalStateException after it has been populated. */ static SdkInformation gatherStaticSdkInformationFromThisClass() { List<AndroidRelease> allReleases = new ArrayList<>(); @@ -678,7 +957,8 @@ public final class AndroidVersions { for (Class<?> clazz : AndroidVersions.class.getClasses()) { if (AndroidRelease.class.isAssignableFrom(clazz) && !clazz.isInterface() - && !Modifier.isAbstract(clazz.getModifiers())) { + && !Modifier.isAbstract(clazz.getModifiers()) + && clazz != Unbound.class) { try { AndroidRelease rel = (AndroidRelease) clazz.getDeclaredConstructor().newInstance(); allReleases.add(rel); @@ -691,7 +971,7 @@ public final class AndroidVersions { | IllegalArgumentException | IllegalAccessException | InvocationTargetException ex) { - throw new RuntimeException( + throw new IllegalStateException( "Classes " + clazz.getName() + "should be accessible via " @@ -731,50 +1011,19 @@ public final class AndroidVersions { return information.computeCurrentSdk(sdk, release, codename, asList(activeCodeNames)); } - /** - * If we are working in android source, this code detects the list of active code names if any. - */ - private static List<String> getActiveCodeNamesIfAny(Class<?> targetClass) { - try { - Field activeCodeFields = targetClass.getDeclaredField("ACTIVE_CODENAMES"); - String[] activeCodeNames = (String[]) activeCodeFields.get(null); - if (activeCodeNames == null) { - return new ArrayList<>(); - } - return asList(activeCodeNames); - } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException ex) { - return new ArrayList<>(); - } - } - private static final SdkInformation information; static { AndroidRelease currentRelease = null; information = gatherStaticSdkInformationFromThisClass(); try { - Class<?> buildClass = - Class.forName("android.os.Build", false, Thread.currentThread().getContextClassLoader()); - System.out.println("build class " + buildClass); - Class<?> versionClass = null; - for (Class<?> c : buildClass.getClasses()) { - if (c.getSimpleName().equals("VERSION")) { - versionClass = c; - System.out.println("Version class " + versionClass); - break; - } - } - if (versionClass != null) { - // 33, 34, etc.... - int sdkInt = (int) ReflectionHelpers.getStaticField(versionClass, "SDK_INT"); - // Either unset, or 13, 14, etc.... - String release = ReflectionHelpers.getStaticField(versionClass, "RELEASE"); - // Either REL if release is set, or Tiramasu, UpsideDownCake, etc - String codename = ReflectionHelpers.getStaticField(versionClass, "CODENAME"); - List<String> activeCodeNames = getActiveCodeNamesIfAny(versionClass); - currentRelease = information.computeCurrentSdk(sdkInt, release, codename, activeCodeNames); + InputStream is = AndroidVersions.class.getClassLoader().getResourceAsStream("build.prop"); + if (is != null) { + Properties buildProps = new Properties(); + buildProps.load(is); + currentRelease = computeCurrentSdkFromBuildProps(buildProps); } - } catch (ClassNotFoundException | IllegalArgumentException | UnsatisfiedLinkError e) { + } catch (IOException ioe) { // No op, this class should be usable outside of a Robolectric sandbox. } CURRENT = currentRelease; diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java b/annotations/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java index 95b2c4266..95b2c4266 100644 --- a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java +++ b/annotations/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java diff --git a/annotations/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java b/annotations/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java new file mode 100644 index 000000000..670e695a3 --- /dev/null +++ b/annotations/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java @@ -0,0 +1,169 @@ +package org.robolectric.versioning; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Check versions information aligns with runtime information. Primarily, selected SDK with + * internally detected version number. + */ +@RunWith(JUnit4.class) +public final class AndroidVersionsTest { + + @Test + public void testStandardInitializationT() { + assertThat(AndroidVersions.T.SDK_INT).isEqualTo(33); + assertThat(AndroidVersions.T.SHORT_CODE).isEqualTo("T"); + assertThat(AndroidVersions.T.VERSION).isEqualTo("13.0"); + assertThat(new AndroidVersions.T().getSdkInt()).isEqualTo(33); + assertThat(new AndroidVersions.T().getShortCode()).isEqualTo("T"); + assertThat(new AndroidVersions.T().getVersion()).isEqualTo("13.0"); + assertThat(new AndroidVersions.T().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationSv2() { + assertThat(AndroidVersions.Sv2.SDK_INT).isEqualTo(32); + assertThat(AndroidVersions.Sv2.SHORT_CODE).isEqualTo("Sv2"); + assertThat(AndroidVersions.Sv2.VERSION).isEqualTo("12.1"); + assertThat(new AndroidVersions.Sv2().getSdkInt()).isEqualTo(32); + assertThat(new AndroidVersions.Sv2().getShortCode()).isEqualTo("Sv2"); + assertThat(new AndroidVersions.Sv2().getVersion()).isEqualTo("12.1"); + assertThat(new AndroidVersions.Sv2().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationS() { + assertThat(AndroidVersions.S.SDK_INT).isEqualTo(31); + assertThat(AndroidVersions.S.SHORT_CODE).isEqualTo("S"); + assertThat(AndroidVersions.S.VERSION).isEqualTo("12.0"); + assertThat(new AndroidVersions.S().getSdkInt()).isEqualTo(31); + assertThat(new AndroidVersions.S().getShortCode()).isEqualTo("S"); + assertThat(new AndroidVersions.S().getVersion()).isEqualTo("12.0"); + assertThat(new AndroidVersions.S().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationR() { + assertThat(AndroidVersions.R.SDK_INT).isEqualTo(30); + assertThat(AndroidVersions.R.SHORT_CODE).isEqualTo("R"); + assertThat(AndroidVersions.R.VERSION).isEqualTo("11.0"); + assertThat(new AndroidVersions.R().getSdkInt()).isEqualTo(30); + assertThat(new AndroidVersions.R().getShortCode()).isEqualTo("R"); + assertThat(new AndroidVersions.R().getVersion()).isEqualTo("11.0"); + assertThat(new AndroidVersions.R().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationQ() { + assertThat(AndroidVersions.Q.SDK_INT).isEqualTo(29); + assertThat(AndroidVersions.Q.SHORT_CODE).isEqualTo("Q"); + assertThat(AndroidVersions.Q.VERSION).isEqualTo("10.0"); + assertThat(new AndroidVersions.Q().getSdkInt()).isEqualTo(29); + assertThat(new AndroidVersions.Q().getShortCode()).isEqualTo("Q"); + assertThat(new AndroidVersions.Q().getVersion()).isEqualTo("10.0"); + assertThat(new AndroidVersions.Q().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationP() { + assertThat(AndroidVersions.P.SDK_INT).isEqualTo(28); + assertThat(AndroidVersions.P.SHORT_CODE).isEqualTo("P"); + assertThat(AndroidVersions.P.VERSION).isEqualTo("9.0"); + assertThat(new AndroidVersions.P().getSdkInt()).isEqualTo(28); + assertThat(new AndroidVersions.P().getShortCode()).isEqualTo("P"); + assertThat(new AndroidVersions.P().getVersion()).isEqualTo("9.0"); + assertThat(new AndroidVersions.P().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationOMR1() { + assertThat(AndroidVersions.OMR1.SDK_INT).isEqualTo(27); + assertThat(AndroidVersions.OMR1.SHORT_CODE).isEqualTo("OMR1"); + assertThat(AndroidVersions.OMR1.VERSION).isEqualTo("8.1"); + assertThat(new AndroidVersions.OMR1().getSdkInt()).isEqualTo(27); + assertThat(new AndroidVersions.OMR1().getShortCode()).isEqualTo("OMR1"); + assertThat(new AndroidVersions.OMR1().getVersion()).isEqualTo("8.1"); + assertThat(new AndroidVersions.OMR1().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationO() { + assertThat(AndroidVersions.O.SDK_INT).isEqualTo(26); + assertThat(AndroidVersions.O.SHORT_CODE).isEqualTo("O"); + assertThat(AndroidVersions.O.VERSION).isEqualTo("8.0"); + assertThat(new AndroidVersions.O().getSdkInt()).isEqualTo(26); + assertThat(new AndroidVersions.O().getShortCode()).isEqualTo("O"); + assertThat(new AndroidVersions.O().getVersion()).isEqualTo("8.0"); + assertThat(new AndroidVersions.O().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationNMR1() { + assertThat(AndroidVersions.NMR1.SDK_INT).isEqualTo(25); + assertThat(AndroidVersions.NMR1.SHORT_CODE).isEqualTo("NMR1"); + assertThat(AndroidVersions.NMR1.VERSION).isEqualTo("7.1"); + assertThat(new AndroidVersions.NMR1().getSdkInt()).isEqualTo(25); + assertThat(new AndroidVersions.NMR1().getShortCode()).isEqualTo("NMR1"); + assertThat(new AndroidVersions.NMR1().getVersion()).isEqualTo("7.1"); + assertThat(new AndroidVersions.NMR1().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationN() { + assertThat(AndroidVersions.N.SDK_INT).isEqualTo(24); + assertThat(AndroidVersions.N.SHORT_CODE).isEqualTo("N"); + assertThat(AndroidVersions.N.VERSION).isEqualTo("7.0"); + assertThat(new AndroidVersions.N().getSdkInt()).isEqualTo(24); + assertThat(new AndroidVersions.N().getShortCode()).isEqualTo("N"); + assertThat(new AndroidVersions.N().getVersion()).isEqualTo("7.0"); + assertThat(new AndroidVersions.N().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationM() { + assertThat(AndroidVersions.M.SDK_INT).isEqualTo(23); + assertThat(AndroidVersions.M.SHORT_CODE).isEqualTo("M"); + assertThat(AndroidVersions.M.VERSION).isEqualTo("6.0"); + assertThat(new AndroidVersions.M().getSdkInt()).isEqualTo(23); + assertThat(new AndroidVersions.M().getShortCode()).isEqualTo("M"); + assertThat(new AndroidVersions.M().getVersion()).isEqualTo("6.0"); + assertThat(new AndroidVersions.M().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationLMR1() { + assertThat(AndroidVersions.LMR1.SDK_INT).isEqualTo(22); + assertThat(AndroidVersions.LMR1.SHORT_CODE).isEqualTo("LMR1"); + assertThat(AndroidVersions.LMR1.VERSION).isEqualTo("5.1"); + assertThat(new AndroidVersions.LMR1().getSdkInt()).isEqualTo(22); + assertThat(new AndroidVersions.LMR1().getShortCode()).isEqualTo("LMR1"); + assertThat(new AndroidVersions.LMR1().getVersion()).isEqualTo("5.1"); + assertThat(new AndroidVersions.LMR1().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationL() { + assertThat(AndroidVersions.L.SDK_INT).isEqualTo(21); + assertThat(AndroidVersions.L.SHORT_CODE).isEqualTo("L"); + assertThat(AndroidVersions.L.VERSION).isEqualTo("5.0"); + assertThat(new AndroidVersions.L().getSdkInt()).isEqualTo(21); + assertThat(new AndroidVersions.L().getShortCode()).isEqualTo("L"); + assertThat(new AndroidVersions.L().getVersion()).isEqualTo("5.0"); + assertThat(new AndroidVersions.L().isReleased()).isEqualTo(true); + } + + @Test + public void testStandardInitializationK() { + assertThat(AndroidVersions.K.SDK_INT).isEqualTo(19); + assertThat(AndroidVersions.K.SHORT_CODE).isEqualTo("K"); + assertThat(AndroidVersions.K.VERSION).isEqualTo("4.4"); + assertThat(new AndroidVersions.K().getSdkInt()).isEqualTo(19); + assertThat(new AndroidVersions.K().getShortCode()).isEqualTo("K"); + assertThat(new AndroidVersions.K().getVersion()).isEqualTo("4.4"); + assertThat(new AndroidVersions.K().isReleased()).isEqualTo(true); + } +} diff --git a/build.gradle b/build.gradle index 29ad8a91c..0f79ab057 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ import org.gradle.plugins.ide.idea.model.IdeaModel - +import org.robolectric.gradle.SpotlessPlugin buildscript { apply from: 'dependencies.gradle' @@ -33,21 +33,8 @@ allprojects { } apply plugin: 'idea' -apply plugin: "com.diffplug.spotless" - -// Apply Spotless for buildSrc and build scripts -spotless { - java { - target("buildSrc/**/*.java") - googleJavaFormat("1.17.0") - } - groovy { - target("buildSrc/**/*.groovy") - } - groovyGradle { - target('*.gradle', "**/*.gradle") - } -} +// apply SpotlessPlugin +apply plugin: SpotlessPlugin project.ext.configAnnotationProcessing = [] project.afterEvaluate { @@ -58,8 +45,9 @@ project.afterEvaluate { // prevent compiler from complaining about duplicate classes... def excludeFromCompile = compilerConfiguration.appendNode 'excludeFromCompile' configAnnotationProcessing.each { Project subProject -> + def buildDirectory = subProject.layout.buildDirectory.get().asFile excludeFromCompile.appendNode('directory', - [url: "file://${subProject.buildDir}/classes/java/main/generated", includeSubdirectories: "true"]) + [url: "file://$buildDirectory/classes/java/main/generated", includeSubdirectories: "true"]) } // replace existing annotationProcessing tag with a new one... @@ -73,13 +61,16 @@ project.afterEvaluate { processor(name: "org.robolectric.annotation.processing.RobolectricProcessor") processorPath(useClasspath: "false") { - def processorRuntimeCfg = project.project(":processor").configurations['runtime'] - processorRuntimeCfg.allArtifacts.each { artifact -> - entry(name: artifact.file) - } - processorRuntimeCfg.files.each { file -> - entry(name: file) - } + project.project(":processor") + .configurations.named("runtime") + .configure { runtimeConfiguration -> + runtimeConfiguration.allArtifacts.each { artifact -> + entry(name: artifact.file) + } + runtimeConfiguration.files.each { file -> + entry(name: file) + } + } } } } @@ -91,7 +82,9 @@ project.afterEvaluate { apply plugin: 'nebula-aggregate-javadocs' rootProject.gradle.projectsEvaluated { - rootProject.tasks['aggregateJavadocs'].failOnError = false + rootProject.tasks.named("aggregateJavadocs").configure { + it.failOnError = false + } } gradle.projectsEvaluated { @@ -122,10 +115,10 @@ gradle.projectsEvaluated { gradle.projectsEvaluated { tasks.register('aggregateJsondocs', Copy) { project.subprojects.findAll { it.plugins.hasPlugin(ShadowsPlugin) }.each { subproject -> - dependsOn subproject.tasks["compileJava"] - from "${subproject.buildDir}/docs/json" + dependsOn subproject.tasks.named("compileJava") + from subproject.layout.buildDirectory.dir("docs/json") } - into "$buildDir/docs/json" + into layout.buildDirectory.dir("docs/json") } } diff --git a/buildSrc/src/main/groovy/AndroidSdk.groovy b/buildSrc/src/main/groovy/AndroidSdk.groovy index 59bbb5541..5a6ed794b 100644 --- a/buildSrc/src/main/groovy/AndroidSdk.groovy +++ b/buildSrc/src/main/groovy/AndroidSdk.groovy @@ -1,5 +1,5 @@ class AndroidSdk implements Comparable<AndroidSdk> { - static final PREINSTRUMENTED_VERSION = 5 + static final PREINSTRUMENTED_VERSION = 6 static final KITKAT = new AndroidSdk(19, "4.4_r1", "r2") static final LOLLIPOP = new AndroidSdk(21, "5.0.2_r3", "r0") @@ -9,13 +9,13 @@ class AndroidSdk implements Comparable<AndroidSdk> { static final N_MR1 = new AndroidSdk(25, "7.1.0_r7", "r1") static final O = new AndroidSdk(26, "8.0.0_r4", "r1") static final O_MR1 = new AndroidSdk(27, "8.1.0", "4611349") - static final P = new AndroidSdk(28, "9", "4913185-2"); - static final Q = new AndroidSdk(29, "10", "5803371"); - static final R = new AndroidSdk(30, "11", "6757853"); - static final S = new AndroidSdk(31, "12", "7732740"); - static final S_V2 = new AndroidSdk(32, "12.1", "8229987"); - static final TIRAMISU = new AndroidSdk(33, "13", "9030017"); - static final U = new AndroidSdk(34, "14", "10818077"); + static final P = new AndroidSdk(28, "9", "4913185-2") + static final Q = new AndroidSdk(29, "10", "5803371") + static final R = new AndroidSdk(30, "11", "6757853") + static final S = new AndroidSdk(31, "12", "7732740") + static final S_V2 = new AndroidSdk(32, "12.1", "8229987") + static final TIRAMISU = new AndroidSdk(33, "13", "9030017") + static final U = new AndroidSdk(34, "14", "10818077") static final List<AndroidSdk> ALL_SDKS = [ KITKAT, @@ -26,7 +26,7 @@ class AndroidSdk implements Comparable<AndroidSdk> { static final MAX_SDK = Collections.max(ALL_SDKS) public final int apiLevel - private final String androidVersion + public final String androidVersion private final String frameworkSdkBuildVersion AndroidSdk(int apiLevel, String androidVersion, String frameworkSdkBuildVersion) { diff --git a/buildSrc/src/main/groovy/ProvideBuildClasspathTask.groovy b/buildSrc/src/main/groovy/ProvideBuildClasspathTask.groovy index 8a6d0f279..014b1b28c 100644 --- a/buildSrc/src/main/groovy/ProvideBuildClasspathTask.groovy +++ b/buildSrc/src/main/groovy/ProvideBuildClasspathTask.groovy @@ -6,18 +6,18 @@ class ProvideBuildClasspathTask extends DefaultTask { @OutputFile File outFile @TaskAction - public void writeProperties() throws Exception { + void writeProperties() throws Exception { final Properties props = new Properties() - String preinstrumentedKey = "robolectric.usePreinstrumentedJars"; + String preinstrumentedKey = "robolectric.usePreinstrumentedJars" boolean usePreinstrumentedJars = Boolean.parseBoolean( - System.getProperty(preinstrumentedKey, "true")); + System.getProperty(preinstrumentedKey, "true")) AndroidSdk.ALL_SDKS.each { androidSdk -> String coordinates = usePreinstrumentedJars ? - androidSdk.preinstrumentedCoordinates : androidSdk.coordinates; + androidSdk.preinstrumentedCoordinates : androidSdk.coordinates def config = project.configurations.create("sdk${androidSdk.apiLevel}") project.dependencies.add( diff --git a/buildSrc/src/main/groovy/ShadowsPlugin.groovy b/buildSrc/src/main/groovy/ShadowsPlugin.groovy index 27aa14fa4..a0563ee85 100644 --- a/buildSrc/src/main/groovy/ShadowsPlugin.groovy +++ b/buildSrc/src/main/groovy/ShadowsPlugin.groovy @@ -16,37 +16,40 @@ class ShadowsPlugin implements Plugin<Project> { annotationProcessor project.project(":processor") } - def compileJavaTask = project.tasks["compileJava"] - // write generated Java into its own dir... see https://github.com/gradle/gradle/issues/4956 - def generatedSrcRelPath = 'build/generated/src/apt/main' - def generatedSrcDir = project.file(generatedSrcRelPath) - - project.sourceSets.main.java { srcDir generatedSrcRelPath } - project.mkdir(generatedSrcDir) - compileJavaTask.options.annotationProcessorGeneratedSourcesDirectory = generatedSrcDir - compileJavaTask.outputs.dir(generatedSrcDir) - - compileJavaTask.doFirst { - options.compilerArgs.add("-Aorg.robolectric.annotation.processing.jsonDocsEnabled=true") - options.compilerArgs.add("-Aorg.robolectric.annotation.processing.jsonDocsDir=${project.buildDir}/docs/json") - options.compilerArgs.add("-Aorg.robolectric.annotation.processing.shadowPackage=${project.shadows.packageName}") - options.compilerArgs.add("-Aorg.robolectric.annotation.processing.sdkCheckMode=${project.shadows.sdkCheckMode}") - options.compilerArgs.add("-Aorg.robolectric.annotation.processing.sdks=${project.rootProject.buildDir}/sdks.txt") + def generatedSrcDir = project.file("build/generated/src/apt/main") + + project.tasks.named("compileJava").configure { task -> + task.options.annotationProcessorGeneratedSourcesDirectory = generatedSrcDir + + task.doFirst { + options.compilerArgs.add("-Aorg.robolectric.annotation.processing.jsonDocsEnabled=true") + options.compilerArgs.add("-Aorg.robolectric.annotation.processing.jsonDocsDir=${project.layout.buildDirectory.get().asFile}/docs/json") + options.compilerArgs.add("-Aorg.robolectric.annotation.processing.shadowPackage=${project.shadows.packageName}") + options.compilerArgs.add("-Aorg.robolectric.annotation.processing.sdkCheckMode=${project.shadows.sdkCheckMode}") + options.compilerArgs.add("-Aorg.robolectric.annotation.processing.sdks=${project.rootProject.layout.buildDirectory.get().asFile}/sdks.txt") + } } // include generated sources in javadoc jar - project.tasks['javadoc'].source(generatedSrcDir) + project.tasks.named("javadoc").configure { task -> + task.source(generatedSrcDir) + } // verify that we have the apt-generated files in our javadoc and sources jars - project.tasks['javadocJar'].doLast { task -> - def shadowPackageNameDir = project.shadows.packageName.replaceAll(/\./, '/') - checkForFile(task.archivePath, "${shadowPackageNameDir}/Shadows.html") + project.tasks.named("javadocJar").configure { task -> + task.doLast { + def shadowPackageNameDir = project.shadows.packageName.replaceAll(/\./, '/') + checkForFile(task.archivePath, "${shadowPackageNameDir}/Shadows.html") + } } - project.tasks['sourcesJar'].doLast { task -> - def shadowPackageNameDir = project.shadows.packageName.replaceAll(/\./, '/') - checkForFile(task.archivePath, "${shadowPackageNameDir}/Shadows.java") + project.tasks.named("sourcesJar").configure { task -> + task.from(generatedSrcDir) + task.doLast { + def shadowPackageNameDir = project.shadows.packageName.replaceAll(/\./, '/') + checkForFile(task.archivePath, "${shadowPackageNameDir}/Shadows.java") + } } project.rootProject.configAnnotationProcessing += project @@ -58,7 +61,7 @@ class ShadowsPlugin implements Plugin<Project> { * * See https://discuss.gradle.org/t/gradle-not-compiles-with-solder-tooling-jar/7583/20 */ - project.tasks.withType(JavaCompile) { options.fork = true } + project.tasks.withType(JavaCompile).configureEach { options.fork = true } } static class ShadowsPluginExtension { diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java b/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java index 7b61f7540..9322d6cd2 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java @@ -57,9 +57,9 @@ public class AarDepsPlugin implements Plugin<Project> { // incremental compile breaks (run `gradlew -i classes` twice to see impact): t -> t.doFirst( - new Action<Task>() { + new Action<>() { @Override - public void execute(Task task) { + public void execute(@NotNull Task task) { List<File> aarFiles = AarDepsPlugin.this.findAarFiles(t.getClasspath()); if (!aarFiles.isEmpty()) { throw new IllegalStateException( @@ -91,8 +91,9 @@ public class AarDepsPlugin implements Plugin<Project> { super.transform( new TransformOutputs() { // This is the one that ExtractAarTransform calls. + @NotNull @Override - public File dir(Object o) { + public File dir(@NotNull Object o) { // ExtractAarTransform needs a place to extract the AAR. We don't really need to // register this as an output, but it'd be tricky to avoid it. File dir = outputs.dir(o); @@ -105,8 +106,9 @@ public class AarDepsPlugin implements Plugin<Project> { return outputs.dir(o); } + @NotNull @Override - public File file(Object o) { + public File file(@NotNull Object o) { throw new IllegalStateException("shouldn't be called"); } }); diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy index 40442bfdc..18249f5b1 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy @@ -4,9 +4,9 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.tasks.testing.Test -public class AndroidProjectConfigPlugin implements Plugin<Project> { +class AndroidProjectConfigPlugin implements Plugin<Project> { @Override - public void apply(Project project) { + void apply(Project project) { project.android.testOptions.unitTests.all { // TODO: DRY up code with RoboJavaModulePlugin... testLogging { @@ -26,8 +26,8 @@ public class AndroidProjectConfigPlugin implements Plugin<Project> { } def forwardedSystemProperties = System.properties - .findAll { k,v -> k.startsWith("robolectric.") } - .collect { k,v -> "-D$k=$v" } + .findAll { k, v -> k.startsWith("robolectric.") } + .collect { k, v -> "-D$k=$v" } jvmArgs = forwardedSystemProperties jvmArgs += [ '--add-opens=java.base/java.lang=ALL-UNNAMED', @@ -51,8 +51,8 @@ public class AndroidProjectConfigPlugin implements Plugin<Project> { } project.task('provideBuildClasspath', type: ProvideBuildClasspathTask) { - File outDir = new File(project.buildDir, "generated/robolectric") - outFile = new File(outDir, 'robolectric-deps.properties') + File outDir = project.layout.buildDirectory.dir("generated/robolectric").get().asFile + outFile = new File(outDir, "robolectric-deps.properties") project.android.sourceSets['test'].resources.srcDir(outDir) } @@ -66,7 +66,7 @@ public class AndroidProjectConfigPlugin implements Plugin<Project> { } // Only run tests in the debug variant. This is to avoid running tests twice when `./gradlew test` is run at the top-level. - project.tasks.withType(Test) { + project.tasks.withType(Test).configureEach { onlyIf { variantName.toLowerCase().contains('debug') } } } diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy index d48f89e1c..38f704159 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy @@ -11,11 +11,11 @@ class RoboJavaModulePlugin implements Plugin<Project> { def skipErrorprone = System.getenv('SKIP_ERRORPRONE') == "true" if (!skipErrorprone) { - apply plugin: "net.ltgt.errorprone" - project.dependencies { - errorprone(libs.error.prone.core) - errorproneJavac(libs.error.prone.javac) - } + apply plugin: "net.ltgt.errorprone" + project.dependencies { + errorprone(libs.error.prone.core) + errorproneJavac(libs.error.prone.javac) + } } apply plugin: AarDepsPlugin @@ -23,7 +23,7 @@ class RoboJavaModulePlugin implements Plugin<Project> { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 - tasks.withType(JavaCompile) { task -> + tasks.withType(JavaCompile).configureEach { task -> sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -68,8 +68,8 @@ class RoboJavaModulePlugin implements Plugin<Project> { } def forwardedSystemProperties = System.properties - .findAll { k,v -> k.startsWith("robolectric.") } - .collect { k,v -> "-D$k=$v" } + .findAll { k, v -> k.startsWith("robolectric.") } + .collect { k, v -> "-D$k=$v" } jvmArgs = forwardedSystemProperties jvmArgs += [ '--add-opens=java.base/java.lang=ALL-UNNAMED', diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/SpotlessPlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/SpotlessPlugin.groovy new file mode 100644 index 000000000..8622fd2a0 --- /dev/null +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/SpotlessPlugin.groovy @@ -0,0 +1,27 @@ +package org.robolectric.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class SpotlessPlugin implements Plugin<Project> { + void apply(Project project) { + project.getPlugins().apply('com.diffplug.spotless') + + project.spotless { + kotlin { + // Add configurations for Kotlin files + target '**/*.kt' + ktfmt('0.42').googleStyle() + } + groovy { + // Add configurations for Groovy files + target("**/*.groovy") + } + groovyGradle { + // Add configurations for Groovy Gradle files + target('*.gradle', "**/*.gradle") + } + + } + } +}
\ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c91c590fa..52a90b6aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ robolectric-nativeruntime-dist-compat = "1.0.9" # https://developer.android.com/studio/releases -android-gradle = "8.2.2" +android-gradle = "8.3.1" # https://github.com/google/conscrypt/tags conscrypt = "2.5.2" @@ -27,23 +27,23 @@ error-prone-javac = "9+181-r4173-1" error-prone-gradle = "3.1.0" # https://kotlinlang.org/docs/releases.html#release-details -kotlin = "1.9.22" +kotlin = "1.9.23" # https://github.com/Kotlin/kotlinx.coroutines/releases/ -kotlinx-coroutines = '1.7.3' +kotlinx-coroutines = '1.8.0' # https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md spotless-gradle = "6.25.0" # https://detekt.dev/changelog -detekt-gradle = "1.23.5" +detekt-gradle = "1.23.6" # https://hc.apache.org/news.html apache-http-core = "4.0.1" apache-http-client = "4.0.3" # https://asm.ow2.io/versions.html -asm = "9.6" +asm = "9.7" # https://github.com/google/auto/releases auto-common = "1.2.2" @@ -80,7 +80,7 @@ jetbrains-annotations = "24.1.0" junit4 = "4.13.2" # https://github.com/google/libphonenumber/releases -libphonenumber = "8.13.30" +libphonenumber = "8.13.33" # https://github.com/mockito/mockito/releases mockito = "4.11.0" @@ -89,7 +89,7 @@ mockito = "4.11.0" mockk = "1.13.7" # https://github.com/takahirom/roborazzi/releases -roborazzi = "1.9.0" +roborazzi = "1.11.0" # https://square.github.io/okhttp/changelogs/changelog/ okhttp = "4.12.0" @@ -121,7 +121,7 @@ androidx-test-services = "1.4.2" # for shadows/playservices/build.gradle androidx-fragment-for-shadows = "1.2.0" -play-services-base-for-shadows = "8.4.0" +play-services-for-shadows = "17.0.0" # https://developers.google.com/android/guides/releases play-services-basement = "18.0.1" @@ -236,14 +236,15 @@ androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "a androidx-test-ext-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-ext-truth" } androidx-fragment-for-shadows = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment-for-shadows" } -play-services-base-for-shadows = { module = "com.google.android.gms:play-services-base", version.ref = "play-services-base-for-shadows" } -play-services-basement-for-shadows = { module = "com.google.android.gms:play-services-basement", version.ref = "play-services-base-for-shadows" } +play-services-auth-for-shadows = { module = "com.google.android.gms:play-services-auth", version.ref = "play-services-for-shadows" } +play-services-base-for-shadows = { module = "com.google.android.gms:play-services-base", version.ref = "play-services-for-shadows" } +play-services-basement-for-shadows = { module = "com.google.android.gms:play-services-basement", version.ref = "play-services-for-shadows" } play-services-basement = { module = "com.google.android.gms:play-services-basement", version.ref = "play-services-basement" } [bundles] -play-services-base-for-shadows = ["androidx-fragment-for-shadows", "play-services-base-for-shadows", "play-services-basement-for-shadows"] -powermock = ["powermock-module-junit4", "powermock-module-junit4-rule", "powermock-api-mockito2", "powermock-classloading-xstream"] -sqlite4java-native = ["sqlite4java-osx", "sqlite4java-linux-amd64", "sqlite4java-win32-x64", "sqlite4java-linux-i386", "sqlite4java-win32-x86"] +play-services-for-shadows = [ "androidx-fragment-for-shadows", "play-services-auth-for-shadows", "play-services-base-for-shadows", "play-services-basement-for-shadows" ] +powermock = [ "powermock-module-junit4", "powermock-module-junit4-rule", "powermock-api-mockito2", "powermock-classloading-xstream" ] +sqlite4java-native = [ "sqlite4java-osx", "sqlite4java-linux-amd64", "sqlite4java-win32-x64", "sqlite4java-linux-i386", "sqlite4java-win32-x86" ] [plugins] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differindex d64cd4917..e6441136f 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce5..b82aa23a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/integration_tests/agp/src/main/AndroidManifest.xml b/integration_tests/agp/src/main/AndroidManifest.xml index 4e2dd6d7b..e37688119 100644 --- a/integration_tests/agp/src/main/AndroidManifest.xml +++ b/integration_tests/agp/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.robolectric.integrationtests.agp"> +<manifest> <application /> </manifest> diff --git a/integration_tests/agp/testsupport/src/main/AndroidManifest.xml b/integration_tests/agp/testsupport/src/main/AndroidManifest.xml index 1478552b6..ec0ff6152 100644 --- a/integration_tests/agp/testsupport/src/main/AndroidManifest.xml +++ b/integration_tests/agp/testsupport/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.robolectric.integrationtests.agp.testsupport"> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application> <activity android:name="TestActivity"/> </application> diff --git a/integration_tests/compat-target28/src/main/AndroidManifest.xml b/integration_tests/compat-target28/src/main/AndroidManifest.xml index 9419a324e..b4251821a 100644 --- a/integration_tests/compat-target28/src/main/AndroidManifest.xml +++ b/integration_tests/compat-target28/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:tools="http://schemas.android.com/tools" - package="org.robolectric.integrationtests.compattarget28" xmlns:android="http://schemas.android.com/apk/res/android"> <application android:appComponentFactory="org.robolectric.integrationtests.compattarget28.TestAppComponentFactory" diff --git a/integration_tests/ctesque/src/sharedTest/java/android/view/KeyCharacterMapTest.java b/integration_tests/ctesque/src/sharedTest/java/android/view/KeyCharacterMapTest.java index aadad99c4..20e84a22c 100644 --- a/integration_tests/ctesque/src/sharedTest/java/android/view/KeyCharacterMapTest.java +++ b/integration_tests/ctesque/src/sharedTest/java/android/view/KeyCharacterMapTest.java @@ -86,6 +86,9 @@ public final class KeyCharacterMapTest { // Just assert that we got something back, there are many ways to return correct KeyEvents for // this sequence. assertThat(keyCharacterMap.getEvents("Test".toCharArray())).isNotEmpty(); + + char c = 'a'; + keyCharacterMap.getEvents(new char[] {c}); } @Test diff --git a/integration_tests/ctesque/src/sharedTest/java/android/view/KeyEventTest.java b/integration_tests/ctesque/src/sharedTest/java/android/view/KeyEventTest.java new file mode 100644 index 000000000..6772cb7e1 --- /dev/null +++ b/integration_tests/ctesque/src/sharedTest/java/android/view/KeyEventTest.java @@ -0,0 +1,61 @@ +package android.view; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; + +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.internal.DoNotInstrument; + +/** + * Test {@link KeyEventTest}. + * + * <p>Inspired from Android cts/tests/tests/view/src/android/view/cts/KeyEventTest.java + */ +@DoNotInstrument +@RunWith(AndroidJUnit4.class) +public final class KeyEventTest { + + @Test + public void testKeyCodeFromString() { + assertEquals(KeyEvent.KEYCODE_A, KeyEvent.keyCodeFromString("KEYCODE_A")); + assertEquals( + KeyEvent.KEYCODE_A, KeyEvent.keyCodeFromString(Integer.toString(KeyEvent.KEYCODE_A))); + assertEquals(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("keycode_a")); + assertEquals(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("a")); + assertEquals(0, KeyEvent.keyCodeFromString("0")); + assertEquals(1, KeyEvent.keyCodeFromString("1")); + assertEquals(KeyEvent.KEYCODE_HOME, KeyEvent.keyCodeFromString("3")); + assertEquals( + KeyEvent.KEYCODE_POWER, + KeyEvent.keyCodeFromString(Integer.toString(KeyEvent.KEYCODE_POWER))); + assertEquals( + KeyEvent.KEYCODE_MENU, KeyEvent.keyCodeFromString(Integer.toString(KeyEvent.KEYCODE_MENU))); + assertEquals(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("back")); + + assertEquals( + KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("KEYCODE_NOT_A_REAL_KEYCODE")); + assertEquals(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("NOT_A_REAL_KEYCODE")); + assertEquals(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("KEYCODE")); + assertEquals(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("KEYCODE_")); + assertEquals(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.keyCodeFromString("")); + assertEquals( + KeyEvent.getMaxKeyCode(), + KeyEvent.keyCodeFromString(Integer.toString(KeyEvent.getMaxKeyCode()))); + } + + /** + * Verify the "starting in {@link android.os.Build.VERSION_CODES#Q} the prefix "KEYCODE_" is + * optional." statement in {@link KeyEvent#keyCodeFromString(String)} reference docs. + */ + @Test + public void testKeyCodeFromString_prefixOptionalFromQ() { + assumeTrue(VERSION.SDK_INT >= VERSION_CODES.Q); + assertEquals(KeyEvent.KEYCODE_A, KeyEvent.keyCodeFromString("A")); + + assertEquals(KeyEvent.KEYCODE_POWER, KeyEvent.keyCodeFromString("POWER")); + } +} diff --git a/integration_tests/dependency-on-stubs/src/test/java/org/robolectric/ShadowLogResolutionTest.java b/integration_tests/dependency-on-stubs/src/test/java/org/robolectric/ShadowLogResolutionTest.java new file mode 100644 index 000000000..0324e81c1 --- /dev/null +++ b/integration_tests/dependency-on-stubs/src/test/java/org/robolectric/ShadowLogResolutionTest.java @@ -0,0 +1,22 @@ +package org.robolectric; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.robolectric.shadows.ShadowLog; + +/** + * This test attempts to reference ShadowLow from outside of the context of a Robolectric + * environment. ShadowLog contained reflector interfaces that referenced `@hide` classes (e.g. + * TerribleFailure), which cannot be referenced outside of a Robolectric ClassLoader. + * + * @see <a href="https://github.com/robolectric/robolectric/issues/8957">Issue 8957 </a> for more + * details. + */ +@RunWith(JUnit4.class) +public class ShadowLogResolutionTest { + @Test + public void reference_shadowLog_outsideRobolectric() { + ShadowLog.stream = System.out; + } +} diff --git a/integration_tests/jacoco-offline/build.gradle b/integration_tests/jacoco-offline/build.gradle index ea668f70a..423a62395 100644 --- a/integration_tests/jacoco-offline/build.gradle +++ b/integration_tests/jacoco-offline/build.gradle @@ -27,67 +27,68 @@ def unitTestTaskName = "test" def compileSourceTaskName = "classes" -def javaDirPath = "${buildDir.path}/classes/java/main" +def javaDir = layout.buildDirectory.dir("classes/java/main").get().asFile -def kotlinDirPath = "${buildDir.path}/classes/kotlin/main" +def kotlinDir = layout.buildDirectory.dir("classes/kotlin/main").get().asFile -def jacocoInstrumentedClassesOutputDirPath = "${buildDir.path}/$jacocoVersion/classes/java/classes-instrumented" +def jacocoInstrumentedClassesOutputDir = layout.buildDirectory.dir("$jacocoVersion/classes/java/classes-instrumented").get().asFile // make sure it's evaluated after the AGP evaluation. afterEvaluate { - tasks[compileSourceTaskName].doLast { - println "[JaCoCo]:Generating JaCoCo instrumented classes for the build." + tasks.named(compileSourceTaskName).configure { task -> + task.doLast { + println "[JaCoCo]:Generating JaCoCo instrumented classes for the build." - def jacocoInstrumentOutputDirPathFile = new File(jacocoInstrumentedClassesOutputDirPath) - if (jacocoInstrumentOutputDirPathFile.exists()) { - println "[JaCoCo]:Classes had been instrumented." - return - } + if (jacocoInstrumentedClassesOutputDir.exists()) { + println "[JaCoCo]:Classes had been instrumented." + return + } - ant.taskdef(name: 'instrument', - classname: 'org.jacoco.ant.InstrumentTask', - classpath: configurations.jacocoAnt.asPath) - - def classesDirPathFile = new File(javaDirPath) - if (classesDirPathFile.exists()) { - ant.instrument(destdir: jacocoInstrumentedClassesOutputDirPath) { - fileset( - dir: javaDirPath, - excludes: [] - ) + ant.taskdef(name: 'instrument', + classname: 'org.jacoco.ant.InstrumentTask', + classpath: configurations.jacocoAnt.asPath) + + if (javaDir.exists()) { + ant.instrument(destdir: jacocoInstrumentedClassesOutputDir.path) { + fileset( + dir: javaDir.path, + excludes: [] + ) + } + } else { + println "Classes directory with path: $javaDir does not existed." } - } else { - println "Classes directory with path: " + classesDirPathFile + " was not existed." - } - def classesDirPathFileKotlin = new File(kotlinDirPath) - if (classesDirPathFileKotlin.exists()) { - ant.instrument(destdir: jacocoInstrumentedClassesOutputDirPath) { - fileset( - dir: kotlinDirPath, - excludes: [] - ) + if (kotlinDir.exists()) { + ant.instrument(destdir: jacocoInstrumentedClassesOutputDir.path) { + fileset( + dir: kotlinDir.path, + excludes: [] + ) + } + } else { + println "Classes directory with path: $kotlinDir does not existed." } - } else { - println "Classes directory with path: " + classesDirPathFileKotlin + " was not existed." } } - def executionDataFilePath = "${buildDir.path}/jacoco/${unitTestTaskName}.exec" + def executionDataFilePath = layout.buildDirectory.dir("jacoco").get().file("${unitTestTaskName}.exec").getAsFile().path // put JaCoCo instrumented classes and JaCoCoRuntime to the beginning of the JVM classpath. - tasks["${unitTestTaskName}"].doFirst { - jacoco { - // disable JaCoCo on-the-fly from Gradle JaCoCo plugin. - enabled = false - } + tasks.named(unitTestTaskName).configure { task -> + task.doFirst { + jacoco { + // disable JaCoCo on-the-fly from Gradle JaCoCo plugin. + enabled = false + } - println "[JaCoCo]:Modifying classpath of tests JVM." + println "[JaCoCo]:Modifying classpath of tests JVM." - systemProperty 'jacoco-agent.destfile', executionDataFilePath + systemProperty 'jacoco-agent.destfile', executionDataFilePath - classpath = files(jacocoInstrumentedClassesOutputDirPath) + classpath + configurations.jacocoRuntime + classpath = files(jacocoInstrumentedClassesOutputDir.path) + classpath + configurations.jacocoRuntime - println "Final test JVM classpath is ${classpath.getAsPath()}" + println "Final test JVM classpath is ${classpath.getAsPath()}" + } } } diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle index 550f08a45..c95d64f3f 100644 --- a/integration_tests/kotlin/build.gradle +++ b/integration_tests/kotlin/build.gradle @@ -1,17 +1,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.robolectric.gradle.RoboJavaModulePlugin - apply plugin: RoboJavaModulePlugin apply plugin: 'kotlin' -apply plugin: "com.diffplug.spotless" - -spotless { - kotlin { - target '**/*.kt' - ktfmt('0.42').googleStyle() - } -} - compileKotlin { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } diff --git a/integration_tests/memoryleaks/src/main/AndroidManifest.xml b/integration_tests/memoryleaks/src/main/AndroidManifest.xml index 5cafa7793..9515bdd60 100644 --- a/integration_tests/memoryleaks/src/main/AndroidManifest.xml +++ b/integration_tests/memoryleaks/src/main/AndroidManifest.xml @@ -2,9 +2,6 @@ <!-- Manifest for androidx memoryleaks test module --> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.robolectric.integrationtests.memoryleaks"> - - <application> - </application> +<manifest> + <application /> </manifest> diff --git a/integration_tests/nativegraphics/src/main/AndroidManifest.xml b/integration_tests/nativegraphics/src/main/AndroidManifest.xml index 279252449..7a3694f72 100644 --- a/integration_tests/nativegraphics/src/main/AndroidManifest.xml +++ b/integration_tests/nativegraphics/src/main/AndroidManifest.xml @@ -1,9 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Manifest for native graphics tests --> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.robolectric.integrationtests.nativegraphics"> - <uses-sdk - android:minSdkVersion="26" - android:targetSdkVersion="34" /> +<manifest> <application /> </manifest> diff --git a/integration_tests/roborazzi/build.gradle b/integration_tests/roborazzi/build.gradle index 7c7000c5b..034a69bba 100644 --- a/integration_tests/roborazzi/build.gradle +++ b/integration_tests/roborazzi/build.gradle @@ -1,18 +1,8 @@ import org.robolectric.gradle.AndroidProjectConfigPlugin - apply plugin: 'com.android.library' apply plugin: AndroidProjectConfigPlugin apply plugin: 'kotlin-android' -apply plugin: "com.diffplug.spotless" apply plugin: "io.github.takahirom.roborazzi" - -spotless { - kotlin { - target '**/*.kt' - ktfmt('0.42').googleStyle() - } -} - android { compileSdk 34 namespace 'org.robolectric.integration.roborazzi' @@ -54,7 +44,6 @@ android { } } } - dependencies { api project(":robolectric") testImplementation libs.androidx.test.core diff --git a/integration_tests/room/build.gradle b/integration_tests/room/build.gradle index 4a12143be..52831db58 100644 --- a/integration_tests/room/build.gradle +++ b/integration_tests/room/build.gradle @@ -30,7 +30,6 @@ dependencies { testImplementation project(":robolectric") testImplementation libs.junit4 testImplementation libs.guava.testlib - testImplementation libs.guava.testlib testImplementation libs.truth implementation libs.androidx.room.runtime annotationProcessor libs.androidx.room.compiler diff --git a/integration_tests/room/src/main/AndroidManifest.xml b/integration_tests/room/src/main/AndroidManifest.xml index 3707a8dcb..283a0587e 100644 --- a/integration_tests/room/src/main/AndroidManifest.xml +++ b/integration_tests/room/src/main/AndroidManifest.xml @@ -1,10 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Manifest for androidx memoryleaks test module + Manifest for androidx room test module --> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.robolectric.integrationtests.room"> - - <application> - </application> +<manifest> + <application /> </manifest> diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle index d408f0de7..6353b6b91 100644 --- a/integration_tests/sparsearray/build.gradle +++ b/integration_tests/sparsearray/build.gradle @@ -1,18 +1,8 @@ import org.robolectric.gradle.AndroidProjectConfigPlugin - apply plugin: 'com.android.library' apply plugin: AndroidProjectConfigPlugin apply plugin: 'kotlin-android' -apply plugin: "com.diffplug.spotless" apply plugin: "io.gitlab.arturbosch.detekt" - -spotless { - kotlin { - target '**/*.kt' - ktfmt('0.42').googleStyle() - } -} - android { compileSdk 34 namespace 'org.robolectric.sparsearray' @@ -37,7 +27,6 @@ android { } } } - dependencies { compileOnly AndroidSdk.MAX_SDK.coordinates implementation project(path: ':shadowapi', configuration: 'default') diff --git a/integration_tests/sparsearray/src/main/AndroidManifest.xml b/integration_tests/sparsearray/src/main/AndroidManifest.xml index 6ce6065f5..955c5548d 100644 --- a/integration_tests/sparsearray/src/main/AndroidManifest.xml +++ b/integration_tests/sparsearray/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest package="org.robolectric.sparsearray"> +<manifest> <application /> </manifest> diff --git a/shadows/versioning/build.gradle b/integration_tests/versioning/build.gradle index 68a8fb769..8ae425b51 100644 --- a/shadows/versioning/build.gradle +++ b/integration_tests/versioning/build.gradle @@ -9,7 +9,6 @@ configurations { } dependencies { - api project(":shadowapi") compileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK (AndroidSdk.s.coordinates) { force = true } testImplementation project(":robolectric") testImplementation libs.truth diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java b/integration_tests/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java index 4666c6a0e..09b394a1a 100644 --- a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java +++ b/integration_tests/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java @@ -2,12 +2,11 @@ package org.robolectric.versioning; import static com.google.common.truth.Truth.assertThat; -import android.os.Build.VERSION; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import org.robolectric.versioning.AndroidVersions.T; +import org.robolectric.shadows.ShadowBuild; /** * Check versions information aligns with runtime information. Primarily, selected SDK with @@ -17,11 +16,17 @@ import org.robolectric.versioning.AndroidVersions.T; public final class AndroidVersionsTest { @Test - @Config(sdk = T.SDK_INT) + @Config(sdk = 33) + public void ignoreShadowBuildValues() { + ShadowBuild.setVersionCodename("_*&^%%&"); + assertThat(AndroidVersions.T.SDK_INT).isEqualTo(33); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("T"); + } + + @Test + @Config(sdk = 33) public void testStandardInitializationT() { - assertThat(VERSION.SDK_INT).isEqualTo(33); - assertThat(VERSION.RELEASE).isEqualTo("13"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.T.SDK_INT).isEqualTo(33); assertThat(AndroidVersions.T.SHORT_CODE).isEqualTo("T"); assertThat(new AndroidVersions.T().getVersion()).isEqualTo("13.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("T"); @@ -30,9 +35,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 32) public void testStandardInitializationSv2() { - assertThat(VERSION.SDK_INT).isEqualTo(32); - assertThat(VERSION.RELEASE).isEqualTo("12"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.Sv2.SDK_INT).isEqualTo(32); assertThat(AndroidVersions.Sv2.SHORT_CODE).isEqualTo("Sv2"); assertThat(new AndroidVersions.Sv2().getVersion()).isEqualTo("12.1"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Sv2"); @@ -41,9 +44,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 31) public void testStandardInitializationS() { - assertThat(VERSION.SDK_INT).isEqualTo(31); - assertThat(VERSION.RELEASE).isEqualTo("12"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.S.SDK_INT).isEqualTo(31); assertThat(AndroidVersions.S.SHORT_CODE).isEqualTo("S"); assertThat(new AndroidVersions.S().getVersion()).isEqualTo("12.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("S"); @@ -52,9 +53,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 30) public void testStandardInitializationR() { - assertThat(VERSION.SDK_INT).isEqualTo(30); - assertThat(VERSION.RELEASE).isEqualTo("11"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.R.SDK_INT).isEqualTo(30); assertThat(AndroidVersions.R.SHORT_CODE).isEqualTo("R"); assertThat(new AndroidVersions.R().getVersion()).isEqualTo("11.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("R"); @@ -63,9 +62,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 29) public void testStandardInitializationQ() { - assertThat(VERSION.SDK_INT).isEqualTo(29); - assertThat(VERSION.RELEASE).isEqualTo("10"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.Q.SDK_INT).isEqualTo(29); assertThat(AndroidVersions.Q.SHORT_CODE).isEqualTo("Q"); assertThat(new AndroidVersions.Q().getVersion()).isEqualTo("10.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Q"); @@ -74,9 +71,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 28) public void testStandardInitializationP() { - assertThat(VERSION.SDK_INT).isEqualTo(28); - assertThat(VERSION.RELEASE).isEqualTo("9"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.P.SDK_INT).isEqualTo(28); assertThat(AndroidVersions.P.SHORT_CODE).isEqualTo("P"); assertThat(new AndroidVersions.P().getVersion()).isEqualTo("9.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("P"); @@ -85,9 +80,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 27) public void testStandardInitializationOMR1() { - assertThat(VERSION.SDK_INT).isEqualTo(27); - assertThat(VERSION.RELEASE).isEqualTo("8.1.0"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.OMR1.SDK_INT).isEqualTo(27); assertThat(AndroidVersions.OMR1.SHORT_CODE).isEqualTo("OMR1"); assertThat(new AndroidVersions.OMR1().getVersion()).isEqualTo("8.1"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("OMR1"); @@ -96,9 +89,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 26) public void testStandardInitializationO() { - assertThat(VERSION.SDK_INT).isEqualTo(26); - assertThat(VERSION.RELEASE).isEqualTo("8.0.0"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.O.SDK_INT).isEqualTo(26); assertThat(AndroidVersions.O.SHORT_CODE).isEqualTo("O"); assertThat(new AndroidVersions.O().getVersion()).isEqualTo("8.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("O"); @@ -107,9 +98,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 25) public void testStandardInitializationNMR1() { - assertThat(VERSION.SDK_INT).isEqualTo(25); - assertThat(VERSION.RELEASE).isEqualTo("7.1"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.NMR1.SDK_INT).isEqualTo(25); assertThat(AndroidVersions.NMR1.SHORT_CODE).isEqualTo("NMR1"); assertThat(new AndroidVersions.NMR1().getVersion()).isEqualTo("7.1"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("NMR1"); @@ -118,9 +107,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 24) public void testStandardInitializationN() { - assertThat(VERSION.SDK_INT).isEqualTo(24); - assertThat(VERSION.RELEASE).isEqualTo("7.0"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.N.SDK_INT).isEqualTo(24); assertThat(AndroidVersions.N.SHORT_CODE).isEqualTo("N"); assertThat(new AndroidVersions.N().getVersion()).isEqualTo("7.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("N"); @@ -129,9 +116,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 23) public void testStandardInitializationM() { - assertThat(VERSION.SDK_INT).isEqualTo(23); - assertThat(VERSION.RELEASE).isEqualTo("6.0.1"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.M.SDK_INT).isEqualTo(23); assertThat(AndroidVersions.M.SHORT_CODE).isEqualTo("M"); assertThat(new AndroidVersions.M().getVersion()).isEqualTo("6.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("M"); @@ -140,9 +125,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 22) public void testStandardInitializationLMR1() { - assertThat(VERSION.SDK_INT).isEqualTo(22); - assertThat(VERSION.RELEASE).isEqualTo("5.1.1"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.LMR1.SDK_INT).isEqualTo(22); assertThat(AndroidVersions.LMR1.SHORT_CODE).isEqualTo("LMR1"); assertThat(new AndroidVersions.LMR1().getVersion()).isEqualTo("5.1"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("LMR1"); @@ -151,9 +134,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 21) public void testStandardInitializationL() { - assertThat(VERSION.SDK_INT).isEqualTo(21); - assertThat(VERSION.RELEASE).isEqualTo("5.0.2"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.L.SDK_INT).isEqualTo(21); assertThat(AndroidVersions.L.SHORT_CODE).isEqualTo("L"); assertThat(new AndroidVersions.L().getVersion()).isEqualTo("5.0"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("L"); @@ -162,9 +143,7 @@ public final class AndroidVersionsTest { @Test @Config(sdk = 19) public void testStandardInitializationK() { - assertThat(VERSION.SDK_INT).isEqualTo(19); - assertThat(VERSION.RELEASE).isEqualTo("4.4"); - assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.K.SDK_INT).isEqualTo(19); assertThat(AndroidVersions.K.SHORT_CODE).isEqualTo("K"); assertThat(new AndroidVersions.K().getVersion()).isEqualTo("4.4"); assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("K"); diff --git a/shadows/versioning/src/test/resources/AndroidManifest.xml b/integration_tests/versioning/src/test/resources/AndroidManifest.xml index 65383ac0b..65383ac0b 100644 --- a/shadows/versioning/src/test/resources/AndroidManifest.xml +++ b/integration_tests/versioning/src/test/resources/AndroidManifest.xml diff --git a/preinstrumented/build.gradle b/preinstrumented/build.gradle index 0de49e0bf..8e6481622 100644 --- a/preinstrumented/build.gradle +++ b/preinstrumented/build.gradle @@ -19,7 +19,6 @@ java { dependencies { implementation libs.guava implementation project(":sandbox") - implementation project(":shadows:versioning") testImplementation libs.junit4 testImplementation libs.mockito @@ -35,7 +34,7 @@ tasks.register('instrumentAll') { sdksToInstrument().each { androidSdk -> println("Instrumenting ${androidSdk.coordinates}") def inputPath = "${androidAllMavenLocal}/${androidSdk.version}/${androidSdk.jarFileName}" - def outputPath = "${buildDir}/${androidSdk.preinstrumentedJarFileName}" + def outputPath = layout.buildDirectory.file(androidSdk.preinstrumentedJarFileName).get().asFile.path javaexec { classpath = sourceSets.main.runtimeClasspath @@ -66,7 +65,7 @@ if (System.getenv('PUBLISH_PREINSTRUMENTED_JARS') == "true") { publications { sdksToInstrument().each { androidSdk -> "sdk${androidSdk.apiLevel}"(MavenPublication) { - artifact "${buildDir}/${androidSdk.preinstrumentedJarFileName}" + artifact = layout.buildDirectory.file(androidSdk.preinstrumentedJarFileName).get().asFile.path artifactId 'android-all-instrumented' artifact emptySourcesJar artifact emptyJavadocJar @@ -120,34 +119,34 @@ if (System.getenv('PUBLISH_PREINSTRUMENTED_JARS') == "true") { } - // Workaround for https://github.com/gradle/gradle/issues/26132 - // For some reason, Gradle has inferred that all publishing tasks depend on all signing tasks, - // so we must explicitly declare this here. - afterEvaluate { - tasks.all { - if (name.startsWith("publishSdk")) { - sdksToInstrument().each { androidSdk -> - dependsOn(tasks.named("signSdk${androidSdk.apiLevel}Publication")) + // Workaround for https://github.com/gradle/gradle/issues/26132 + // For some reason, Gradle has inferred that all publishing tasks depend on all signing tasks, + // so we must explicitly declare this here. + afterEvaluate { + tasks.configureEach { + if (name.startsWith("publishSdk")) { + sdksToInstrument().each { androidSdk -> + dependsOn(tasks.named("signSdk${androidSdk.apiLevel}Publication")) + } + } } - } } - } } static def sdksToInstrument() { - var result = AndroidSdk.ALL_SDKS - var preInstrumentedSdkVersions = (System.getenv('PREINSTRUMENTED_SDK_VERSIONS') ?: "") - if (preInstrumentedSdkVersions.length() > 0) { - var sdkFilter = preInstrumentedSdkVersions.split(",").collect { it as Integer } - if (sdkFilter.size > 0) { - result = result.findAll { sdkFilter.contains(it.apiLevel) } + var result = AndroidSdk.ALL_SDKS + var preInstrumentedSdkVersions = (System.getenv('PREINSTRUMENTED_SDK_VERSIONS') ?: "") + if (preInstrumentedSdkVersions.length() > 0) { + var sdkFilter = preInstrumentedSdkVersions.split(",").collect { it as Integer } + if (sdkFilter.size > 0) { + result = result.findAll { sdkFilter.contains(it.apiLevel) } + } } - } - return result + return result } clean.doFirst { AndroidSdk.ALL_SDKS.each { androidSdk -> - delete "${buildDir}/${androidSdk.preinstrumentedJarFileName}" + delete layout.buildDirectory.file(androidSdk.preinstrumentedJarFileName) } } diff --git a/processor/Android.bp b/processor/Android.bp index 7b0772aa6..be7320178 100644 --- a/processor/Android.bp +++ b/processor/Android.bp @@ -12,11 +12,13 @@ package { default_applicable_licenses: ["external_robolectric_license"], } -java_library_host { +java_library { name: "libRobolectric_processor_upstream", + host_supported: true, + device_supported: false, srcs: ["src/main/java/**/*.java"], java_resource_dirs: ["src/main/resources"], - java_resources: ["sdks.txt"], + //java_resources: ["sdks.txt"], use_tools_jar: true, plugins: [ "auto_service_plugin", diff --git a/processor/build.gradle b/processor/build.gradle index c2f00d77d..695bc8182 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -6,10 +6,11 @@ apply plugin: RoboJavaModulePlugin apply plugin: DeployedRoboJavaModulePlugin class GenerateSdksFileTask extends DefaultTask { - @OutputFile File outFile + @OutputFile + File outFile @TaskAction - public void writeProperties() throws Exception { + void writeProperties() throws Exception { File outDir = outFile.parentFile if (!outDir.directory) outDir.mkdirs() outFile.withPrintWriter { out -> @@ -26,15 +27,16 @@ class GenerateSdksFileTask extends DefaultTask { } task('generateSdksFile', type: GenerateSdksFileTask) { - outFile = new File(project.rootProject.buildDir, 'sdks.txt') + outFile = project.rootProject.layout.buildDirectory.file('sdks.txt').get().asFile } -tasks['classes'].dependsOn(generateSdksFile) +tasks.named("classes").configure { task -> + task.dependsOn(generateSdksFile) +} dependencies { api project(":annotations") api project(":shadowapi") - implementation project(":shadows:versioning") compileOnly libs.findbugs.jsr305 api libs.asm diff --git a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java index f767765a4..b82622885 100644 --- a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java +++ b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java @@ -5,6 +5,7 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; @@ -27,21 +28,25 @@ import org.robolectric.annotation.processing.validator.ResetterValidator; import org.robolectric.annotation.processing.validator.SdkStore; import org.robolectric.annotation.processing.validator.Validator; -/** - * Annotation processor entry point for Robolectric annotations. - */ +/** Annotation processor entry point for Robolectric annotations. */ @SupportedOptions({ - RobolectricProcessor.PACKAGE_OPT, - RobolectricProcessor.SHOULD_INSTRUMENT_PKG_OPT}) + RobolectricProcessor.PACKAGE_OPT, + RobolectricProcessor.SHOULD_INSTRUMENT_PKG_OPT +}) @SupportedAnnotationTypes("org.robolectric.annotation.*") public class RobolectricProcessor extends AbstractProcessor { static final String PACKAGE_OPT = "org.robolectric.annotation.processing.shadowPackage"; - static final String SHOULD_INSTRUMENT_PKG_OPT = + static final String SHOULD_INSTRUMENT_PKG_OPT = "org.robolectric.annotation.processing.shouldInstrumentPackage"; static final String JSON_DOCS_DIR = "org.robolectric.annotation.processing.jsonDocsDir"; static final String JSON_DOCS_ENABLED = "org.robolectric.annotation.processing.jsonDocsEnabled"; static final String SDK_CHECK_MODE = "org.robolectric.annotation.processing.sdkCheckMode"; private static final String SDKS_FILE = "org.robolectric.annotation.processing.sdks"; + + /** required for Android Development. */ + private static final String VALIDATE_COMPILE_SDKS = + "org.robolectric.annotation.processing.validateCompileSdk"; + private static final String PRIORITY = "org.robolectric.annotation.processing.priority"; private RobolectricModel.Builder modelBuilder; @@ -56,24 +61,30 @@ public class RobolectricProcessor extends AbstractProcessor { private final Map<TypeElement, Validator> elementValidators = new HashMap<>(13); private File jsonDocsDir; private boolean jsonDocsEnabled; + private boolean validateCompiledSdk; + private String overrideSdkLocation; + private int overrideSdkInt; - /** - * Default constructor. - */ - public RobolectricProcessor() { - } + /** Default constructor. */ + public RobolectricProcessor() {} /** - * Constructor to use for testing passing options in. Only - * necessary until compile-testing supports passing options - * in. + * Constructor to use for testing passing options in. Only necessary until compile-testing + * supports passing options in. * - * @param options simulated options that would ordinarily - * be passed in the {@link ProcessingEnvironment}. + * @param options simulated options that would ordinarily be passed in the {@link + * ProcessingEnvironment}. */ @VisibleForTesting public RobolectricProcessor(Map<String, String> options) { + this(options, null, -1); + } + + public RobolectricProcessor( + Map<String, String> options, String overrideSdkLocation, int overrideSdkInt) { processOptions(options); + this.overrideSdkLocation = overrideSdkLocation; + this.overrideSdkInt = overrideSdkInt; } @Override @@ -82,7 +93,8 @@ public class RobolectricProcessor extends AbstractProcessor { processOptions(environment.getOptions()); modelBuilder = new RobolectricModel.Builder(environment); - SdkStore sdkStore = new SdkStore(sdksFile); + SdkStore sdkStore = + new SdkStore(sdksFile, validateCompiledSdk, overrideSdkLocation, overrideSdkInt); addValidator(new ImplementationValidator(modelBuilder, environment)); addValidator(new ImplementsValidator(modelBuilder, environment, sdkCheckMode, sdkStore)); @@ -132,10 +144,12 @@ public class RobolectricProcessor extends AbstractProcessor { this.jsonDocsDir = new File(options.getOrDefault(JSON_DOCS_DIR, "build/docs/json")); this.jsonDocsEnabled = "true".equalsIgnoreCase(options.get(JSON_DOCS_ENABLED)); this.sdkCheckMode = - SdkCheckMode.valueOf(options.getOrDefault(SDK_CHECK_MODE, "WARN").toUpperCase()); + SdkCheckMode.valueOf( + options.getOrDefault(SDK_CHECK_MODE, "WARN").toUpperCase(Locale.ROOT)); + this.validateCompiledSdk = + "true".equalsIgnoreCase(options.getOrDefault(VALIDATE_COMPILE_SDKS, "false")); this.sdksFile = getSdksFile(options, SDKS_FILE); - this.priority = - Integer.parseInt(options.getOrDefault(PRIORITY, "0")); + this.priority = Integer.parseInt(options.getOrDefault(PRIORITY, "0")); if (this.shadowPackage == null) { throw new IllegalArgumentException("no package specified for " + PACKAGE_OPT); diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java index e08b3acd1..06008a57b 100644 --- a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java +++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java @@ -16,16 +16,23 @@ import java.io.InputStreamReader; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.function.Supplier; import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -39,17 +46,27 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.InDevelopment; import org.robolectric.versioning.AndroidVersionInitTools; +import org.robolectric.versioning.AndroidVersions; /** Encapsulates a collection of Android framework jars. */ public class SdkStore { private final Set<Sdk> sdks = new TreeSet<>(); private boolean loaded = false; + private final boolean loadFromClasspath; + private final String overrideSdkLocation; + private final int overrideSdkInt; private final String sdksFile; - public SdkStore(String sdksFile) { + /** */ + public SdkStore( + String sdksFile, boolean loadFromClasspath, String overrideSdkLocation, int overrideSdkInt) { this.sdksFile = sdksFile; + this.loadFromClasspath = loadFromClasspath; + this.overrideSdkLocation = overrideSdkLocation; + this.overrideSdkInt = overrideSdkInt; } List<Sdk> sdksMatching(Implementation implementation, int classMinSdk, int classMaxSdk) { @@ -73,7 +90,7 @@ public class SdkStore { List<Sdk> matchingSdks = new ArrayList<>(); for (Sdk sdk : sdks) { - Integer sdkInt = sdk.sdkInt; + int sdkInt = sdk.sdkRelease.getSdkInt(); if (sdkInt >= minSdk && sdkInt <= maxSdk) { matchingSdks.add(sdk); } @@ -83,21 +100,90 @@ public class SdkStore { private synchronized void loadSdksOnce() { if (!loaded) { - sdks.addAll(loadFromSdksFile(sdksFile)); + sdks.addAll( + loadFromSources(loadFromClasspath, sdksFile, overrideSdkLocation, overrideSdkInt)); loaded = true; } } - private static ImmutableList<Sdk> loadFromSdksFile(String fileName) { - if (fileName == null || Files.notExists(Paths.get(fileName))) { - return ImmutableList.of(); + /** + * @return a list of sdk_int's to jar locations as a string, one tuple per line. + */ + @Override + @SuppressWarnings("JdkCollectors") + public String toString() { + loadSdksOnce(); + StringBuilder builder = new StringBuilder(); + builder.append("SdkStore ["); + for (Sdk sdk : sdks.stream().sorted().collect(Collectors.toList())) { + builder.append(" " + sdk.sdkRelease.getSdkInt() + " : " + sdk.path + "\n"); } + builder.append("]"); + return builder.toString(); + } - try (InputStream resIn = new FileInputStream(fileName)) { - if (resIn == null) { - throw new RuntimeException("no such file " + fileName); + /** + * Scans the jvm properties for the command that executed it, in this command will be the + * classpath. <br> + * <br> + * Scans all jars on the classpath for the first one with a /build.prop on resource. This is + * assumed to be the sdk that the processor is running with. + * + * @return the detected sdk location. + */ + private static String compilationSdkTarget() { + String cmd = System.getProperty("sun.java.command"); + Pattern pattern = Pattern.compile("((-cp)|(-classpath))\\s(?<cp>[a-zA-Z-_0-9\\-\\:\\/\\.]*)"); + Matcher matcher = pattern.matcher(cmd); + if (matcher.find()) { + String classpathString = matcher.group("cp"); + List<String> cp = Arrays.asList(classpathString.split(":")); + for (String fileStr : cp) { + try (JarFile jarFile = new JarFile(fileStr)) { + ZipEntry entry = jarFile.getEntry("build.prop"); + if (entry != null) { + return fileStr; + } + } catch (IOException ioe) { + System.out.println("Error detecting compilation SDK: " + ioe.getMessage()); + ioe.printStackTrace(); + } } + } + return null; + } + /** + * Returns a list of sdks to process, either the compilation's classpaths sdk in a list of size + * one, or the list of sdks in a sdkFile. + * + * @param localSdk validate sdk found in compile time classpath, takes precedence over sdkFile + * @param sdkFileName the sdkFile name, may be null, or empty + * @param overrideSdkLocation if provided overrides the default lookup of the localSdk, iff + * localSdk is on. + * @return a list of sdks to check with annotation processing validators. + */ + private static ImmutableList<Sdk> loadFromSources( + boolean localSdk, String sdkFileName, String overrideSdkLocation, int overrideSdkInt) { + if (localSdk) { + Sdk sdk = null; + if (overrideSdkLocation != null) { + sdk = new Sdk(overrideSdkLocation, overrideSdkInt); + } else { + String target = compilationSdkTarget(); + if (target != null) { + sdk = new Sdk(target); + } + } + return sdk == null ? ImmutableList.of() : ImmutableList.of(sdk); + } + if (sdkFileName == null || Files.notExists(Paths.get(sdkFileName))) { + return ImmutableList.of(); + } + try (InputStream resIn = new FileInputStream(sdkFileName)) { + if (resIn == null) { + throw new RuntimeException("no such file " + sdkFileName); + } BufferedReader in = new BufferedReader(new InputStreamReader(resIn, Charset.defaultCharset())); List<Sdk> sdks = new ArrayList<>(); @@ -109,7 +195,7 @@ public class SdkStore { } return ImmutableList.copyOf(sdks); } catch (IOException e) { - throw new RuntimeException("failed reading " + fileName, e); + throw new RuntimeException("failed reading " + sdkFileName, e); } } @@ -132,14 +218,29 @@ public class SdkStore { private final String path; private final JarFile jarFile; + final AndroidVersions.AndroidRelease sdkRelease; final int sdkInt; private final Map<String, ClassInfo> classInfos = new HashMap<>(); private static File tempDir; Sdk(String path) { + this(path, null); + } + + Sdk(String path, Integer sdkInt) { this.path = path; - this.jarFile = ensureJar(); - this.sdkInt = readSdkInt(); + if (path.startsWith("classpath:") || path.endsWith(".jar")) { + this.jarFile = ensureJar(); + } else { + this.jarFile = null; + } + if (sdkInt == null) { + this.sdkRelease = readSdkVersion(); + this.sdkInt = sdkRelease.getSdkInt(); + } else { + this.sdkRelease = AndroidVersions.getReleaseForSdkInt(sdkInt); + this.sdkInt = sdkRelease.getSdkInt(); + } } /** @@ -156,34 +257,40 @@ public class SdkStore { String className = getClassFQName(sdkClassElem); ClassInfo classInfo = getClassInfo(className); - if (classInfo == null) { + if (classInfo == null && !suppressWarnings(sdkClassElem, null)) { return "No such class " + className; } MethodExtraInfo sdkMethod = classInfo.findMethod(methodElement, looseSignatures); - if (sdkMethod == null) { + if (sdkMethod == null && !suppressWarnings(methodElement, null)) { return "No such method in " + className; } - - MethodExtraInfo implMethod = new MethodExtraInfo(methodElement); - if (!sdkMethod.equals(implMethod) - && !suppressWarnings(methodElement, "robolectric.ShadowReturnTypeMismatch")) { - if (implMethod.isStatic != sdkMethod.isStatic) { - return "@Implementation for " + methodElement.getSimpleName() - + " is " + (implMethod.isStatic ? "static" : "not static") - + " unlike the SDK method"; - } - if (!implMethod.returnType.equals(sdkMethod.returnType)) { - if ( - (looseSignatures && typeIsOkForLooseSignatures(implMethod, sdkMethod)) - || (looseSignatures && implMethod.returnType.equals("java.lang.Object[]")) - // Number is allowed for int or long return types - || typeIsNumeric(sdkMethod, implMethod)) { - return null; - } else { - return "@Implementation for " + methodElement.getSimpleName() - + " has a return type of " + implMethod.returnType - + ", not " + sdkMethod.returnType + " as in the SDK method"; + if (sdkMethod != null) { + MethodExtraInfo implMethod = new MethodExtraInfo(methodElement); + if (!sdkMethod.equals(implMethod) + && !suppressWarnings(methodElement, "robolectric.ShadowReturnTypeMismatch")) { + if (implMethod.isStatic != sdkMethod.isStatic) { + return "@Implementation for " + + methodElement.getSimpleName() + + " is " + + (implMethod.isStatic ? "static" : "not static") + + " unlike the SDK method"; + } + if (!implMethod.returnType.equals(sdkMethod.returnType)) { + if ((looseSignatures && typeIsOkForLooseSignatures(implMethod, sdkMethod)) + || (looseSignatures && implMethod.returnType.equals("java.lang.Object[]")) + // Number is allowed for int or long return types + || typeIsNumeric(sdkMethod, implMethod)) { + return null; + } else { + return "@Implementation for " + + methodElement.getSimpleName() + + " has a return type of " + + implMethod.returnType + + ", not " + + sdkMethod.returnType + + " as in the SDK method"; + } } } } @@ -191,32 +298,45 @@ public class SdkStore { return null; } - private static boolean suppressWarnings(ExecutableElement methodElement, String warningName) { + /** + * Warnings (or potentially Errors, depending on processing flags) can be suppressed in one of + * two ways, either with @SuppressWarnings("robolectric.<warningName>"), or with + * the @InDevelopment annotation, if and only the target Sdk is in development. + * + * @param annotatedElement element to inspect for annotations + * @param warningName the name of the warning, if null, @InDevelopment will still be honored. + * @return true if the warning should be suppressed, else false + */ + private boolean suppressWarnings(Element annotatedElement, String warningName) { SuppressWarnings[] suppressWarnings = - methodElement.getAnnotationsByType(SuppressWarnings.class); + annotatedElement.getAnnotationsByType(SuppressWarnings.class); for (SuppressWarnings suppression : suppressWarnings) { for (String name : suppression.value()) { - if (warningName.equals(name)) { + if (warningName != null && warningName.equals(name)) { return true; } } } + InDevelopment[] inDev = annotatedElement.getAnnotationsByType(InDevelopment.class); + if (inDev.length > 0 && !sdkRelease.isReleased()) { + return true; + } return false; } private static boolean typeIsNumeric(MethodExtraInfo sdkMethod, MethodExtraInfo implMethod) { return implMethod.returnType.equals("java.lang.Number") - && isNumericType(sdkMethod.returnType); + && isNumericType(sdkMethod.returnType); } private static boolean typeIsOkForLooseSignatures( MethodExtraInfo implMethod, MethodExtraInfo sdkMethod) { return - // loose signatures allow a return type of Object... - implMethod.returnType.equals("java.lang.Object") - // or Object[] for arrays... - || (implMethod.returnType.equals("java.lang.Object[]") - && sdkMethod.returnType.endsWith("[]")); + // loose signatures allow a return type of Object... + implMethod.returnType.equals("java.lang.Object") + // or Object[] for arrays... + || (implMethod.returnType.equals("java.lang.Object[]") + && sdkMethod.returnType.endsWith("[]")); } private static boolean isNumericType(String type) { @@ -250,9 +370,9 @@ public class SdkStore { * * @return the API level */ - private int readSdkInt() { + private AndroidVersions.AndroidRelease readSdkVersion() { try { - return AndroidVersionInitTools.computeReleaseVersion(jarFile).getSdkInt(); + return AndroidVersionInitTools.computeReleaseVersion(jarFile); } catch (IOException e) { throw new RuntimeException("failed to read build.prop from " + path); } @@ -267,12 +387,13 @@ public class SdkStore { } } catch (IOException e) { - throw new RuntimeException("failed to open SDK " + sdkInt + " at " + path, e); + throw new RuntimeException( + "failed to open SDK " + sdkRelease.getSdkInt() + " at " + path, e); } } private static File copyResourceToFile(String resourcePath) throws IOException { - if (tempDir == null){ + if (tempDir == null) { File tempFile = File.createTempFile("prefix", "suffix"); tempFile.deleteOnExit(); tempDir = tempFile.getParentFile(); @@ -296,15 +417,45 @@ public class SdkStore { private ClassNode loadClassNode(String name) { String classFileName = name.replace('.', '/') + ".class"; - ZipEntry entry = jarFile.getEntry(classFileName); - if (entry == null) { + Supplier<InputStream> inputStreamSupplier = null; + + if (jarFile != null) { + // working with a jar file. + ZipEntry entry = jarFile.getEntry(classFileName); + if (entry == null) { + return null; + } + inputStreamSupplier = + () -> { + try { + return jarFile.getInputStream(entry); + } catch (IOException ioe) { + throw new RuntimeException("could not read zip entry", ioe); + } + }; + } else { + // working with an exploded path location. + Path working = Path.of(path, classFileName); + File classFile = working.toFile(); + if (classFile.isFile()) { + inputStreamSupplier = + () -> { + try { + return new FileInputStream(classFile); + } catch (IOException ioe) { + throw new RuntimeException("could not read file in path " + working, ioe); + } + }; + } + } + if (inputStreamSupplier == null) { return null; } - try (InputStream inputStream = jarFile.getInputStream(entry)) { + try (InputStream inputStream = inputStreamSupplier.get()) { ClassReader classReader = new ClassReader(inputStream); ClassNode classNode = new ClassNode(); - classReader.accept(classNode, - ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + classReader.accept( + classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); return classNode; } catch (IOException e) { throw new RuntimeException("failed to analyze " + classFileName + " in " + path, e); @@ -313,7 +464,7 @@ public class SdkStore { @Override public int compareTo(Sdk sdk) { - return sdk.sdkInt - sdkInt; + return sdk.sdkRelease.getSdkInt() - sdkRelease.getSdkInt(); } } @@ -321,8 +472,7 @@ public class SdkStore { private final Map<MethodInfo, MethodExtraInfo> methods = new HashMap<>(); private final Map<MethodInfo, MethodExtraInfo> erasedParamTypesMethods = new HashMap<>(); - private ClassInfo() { - } + private ClassInfo() {} public ClassInfo(ClassNode classNode) { for (Object aMethod : classNode.methods) { @@ -401,20 +551,17 @@ public class SdkStore { return false; } MethodInfo that = (MethodInfo) o; - return Objects.equals(name, that.name) - && Objects.equals(paramTypes, that.paramTypes); + return Objects.equals(name, that.name) && Objects.equals(paramTypes, that.paramTypes); } @Override public int hashCode() { return Objects.hash(name, paramTypes); } + @Override public String toString() { - return "MethodInfo{" - + "name='" + name + '\'' - + ", paramTypes=" + paramTypes - + '}'; + return "MethodInfo{" + "name='" + name + '\'' + ", paramTypes=" + paramTypes + '}'; } } diff --git a/processor/src/test/java/org/robolectric/annotation/processing/validator/InDevelopmentValidatorTest.java b/processor/src/test/java/org/robolectric/annotation/processing/validator/InDevelopmentValidatorTest.java new file mode 100644 index 000000000..6e3581357 --- /dev/null +++ b/processor/src/test/java/org/robolectric/annotation/processing/validator/InDevelopmentValidatorTest.java @@ -0,0 +1,71 @@ +package org.robolectric.annotation.processing.validator; + +import static com.google.common.truth.Truth.assertAbout; +import static org.robolectric.annotation.processing.validator.SingleClassSubject.singleClass; + +import com.example.objects.Dummy; +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.robolectric.versioning.AndroidVersions; + +@RunWith(JUnit4.class) +public final class InDevelopmentValidatorTest { + + private static String getClassRootDir(Class<?> clazz) { + // Get the URL representation of the class location + URL resource = clazz.getResource(clazz.getSimpleName() + ".class"); + if (resource == null) { + return null; // dynamic class + } + if (resource.getProtocol().equals("file")) { + File path = new File(URI.create(resource.toString()).getPath()).getParentFile(); + int packagePartSize = clazz.getPackageName().split("\\.").length; + for (int i = 0; i < packagePartSize; i++) { + path = path.getParentFile(); + } + return path.getAbsolutePath(); + } else if (resource.getProtocol().equals("jar")) { + // Extract path to JAR and find root + String path = resource.getPath().substring(5, resource.getPath().indexOf("!")); + File jarFile = new File(URI.create(path).getPath()); + return jarFile.getAbsolutePath(); + } else { + return null; // Not a supported class location + } + } + + @Test + public void implementationWithInDevelopmentCompiles() { + AndroidVersions.AndroidRelease unreleased = + AndroidVersions.getUnreleased().stream().findFirst().get(); + final String testClass = + "org.robolectric.annotation.processing.shadows.ShadowImplementsInDevelopment"; + HashMap<String, String> props = new HashMap<>(); + props.put("org.robolectric.annotation.processing.sdkCheckMode", "ERROR"); + props.put("org.robolectric.annotation.processing.validateCompileSdk", "true"); + assertAbout(singleClass(props, getClassRootDir(Dummy.class), unreleased.getSdkInt())) + .that(testClass) + .compilesWithoutError(); + } + + @Test + public void implementationWithoutInDevelopmentFailsToCompiles() { + AndroidVersions.AndroidRelease unreleased = + AndroidVersions.getUnreleased().stream().findFirst().get(); + final String testClass = + "org.robolectric.annotation.processing.shadows.ShadowImplementsInDevelopmentMissing"; + HashMap<String, String> props = new HashMap<>(); + props.put("org.robolectric.annotation.processing.sdkCheckMode", "ERROR"); + props.put("org.robolectric.annotation.processing.validateCompileSdk", "true"); + assertAbout(singleClass(props, getClassRootDir(Dummy.class), unreleased.getSdkInt())) + .that(testClass) + .failsToCompile() + .withErrorContaining("No such method in com.example.objects.Dummy for SDK 35") + .onLine(11); + } +} diff --git a/processor/src/test/java/org/robolectric/annotation/processing/validator/SingleClassSubject.java b/processor/src/test/java/org/robolectric/annotation/processing/validator/SingleClassSubject.java index c016495a2..c883443ec 100644 --- a/processor/src/test/java/org/robolectric/annotation/processing/validator/SingleClassSubject.java +++ b/processor/src/test/java/org/robolectric/annotation/processing/validator/SingleClassSubject.java @@ -13,6 +13,8 @@ import com.google.testing.compile.CompileTester.LineClause; import com.google.testing.compile.CompileTester.SuccessfulCompilationClause; import com.google.testing.compile.CompileTester.UnsuccessfulCompilationClause; import com.google.testing.compile.JavaFileObjects; +import java.util.HashMap; +import java.util.Map; import javax.tools.JavaFileObject; import org.robolectric.annotation.processing.RobolectricProcessor; import org.robolectric.annotation.processing.Utils; @@ -20,21 +22,47 @@ import org.robolectric.annotation.processing.Utils; public final class SingleClassSubject extends Subject { public static Subject.Factory<SingleClassSubject, String> singleClass() { - return SingleClassSubject::new; } + public static Subject.Factory<SingleClassSubject, String> singleClass( + Map<String, String> processorOpts, String sdkLocation, int sdkInt) { + return (FailureMetadata failureMetadata, String subject) -> + new SingleClassSubject(failureMetadata, subject, processorOpts, sdkLocation, sdkInt); + } + + public static Subject.Factory<SingleClassSubject, String> singleClass( + Map<String, String> processorOpts) { + return (FailureMetadata failureMetadata, String subject) -> + new SingleClassSubject(failureMetadata, subject, processorOpts); + } JavaFileObject source; CompileTester tester; - + public SingleClassSubject(FailureMetadata failureMetadata, String subject) { + this(failureMetadata, subject, DEFAULT_OPTS); + } + + public SingleClassSubject( + FailureMetadata failureMetadata, String subject, Map<String, String> processorOpts) { + this(failureMetadata, subject, processorOpts, null, -1); + } + + public SingleClassSubject( + FailureMetadata failureMetadata, + String subject, + Map<String, String> processorOpts, + String sdkLocation, + int sdkInt) { super(failureMetadata, subject); source = JavaFileObjects.forResource(Utils.toResourcePath(subject)); + Map<String, String> opts = new HashMap<>(DEFAULT_OPTS); + opts.putAll(processorOpts); tester = assertAbout(javaSources()) .that(ImmutableList.of(source, Utils.SHADOW_EXTRACTOR_SOURCE)) - .processedWith(new RobolectricProcessor(DEFAULT_OPTS)); + .processedWith(new RobolectricProcessor(opts, sdkLocation, sdkInt)); } public SuccessfulCompilationClause compilesWithoutError() { @@ -45,7 +73,7 @@ public final class SingleClassSubject extends Subject { } return null; } - + public SingleFileClause failsToCompile() { try { return new SingleFileClause(tester.failsToCompile(), source); @@ -54,17 +82,17 @@ public final class SingleClassSubject extends Subject { } return null; } - + final class SingleFileClause implements CompileTester.ChainingClause<SingleFileClause> { UnsuccessfulCompilationClause unsuccessful; JavaFileObject source; - + public SingleFileClause(UnsuccessfulCompilationClause unsuccessful, JavaFileObject source) { this.unsuccessful = unsuccessful; this.source = source; } - + public SingleLineClause withErrorContaining(final String messageFragment) { try { return new SingleLineClause(unsuccessful.withErrorContaining(messageFragment).in(source)); @@ -86,7 +114,7 @@ public final class SingleClassSubject extends Subject { return this; } - + @Override public SingleFileClause and() { return this; @@ -95,11 +123,11 @@ public final class SingleClassSubject extends Subject { final class SingleLineClause implements CompileTester.ChainingClause<SingleFileClause> { LineClause lineClause; - + public SingleLineClause(LineClause lineClause) { this.lineClause = lineClause; } - + public CompileTester.ChainingClause<SingleFileClause> onLine(long lineNumber) { try { lineClause.onLine(lineNumber); @@ -114,12 +142,11 @@ public final class SingleClassSubject extends Subject { } return null; } - + @Override public SingleFileClause and() { return SingleFileClause.this; } - } } } diff --git a/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowImplementsInDevelopment.java b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowImplementsInDevelopment.java new file mode 100644 index 000000000..4330c7a79 --- /dev/null +++ b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowImplementsInDevelopment.java @@ -0,0 +1,16 @@ +package org.robolectric.annotation.processing.shadows; + +import com.example.objects.Dummy; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.InDevelopment; + +@Implements(Dummy.class) +public final class ShadowImplementsInDevelopment { + + @Implementation + @InDevelopment + protected Object doSomething() { + return null; + } +} diff --git a/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowImplementsInDevelopmentMissing.java b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowImplementsInDevelopmentMissing.java new file mode 100644 index 000000000..80f69ef02 --- /dev/null +++ b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowImplementsInDevelopmentMissing.java @@ -0,0 +1,14 @@ +package org.robolectric.annotation.processing.shadows; + +import com.example.objects.Dummy; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(Dummy.class) +public final class ShadowImplementsInDevelopmentMissing { + + @Implementation + protected Object doSomething() { + return null; + } +} diff --git a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java index d90117be8..60f805d72 100644 --- a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java +++ b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java @@ -614,7 +614,7 @@ public class AndroidManifest implements UsesSdk { * <p>Note that if {@link #targetSdkVersion} isn't set, this value changes the behavior of some * Android code (notably {@link android.content.SharedPreferences}) to emulate old bugs. * - * @return the minimum SDK version, or Jelly Bean (16) by default + * @return the minimum SDK version, or KitKat (19) by default */ @Override public int getMinSdkVersion() { @@ -629,7 +629,7 @@ public class AndroidManifest implements UsesSdk { * <p>Note that this value changes the behavior of some Android code (notably {@link * android.content.SharedPreferences}) to emulate old bugs. * - * @return the minimum SDK version, or Jelly Bean (16) by default + * @return the target SDK version, or KitKat (19) by default */ @Override public int getTargetSdkVersion() { diff --git a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java index def624b88..4cfb8f30c 100644 --- a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java +++ b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java @@ -50,7 +50,6 @@ import org.robolectric.pluginapi.SdkPicker; import org.robolectric.pluginapi.config.ConfigurationStrategy; import org.robolectric.pluginapi.config.ConfigurationStrategy.Configuration; import org.robolectric.pluginapi.config.GlobalConfigProvider; -import org.robolectric.plugins.HierarchicalConfigurationStrategy.ConfigurationImpl; import org.robolectric.util.Logger; import org.robolectric.util.PerfStatsCollector; import org.robolectric.util.ReflectionHelpers; @@ -411,42 +410,9 @@ public class RobolectricTestRunner extends SandboxTestRunner { manifestIdentifier.getApkFile()); } - /** - * Compute the effective Robolectric configuration for a given test method. - * - * <p>Configuration information is collected from package-level {@code robolectric.properties} - * files and {@link Config} annotations on test classes, superclasses, and methods. - * - * <p>Custom TestRunner subclasses may wish to override this method to provide alternate - * configuration. - * - * @param method the test method - * @return the effective Robolectric configuration for the given test method - * @deprecated Provide an implementation of {@link javax.inject.Provider<Config>} instead. This - * method will be removed in Robolectric 4.3. - * @since 2.0 - * @see <a href="http://robolectric.org/migrating/#migrating-to-40">Migration Notes</a> for more - * details. - */ - @Deprecated - public Config getConfig(Method method) { - throw new UnsupportedOperationException(); - } - /** Calculate the configuration for a given test method. */ private Configuration getConfiguration(Method method) { - Configuration configuration = - configurationStrategy.getConfig(getTestClass().getJavaClass(), method); - - // in case #getConfig(Method) has been overridden... - try { - Config config = getConfig(method); - ((ConfigurationImpl) configuration).put(Config.class, config); - } catch (UnsupportedOperationException e) { - // no problem - } - - return configuration; + return configurationStrategy.getConfig(getTestClass().getJavaClass(), method); } /** diff --git a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java index 3b6f76da9..0f9272c03 100644 --- a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java +++ b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java @@ -46,7 +46,7 @@ public class DefaultSdkProvider implements SdkProvider { private static final int RUNNING_JAVA_VERSION = Util.getJavaVersion(); - private static final int PREINSTRUMENTED_VERSION = 5; + private static final int PREINSTRUMENTED_VERSION = 6; private final DependencyResolver dependencyResolver; diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java index 69b17a2f4..dfdb9d495 100644 --- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java +++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java @@ -42,11 +42,9 @@ import org.junit.runner.notification.RunListener; import org.junit.runner.notification.RunNotifier; import org.junit.runners.JUnit4; import org.junit.runners.MethodSorters; -import org.junit.runners.model.FrameworkMethod; import org.robolectric.RobolectricTestRunner.RobolectricFrameworkMethod; import org.robolectric.android.internal.AndroidTestEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.annotation.Config.Implementation; import org.robolectric.annotation.experimental.LazyApplication; import org.robolectric.annotation.experimental.LazyApplication.LazyLoad; import org.robolectric.config.ConfigurationRegistry; @@ -143,20 +141,6 @@ public class RobolectricTestRunnerTest { } @Test - public void supportsOldGetConfigUntil4dot3() throws Exception { - Implementation overriddenConfig = Config.Builder.defaults().build(); - List<FrameworkMethod> children = new SingleSdkRobolectricTestRunner(TestWithTwoMethods.class) { - @Override - public Config getConfig(Method method) { - return overriddenConfig; - } - }.getChildren(); - Config config = ((RobolectricFrameworkMethod) children.get(0)) - .getConfiguration().get(Config.class); - assertThat(config).isSameInstanceAs(overriddenConfig); - } - - @Test public void failureInResetterDoesntBreakAllTests() throws Exception { RobolectricTestRunner runner = new SingleSdkRobolectricTestRunner( diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBatteryManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBatteryManagerTest.java index 0b8986f0d..aee4779e0 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBatteryManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBatteryManagerTest.java @@ -2,7 +2,9 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; +import static android.os.Build.VERSION_CODES.P; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.content.Context; @@ -70,4 +72,26 @@ public class ShadowBatteryManagerTest { shadowBatteryManager.setLongProperty(TEST_ID, Long.MAX_VALUE); assertThat(batteryManager.getLongProperty(TEST_ID)).isEqualTo(Long.MAX_VALUE); } + + @Test + @Config(minSdk = P) + public void testChargeTimeRemaining() { + shadowBatteryManager.setChargeTimeRemaining(0L); + assertThat(batteryManager.computeChargeTimeRemaining()).isEqualTo(0L); + + shadowBatteryManager.setChargeTimeRemaining(20L); + assertThat(batteryManager.computeChargeTimeRemaining()).isEqualTo(20L); + + shadowBatteryManager.setChargeTimeRemaining(-1L); + assertThat(batteryManager.computeChargeTimeRemaining()).isEqualTo(-1L); + } + + @Test + @Config(minSdk = P) + public void testChargeTimeRemainingRejectsInvalidValues() { + assertThrows( + IllegalArgumentException.class, () -> shadowBatteryManager.setChargeTimeRemaining(-50L)); + assertThrows( + IllegalArgumentException.class, () -> shadowBatteryManager.setChargeTimeRemaining(-100L)); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java index 5fbe161d6..dc7230dff 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java @@ -7,6 +7,7 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.O; +import static android.os.Build.VERSION_CODES.S; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -398,6 +399,16 @@ public class ShadowConnectivityManagerTest { } @Test + @Config(minSdk = S) + public void registerBestMatchingNetworkCallback_shouldAddCallback() { + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + ConnectivityManager.NetworkCallback callback = createSimpleCallback(); + connectivityManager.registerBestMatchingNetworkCallback( + builder.build(), callback, new Handler()); + assertThat(shadowOf(connectivityManager).getNetworkCallbacks()).hasSize(1); + } + + @Test @Config(minSdk = LOLLIPOP) public void unregisterCallback_shouldRemoveCallbacks() { NetworkRequest.Builder builder = new NetworkRequest.Builder(); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextHubClientTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextHubClientTest.java index fbc9fa41c..d2d663eae 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextHubClientTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextHubClientTest.java @@ -3,6 +3,7 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.Q; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.robolectric.Shadows.shadowOf; import android.content.Context; import android.hardware.location.ContextHubClient; @@ -13,8 +14,10 @@ import android.hardware.location.ContextHubTransaction; import android.hardware.location.NanoAppMessage; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; @@ -22,6 +25,12 @@ import org.robolectric.shadow.api.Shadow; @Config(minSdk = Q) public final class ShadowContextHubClientTest { + @Before + public void setUp() { + shadowOf(RuntimeEnvironment.getApplication()) + .grantPermissions(android.Manifest.permission.ACCESS_CONTEXT_HUB); + } + @Test public void whenAMessageIsSent_ensureItIsReturnedInTheMessageList() { ContextHubClient contextHubClient = (ContextHubClient) createContextHubClient(); @@ -73,12 +82,12 @@ public final class ShadowContextHubClientTest { @Test public void whenContextHubRevokedPermission_ensureSendingAMessageThrowsSecurityException() { ContextHubClient contextHubClient = (ContextHubClient) createContextHubClient(); - ShadowContextHubClient shadowContextHubClient = Shadow.extract(contextHubClient); NanoAppMessage message = NanoAppMessage.createMessageToNanoApp( /* targetNanoAppId= */ 0L, /* messageType= */ 0, /* messageBody= */ new byte[10]); - shadowContextHubClient.revokePermission(); + shadowOf(RuntimeEnvironment.getApplication()) + .denyPermissions(android.Manifest.permission.ACCESS_CONTEXT_HUB); try { contextHubClient.sendMessageToNanoApp(message); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSurfaceViewTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSurfaceViewTest.java index 2105d786b..b6719eae7 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSurfaceViewTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSurfaceViewTest.java @@ -4,27 +4,36 @@ import static com.google.common.truth.Truth.assertThat; import static org.robolectric.Robolectric.buildActivity; import android.app.Activity; +import android.graphics.PixelFormat; import android.os.Bundle; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.Window; import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Shadows; +import org.robolectric.android.controller.ActivityController; @RunWith(AndroidJUnit4.class) public class ShadowSurfaceViewTest { - private SurfaceHolder.Callback callback1 = new TestCallback(); - private SurfaceHolder.Callback callback2 = new TestCallback(); + private final SurfaceHolder.Callback callback1 = new TestCallback(); + private final SurfaceHolder.Callback callback2 = new TestCallback(); + private final ActivityController<Activity> defaultController = buildActivity(Activity.class); - private SurfaceView view = new SurfaceView(buildActivity(Activity.class).create().get()); - private SurfaceHolder surfaceHolder = view.getHolder(); - private ShadowSurfaceView shadowSurfaceView = (ShadowSurfaceView) Shadows.shadowOf(view); - private ShadowSurfaceView.FakeSurfaceHolder fakeSurfaceHolder = + private final SurfaceView view = new SurfaceView(defaultController.create().get()); + private final SurfaceHolder surfaceHolder = view.getHolder(); + private final ShadowSurfaceView shadowSurfaceView = Shadows.shadowOf(view); + private final ShadowSurfaceView.FakeSurfaceHolder fakeSurfaceHolder = shadowSurfaceView.getFakeSurfaceHolder(); + @After + public void tearDown() { + defaultController.close(); + } + @Test public void addCallback() { assertThat(fakeSurfaceHolder.getCallbacks()).isEmpty(); @@ -54,8 +63,27 @@ public class ShadowSurfaceViewTest { @Test public void canCreateASurfaceView_attachedToAWindowWithActionBar() { - TestActivity testActivity = buildActivity(TestActivity.class).create().start().resume().visible().get(); - assertThat(testActivity).isNotNull(); + try (ActivityController<TestActivity> controller = buildActivity(TestActivity.class)) { + TestActivity testActivity = controller.create().start().resume().visible().get(); + assertThat(testActivity).isNotNull(); + } + } + + @Test + public void requestedFormat_default_getRGB565() { + assertThat(fakeSurfaceHolder.getRequestedFormat()).isEqualTo(PixelFormat.RGB_565); + } + + @Test + public void requestedFormat_setNewFormat_getNewFormat() { + view.getHolder().setFormat(PixelFormat.HSV_888); + assertThat(fakeSurfaceHolder.getRequestedFormat()).isEqualTo(PixelFormat.HSV_888); + } + + @Test + public void requestedFormat_setFormatOpaque_getRGB565() { + view.getHolder().setFormat(PixelFormat.OPAQUE); + assertThat(fakeSurfaceHolder.getRequestedFormat()).isEqualTo(PixelFormat.RGB_565); } private static class TestCallback implements SurfaceHolder.Callback { diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java index afabf1db0..d560d60ea 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java @@ -867,6 +867,15 @@ public class ShadowTelephonyManagerTest { } @Test + @Config(minSdk = P) + public void shouldGetSimCarrierIdName() { + String expectedCarrierIdName = "Fi"; + shadowOf(telephonyManager).setSimCarrierIdName(expectedCarrierIdName); + + assertThat(telephonyManager.getSimCarrierIdName().toString()).isEqualTo(expectedCarrierIdName); + } + + @Test @Config(minSdk = Q) public void shouldGetCarrierIdFromSimMccMnc() { int expectedCarrierId = 419; diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java index 1c6d8c19c..ab480d69a 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java @@ -18,6 +18,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Priority; @@ -305,13 +306,8 @@ public class ShadowWrangler implements ClassHandler { Class<?>[] types, ShadowInfo shadowInfo, Class<?> shadowClass) { - Method method = findShadowMethodDeclaredOnClass(shadowClass, name, types); - - if (method == null && shadowInfo.looseSignatures) { - Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray(); - method = findShadowMethodDeclaredOnClass(shadowClass, name, genericTypes); - } - + Method method = + findShadowMethodDeclaredOnClass(shadowClass, name, types, shadowInfo.looseSignatures); if (method != null) { return method; } else { @@ -334,41 +330,49 @@ public class ShadowWrangler implements ClassHandler { } private Method findShadowMethodDeclaredOnClass( - Class<?> shadowClass, String methodName, Class<?>[] paramClasses) { - try { - Method method = shadowClass.getDeclaredMethod(methodName, paramClasses); - - // todo: allow per-version overloading - // if (method == null) { - // String methodPrefix = name + "$$"; - // for (Method candidateMethod : shadowClass.getDeclaredMethods()) { - // if (candidateMethod.getName().startsWith(methodPrefix)) { - // - // } - // } - // } - - if (isValidShadowMethod(method)) { - method.setAccessible(true); - return method; - } else { - return null; + Class<?> shadowClass, String methodName, Class<?>[] paramClasses, boolean looseSignatures) { + Method foundMethod = null; + for (Method method : shadowClass.getDeclaredMethods()) { + if (!method.getName().equals(methodName) + || method.getParameterCount() != paramClasses.length) { + continue; } - } catch (NoSuchMethodException e) { - return null; - } - } + if (!Modifier.isPublic(method.getModifiers()) + && !Modifier.isProtected(method.getModifiers())) { + continue; + } - private boolean isValidShadowMethod(Method method) { - int modifiers = method.getModifiers(); - if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) { - return false; + if (Arrays.equals(method.getParameterTypes(), paramClasses) + && shadowMatcher.matches(method)) { + // Found an exact match, we can exit early. + foundMethod = method; + break; + } + if (looseSignatures) { + boolean allParameterTypesAreObject = true; + for (Class<?> paramClass : method.getParameterTypes()) { + if (!paramClass.equals(Object.class)) { + allParameterTypesAreObject = false; + break; + } + } + if (allParameterTypesAreObject && shadowMatcher.matches(method)) { + // Found a looseSignatures match, but continue looking for an exact match. + foundMethod = method; + } + } } - return shadowMatcher.matches(method); + if (foundMethod != null) { + foundMethod.setAccessible(true); + return foundMethod; + } else { + return null; + } } + @Override public Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable { diff --git a/shadows/framework/Android.bp b/shadows/framework/Android.bp index 8e9f0e13b..493b84870 100644 --- a/shadows/framework/Android.bp +++ b/shadows/framework/Android.bp @@ -11,8 +11,10 @@ package { default_applicable_licenses: ["external_robolectric_license"], } -java_library_host { +java_library { name: "Robolectric_shadows_framework_upstream", + host_supported: true, + device_supported: false, srcs: [ "src/main/java/**/*.java", "src/main/java/**/*.kt", @@ -21,6 +23,7 @@ java_library_host { javacflags: [ "-Aorg.robolectric.annotation.processing.shadowPackage=org.robolectric", "-Aorg.robolectric.annotation.processing.sdkCheckMode=ERROR", + "-Aorg.robolectric.annotation.processing.validateCompileSdk=true", // Uncomment the below to debug annotation processors not firing. //"-verbose", //"-XprintRounds", @@ -41,7 +44,6 @@ java_library_host { "auto_value_annotations", "jsr305", "icu4j", - "robolectric-accessibility-test-framework-2.1", "robolectric-javax.annotation-api-1.2", "robolectric-sqlite4java-0.282", @@ -54,9 +56,7 @@ java_library_host { // aar files that make up android and jetpack "robolectric-host-android_all", "robolectric-host-androidx-test-core_upstream", - //"robolectric-host-androidx-test-ext-junit_upstream", "robolectric-host-androidx-test-monitor_upstream", - //"robolectric-host-androidx-test-runner_upstream", ], plugins: [ "auto_value_plugin_1.9", diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle index 74409a64f..492f86502 100644 --- a/shadows/framework/build.gradle +++ b/shadows/framework/build.gradle @@ -33,7 +33,7 @@ tasks.register('copySqliteNatives', Copy) { } } } - into project.file("$buildDir/resources/main/sqlite4java") + into project.file(layout.buildDirectory.dir("resources/main/sqlite4java")) } jar { @@ -51,7 +51,6 @@ dependencies { api project(":pluginapi") api project(":sandbox") api project(":shadowapi") - api project(":shadows:versioning") api project(":utils") api project(":utils:reflector") diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java index 7d3a1d5c6..b01322af8 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java @@ -6,6 +6,7 @@ import android.content.res.ApkAssets; import android.content.res.AssetManager; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.InDevelopment; import org.robolectric.versioning.AndroidVersions.U; import org.robolectric.versioning.AndroidVersions.V; @@ -74,6 +75,7 @@ public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 { } @Implementation(minSdk = V.SDK_INT) + @InDevelopment protected static void nativeSetConfiguration( long ptr, int mcc, @@ -129,6 +131,7 @@ public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 { } @Implementation(minSdk = V.SDK_INT) + @InDevelopment protected static void nativeSetApkAssets( long ptr, @NonNull ApkAssets[] apkAssets, boolean invalidateCaches, boolean preset) { nativeSetApkAssets(ptr, apkAssets, invalidateCaches); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBatteryManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBatteryManager.java index 6dad75085..aeceac4db 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBatteryManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBatteryManager.java @@ -2,8 +2,10 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; +import static android.os.Build.VERSION_CODES.P; import android.os.BatteryManager; +import com.google.common.base.Preconditions; import java.util.HashMap; import java.util.Map; import org.robolectric.annotation.Implementation; @@ -12,6 +14,7 @@ import org.robolectric.annotation.Implements; @Implements(BatteryManager.class) public class ShadowBatteryManager { private boolean isCharging = false; + private long chargeTimeRemaining = 0; private final Map<Integer, Long> longProperties = new HashMap<>(); private final Map<Integer, Integer> intProperties = new HashMap<>(); @@ -41,4 +44,17 @@ public class ShadowBatteryManager { public void setLongProperty(int id, long value) { longProperties.put(id, value); } + + @Implementation(minSdk = P) + protected long computeChargeTimeRemaining() { + return chargeTimeRemaining; + } + + /** Sets the value to be returned from {@link BatteryManager#computeChargeTimeRemaining} */ + public void setChargeTimeRemaining(long chargeTimeRemaining) { + Preconditions.checkArgument( + chargeTimeRemaining == -1 || chargeTimeRemaining >= 0, + "chargeTimeRemaining must be -1 or non-negative."); + this.chargeTimeRemaining = chargeTimeRemaining; + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java index a051af131..010f9619b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java @@ -4,6 +4,7 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.O; +import static android.os.Build.VERSION_CODES.S; import static org.robolectric.RuntimeEnvironment.getApiLevel; import static org.robolectric.Shadows.shadowOf; @@ -152,6 +153,14 @@ public class ShadowConnectivityManager { networkCallbacks.add(networkCallback); } + @Implementation(minSdk = S) + protected void registerBestMatchingNetworkCallback( + NetworkRequest request, + ConnectivityManager.NetworkCallback networkCallback, + Handler handler) { + networkCallbacks.add(networkCallback); + } + @Implementation(minSdk = LOLLIPOP) protected void unregisterNetworkCallback(ConnectivityManager.NetworkCallback networkCallback) { if (networkCallback == null) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextHubClient.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextHubClient.java index 6b8dc0c04..04d264a5d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextHubClient.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextHubClient.java @@ -13,6 +13,7 @@ import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.HiddenApi; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -26,12 +27,12 @@ import org.robolectric.util.reflector.ForType; public class ShadowContextHubClient { @RealObject private ContextHubClient realContextHubClient; private final List<NanoAppMessage> messages = new ArrayList<>(); - private Boolean hasPermission = true; @Implementation(minSdk = VERSION_CODES.P) @HiddenApi protected int sendMessageToNanoApp(NanoAppMessage message) { - if (!hasPermission) { + if (!ShadowInstrumentation.hasRequiredPermission( + RuntimeEnvironment.getApplication(), android.Manifest.permission.ACCESS_CONTEXT_HUB)) { throw new SecurityException("ShadowContextHubClient does not have permission"); } if (isClosed()) { @@ -47,10 +48,6 @@ public class ShadowContextHubClient { reflector(ContextHubClientReflector.class, realContextHubClient).getIsClosed().set(true); } - public void revokePermission() { - hasPermission = false; - } - public List<NanoAppMessage> getMessages() { return ImmutableList.copyOf(messages); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java index aff4fd06e..d33cf20d9 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java @@ -361,8 +361,8 @@ public class ShadowInstrumentation { // The receiver must hold the permission specified by sendBroadcast, and the broadcaster must // hold the permission specified by registerReceiver. boolean hasPermissionFromManifest = - hasRequiredPermissionForBroadcast(wrapper.context, receiverPermission) - && hasRequiredPermissionForBroadcast(broadcastContext, wrapper.broadcastPermission); + hasRequiredPermission(wrapper.context, receiverPermission) + && hasRequiredPermission(broadcastContext, wrapper.broadcastPermission); // Many existing tests don't declare manifest permissions, relying on the old equality check. boolean hasPermissionForBackwardsCompatibility = TextUtils.equals(receiverPermission, wrapper.broadcastPermission); @@ -379,8 +379,7 @@ public class ShadowInstrumentation { } /** A null {@code requiredPermission} indicates that no permission is required. */ - private static boolean hasRequiredPermissionForBroadcast( - Context context, @Nullable String requiredPermission) { + static boolean hasRequiredPermission(Context context, @Nullable String requiredPermission) { if (requiredPermission == null) { return true; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowKeyCharacterMap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowKeyCharacterMap.java index ea29a43dc..92ff331f6 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowKeyCharacterMap.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowKeyCharacterMap.java @@ -182,11 +182,8 @@ public class ShadowKeyCharacterMap { return ReflectionHelpers.callConstructor(KeyCharacterMap.class); } - @Implementation - protected KeyEvent[] getEvents(char[] chars) { - if (chars == null) { - throw new IllegalArgumentException("chars must not be null."); - } + @Implementation(minSdk = KITKAT_WATCH) + protected static KeyEvent[] nativeGetEvents(long ptr, char[] chars) { int eventsPerChar = 2; KeyEvent[] events = new KeyEvent[chars.length * eventsPerChar]; @@ -198,13 +195,28 @@ public class ShadowKeyCharacterMap { return events; } - @Implementation - protected int getKeyboardType() { + @Implementation(maxSdk = KITKAT) + protected static KeyEvent[] nativeGetEvents(int ptr, char[] chars) { + return nativeGetEvents((long) ptr, chars); + } + + @Implementation(minSdk = KITKAT_WATCH) + protected static int nativeGetKeyboardType(long ptr) { return KeyCharacterMap.FULL; } - @Implementation - protected int get(int keyCode, int metaState) { + @Implementation(maxSdk = KITKAT) + protected static int nativeGetKeyboardType(int ptr) { + return KeyCharacterMap.FULL; + } + + @Implementation(maxSdk = KITKAT) + protected static char nativeGetCharacter(int ptr, int keyCode, int metaState) { + return nativeGetCharacter((long) ptr, keyCode, metaState); + } + + @Implementation(minSdk = KITKAT_WATCH) + protected static char nativeGetCharacter(long ptr, int keyCode, int metaState) { boolean metaShiftOn = (metaState & KeyEvent.META_SHIFT_ON) != 0; Character character = KEY_CODE_TO_CHAR.get(keyCode); if (character == null) { @@ -216,7 +228,7 @@ public class ShadowKeyCharacterMap { } } - public KeyEvent getDownEvent(char a) { + private static KeyEvent getDownEvent(char a) { return new KeyEvent( 0, 0, @@ -228,7 +240,7 @@ public class ShadowKeyCharacterMap { 0); } - public KeyEvent getUpEvent(char a) { + private static KeyEvent getUpEvent(char a) { return new KeyEvent( 0, 0, @@ -240,24 +252,14 @@ public class ShadowKeyCharacterMap { 0); } - @Implementation - protected char getDisplayLabel(int keyCode) { + @Implementation(minSdk = KITKAT_WATCH) + protected static char nativeGetDisplayLabel(long ptr, int keyCode) { return KEY_CODE_TO_CHAR.getOrDefault(keyCode, (char) 0); } - @Implementation - protected boolean isPrintingKey(int keyCode) { - int type = Character.getType(getDisplayLabel(keyCode)); - switch (type) { - case Character.SPACE_SEPARATOR: - case Character.LINE_SEPARATOR: - case Character.PARAGRAPH_SEPARATOR: - case Character.CONTROL: - case Character.FORMAT: - return false; - default: - return true; - } + @Implementation(maxSdk = KITKAT) + protected static char nativeGetDisplayLabel(int ptr, int keyCode) { + return KEY_CODE_TO_CHAR.getOrDefault(keyCode, (char) 0); } @Implementation(minSdk = KITKAT_WATCH) @@ -278,7 +280,7 @@ public class ShadowKeyCharacterMap { return character; } - private int toCharKeyCode(char a) { + private static int toCharKeyCode(char a) { if (CHAR_TO_KEY_CODE.containsKey(Character.toUpperCase(a))) { return CHAR_TO_KEY_CODE.get(Character.toUpperCase(a)); } else if (CHAR_TO_KEY_CODE_SHIFT_ON.containsKey(a)) { @@ -288,7 +290,7 @@ public class ShadowKeyCharacterMap { } } - private int getMetaState(char a) { + private static int getMetaState(char a) { if (Character.isUpperCase(a) || CHAR_TO_KEY_CODE_SHIFT_ON.containsKey(a)) { return KeyEvent.META_SHIFT_ON; } else { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowKeyEvent.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowKeyEvent.java new file mode 100644 index 000000000..8f41061d3 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowKeyEvent.java @@ -0,0 +1,53 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.view.KeyEvent; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.L; + +/** Shadow for {@link KeyEvent}. */ +@Implements(KeyEvent.class) +public class ShadowKeyEvent extends ShadowInputEvent { + + private static final Map<String, Integer> keyCodeMap; + + static { + keyCodeMap = new HashMap<>(); + if (RuntimeEnvironment.getApiLevel() >= L.SDK_INT) { + String keyCodeConstantPrefix = reflector(KeyEventReflector.class).getLabelPrefix(); + for (Field field : KeyEvent.class.getDeclaredFields()) { + if (field.getName().startsWith(keyCodeConstantPrefix) + && Modifier.isStatic(field.getModifiers())) { + String keyCodeString = field.getName().substring(keyCodeConstantPrefix.length()); + try { + keyCodeMap.put(keyCodeString, field.getInt(null)); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "unable to get reflectively get value for " + keyCodeString, e); + } + } + } + } + } + + @Implementation(minSdk = L.SDK_INT) + protected static int nativeKeyCodeFromString(String keyCode) { + return keyCodeMap.getOrDefault(keyCode, KeyEvent.KEYCODE_UNKNOWN); + } + + @ForType(KeyEvent.class) + private interface KeyEventReflector { + + @Accessor("LABEL_PREFIX") + String getLabelPrefix(); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLog.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLog.java index 82e4e43d2..b71d462b3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLog.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLog.java @@ -115,17 +115,17 @@ public class ShadowLog { protected static int wtf(String tag, String msg, Throwable throwable) { addLog(Log.ASSERT, tag, msg, throwable); // invoking the wtfHandler - TerribleFailure terribleFailure = + Throwable terribleFailure = reflector(TerribleFailureReflector.class).newTerribleFailure(msg, throwable); if (wtfIsFatal) { Util.sneakyThrow(terribleFailure); } TerribleFailureHandler terribleFailureHandler = reflector(LogReflector.class).getWtfHandler(); if (RuntimeEnvironment.getApiLevel() >= L.SDK_INT) { - terribleFailureHandler.onTerribleFailure(tag, terribleFailure, false); + terribleFailureHandler.onTerribleFailure(tag, (TerribleFailure) terribleFailure, false); } else { reflector(TerribleFailureHandlerReflector.class, terribleFailureHandler) - .onTerribleFailure(tag, terribleFailure); + .onTerribleFailure(tag, (TerribleFailure) terribleFailure); } return 0; } @@ -374,7 +374,8 @@ public class ShadowLog { @ForType(TerribleFailure.class) interface TerribleFailureReflector { + // The return value should be generic because TerribleFailure is a hidden class. @Constructor - TerribleFailure newTerribleFailure(String msg, Throwable cause); + Throwable newTerribleFailure(String msg, Throwable cause); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java index b36cfb55b..dc5345ace 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java @@ -28,6 +28,7 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.HiddenApi; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.InDevelopment; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; import org.robolectric.res.android.NativeObjRegistry; @@ -317,25 +318,24 @@ public class ShadowMotionEvent extends ShadowInputEvent { int pointerCount, PointerProperties[] pointerIds, PointerCoords[] pointerCoords) { - return - nativeInitialize( - nativePtr, - deviceId, - source, - action, - flags, - edgeFlags, - metaState, - buttonState, - xOffset, - yOffset, - xPrecision, - yPrecision, - downTimeNanos, - eventTimeNanos, - pointerCount, - pointerIds, - pointerCoords); + return nativeInitialize( + nativePtr, + deviceId, + source, + action, + flags, + edgeFlags, + metaState, + buttonState, + xOffset, + yOffset, + xPrecision, + yPrecision, + downTimeNanos, + eventTimeNanos, + pointerCount, + pointerIds, + pointerCoords); } @Implementation(maxSdk = KITKAT_WATCH) @@ -816,6 +816,7 @@ public class ShadowMotionEvent extends ShadowInputEvent { @Implementation(minSdk = LOLLIPOP) @HiddenApi + @InDevelopment protected static float nativeGetXOffset(long nativePtr) { NativeInput.MotionEvent event = getNativeMotionEvent(nativePtr); return event.getXOffset(); @@ -829,6 +830,7 @@ public class ShadowMotionEvent extends ShadowInputEvent { @Implementation(minSdk = LOLLIPOP) @HiddenApi + @InDevelopment protected static float nativeGetYOffset(long nativePtr) { NativeInput.MotionEvent event = getNativeMotionEvent(nativePtr); return event.getYOffset(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java index f064905d8..8a3eaee0f 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java @@ -23,6 +23,7 @@ import android.graphics.Shader; import android.graphics.Typeface; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.InDevelopment; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.TextLayoutMode; import org.robolectric.config.ConfigurationRegistry; @@ -574,6 +575,7 @@ public class ShadowPaint { } @Implementation(minSdk = V.SDK_INT) + @InDevelopment protected static float nGetRunCharacterAdvance( Object /* long */ paintPtr, Object /* char[] */ text, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java index dc206ae80..ec5a63cc4 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java @@ -21,6 +21,7 @@ import java.util.Queue; import java.util.concurrent.Executor; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.InDevelopment; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; import org.robolectric.util.ReflectionHelpers; @@ -114,6 +115,7 @@ public class ShadowSpeechRecognizer { * sets the latest SpeechRecognizer. */ @Implementation(maxSdk = U.SDK_INT) // TODO(hoisie): Update this to support Android V + @InDevelopment protected void handleChangeListener(RecognitionListener listener) { recognitionListener = listener; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceView.java index 7a258dad8..2a2dec73b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceView.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceView.java @@ -1,6 +1,7 @@ package org.robolectric.shadows; import android.graphics.Canvas; +import android.graphics.PixelFormat; import android.graphics.Rect; import android.view.Surface; import android.view.SurfaceHolder; @@ -32,6 +33,8 @@ public class ShadowSurfaceView extends ShadowView { */ public static class FakeSurfaceHolder implements SurfaceHolder { private final Set<Callback> callbacks = new HashSet<>(); + // The default format is RGB_565. + private int requestedFormat = PixelFormat.RGB_565; @Override public void addCallback(Callback callback) { @@ -65,7 +68,12 @@ public class ShadowSurfaceView extends ShadowView { } @Override - public void setFormat(int i) { + public void setFormat(int format) { + if (format == PixelFormat.OPAQUE) { + requestedFormat = PixelFormat.RGB_565; + } else { + requestedFormat = format; + } } @Override @@ -95,5 +103,15 @@ public class ShadowSurfaceView extends ShadowView { public Surface getSurface() { return null; } + + /** + * Retrieve the requested format by the developers or by Android Frameworks internal logic. + * + * @return The requested format, and the default value is {@link PixelFormat#RGB_565}. + * @see PixelFormat + */ + public int getRequestedFormat() { + return requestedFormat; + } } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java index 871e9f3a8..4c8c369fa 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java @@ -153,6 +153,7 @@ public class ShadowTelephonyManager { private static final Map<Integer, String> simCountryIsoMap = Collections.synchronizedMap(new LinkedHashMap<>()); private int simCarrierId; + private CharSequence simCarrierIdName; private int carrierIdFromSimMccMnc; private String subscriberId; private static volatile /*UiccSlotInfo[]*/ Object uiccSlotInfos; @@ -1225,6 +1226,16 @@ public class ShadowTelephonyManager { this.simCarrierId = simCarrierId; } + @Implementation(minSdk = P) + protected CharSequence getSimCarrierIdName() { + return simCarrierIdName; + } + + /** Sets the value to be returned by {@link #getSimCarrierIdName()}. */ + public void setSimCarrierIdName(CharSequence simCarrierIdName) { + this.simCarrierIdName = simCarrierIdName; + } + @Implementation protected String getSubscriberId() { checkReadPhoneStatePermission(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTrace.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTrace.java index 63a7d7c8c..5d8b52be7 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTrace.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTrace.java @@ -18,6 +18,7 @@ import java.util.Set; import java.util.function.Supplier; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.InDevelopment; import org.robolectric.annotation.Resetter; /** @@ -112,6 +113,7 @@ public class ShadowTrace { } @Implementation + @InDevelopment protected static long nativeGetEnabledTags() { return tags; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewConfiguration.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewConfiguration.java index d2c28b87a..33f800f7d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewConfiguration.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewConfiguration.java @@ -37,25 +37,16 @@ import org.robolectric.util.reflector.ForType; public class ShadowViewConfiguration { private static final int SCROLL_BAR_SIZE = 10; - private static final int SCROLL_BAR_FADE_DURATION = 250; - private static final int SCROLL_BAR_DEFAULT_DELAY = 300; - private static final int FADING_EDGE_LENGTH = 12; private static final int PRESSED_STATE_DURATION = 125; private static final int LONG_PRESS_TIMEOUT = 500; - private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500; private static final int TAP_TIMEOUT = 115; - private static final int JUMP_TAP_TIMEOUT = 500; private static final int DOUBLE_TAP_TIMEOUT = 300; - private static final int ZOOM_CONTROLS_TIMEOUT = 3000; - private static final int EDGE_SLOP = 12; private static final int TOUCH_SLOP = 16; private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2; private static final int DOUBLE_TAP_SLOP = 100; private static final int WINDOW_TOUCH_SLOP = 16; - private static final int MINIMUM_FLING_VELOCITY = 50; private static final int MAXIMUM_FLING_VELOCITY = 4000; private static final int MAXIMUM_DRAWING_CACHE_SIZE = 320 * 480 * 4; - private static final float SCROLL_FRICTION = 0.015f; private int edgeSlop; private int fadingEdgeLength; @@ -72,10 +63,10 @@ public class ShadowViewConfiguration { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); float density = metrics.density; - edgeSlop = (int) (density * EDGE_SLOP + 0.5f); - fadingEdgeLength = (int) (density * FADING_EDGE_LENGTH + 0.5f); - minimumFlingVelocity = (int) (density * MINIMUM_FLING_VELOCITY + 0.5f); - maximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f); + edgeSlop = (int) (density * ViewConfiguration.getEdgeSlop() + 0.5f); + fadingEdgeLength = (int) (density * ViewConfiguration.getFadingEdgeLength() + 0.5f); + minimumFlingVelocity = (int) (density * ViewConfiguration.getMinimumFlingVelocity() + 0.5f); + maximumFlingVelocity = (int) (density * ViewConfiguration.getMaximumFlingVelocity() + 0.5f); scrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); touchSlop = (int) (density * TOUCH_SLOP + 0.5f); pagingTouchSlop = (int) (density * PAGING_TOUCH_SLOP + 0.5f); @@ -108,21 +99,6 @@ public class ShadowViewConfiguration { } @Implementation - protected static int getScrollBarFadeDuration() { - return SCROLL_BAR_FADE_DURATION; - } - - @Implementation - protected static int getScrollDefaultDelay() { - return SCROLL_BAR_DEFAULT_DELAY; - } - - @Implementation - protected static int getFadingEdgeLength() { - return FADING_EDGE_LENGTH; - } - - @Implementation protected int getScaledFadingEdgeLength() { return fadingEdgeLength; } @@ -143,21 +119,11 @@ public class ShadowViewConfiguration { } @Implementation - protected static int getJumpTapTimeout() { - return JUMP_TAP_TIMEOUT; - } - - @Implementation protected static int getDoubleTapTimeout() { return DOUBLE_TAP_TIMEOUT; } @Implementation - protected static int getEdgeSlop() { - return EDGE_SLOP; - } - - @Implementation protected int getScaledEdgeSlop() { return edgeSlop; } @@ -193,11 +159,6 @@ public class ShadowViewConfiguration { } @Implementation - protected static int getMinimumFlingVelocity() { - return MINIMUM_FLING_VELOCITY; - } - - @Implementation protected int getScaledMinimumFlingVelocity() { return minimumFlingVelocity; } @@ -218,21 +179,6 @@ public class ShadowViewConfiguration { } @Implementation - protected static long getZoomControlsTimeout() { - return ZOOM_CONTROLS_TIMEOUT; - } - - @Implementation - protected static long getGlobalActionKeyTimeout() { - return GLOBAL_ACTIONS_KEY_TIMEOUT; - } - - @Implementation - protected static float getScrollFriction() { - return SCROLL_FRICTION; - } - - @Implementation protected boolean hasPermanentMenuKey() { return hasPermanentMenuKey; } diff --git a/shadows/playservices/build.gradle b/shadows/playservices/build.gradle index b00983893..6b6759c33 100644 --- a/shadows/playservices/build.gradle +++ b/shadows/playservices/build.gradle @@ -16,18 +16,18 @@ dependencies { api project(":annotations") api libs.guava - compileOnly libs.bundles.play.services.base.for.shadows + compileOnly libs.bundles.play.services.for.shadows compileOnly AndroidSdk.MAX_SDK.coordinates testCompileOnly AndroidSdk.MAX_SDK.coordinates - testCompileOnly libs.bundles.play.services.base.for.shadows + testCompileOnly libs.bundles.play.services.for.shadows testImplementation project(":robolectric") testImplementation libs.junit4 testImplementation libs.truth testImplementation libs.mockito - testRuntimeOnly libs.bundles.play.services.base.for.shadows + testRuntimeOnly libs.bundles.play.services.for.shadows testRuntimeOnly AndroidSdk.MAX_SDK.coordinates } diff --git a/shadows/playservices/src/main/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtil.java b/shadows/playservices/src/main/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtil.java index deb738b27..25ed90e1a 100644 --- a/shadows/playservices/src/main/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtil.java +++ b/shadows/playservices/src/main/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtil.java @@ -93,11 +93,6 @@ public class ShadowGooglePlayServicesUtil { } @Implementation - public static synchronized String getOpenSourceSoftwareLicenseInfo(Context context) { - return googlePlayServicesUtilImpl.getOpenSourceSoftwareLicenseInfo(context); - } - - @Implementation public static synchronized int isGooglePlayServicesAvailable(Context context) { return googlePlayServicesUtilImpl.isGooglePlayServicesAvailable(context); } @@ -133,10 +128,6 @@ public class ShadowGooglePlayServicesUtil { context, requestCode, new Intent(), PendingIntent.FLAG_CANCEL_CURRENT); } - public String getOpenSourceSoftwareLicenseInfo(Context context) { - return "license"; - } - public Context getRemoteContext(Context context) { return RuntimeEnvironment.getApplication(); } diff --git a/shadows/playservices/src/main/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailability.java b/shadows/playservices/src/main/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailability.java index cb9bf194b..867184a39 100644 --- a/shadows/playservices/src/main/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailability.java +++ b/shadows/playservices/src/main/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailability.java @@ -45,10 +45,6 @@ public class ShadowGoogleApiAvailability { return openSourceSoftwareLicenseInfo; } - public void setOpenSourceSoftwareLicenseInfo(final String openSourceSoftwareLicenseInfo){ - this.openSourceSoftwareLicenseInfo = openSourceSoftwareLicenseInfo; - } - @Implementation public Dialog getErrorDialog(Activity activity, int errorCode, int requestCode) { return errorDialog; diff --git a/shadows/playservices/src/test/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtilTest.java b/shadows/playservices/src/test/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtilTest.java index cd75b9632..781072d51 100644 --- a/shadows/playservices/src/test/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtilTest.java +++ b/shadows/playservices/src/test/java/org/robolectric/shadows/gms/ShadowGooglePlayServicesUtilTest.java @@ -107,13 +107,6 @@ public class ShadowGooglePlayServicesUtilTest { } @Test - public void getOpenSourceSoftwareLicenseInfo_defaultNotNull() { - assertNotNull( - GooglePlayServicesUtil.getOpenSourceSoftwareLicenseInfo( - RuntimeEnvironment.getApplication())); - } - - @Test public void isGooglePlayServicesAvailable_defaultServiceMissing() { assertEquals( ConnectionResult.SERVICE_MISSING, diff --git a/shadows/playservices/src/test/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailabilityTest.java b/shadows/playservices/src/test/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailabilityTest.java index 7e12ec712..796b8d933 100644 --- a/shadows/playservices/src/test/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailabilityTest.java +++ b/shadows/playservices/src/test/java/org/robolectric/shadows/gms/common/ShadowGoogleApiAvailabilityTest.java @@ -89,23 +89,6 @@ public class ShadowGoogleApiAvailabilityTest { } @Test - public void setOpenSourceSoftwareLicenseInfo() { - //Given mock open source license info - final String expected = "Mock open source license info"; - final ShadowGoogleApiAvailability shadowGoogleApiAvailability - = Shadows.shadowOf(GoogleApiAvailability.getInstance()); - shadowGoogleApiAvailability.setOpenSourceSoftwareLicenseInfo(expected); - - //When getting the actual value - final String actual = GoogleApiAvailability.getInstance() - .getOpenSourceSoftwareLicenseInfo(roboContext); - - //Then verify that its not null, not empty, and equal to the expected value - assertThat(actual) - .isEqualTo(expected); - } - - @Test public void setErrorDialog(){ final ShadowGoogleApiAvailability shadowGoogleApiAvailability = Shadows.shadowOf(GoogleApiAvailability.getInstance()); diff --git a/testapp/src/main/AndroidManifest.xml b/testapp/src/main/AndroidManifest.xml index d594f2323..9ff190737 100644 --- a/testapp/src/main/AndroidManifest.xml +++ b/testapp/src/main/AndroidManifest.xml @@ -1,8 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.robolectric.testapp" - android:versionCode="123" - android:versionName="aVersionName"> + package="org.robolectric.testapp"> <application android:theme="@style/Theme.Robolectric" android:enabled="true"> diff --git a/testapp/src/main/res/font/downloadable.xml b/testapp/src/main/res/font/downloadable.xml index 9caa5388d..3cda34a90 100644 --- a/testapp/src/main/res/font/downloadable.xml +++ b/testapp/src/main/res/font/downloadable.xml @@ -1,5 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <font-family xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="FontValidation" android:fontProviderAuthority="com.example.fontprovider.authority" android:fontProviderPackage="com.example.fontprovider" android:fontProviderQuery="example font"/> diff --git a/utils/src/main/java/org/robolectric/util/TempDirectory.java b/utils/src/main/java/org/robolectric/util/TempDirectory.java index b45438a05..cffcbcb59 100644 --- a/utils/src/main/java/org/robolectric/util/TempDirectory.java +++ b/utils/src/main/java/org/robolectric/util/TempDirectory.java @@ -13,10 +13,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -35,15 +33,6 @@ public class TempDirectory { */ private static final int DELETE_THREAD_POOL_SIZE = 5; - /** - * Assets related to the Robolectric Native Runtime (shared object files, ICU dat file, fonts) are - * extracted from a Jar file into a TempDirectory during runtime in order to be used. On Windows, - * it is not possible for the current JVM process to delete certain files due to OS constraints. - */ - @SuppressWarnings("ConstantCaseForConstants") - private static final List<String> KNOWN_WINDOWS_UNDELETEABLE_FILES = - Collections.unmodifiableList(Arrays.asList("robolectric-nativeruntime.dll", "icudt68l.dat")); - private static final String TEMP_DIR_PREFIX = "robolectric-"; static final String OBSOLETE_MARKER_FILE_NAME = ".obsolete"; @@ -153,15 +142,15 @@ public class TempDirectory { Files.delete(basePath); } catch (IOException e) { if (isWindows()) { + // Windows is much more protective of files that have been opened in native code. For + // instance, unlike in Mac and Linux, it's not possible to delete nativeruntime files + // (dlls, fonts, icu data) in the same process where they were opened. Because of + // this, we need extra cleanup logic for Windows, and we avoid logging to prevent noise + // and confusion. createFile(OBSOLETE_MARKER_FILE_NAME, ""); - // Avoid spurious logging. - for (String s : KNOWN_WINDOWS_UNDELETEABLE_FILES) { - if (e.getLocalizedMessage().contains(s)) { - return; - } - } + } else { + Logger.error("Failed to destroy temp directory", e); } - Logger.error("Failed to destroy temp directory", e); } } diff --git a/utils/src/test/java/org/robolectric/util/TempDirectoryTest.kt b/utils/src/test/java/org/robolectric/util/TempDirectoryTest.kt index 5692400c1..f9b172318 100644 --- a/utils/src/test/java/org/robolectric/util/TempDirectoryTest.kt +++ b/utils/src/test/java/org/robolectric/util/TempDirectoryTest.kt @@ -69,7 +69,6 @@ class TempDirectoryTest { if (!Files.exists(path)) { latch.countDown() } else { - System.err.println("DSP rescheduling") val ignored = service.schedule( { waitForDirectoryDeletion(path, latch, service) }, |