content: /*
   * Copyright (C) 2012 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.app.Instrumentation;
  import android.os.Bundle;
  import android.util.Log;
  import androidx.annotation.VisibleForTesting;
  import androidx.test.filters.AbstractFilter;
  import androidx.test.filters.CustomFilter;
  import androidx.test.filters.RequiresDevice;
  import androidx.test.filters.SdkSuppress;
  import androidx.test.internal.runner.ClassPathScanner.ChainedClassNameFilter;
  import androidx.test.internal.runner.ClassPathScanner.ExcludeClassNamesFilter;
  import androidx.test.internal.runner.ClassPathScanner.ExcludePackageNameFilter;
  import androidx.test.internal.runner.ClassPathScanner.ExternalClassNameFilter;
  import androidx.test.internal.runner.ClassPathScanner.InclusivePackageNamesFilter;
  import androidx.test.internal.runner.filters.TestsRegExFilter;
  import androidx.test.internal.util.Checks;
  import androidx.tracing.Trace;
  import java.io.IOException;
  import java.lang.annotation.Annotation;
  import java.lang.reflect.InvocationTargetException;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.regex.Pattern;
  import org.junit.runner.Description;
  import org.junit.runner.Request;
  import org.junit.runner.Runner;
  import org.junit.runner.manipulation.Filter;
  import org.junit.runner.manipulation.NoTestsRemainException;
  import org.junit.runner.notification.RunNotifier;
  import org.junit.runners.Suite;
  import org.junit.runners.model.InitializationError;
  import org.junit.runners.model.RunnerBuilder;
  
  /**
   * A builder for {@link Request} that builds up tests to run, filtered on provided set of
   * restrictions.
   */
  public class TestRequestBuilder {
    private static final String TAG = "TestRequestBuilder";
  
    static final String MISSING_ARGUMENTS_MSG =
        "Must provide either classes to run, or paths to scan";
    static final String AMBIGUOUS_ARGUMENTS_MSG =
        "Ambiguous arguments: cannot provide both test package and test class(es) to run";
  
    private final List<String> pathsToScan = new ArrayList<>();
    private Set<String> includedPackages = new HashSet<>();
    private Set<String> excludedPackages = new HashSet<>();
    private Set<String> includedClasses = new LinkedHashSet<>();
    private Set<String> excludedClasses = new HashSet<>();
    private ClassAndMethodFilter classMethodFilter = new ClassAndMethodFilter();
    private final TestsRegExFilter testsRegExFilter = new TestsRegExFilter();
    private Filter filter =
        new AnnotationExclusionFilter(androidx.test.filters.Suppress.class)
            .intersect(new SdkSuppressFilter())
            .intersect(new RequiresDeviceFilter())
            .intersect(classMethodFilter)
            .intersect(testsRegExFilter)
            .intersect(new CustomFilters());
    private List<Class<? extends RunnerBuilder>> customRunnerBuilderClasses = new ArrayList<>();
    private boolean skipExecution = false;
    private final DeviceBuild deviceBuild;
    private long perTestTimeout = 0;
    private ClassLoader classLoader;
  
    /**
     * Instructs the test builder if JUnit3 suite() methods should be executed.
     *
     * <p>Currently set to false if any method filter is set, for consistency with
     * InstrumentationTestRunner.
     */
    private boolean ignoreSuiteMethods = false;
  
    /**
     * Accessor interface for retrieving device build properties.
     *
     * <p>Used so unit tests can mock calls
     */
    interface DeviceBuild {
      /** Returns the SDK API level for current device. */
      int getSdkVersionInt();
  
      /** Returns the hardware type of the current device. */
      String getHardware();
  
      /** Returns the version code name of the current device. */
      String getCodeName();
    }
  
    private static class DeviceBuildImpl implements DeviceBuild {
      @Override
      public int getSdkVersionInt() {
        return android.os.Build.VERSION.SDK_INT;
      }
  
      @Override
      public String getHardware() {
        return android.os.Build.HARDWARE;
      }
  
      @Override
      public String getCodeName() {
        return android.os.Build.VERSION.CODENAME;
      }
    }
  
    /** Filter that only runs tests whose method or class has been annotated with given filter. */
    private static class AnnotationInclusionFilter extends AbstractFilter {
  
      private final Class<? extends Annotation> annotationClass;
  
      AnnotationInclusionFilter(Class<? extends Annotation> annotation) {
        annotationClass = annotation;
      }
  
      /**
       * Determine if given test description matches filter.
       *
       * @param description the {@link Description} describing the test
       * @return <code>true</code> if matched
       */
      @Override
      protected boolean evaluateTest(Description description) {
        final Class<?> testClass = description.getTestClass();
        return description.getAnnotation(annotationClass) != null
            || (testClass != null && testClass.isAnnotationPresent(annotationClass));
      }
  
      /** {@inheritDoc} */
      @Override
      public String describe() {
        return String.format("annotation %s", annotationClass.getName());
      }
    }
  
    /**
     * A filter for test sizes.
     *
     * <p>Will match if test method has given size annotation, or class does, but only if method does
     * not have any other size annotations. ie method size annotation overrides class size annotation.
     */
    private static class SizeFilter extends AbstractFilter {
  
      private final TestSize testSize;
  
      SizeFilter(TestSize testSize) {
        this.testSize = testSize;
      }
  
      @Override
      public String describe() {
        return "";
      }
  
      @Override
      protected boolean evaluateTest(Description description) {
        // If test method is annotated with test size annotation include it
        if (testSize.testMethodIsAnnotatedWithTestSize(description)) {
          return true;
        } else if (testSize.testClassIsAnnotatedWithTestSize(description)) {
          // size annotation matched at class level. Make sure method doesn't have any other
          // size annotations
          for (Annotation a : description.getAnnotations()) {
            if (TestSize.isAnyTestSize(a.annotationType())) {
              return false;
            }
          }
          return true;
        }
        return false;
      }
    }
  
    /** Filter out tests whose method or class has been annotated with given filter. */
    private static class AnnotationExclusionFilter extends AbstractFilter {
  
      private final Class<? extends Annotation> annotationClass;
  
      AnnotationExclusionFilter(Class<? extends Annotation> annotation) {
        annotationClass = annotation;
      }
  
      @Override
      protected boolean evaluateTest(Description description) {
        final Class<?> testClass = description.getTestClass();
        if ((testClass != null && testClass.isAnnotationPresent(annotationClass))
            || (description.getAnnotation(annotationClass) != null)) {
          return false;
        }
        return true;
      }
  
      /** {@inheritDoc} */
      @Override
      public String describe() {
        return String.format("not annotation %s", annotationClass.getName());
      }
    }
  
    private static class ExtendedSuite extends Suite {
  
      static Suite createSuite(List<Runner> runners) {
        try {
          return new ExtendedSuite(runners);
        } catch (InitializationError e) {
          throw new RuntimeException(
              "Internal Error: "
                  + Suite.class.getName()
                  + "(Class<?>, List<Runner>) should never throw an "
                  + "InitializationError when passed a null Class",
              e);
        }
      }
  
      ExtendedSuite(List<Runner> runners) throws InitializationError {
        super(null, runners);
      }
    }
  
    private static class CustomFilters extends AbstractFilter {
      @Override
      protected boolean evaluateTest(Description description) {
        Collection<Annotation> allAnnotations = description.getAnnotations();
  
        for (Annotation a : allAnnotations) {
          CustomFilter filter = a.annotationType().getAnnotation(CustomFilter.class);
          if (filter != null) {
            // @CustomFilter is present on this annotation, initialize filter class and check if this
            // test should run
            Class<? extends AbstractFilter> filterClass = filter.filterClass();
            try {
              if (!filterClass.getConstructor().newInstance().shouldRun(description)) {
                return false; // skip the test
              }
            } catch (NoSuchMethodException e) {
              throw new IllegalArgumentException(
                  "Must have no argument constructor for class " + filterClass.getName(), e);
            } catch (ClassCastException e) {
              throw new IllegalArgumentException(
                  filterClass.getName() + " does not extend androidx.test.filters.AbstractFilter", e);
            } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
              throw new IllegalArgumentException("Failed to create: " + filterClass.getName(), e);
            }
          }
        }
        return true; // run the test
      }
  
      @Override
      public String describe() {
        return "skip tests annotated with custom filters if necessary";
      }
    }
  
    private class SdkSuppressFilter extends AbstractFilter {
  
      @Override
      protected boolean evaluateTest(Description description) {
        final SdkSuppress sdkSuppress = getAnnotationForTest(description);
        if (sdkSuppress != null) {
          if ((getDeviceSdkInt() >= sdkSuppress.minSdkVersion()
                  && getDeviceSdkInt() <= sdkSuppress.maxSdkVersion()
                  && !isInExcludedSdks(sdkSuppress.excludedSdks()))
              || getDeviceCodeName().equals(sdkSuppress.codeName())) {
            return true; // run the test
          }
          return false; // don't run the test
        }
        return true; // no SdkSuppress, run the test
      }
  
      private SdkSuppress getAnnotationForTest(Description description) {
        final SdkSuppress s = description.getAnnotation(SdkSuppress.class);
        if (s != null) {
          return s;
        }
        final Class<?> testClass = description.getTestClass();
        if (testClass != null) {
          return testClass.getAnnotation(SdkSuppress.class);
        }
        return null;
      }
  
      /** {@inheritDoc} */
      @Override
      public String describe() {
        return String.format("skip tests annotated with SdkSuppress if necessary");
      }
    }
  
    /** Class that filters out tests annotated with {@link RequiresDevice} when running on emulator */
    @VisibleForTesting
    class RequiresDeviceFilter extends AnnotationExclusionFilter {
  
      static final String EMULATOR_HARDWARE_GOLDFISH = "goldfish";
      static final String EMULATOR_HARDWARE_RANCHU = "ranchu";
      // TODO(b/65053549) Remove once we have a more generic solution
      static final String EMULATOR_HARDWARE_GCE = "gce_x86";
  
      private final Set<String> emulatorHardwareNames =
          new HashSet<>(
              Arrays.asList(
                  EMULATOR_HARDWARE_GOLDFISH, EMULATOR_HARDWARE_RANCHU, EMULATOR_HARDWARE_GCE));
  
      RequiresDeviceFilter() {
        super(RequiresDevice.class);
      }
  
      @Override
      protected boolean evaluateTest(Description description) {
        if (!super.evaluateTest(description)) {
          // annotation is present - check if device is an emulator
          return !emulatorHardwareNames.contains(getDeviceHardware());
        }
        return true;
      }
  
      /** {@inheritDoc} */
      @Override
      public String describe() {
        return String.format("skip tests annotated with RequiresDevice if necessary");
      }
    }
  
    private static class ShardingFilter extends Filter {
      private final int numShards;
      private final int shardIndex;
  
      ShardingFilter(int numShards, int shardIndex) {
        this.numShards = numShards;
        this.shardIndex = shardIndex;
      }
  
      @Override
      public boolean shouldRun(Description description) {
        if (description.isTest()) {
          return (Math.abs(description.hashCode()) % numShards) == shardIndex;
        }
  
        // The description is a suite, so assume that it can be run so that filtering is
        // applied to its children. If after filtering it has no children then it will be
        // automatically filtered out.
        return true;
      }
  
      /** {@inheritDoc} */
      @Override
      public String describe() {
        return String.format("Shard %s of %s shards", shardIndex, numShards);
      }
    }
  
    /**
     * A {@link Request} that doesn't report an error if all tests are filtered out. Done for
     * consistency with InstrumentationTestRunner.
     */
    private static class LenientFilterRequest extends Request {
      private final Request request;
      private final Filter filter;
  
      public LenientFilterRequest(Request classRequest, Filter filter) {
        request = classRequest;
        this.filter = filter;
      }
  
      @Override
      public Runner getRunner() {
        try {
          Runner runner = request.getRunner();
          filter.apply(runner);
          return runner;
        } catch (NoTestsRemainException e) {
          // don't treat filtering out all tests as an error
          return new BlankRunner();
        }
      }
    }
  
    /** A {@link Runner} that doesn't do anything */
    private static class BlankRunner extends Runner {
      @Override
      public Description getDescription() {
        return Description.createSuiteDescription("no tests found");
      }
  
      @Override
      public void run(RunNotifier notifier) {
        // do nothing
      }
    }
  
    /** A {@link Filter} to support the ability to filter out multiple class#method combinations. */
    private static class ClassAndMethodFilter extends AbstractFilter {
  
      private Map<String, MethodFilter> methodFilters = new HashMap<>();
  
      @Override
      public boolean evaluateTest(Description description) {
        if (methodFilters.isEmpty()) {
          return true;
        }
        String className = description.getClassName();
        MethodFilter methodFilter = methodFilters.get(className);
        if (methodFilter != null) {
          return methodFilter.shouldRun(description);
        }
        // This test class was not explicitly excluded and none of it's test methods were
        // explicitly included or excluded. Should be run, return true:
        return true;
      }
  
      @Override
      public String describe() {
        return "Class and method filter";
      }
  
      public void addMethod(String className, String methodName) {
        MethodFilter methodFilter = methodFilters.get(className);
        if (methodFilter == null) {
          methodFilter = new MethodFilter(className);
          methodFilters.put(className, methodFilter);
        }
        methodFilter.addInclusionMethod(methodName);
      }
  
      public void removeMethod(String className, String methodName) {
        MethodFilter methodFilter = methodFilters.get(className);
        if (methodFilter == null) {
          methodFilter = new MethodFilter(className);
          methodFilters.put(className, methodFilter);
        }
        methodFilter.addExclusionMethod(methodName);
      }
    }
  
    /** A {@link Filter} used to filter out desired test methods from a given class */
    private static class MethodFilter extends AbstractFilter {
  
      private final String className;
      private Set<String> includedMethods = new HashSet<>();
      private Set<String> excludedMethods = new HashSet<>();
  
      /**
       * Constructs a method filter for a given class
       *
       * @param className name of the class the method belongs to
       */
      public MethodFilter(String className) {
        this.className = className;
      }
  
      @Override
      public String describe() {
        return "Method filter for " + className + " class";
      }
  
      @Override
      public boolean evaluateTest(Description description) {
        String methodName = description.getMethodName();
  
        // The method name could be null, e.g. if the class is marked with @Ignore. In that
        // case there is no matching method to run so filter the test out.
        if (methodName == null) {
          return false;
        }
  
        // Parameterized tests append "[#]" at the end of the method names.
        // For instance, "getFoo" would become "getFoo[0]".
        // Method filters should be applied against both the parameterized name and root name
        String rootMethodName = stripParameterizedSuffix(methodName);
        if (excludedMethods.contains(methodName) || excludedMethods.contains(rootMethodName)) {
          return false;
        }
        // don't filter out descriptions with method name "initializationError", since
        // Junit will generate such descriptions in error cases, See ErrorReportingRunner
        return includedMethods.isEmpty()
            || includedMethods.contains(methodName)
            || includedMethods.contains(rootMethodName)
            || methodName.equals("initializationError");
      }
  
      // Strips out the parameterized suffix if it exists
      private String stripParameterizedSuffix(String name) {
        Pattern suffixPattern = Pattern.compile(".+(\\[[0-9]+\\])$");
        if (suffixPattern.matcher(name).matches()) {
          name = name.substring(0, name.lastIndexOf('['));
        }
        return name;
      }
  
      public void addInclusionMethod(String methodName) {
        includedMethods.add(methodName);
      }
  
      public void addExclusionMethod(String methodName) {
        excludedMethods.add(methodName);
      }
    }
  
    /**
     * Creates a TestRequestBuilder
     *
     * @param instr the {@link Instrumentation} to pass to applicable tests
     * @param bundle the {@link Bundle} to pass to applicable tests
     * @deprecated use {@link TestRequestBuilder()}
     */
    @Deprecated
    public TestRequestBuilder(Instrumentation instr, Bundle bundle) {
      this();
    }
  
    /** Creates a TestRequestBuilder */
    public TestRequestBuilder() {
      this(new DeviceBuildImpl());
    }
  
    /** Alternate TestRequestBuilder constructor that accepts a custom DeviceBuild */
    @VisibleForTesting
    TestRequestBuilder(DeviceBuild deviceBuildAccessor) {
      deviceBuild = Checks.checkNotNull(deviceBuildAccessor);
  
      maybeAddLegacySuppressFilter();
    }
  
    // add legacy Suppress filer iff it is on classpath
    private void maybeAddLegacySuppressFilter() {
      try {
        Class<? extends Annotation> legacySuppressClass =
            (Class<? extends Annotation>)
                Class.forName("android.test.suitebuilder.annotation.Suppress");
        filter = filter.intersect(new AnnotationExclusionFilter(legacySuppressClass));
      } catch (ClassNotFoundException e) {
        // ignore
      }
    }
  
    /**
     * Instruct builder to scan the given paths and add all test classes found. Cannot be used in
     * conjunction with {@link #addTestClass} or {@link #addTestMethod} is used.
     *
     * @param paths the list of paths (.dex and .apk files) to scan
     */
    public TestRequestBuilder addPathsToScan(Iterable<String> paths) {
      for (String path : paths) {
        addPathToScan(path);
      }
      return this;
    }
  
    /**
     * Instruct builder to scan given path and add all test classes found. Cannot be used in
     * conjunction with {@link #addTestClass} or {@link #addTestMethod} is used.
     *
     * @param path a filepath to scan for test methods (.dex and .apk files)
     */
    public TestRequestBuilder addPathToScan(String path) {
      pathsToScan.add(path);
      return this;
    }
  
    /**
     * Set the {@link ClassLoader} to be used to load test cases.
     *
     * @param loader {@link ClassLoader} to load test cases with.
     */
    public TestRequestBuilder setClassLoader(ClassLoader loader) {
      classLoader = loader;
      return this;
    }
  
    /**
     * Instructs the test builder if JUnit3 suite() methods should be executed.
     *
     * @param ignoreSuiteMethods true to ignore all suite methods.
     */
    public TestRequestBuilder ignoreSuiteMethods(boolean ignoreSuiteMethods) {
      this.ignoreSuiteMethods = ignoreSuiteMethods;
      return this;
    }
  
    /**
     * Add a test class to be executed. All test methods in this class will be executed, unless a test
     * method was explicitly included or excluded.
     *
     * @param className
     */
    public TestRequestBuilder addTestClass(String className) {
      includedClasses.add(className);
      return this;
    }
  
    /**
     * Excludes a test class. All test methods in this class will be excluded.
     *
     * @param className
     */
    public TestRequestBuilder removeTestClass(String className) {
      excludedClasses.add(className);
      return this;
    }
  
    /** Adds a test method to run. */
    public TestRequestBuilder addTestMethod(String testClassName, String testMethodName) {
      includedClasses.add(testClassName);
      classMethodFilter.addMethod(testClassName, testMethodName);
      return this;
    }
  
    /** Excludes a test method from being run. */
    public TestRequestBuilder removeTestMethod(String testClassName, String testMethodName) {
      classMethodFilter.removeMethod(testClassName, testMethodName);
      return this;
    }
  
    /**
     * Run only tests within given java package. Cannot be used in conjunction with
     * addTestClass/Method.
     *
     * <p>At least one {@link #addPathToScan} also must be provided.
     *
     * @param testPackage the fully qualified java package name
     */
    public TestRequestBuilder addTestPackage(String testPackage) {
      includedPackages.add(testPackage);
      return this;
    }
  
    /**
     * Excludes all tests within given java package. Cannot be used in conjunction with
     * addTestClass/Method.
     *
     * <p>At least one {@link #addPathToScan} also must be provided.
     *
     * @param testPackage the fully qualified java package name
     */
    public TestRequestBuilder removeTestPackage(String testPackage) {
      excludedPackages.add(testPackage);
      return this;
    }
  
    /**
     * Sets the test name filter regular expression filter.
     *
     * <p>Will filter out tests not matching the given regex.
     *
     * @param testsRegex a regex for matching against <code>java_package.class#method</code>
     */
    public TestRequestBuilder setTestsRegExFilter(String testsRegex) {
      this.testsRegExFilter.setPattern(testsRegex);
      return this;
    }
  
    /**
     * Run only tests with given size
     *
     * @param forTestSize
     */
    public TestRequestBuilder addTestSizeFilter(TestSize forTestSize) {
      if (!TestSize.NONE.equals(forTestSize)) {
        addFilter(new SizeFilter(forTestSize));
      } else {
        Log.e(TAG, String.format("Unrecognized test size '%s'", forTestSize.getSizeQualifierName()));
      }
      return this;
    }
  
    /**
     * Only run tests annotated with given annotation class.
     *
     * @param annotation the full class name of annotation
     */
    public TestRequestBuilder addAnnotationInclusionFilter(String annotation) {
      Class<? extends Annotation> annotationClass = loadAnnotationClass(annotation);
      if (annotationClass != null) {
        addFilter(new AnnotationInclusionFilter(annotationClass));
      }
      return this;
    }
  
    /**
     * Skip tests annotated with given annotation class.
     *
     * @param notAnnotation the full class name of annotation
     */
    public TestRequestBuilder addAnnotationExclusionFilter(String notAnnotation) {
      Class<? extends Annotation> annotationClass = loadAnnotationClass(notAnnotation);
      if (annotationClass != null) {
        addFilter(new AnnotationExclusionFilter(annotationClass));
      }
      return this;
    }
  
    public TestRequestBuilder addShardingFilter(int numShards, int shardIndex) {
      return addFilter(new ShardingFilter(numShards, shardIndex));
    }
  
    public TestRequestBuilder addFilter(Filter filter) {
      this.filter = this.filter.intersect(filter);
      return this;
    }
  
    public TestRequestBuilder addCustomRunnerBuilderClass(
        Class<? extends RunnerBuilder> runnerBuilderClass) {
      customRunnerBuilderClasses.add(runnerBuilderClass);
      return this;
    }
  
    /**
     * Build a request that will generate test started and test ended events, but will skip actual
     * test execution.
     */
    public TestRequestBuilder setSkipExecution(boolean b) {
      skipExecution = b;
      return this;
    }
  
    /** Sets milliseconds timeout value applied to each test where 0 means no timeout */
    public TestRequestBuilder setPerTestTimeout(long millis) {
      perTestTimeout = millis;
      return this;
    }
  
    /** Convenience method to set builder attributes from {@link RunnerArgs} */
    public TestRequestBuilder addFromRunnerArgs(RunnerArgs runnerArgs) {
      for (RunnerArgs.TestArg test : runnerArgs.tests) {
        if (test.methodName == null) {
          addTestClass(test.testClassName);
        } else {
          addTestMethod(test.testClassName, test.methodName);
        }
      }
      for (RunnerArgs.TestArg test : runnerArgs.notTests) {
        if (test.methodName == null) {
          removeTestClass(test.testClassName);
        } else {
          removeTestMethod(test.testClassName, test.methodName);
        }
      }
      for (String pkg : runnerArgs.testPackages) {
        addTestPackage(pkg);
      }
      for (String pkg : runnerArgs.notTestPackages) {
        removeTestPackage(pkg);
      }
      if (runnerArgs.testSize != null) {
        addTestSizeFilter(TestSize.fromString(runnerArgs.testSize));
      }
      for (String annotation : runnerArgs.annotations) {
        addAnnotationInclusionFilter(annotation);
      }
      for (String notAnnotation : runnerArgs.notAnnotations) {
        addAnnotationExclusionFilter(notAnnotation);
      }
      for (Filter filter : runnerArgs.filters) {
        addFilter(filter);
      }
      if (runnerArgs.testTimeout > 0) {
        setPerTestTimeout(runnerArgs.testTimeout);
      }
      if (runnerArgs.numShards > 0
          && runnerArgs.shardIndex >= 0
          && runnerArgs.shardIndex < runnerArgs.numShards) {
        addShardingFilter(runnerArgs.numShards, runnerArgs.shardIndex);
      }
      if (runnerArgs.logOnly || runnerArgs.listTestsForOrchestrator) {
        setSkipExecution(true);
      }
      if (runnerArgs.classLoader != null) {
        setClassLoader(runnerArgs.classLoader);
      }
      for (Class<? extends RunnerBuilder> runnerBuilderClass : runnerArgs.runnerBuilderClasses) {
        addCustomRunnerBuilderClass(runnerBuilderClass);
      }
      if (runnerArgs.testsRegEx != null) {
        setTestsRegExFilter(runnerArgs.testsRegEx);
      }
      return this;
    }
  
    /**
     * Builds the {@link Request} based on provided data.
     *
     * @throws java.lang.IllegalArgumentException if provided set of data is not valid
     */
    public Request build() {
      Trace.beginSection("build test request");
      try {
        includedPackages.removeAll(excludedPackages);
        validate(includedClasses);
  
        boolean scanningPath = includedClasses.isEmpty();
  
        // If scanning then suite methods are not supported.
        boolean ignoreSuiteMethods = this.ignoreSuiteMethods || scanningPath;
  
        RunnerBuilder runnerBuilder = getRunnerBuilder(ignoreSuiteMethods, perTestTimeout);
  
        TestLoader loader = TestLoader.Factory.create(classLoader, runnerBuilder, scanningPath);
        Collection<String> classNames;
        if (scanningPath) {
          // no class restrictions have been specified. Load all classes.
          Log.d(TAG, "Using class path scanning to discover tests");
          classNames = getClassNamesFromClassPath();
        } else {
          // If the set of excludedClasses is equal to the set of includedClasses, no tests should
          // run (that's what includeMethods and excludeMethods do currently).
          // E.g.,
          // 1. adb shell am instrument -w -e class com.android.foo.FooTest#testFoo -e notClass
          // com.android.foo.FooTest#testFoo com.android.foo/androidx.test.runner.AndroidJUnitRunner
          // 2. adb shell am instrument -w -e class com.android.foo.FooTest -e notClass
          // com.android.foo.FooTest com.android.foo/androidx.test.runner.AndroidJUnitRunner
          // should perform the same result (no tests run).
          includedClasses.removeAll(excludedClasses);
          classNames = includedClasses;
        }
  
        List<Runner> runners = loader.getRunnersFor(classNames);
  
        Suite suite = ExtendedSuite.createSuite(runners);
        Request request = Request.runner(suite);
        return new LenientFilterRequest(request, filter);
      } finally {
        Trace.endSection();
      }
    }
  
    /** Validate that the set of options provided to this builder are valid and not conflicting */
    private void validate(Set<String> classNames) {
      if (classNames.isEmpty() && pathsToScan.isEmpty()) {
        throw new IllegalArgumentException(MISSING_ARGUMENTS_MSG);
      }
      // TODO(b/73905202): consider failing if both test classes and scan paths are given.
      // Right now that is allowed though
    }
  
    /**
     * Get the {@link RunnerBuilder} to use to create the {@link Runner} instances.
     *
     * @return a {@link RunnerBuilder}.
     */
    private RunnerBuilder getRunnerBuilder(boolean shouldIgnoreSuiteMethods, long perTestTimeout) {
      RunnerBuilder builder;
      if (skipExecution) {
        // If all that is needed is the list of tests then replace the Runner which will
        // run the test with one that will simply fire events for each of the tests.
        builder = new AndroidLogOnlyBuilder(shouldIgnoreSuiteMethods, customRunnerBuilderClasses);
      } else {
        builder =
            new AndroidRunnerBuilder(
                shouldIgnoreSuiteMethods, perTestTimeout, customRunnerBuilderClasses);
      }
      return builder;
    }
  
    private Collection<String> getClassNamesFromClassPath() {
      if (pathsToScan.isEmpty()) {
        throw new IllegalStateException("neither test class to execute or class paths were provided");
      }
      Log.i(TAG, String.format("Scanning classpath to find tests in paths %s", pathsToScan));
      ClassPathScanner scanner = createClassPathScanner(pathsToScan);
  
      ChainedClassNameFilter filter = new ChainedClassNameFilter();
      // exclude inner classes
      filter.add(new ExternalClassNameFilter());
      for (String pkg : ClassPathScanner.getDefaultExcludedPackages()) {
        // Add the test packages to the exclude list unless they were explictly included.
        if (!includedPackages.contains(pkg)) {
          excludedPackages.add(pkg);
        }
      }
      if (!includedPackages.isEmpty()) {
        filter.add(new InclusivePackageNamesFilter(includedPackages));
      }
      for (String pkg : excludedPackages) {
        filter.add(new ExcludePackageNameFilter(pkg));
      }
      filter.add(new ExcludeClassNamesFilter(excludedClasses));
      try {
        return scanner.getClassPathEntries(filter);
      } catch (IOException e) {
        Log.e(TAG, "Failed to scan classes", e);
      }
      return Collections.emptyList();
    }
  
    /**
     * Factory method for {@link ClassPathScanner}.
     *
     * <p>Exposed so unit tests can mock.
     */
    ClassPathScanner createClassPathScanner(List<String> classPath) {
      return new ClassPathScanner(classPath);
    }
  
    @SuppressWarnings("unchecked")
    private Class<? extends Annotation> loadAnnotationClass(String className) {
      try {
        Class<?> clazz = Class.forName(className);
        return (Class<? extends Annotation>) clazz;
      } catch (ClassNotFoundException e) {
        Log.e(TAG, String.format("Could not find annotation class: %s", className));
      } catch (ClassCastException e) {
        Log.e(TAG, String.format("Class %s is not an annotation", className));
      }
      return null;
    }
  
    private int getDeviceSdkInt() {
      return deviceBuild.getSdkVersionInt();
    }
  
    private String getDeviceHardware() {
      return deviceBuild.getHardware();
    }
  
    private String getDeviceCodeName() {
      return deviceBuild.getCodeName();
    }
  
    private boolean isInExcludedSdks(int[] excludedSdks) {
      for (int sdk : excludedSdks) {
        if (sdk == getDeviceSdkInt()) {
          return true;
        }
      }
      return false;
    }
  }
  