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) {
"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/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/CommandLineFlags.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/AnnotationProcessingUtils.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/Feature.java",
......@@ -2822,6 +2823,7 @@ if (is_android) {
"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/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/MinAndroidSdkLevelSkipCheckTest.java",
"test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java",
......@@ -2836,6 +2838,7 @@ if (is_android) {
":base_java_process_launcher_test_support",
":base_java_test_support",
"//third_party/android_support_test_runner:runner_java",
"//third_party/hamcrest:hamcrest_java",
]
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 {
@Override
public boolean shouldSkip(FrameworkMethod method) {
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 (!v.message().isEmpty()) {
Log.i(TAG, "%s is disabled: %s", method.getName(), v.message());
......@@ -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()) {
if (deviceTypeApplies(deviceType)) {
Log.i(TAG, "Test " + method.getDeclaringClass().getName() + "#"
......
......@@ -27,7 +27,8 @@ public class MinAndroidSdkLevelSkipCheck extends SkipCheck {
@Override
public boolean shouldSkip(FrameworkMethod frameworkMethod) {
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());
}
if (Build.VERSION.SDK_INT < minSdkLevel) {
......
......@@ -35,7 +35,8 @@ public class RestrictionSkipCheck extends SkipCheck {
public boolean shouldSkip(FrameworkMethod frameworkMethod) {
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()) {
if (restrictionApplies(restrictionVal)) {
Log.i(TAG, "Test " + frameworkMethod.getDeclaringClass().getName() + "#"
......
......@@ -10,11 +10,7 @@ import org.junit.runners.model.FrameworkMethod;
import org.chromium.base.Log;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Check whether a test case should be skipped.
......@@ -49,23 +45,5 @@ public abstract class SkipCheck {
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.model.FrameworkMethod;
import org.robolectric.annotation.Config;
import org.chromium.testing.local.LocalRobolectricTestRunner;
......@@ -30,7 +29,7 @@ public class SkipCheckTest {
private static class TestableSkipCheck extends SkipCheck {
public static <T extends Annotation> List<T> getAnnotationsForTesting(
AnnotatedElement element, Class<T> annotationClass) {
return getAnnotations(element, annotationClass);
return AnnotationProcessingUtils.getAnnotations(element, annotationClass);
}
@Override
......
......@@ -11,13 +11,12 @@ import android.os.StrictMode;
import android.text.TextUtils;
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.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.util.FeatureUtilities;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
......@@ -29,7 +28,6 @@ import java.lang.annotation.Target;
* @see ChromeHome.Processor
* @see FeatureUtilities#isChromeHomeEnabled()
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface ChromeHome {
......@@ -43,7 +41,7 @@ public @interface ChromeHome {
* used by explicitly calling methods ({@link #setPrefs(boolean)} and {@link #clearTestState()})
* or by using the {@link ChromeHome} annotation on tests.
*/
class Processor extends AnnotationProcessor<ChromeHome> {
class Processor extends AnnotationRule {
private Boolean mOldState;
public Processor() {
......@@ -52,7 +50,7 @@ public @interface ChromeHome {
@Override
protected void before() throws Throwable {
boolean enabled = getRequestedState();
boolean enabled = getAnnotation(ChromeHome.class).value();
if (enabled) {
Features.getInstance().enable(ChromeFeatureList.CHROME_HOME);
} else {
......@@ -108,12 +106,5 @@ public @interface ChromeHome {
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;
import android.support.annotation.Nullable;
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.test.util.AnnotationRule;
import org.chromium.chrome.browser.ChromeFeatureList;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
......@@ -151,23 +149,14 @@ public class Features {
* to enable, or get rid of exceptions when the production code tries to check for enabled
* features.
*/
private static abstract class Processor extends ExternalResource {
private Description mDescription;
@Override
public Statement apply(Statement base, Description description) {
mDescription = description;
return super.apply(base, description);
private static abstract class Processor extends AnnotationRule {
public Processor() {
super(EnableFeatures.class, DisableFeatures.class);
}
@Override
protected void before() throws Throwable {
collectDisabledFeatures(
mDescription.getTestClass().getAnnotation(DisableFeatures.class));
collectEnabledFeatures(mDescription.getTestClass().getAnnotation(EnableFeatures.class));
collectDisabledFeatures(mDescription.getAnnotation(DisableFeatures.class));
collectEnabledFeatures(mDescription.getAnnotation(EnableFeatures.class));
collectFeatures();
applyFeatures();
}
......@@ -178,12 +167,14 @@ public class Features {
abstract protected void applyFeatures();
private void collectEnabledFeatures(@Nullable EnableFeatures annotation) {
if (annotation != null) getInstance().enable(annotation.value());
}
private void collectDisabledFeatures(@Nullable DisableFeatures annotation) {
if (annotation != null) getInstance().disable(annotation.value());
private void collectFeatures() {
for (Annotation annotation : getAnnotations()) {
if (annotation instanceof EnableFeatures) {
getInstance().enable(((EnableFeatures) annotation).value());
} else if (annotation instanceof DisableFeatures) {
getInstance().disable(((DisableFeatures) 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