Commit 67a6d728 authored by Nicolas Dossou-gbete's avatar Nicolas Dossou-gbete Committed by Commit Bot

🔬 Add utility for processing annotations in tests

Introduces AnnotationProcessingUtils, which provides:
- AnnotationExtractor, a class allowing to retrieve one or many
annotations in reverse declaration order from a class or a test
description
- AnnotationProcessor, a rule to simplify the use of the above
extractor in the context of tests (here used as parent for @Features
rule).
- Utility methods to simplify the use of the above extractor in other
contexts (here used in SkipChecks)

Bug: 783160,754778
Change-Id: I8ca0793e1229c6268266c75791a44615b8f93f57
Reviewed-on: https://chromium-review.googlesource.com/789192
Commit-Queue: Nicolas Dossou-Gbété <dgn@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#520789}
parent d756c2df
...@@ -2759,11 +2759,12 @@ if (is_android) { ...@@ -2759,11 +2759,12 @@ if (is_android) {
"test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java", "test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java",
"test/android/javatests/src/org/chromium/base/test/params/ParameterProvider.java", "test/android/javatests/src/org/chromium/base/test/params/ParameterProvider.java",
"test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java", "test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java",
"test/android/javatests/src/org/chromium/base/test/util/AnnotationProcessor.java", "test/android/javatests/src/org/chromium/base/test/util/AnnotationRule.java",
"test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java", "test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java",
"test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java", "test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java",
"test/android/javatests/src/org/chromium/base/test/util/DisableIf.java", "test/android/javatests/src/org/chromium/base/test/util/DisableIf.java",
"test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java", "test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java",
"test/android/javatests/src/org/chromium/base/test/util/AnnotationProcessingUtils.java",
"test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java", "test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java",
"test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java", "test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java",
"test/android/javatests/src/org/chromium/base/test/util/Feature.java", "test/android/javatests/src/org/chromium/base/test/util/Feature.java",
...@@ -2822,6 +2823,7 @@ if (is_android) { ...@@ -2822,6 +2823,7 @@ if (is_android) {
"android/junit/src/org/chromium/base/process_launcher/ChildProcessConnectionTest.java", "android/junit/src/org/chromium/base/process_launcher/ChildProcessConnectionTest.java",
"test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java", "test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java",
"test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java", "test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java",
"test/android/junit/src/org/chromium/base/test/util/AnnotationProcessingUtilsTest.java",
"test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java", "test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java",
"test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java", "test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java",
"test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java", "test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java",
...@@ -2836,6 +2838,7 @@ if (is_android) { ...@@ -2836,6 +2838,7 @@ if (is_android) {
":base_java_process_launcher_test_support", ":base_java_process_launcher_test_support",
":base_java_test_support", ":base_java_test_support",
"//third_party/android_support_test_runner:runner_java", "//third_party/android_support_test_runner:runner_java",
"//third_party/hamcrest:hamcrest_java",
] ]
srcjar_deps = [ ":base_build_config_gen" ] srcjar_deps = [ ":base_build_config_gen" ]
} }
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test.util;
import android.support.annotation.Nullable;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.lang.annotation.Annotation;
/**
* Test rule that is activated when a test or its class has a specific annotation.
* It allows to run some code before the test (and the {@link org.junit.Before}) runs,
* and guarantees to also run code after.
*
* Usage:
*
* <pre>
* public class Test {
* &#64;Rule
* public AnnotationProcessor<Foo> rule = new AnnotationProcessor(Foo.class) {
* &#64;Override
* protected void before() { ... }
*
* &#64;Override
* protected void after() { ... }
* };
*
* &#64;Test
* &#64;Foo
* public void myTest() { ... }
* }
* </pre>
*
* @param <T> type of the annotation to match on the test case.
*/
public abstract class AnnotationProcessor<T extends Annotation> extends ExternalResource {
private final Class<T> mAnnotationClass;
private Description mTestDescription;
@Nullable
private T mClassAnnotation;
@Nullable
private T mTestAnnotation;
public AnnotationProcessor(Class<T> annotationClass) {
mAnnotationClass = annotationClass;
}
@Override
public Statement apply(Statement base, Description description) {
mTestDescription = description;
mClassAnnotation = description.getTestClass().getAnnotation(mAnnotationClass);
mTestAnnotation = description.getAnnotation(mAnnotationClass);
if (mClassAnnotation == null && mTestAnnotation == null) return base;
// Return the wrapped statement to execute before() and after().
return super.apply(base, description);
}
/** @return {@link Description} of the current test. */
protected Description getTestDescription() {
return mTestDescription;
}
/** @return the annotation on the test class. */
@Nullable
protected T getClassAnnotation() {
return mClassAnnotation;
}
/** @return the annotation on the test method. */
@Nullable
protected T getTestAnnotation() {
return mTestAnnotation;
}
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test.util;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
/**
* Test rule that is activated when a test or its class has a specific annotation.
* It allows to run some code before the test (and the {@link org.junit.Before}) runs,
* and guarantees to also run code after.
*
* Usage:
*
* <pre>
* public class Test {
* &#64;Rule
* public AnnotationRule rule = new AnnotationRule(Foo.class) {
* &#64;Override
* protected void before() { ... }
*
* &#64;Override
* protected void after() { ... }
* };
*
* &#64;Test
* &#64;Foo
* public void myTest() { ... }
* }
* </pre>
*
* It can also be used to trigger for multiple annotations:
*
* <pre>
* &#64;DisableFoo
* public class Test {
* &#64;Rule
* public AnnotationRule rule = new AnnotationRule(EnableFoo.class, DisableFoo.class) {
* &#64;Override
* protected void before() {
* // Loops through all the picked up annotations. For myTest(), it would process
* // DisableFoo first, then EnableFoo.
* for (Annotation annotation : getAnnotations()) {
* if (annotation instanceof EnableFoo) { ... }
* else if (annotation instanceof DisableFoo) { ... }
* }
* }
*
* &#64;Override
* protected void after() {
* // For myTest(), would return EnableFoo as it's directly set on the method.
* Annotation a = getClosestAnnotation();
* ...
* }
* };
*
* &#64;Test
* &#64;EnableFoo
* public void myTest() { ... }
* }
* </pre>
*
* @see AnnotationProcessingUtils.AnnotationExtractor
*/
public abstract class AnnotationRule extends ExternalResource {
private final AnnotationProcessingUtils.AnnotationExtractor mAnnotationExtractor;
private List<Annotation> mCollectedAnnotations;
private Description mTestDescription;
@SafeVarargs
public AnnotationRule(Class<? extends Annotation> firstAnnotationType,
Class<? extends Annotation>... additionalTypes) {
List<Class<? extends Annotation>> mAnnotationTypes = new ArrayList<>();
mAnnotationTypes.add(firstAnnotationType);
mAnnotationTypes.addAll(Arrays.asList(additionalTypes));
mAnnotationExtractor = new AnnotationProcessingUtils.AnnotationExtractor(mAnnotationTypes);
}
@Override
public Statement apply(Statement base, Description description) {
mTestDescription = description;
mCollectedAnnotations = mAnnotationExtractor.getMatchingAnnotations(description);
if (mCollectedAnnotations.isEmpty()) return base;
// Return the wrapped statement to execute before() and after().
return super.apply(base, description);
}
/** @return {@link Description} of the current test. */
protected Description getTestDescription() {
return mTestDescription;
}
/**
* @return The collected annotations that match the declared type(s).
* @throws NullPointerException if this is called before annotations have been collected,
* which happens when the rule is applied to the {@link Statement}.
*/
protected List<Annotation> getAnnotations() {
return Collections.unmodifiableList(mCollectedAnnotations);
}
/**
* @return The closest annotation matching the provided type, or {@code null} if there is none.
*/
@SuppressWarnings("unchecked")
protected <A extends Annotation> A getAnnotation(Class<A> annnotationType) {
ListIterator<Annotation> iteratorFromEnd =
mCollectedAnnotations.listIterator(mCollectedAnnotations.size());
while (iteratorFromEnd.hasPrevious()) {
Annotation annotation = iteratorFromEnd.previous();
if (annnotationType.isAssignableFrom(annotation.annotationType())) {
return (A) annotation;
}
}
return null;
}
protected Annotation getClosestAnnotation() {
assert !mCollectedAnnotations.isEmpty() : "The rule should not trigger without match.";
return mCollectedAnnotations.get(mCollectedAnnotations.size() - 1);
}
}
...@@ -24,7 +24,8 @@ public class DisableIfSkipCheck extends SkipCheck { ...@@ -24,7 +24,8 @@ public class DisableIfSkipCheck extends SkipCheck {
@Override @Override
public boolean shouldSkip(FrameworkMethod method) { public boolean shouldSkip(FrameworkMethod method) {
if (method == null) return true; if (method == null) return true;
for (DisableIf.Build v : getAnnotations(method, DisableIf.Build.class)) { for (DisableIf.Build v : AnnotationProcessingUtils.getAnnotations(
method.getMethod(), DisableIf.Build.class)) {
if (abi(v) && hardware(v) && product(v) && sdk(v)) { if (abi(v) && hardware(v) && product(v) && sdk(v)) {
if (!v.message().isEmpty()) { if (!v.message().isEmpty()) {
Log.i(TAG, "%s is disabled: %s", method.getName(), v.message()); Log.i(TAG, "%s is disabled: %s", method.getName(), v.message());
...@@ -33,7 +34,8 @@ public class DisableIfSkipCheck extends SkipCheck { ...@@ -33,7 +34,8 @@ public class DisableIfSkipCheck extends SkipCheck {
} }
} }
for (DisableIf.Device d : getAnnotations(method, DisableIf.Device.class)) { for (DisableIf.Device d : AnnotationProcessingUtils.getAnnotations(
method.getMethod(), DisableIf.Device.class)) {
for (String deviceType : d.type()) { for (String deviceType : d.type()) {
if (deviceTypeApplies(deviceType)) { if (deviceTypeApplies(deviceType)) {
Log.i(TAG, "Test " + method.getDeclaringClass().getName() + "#" Log.i(TAG, "Test " + method.getDeclaringClass().getName() + "#"
......
...@@ -27,7 +27,8 @@ public class MinAndroidSdkLevelSkipCheck extends SkipCheck { ...@@ -27,7 +27,8 @@ public class MinAndroidSdkLevelSkipCheck extends SkipCheck {
@Override @Override
public boolean shouldSkip(FrameworkMethod frameworkMethod) { public boolean shouldSkip(FrameworkMethod frameworkMethod) {
int minSdkLevel = 0; int minSdkLevel = 0;
for (MinAndroidSdkLevel m : getAnnotations(frameworkMethod, MinAndroidSdkLevel.class)) { for (MinAndroidSdkLevel m : AnnotationProcessingUtils.getAnnotations(
frameworkMethod.getMethod(), MinAndroidSdkLevel.class)) {
minSdkLevel = Math.max(minSdkLevel, m.value()); minSdkLevel = Math.max(minSdkLevel, m.value());
} }
if (Build.VERSION.SDK_INT < minSdkLevel) { if (Build.VERSION.SDK_INT < minSdkLevel) {
......
...@@ -35,7 +35,8 @@ public class RestrictionSkipCheck extends SkipCheck { ...@@ -35,7 +35,8 @@ public class RestrictionSkipCheck extends SkipCheck {
public boolean shouldSkip(FrameworkMethod frameworkMethod) { public boolean shouldSkip(FrameworkMethod frameworkMethod) {
if (frameworkMethod == null) return true; if (frameworkMethod == null) return true;
for (Restriction restriction : getAnnotations(frameworkMethod, Restriction.class)) { for (Restriction restriction : AnnotationProcessingUtils.getAnnotations(
frameworkMethod.getMethod(), Restriction.class)) {
for (String restrictionVal : restriction.value()) { for (String restrictionVal : restriction.value()) {
if (restrictionApplies(restrictionVal)) { if (restrictionApplies(restrictionVal)) {
Log.i(TAG, "Test " + frameworkMethod.getDeclaringClass().getName() + "#" Log.i(TAG, "Test " + frameworkMethod.getDeclaringClass().getName() + "#"
......
...@@ -10,11 +10,7 @@ import org.junit.runners.model.FrameworkMethod; ...@@ -10,11 +10,7 @@ import org.junit.runners.model.FrameworkMethod;
import org.chromium.base.Log; import org.chromium.base.Log;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/** /**
* Check whether a test case should be skipped. * Check whether a test case should be skipped.
...@@ -49,23 +45,5 @@ public abstract class SkipCheck { ...@@ -49,23 +45,5 @@ public abstract class SkipCheck {
return false; return false;
} }
} }
protected static <T extends Annotation> List<T> getAnnotations(FrameworkMethod frameworkMethod,
Class<T> annotationClass) {
return getAnnotations(frameworkMethod.getMethod(), annotationClass);
}
protected static <T extends Annotation> List<T> getAnnotations(AnnotatedElement element,
Class<T> annotationClass) {
AnnotatedElement parent = (element instanceof Method)
? ((Method) element).getDeclaringClass()
: ((Class) element).getSuperclass();
List<T> annotations = (parent == null)
? new ArrayList<T>()
: getAnnotations(parent, annotationClass);
T annotation = element.getAnnotation(annotationClass);
if (annotation != null) annotations.add(annotation);
return annotations;
}
} }
...@@ -10,7 +10,6 @@ import org.junit.Assert; ...@@ -10,7 +10,6 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.FrameworkMethod;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.chromium.testing.local.LocalRobolectricTestRunner; import org.chromium.testing.local.LocalRobolectricTestRunner;
...@@ -30,7 +29,7 @@ public class SkipCheckTest { ...@@ -30,7 +29,7 @@ public class SkipCheckTest {
private static class TestableSkipCheck extends SkipCheck { private static class TestableSkipCheck extends SkipCheck {
public static <T extends Annotation> List<T> getAnnotationsForTesting( public static <T extends Annotation> List<T> getAnnotationsForTesting(
AnnotatedElement element, Class<T> annotationClass) { AnnotatedElement element, Class<T> annotationClass) {
return getAnnotations(element, annotationClass); return AnnotationProcessingUtils.getAnnotations(element, annotationClass);
} }
@Override @Override
......
...@@ -11,13 +11,12 @@ import android.os.StrictMode; ...@@ -11,13 +11,12 @@ import android.os.StrictMode;
import android.text.TextUtils; import android.text.TextUtils;
import org.chromium.base.CommandLine; import org.chromium.base.CommandLine;
import org.chromium.base.test.util.AnnotationProcessor; import org.chromium.base.test.util.AnnotationRule;
import org.chromium.chrome.browser.ChromeFeatureList; import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager; import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.util.FeatureUtilities; import org.chromium.chrome.browser.util.FeatureUtilities;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
...@@ -29,7 +28,6 @@ import java.lang.annotation.Target; ...@@ -29,7 +28,6 @@ import java.lang.annotation.Target;
* @see ChromeHome.Processor * @see ChromeHome.Processor
* @see FeatureUtilities#isChromeHomeEnabled() * @see FeatureUtilities#isChromeHomeEnabled()
*/ */
@Inherited
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE}) @Target({ElementType.METHOD, ElementType.TYPE})
public @interface ChromeHome { public @interface ChromeHome {
...@@ -43,7 +41,7 @@ public @interface ChromeHome { ...@@ -43,7 +41,7 @@ public @interface ChromeHome {
* used by explicitly calling methods ({@link #setPrefs(boolean)} and {@link #clearTestState()}) * used by explicitly calling methods ({@link #setPrefs(boolean)} and {@link #clearTestState()})
* or by using the {@link ChromeHome} annotation on tests. * or by using the {@link ChromeHome} annotation on tests.
*/ */
class Processor extends AnnotationProcessor<ChromeHome> { class Processor extends AnnotationRule {
private Boolean mOldState; private Boolean mOldState;
public Processor() { public Processor() {
...@@ -52,7 +50,7 @@ public @interface ChromeHome { ...@@ -52,7 +50,7 @@ public @interface ChromeHome {
@Override @Override
protected void before() throws Throwable { protected void before() throws Throwable {
boolean enabled = getRequestedState(); boolean enabled = getAnnotation(ChromeHome.class).value();
if (enabled) { if (enabled) {
Features.getInstance().enable(ChromeFeatureList.CHROME_HOME); Features.getInstance().enable(ChromeFeatureList.CHROME_HOME);
} else { } else {
...@@ -108,12 +106,5 @@ public @interface ChromeHome { ...@@ -108,12 +106,5 @@ public @interface ChromeHome {
commandLine.appendSwitch(ENABLE_FLAGS); commandLine.appendSwitch(ENABLE_FLAGS);
} }
private boolean getRequestedState() {
if (getTestAnnotation() != null) return getTestAnnotation().value();
if (getClassAnnotation() != null) return getClassAnnotation().value();
throw new IllegalStateException("Rule called with no annotation");
}
} }
} }
...@@ -7,13 +7,11 @@ package org.chromium.chrome.test.util.browser; ...@@ -7,13 +7,11 @@ package org.chromium.chrome.test.util.browser;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.chromium.base.CommandLine; import org.chromium.base.CommandLine;
import org.chromium.base.test.util.AnnotationRule;
import org.chromium.chrome.browser.ChromeFeatureList; import org.chromium.chrome.browser.ChromeFeatureList;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.Collections; import java.util.Collections;
...@@ -151,23 +149,14 @@ public class Features { ...@@ -151,23 +149,14 @@ public class Features {
* to enable, or get rid of exceptions when the production code tries to check for enabled * to enable, or get rid of exceptions when the production code tries to check for enabled
* features. * features.
*/ */
private static abstract class Processor extends ExternalResource { private static abstract class Processor extends AnnotationRule {
private Description mDescription; public Processor() {
super(EnableFeatures.class, DisableFeatures.class);
@Override
public Statement apply(Statement base, Description description) {
mDescription = description;
return super.apply(base, description);
} }
@Override @Override
protected void before() throws Throwable { protected void before() throws Throwable {
collectDisabledFeatures( collectFeatures();
mDescription.getTestClass().getAnnotation(DisableFeatures.class));
collectEnabledFeatures(mDescription.getTestClass().getAnnotation(EnableFeatures.class));
collectDisabledFeatures(mDescription.getAnnotation(DisableFeatures.class));
collectEnabledFeatures(mDescription.getAnnotation(EnableFeatures.class));
applyFeatures(); applyFeatures();
} }
...@@ -178,12 +167,14 @@ public class Features { ...@@ -178,12 +167,14 @@ public class Features {
abstract protected void applyFeatures(); abstract protected void applyFeatures();
private void collectEnabledFeatures(@Nullable EnableFeatures annotation) { private void collectFeatures() {
if (annotation != null) getInstance().enable(annotation.value()); for (Annotation annotation : getAnnotations()) {
if (annotation instanceof EnableFeatures) {
getInstance().enable(((EnableFeatures) annotation).value());
} else if (annotation instanceof DisableFeatures) {
getInstance().disable(((DisableFeatures) annotation).value());
}
} }
private void collectDisabledFeatures(@Nullable DisableFeatures annotation) {
if (annotation != null) getInstance().disable(annotation.value());
} }
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment