TestSize.java

/*
 * Copyright (C) 2016 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 androidx.test.internal.runner;

import android.support.annotation.VisibleForTesting;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.junit.runner.Description;

/**
 * This class represents a test size qualifier which can be used to filter out tests from a test
 * suite.
 *
 * <p>It quietly handles runner size filter annotations and old platform size annotations.
 */
public final class TestSize {

  /** @see androidx.test.filters.SmallTest */
  public static final TestSize SMALL =
      new TestSize(
          "small",
          androidx.test.filters.SmallTest.class,
          "android.test.suitebuilder.annotation.SmallTest",
          200 /* in ms */);

  /** @see androidx.test.filters.MediumTest */
  public static final TestSize MEDIUM =
      new TestSize(
          "medium",
          androidx.test.filters.MediumTest.class,
          "android.test.suitebuilder.annotation.MediumTest",
          1000 /* in ms */);

  /** @see androidx.test.filters.LargeTest */
  public static final TestSize LARGE =
      new TestSize(
          "large",
          androidx.test.filters.LargeTest.class,
          "android.test.suitebuilder.annotation.LargeTest",
          Float.MAX_VALUE /* no threshold */);

  /**
   * A "null object" that is returned in case no test size matches, to avoid a null return value.
   */
  public static final TestSize NONE = new TestSize("", null, null, 0);

  private static final Set<TestSize> ALL_SIZES =
      Collections.unmodifiableSet(new HashSet<>(Arrays.asList(SMALL, MEDIUM, LARGE)));

  private final String sizeQualifierName;
  private final Class<? extends Annotation> platformAnnotationClass;
  private final Class<? extends Annotation> runnerFilterAnnotationClass;

  /**
   * This value the maximum allowed runtime (in ms) for a test included in the test size suite. It
   * is used to make an educated guess at which size bucket a test belongs to.
   */
  private final float testSizeRunTimeThreshold;

  @VisibleForTesting
  public TestSize(
      String sizeQualifierName,
      Class<? extends Annotation> runnerFilterAnnotationClass,
      String legacyPlatformAnnotationClassName,
      float testSizeRuntimeThreshold) {
    this.sizeQualifierName = sizeQualifierName;
    this.platformAnnotationClass = loadPlatformAnnotationClass(legacyPlatformAnnotationClassName);
    this.runnerFilterAnnotationClass = runnerFilterAnnotationClass;
    testSizeRunTimeThreshold = testSizeRuntimeThreshold;
  }

  private static Class<? extends Annotation> loadPlatformAnnotationClass(
      String legacyPlatformAnnotationClassName) {
    if (legacyPlatformAnnotationClassName == null) {
      return null;
    }
    try {
      return (Class<? extends Annotation>) Class.forName(legacyPlatformAnnotationClassName);
    } catch (ClassNotFoundException e) {
      // ignore - not present on boot classpath
      return null;
    }
  }

  /** @return the test size name */
  public String getSizeQualifierName() {
    return sizeQualifierName;
  }

  /**
   * @return true if the test method in the {@link Description} is annotated with the test size
   *     annotation class.
   */
  public boolean testMethodIsAnnotatedWithTestSize(Description description) {
    if (description.getAnnotation(runnerFilterAnnotationClass) != null
        || description.getAnnotation(platformAnnotationClass) != null) {
      // If the test method is annotated with a test size annotation include it
      return true;
    }
    // Otherwise exclude it
    return false;
  }

  /**
   * @return true if the test class in the {@link Description} is annotated with the test size
   *     annotation.
   */
  public boolean testClassIsAnnotatedWithTestSize(Description description) {
    final Class<?> testClass = description.getTestClass();
    if (null == testClass) {
      return false;
    }

    if (hasAnnotation(testClass, runnerFilterAnnotationClass)
        || hasAnnotation(testClass, platformAnnotationClass)) {
      // If the test class is annotated with a test size annotation include it.
      return true;
    }
    return false;
  }

  private static boolean hasAnnotation(
      Class<?> testClass, Class<? extends Annotation> annotationClass) {
    return annotationClass != null && testClass.isAnnotationPresent(annotationClass);
  }

  /** @return the suite run time threshold for a given test size. */
  public float getRunTimeThreshold() {
    return testSizeRunTimeThreshold;
  }

  /**
   * Maps a runtime to a test size.
   *
   * @param testRuntime the runtime of the test
   * @return the test size which was mapped to the runtime
   */
  public static TestSize getTestSizeForRunTime(float testRuntime) {
    if (runTimeSmallerThanThreshold(testRuntime, SMALL.getRunTimeThreshold())) {
      return SMALL;
    } else if (runTimeSmallerThanThreshold(testRuntime, MEDIUM.getRunTimeThreshold())) {
      return MEDIUM;
    }
    return LARGE;
  }

  /**
   * @param annotationClass the test size annotation class
   * @return true if the the test size annotation is valid
   */
  public static boolean isAnyTestSize(Class<? extends Annotation> annotationClass) {
    for (TestSize testSize : ALL_SIZES) {
      if (testSize.getRunnerAnnotation() == annotationClass
          || testSize.getFrameworkAnnotation() == annotationClass) {
        return true;
      }
    }
    return false;
  }

  /**
   * Creates a test size instance from a test size string. This method will return {@link
   * TestSize#NONE} if the test size is unknown.
   */
  public static TestSize fromString(final String testSize) {
    TestSize testSizeFromString = NONE;
    for (TestSize testSizeValue : ALL_SIZES) {
      if (testSizeValue.getSizeQualifierName().equals(testSize)) {
        testSizeFromString = testSizeValue;
      }
    }
    return testSizeFromString;
  }

  /**
   * Creates a test size instance from a {@link Description}. This method will return {@link
   * TestSize#NONE} if the description does not contain any test size information.
   */
  public static TestSize fromDescription(Description description) {
    TestSize testSize = NONE;
    // Match on method level first
    for (TestSize testMethodSizeValue : ALL_SIZES) {
      if (testMethodSizeValue.testMethodIsAnnotatedWithTestSize(description)) {
        testSize = testMethodSizeValue;
        break;
      }
    }
    // If size annotation not matched on method level look at class level
    if (NONE.equals(testSize)) {
      for (TestSize testClassSizeValue : ALL_SIZES) {
        if (testClassSizeValue.testClassIsAnnotatedWithTestSize(description)) {
          testSize = testClassSizeValue;
          break;
        }
      }
    }
    return testSize;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    TestSize testSize = (TestSize) o;

    return sizeQualifierName.equals(testSize.sizeQualifierName);
  }

  @Override
  public int hashCode() {
    return sizeQualifierName.hashCode();
  }

  private static boolean runTimeSmallerThanThreshold(float testRuntime, float runtimeThreshold) {
    return Float.compare(testRuntime, runtimeThreshold) < 0;
  }

  private Class<? extends Annotation> getFrameworkAnnotation() {
    return platformAnnotationClass;
  }

  private Class<? extends Annotation> getRunnerAnnotation() {
    return runnerFilterAnnotationClass;
  }
}