Merge "Add rule for toggling changes at test time"
diff --git a/JavaLibrary.bp b/JavaLibrary.bp
index db04186..afba8e9 100644
--- a/JavaLibrary.bp
+++ b/JavaLibrary.bp
@@ -486,6 +486,25 @@
system_modules: "core-all-system-modules",
}
+// Builds platform_compat test rules
+java_library_static {
+ name: "platform_compat-test-rules",
+ visibility: ["//visibility:public"],
+ srcs: [
+ "luni/src/main/java/android/compat/**/*.java",
+ "test-rules/src/platform_compat/**/*.java",
+ "luni/src/main/java/libcore/api/CorePlatformApi.java",
+ "luni/src/main/java/libcore/api/IntraCoreApi.java",
+
+ ],
+ static_libs: [
+ "junit",
+ "guava",
+ "android-support-test",
+ ],
+ platform_apis: true,
+}
+
// Builds the core-tests-support library used by various tests.
java_library_static {
name: "core-tests-support",
diff --git a/luni/src/main/java/android/compat/Compatibility.java b/luni/src/main/java/android/compat/Compatibility.java
index baf46bf..a0cc0a0 100644
--- a/luni/src/main/java/android/compat/Compatibility.java
+++ b/luni/src/main/java/android/compat/Compatibility.java
@@ -21,6 +21,11 @@
import libcore.api.CorePlatformApi;
import libcore.api.IntraCoreApi;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
/**
* Internal APIs for logging and gating compatibility changes.
*
@@ -78,7 +83,28 @@
@CorePlatformApi
public static void setCallbacks(Callbacks callbacks) {
- sCallbacks = callbacks;
+ sCallbacks = Objects.requireNonNull(callbacks);
+ }
+
+ @CorePlatformApi
+ public static void setOverrides(ChangeConfig overrides) {
+ // Setting overrides twice in a row does not need to be supported because
+ // this method is only for enabling/disabling changes for the duration of
+ // a single test.
+ // In production, the app is restarted when changes get enabled or disabled,
+ // and the ChangeConfig is then set exactly once on that app process.
+ if (sCallbacks instanceof OverrideCallbacks) {
+ throw new IllegalStateException("setOverrides has already been called!");
+ }
+ sCallbacks = new OverrideCallbacks(sCallbacks, overrides);
+ }
+
+ @CorePlatformApi
+ public static void clearOverrides() {
+ if (!(sCallbacks instanceof OverrideCallbacks)) {
+ throw new IllegalStateException("No overrides set");
+ }
+ sCallbacks = ((OverrideCallbacks) sCallbacks).delegate;
}
/**
@@ -95,15 +121,111 @@
}
@CorePlatformApi
protected void reportChange(long changeId) {
- System.logW(String.format(
+ throw new IllegalStateException(String.format(
"No Compatibility callbacks set! Reporting change %d", changeId));
}
@CorePlatformApi
protected boolean isChangeEnabled(long changeId) {
- System.logW(String.format(
+ throw new IllegalStateException(String.format(
"No Compatibility callbacks set! Querying change %d", changeId));
- return true;
}
}
+ @CorePlatformApi
+ @IntraCoreApi
+ public static final class ChangeConfig {
+ private final Set<Long> enabled;
+ private final Set<Long> disabled;
+
+ public ChangeConfig(Set<Long> enabled, Set<Long> disabled) {
+ this.enabled = Objects.requireNonNull(enabled);
+ this.disabled = Objects.requireNonNull(disabled);
+ if (enabled.contains(null)) {
+ throw new NullPointerException();
+ }
+ if (disabled.contains(null)) {
+ throw new NullPointerException();
+ }
+ Set<Long> intersection = new HashSet<>(enabled);
+ intersection.retainAll(disabled);
+ if (!intersection.isEmpty()) {
+ throw new IllegalArgumentException("Cannot have changes " + intersection
+ + " enabled and disabled!");
+ }
+ }
+
+ private static long[] toLongArray(Set<Long> values) {
+ long[] result = new long[values.size()];
+ int idx = 0;
+ for (Long value: values) {
+ result[idx++] = value;
+ }
+ return result;
+ }
+
+ public long[] forceEnabledChangesArray() {
+ return toLongArray(enabled);
+ }
+
+ public long[] forceDisabledChangesArray() {
+ return toLongArray(disabled);
+ }
+
+ public Set<Long> forceEnabledSet() {
+ return Collections.unmodifiableSet(enabled);
+ }
+
+ public Set<Long> forceDisabledSet() {
+ return Collections.unmodifiableSet(disabled);
+ }
+
+ public boolean isForceEnabled(long changeId) {
+ return enabled.contains(changeId);
+ }
+
+ public boolean isForceDisabled(long changeId) {
+ return disabled.contains(changeId);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ChangeConfig)) {
+ return false;
+ }
+ ChangeConfig that = (ChangeConfig) o;
+ return enabled.equals(that.enabled) &&
+ disabled.equals(that.disabled);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(enabled, disabled);
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeConfig{enabled=" + enabled + ", disabled=" + disabled + '}';
+ }
+ }
+
+ private static class OverrideCallbacks extends Callbacks {
+ private final Callbacks delegate;
+ private final ChangeConfig changeConfig;
+
+ private OverrideCallbacks(Callbacks delegate, ChangeConfig changeConfig) {
+ this.delegate = Objects.requireNonNull(delegate);
+ this.changeConfig = Objects.requireNonNull(changeConfig);
+ }
+ @Override
+ protected boolean isChangeEnabled(long changeId) {
+ if (changeConfig.isForceEnabled(changeId)) {
+ return true;
+ }
+ if (changeConfig.isForceDisabled(changeId)) {
+ return false;
+ }
+ return delegate.isChangeEnabled(changeId);
+ }
+ }
}
diff --git a/mmodules/core_platform_api/api/platform/current-api.txt b/mmodules/core_platform_api/api/platform/current-api.txt
index 0041b36..14d96eb 100644
--- a/mmodules/core_platform_api/api/platform/current-api.txt
+++ b/mmodules/core_platform_api/api/platform/current-api.txt
@@ -2,9 +2,11 @@
package android.compat {
public final class Compatibility {
+ method public static void clearOverrides();
method public static boolean isChangeEnabled(@android.compat.annotation.ChangeId long);
method public static void reportChange(@android.compat.annotation.ChangeId long);
method public static void setCallbacks(android.compat.Compatibility.Callbacks);
+ method public static void setOverrides(android.compat.Compatibility.ChangeConfig);
}
public static class Compatibility.Callbacks {
@@ -13,6 +15,16 @@
method protected void reportChange(long);
}
+ public static final class Compatibility.ChangeConfig {
+ ctor public Compatibility.ChangeConfig(java.util.Set<java.lang.Long>, java.util.Set<java.lang.Long>);
+ method public long[] forceDisabledChangesArray();
+ method public java.util.Set<java.lang.Long> forceDisabledSet();
+ method public long[] forceEnabledChangesArray();
+ method public java.util.Set<java.lang.Long> forceEnabledSet();
+ method public boolean isForceDisabled(long);
+ method public boolean isForceEnabled(long);
+ }
+
}
package android.compat.annotation {
diff --git a/mmodules/intracoreapi/api/intra/current-api.txt b/mmodules/intracoreapi/api/intra/current-api.txt
index 5e914d2..02cedb4 100644
--- a/mmodules/intracoreapi/api/intra/current-api.txt
+++ b/mmodules/intracoreapi/api/intra/current-api.txt
@@ -6,6 +6,16 @@
method @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public static void reportChange(@android.compat.annotation.ChangeId long);
}
+ @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public static final class Compatibility.ChangeConfig {
+ ctor public Compatibility.ChangeConfig(java.util.Set<java.lang.Long>, java.util.Set<java.lang.Long>);
+ method public long[] forceDisabledChangesArray();
+ method public java.util.Set<java.lang.Long> forceDisabledSet();
+ method public long[] forceEnabledChangesArray();
+ method public java.util.Set<java.lang.Long> forceEnabledSet();
+ method public boolean isForceDisabled(long);
+ method public boolean isForceEnabled(long);
+ }
+
}
package android.compat.annotation {
diff --git a/test-rules/src/platform_compat/java/android/compat/CompatChangeRule.java b/test-rules/src/platform_compat/java/android/compat/CompatChangeRule.java
new file mode 100644
index 0000000..2586f9c
--- /dev/null
+++ b/test-rules/src/platform_compat/java/android/compat/CompatChangeRule.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.compat;
+
+import android.app.Instrumentation;
+import android.compat.Compatibility.Callbacks;
+import android.compat.Compatibility.ChangeConfig;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.support.test.InstrumentationRegistry;
+import android.util.ArraySet;
+
+import com.android.internal.compat.CompatibilityChangeConfig;
+import com.android.internal.compat.IPlatformCompat;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import com.google.common.primitives.Longs;
+
+
+/**
+ * Allows tests to specify the which change to disable.
+ *
+ * <p>To use add the following to the test class. It will only change the behavior of a test method
+ * if it is annotated with {@link EnableCompatChanges} and/or {@link DisableCompatChanges}.
+ *
+ * <pre>
+ * @Rule
+ * public TestRule compatChangeRule = new CompatChangeRule();
+ * </pre>
+ *
+ * <p>Each test method that needs to disable a specific change needs to be annotated
+ * with {@link EnableCompatChanges} and/or {@link DisableCompatChanges} specifying the change id.
+ * e.g.:
+ *
+ * <pre>
+ * @Test
+ * @DisableCompatChanges({42})
+ * public void testAsIfChange42Disabled() {
+ * // check behavior
+ * }
+ *
+ * @Test
+ * @EnableCompatChanges({42})
+ * public void testAsIfChange42Enabled() {
+ * // check behavior
+ *
+ * </pre>
+ */
+public class CompatChangeRule implements TestRule {
+ @Override
+ public Statement apply(final Statement statement, Description description) {
+ Set<Long> enabled = new HashSet<>();
+ Set<Long> disabled = new HashSet<>();
+ EnableCompatChanges enableCompatChanges = description.getAnnotation(
+ EnableCompatChanges.class);
+ DisableCompatChanges disableCompatChanges = description.getAnnotation(
+ DisableCompatChanges.class);
+ if (enableCompatChanges != null) {
+ enabled.addAll(Longs.asList(enableCompatChanges.value()));
+ }
+ if (disableCompatChanges != null) {
+ disabled.addAll(Longs.asList(disableCompatChanges.value()));
+ }
+ ArraySet<Long> intersection = new ArraySet<>(enabled);
+ intersection.retainAll(disabled);
+ if (!intersection.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Changes " + intersection + " are both enabled and disabled.");
+ }
+ if (enabled.isEmpty() && disabled.isEmpty()) {
+ throw new IllegalStateException("Added a CompatChangeRule without specifying any "
+ + "@EnableCompatChanges or @DisableCompatChanges !");
+ }
+ return new CompatChangeStatement(statement, new ChangeConfig(enabled, disabled));
+ }
+
+ private static class CompatChangeStatement extends Statement {
+ private final Statement testStatement;
+ private final ChangeConfig config;
+
+ private CompatChangeStatement(Statement testStatement, ChangeConfig config) {
+ this.testStatement = testStatement;
+ this.config = config;
+ }
+
+ @Override
+ public void evaluate() throws Throwable {
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ String packageName = instrumentation.getTargetContext().getPackageName();
+ IPlatformCompat platformCompat = IPlatformCompat.Stub
+ .asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+ if (platformCompat == null) {
+ throw new IllegalStateException("Could not get IPlatformCompat service!");
+ }
+ Compatibility.setOverrides(config);
+ try {
+ platformCompat.setOverrides(new CompatibilityChangeConfig(config), packageName);
+ try {
+ testStatement.evaluate();
+ } finally {
+ platformCompat.clearOverrides(packageName);
+ }
+ } catch(RemoteException e) {
+ throw new RuntimeException("Could not call IPlatformCompat binder method!", e);
+ } finally {
+ Compatibility.clearOverrides();
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.METHOD)
+ public @interface EnableCompatChanges {
+ long[] value();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.METHOD)
+ public @interface DisableCompatChanges {
+ long[] value();
+ }
+}