public class

RunnerArgs

extends java.lang.Object

 java.lang.Object

↳androidx.test.internal.runner.RunnerArgs

Gradle dependencies

compile group: 'androidx.test', name: 'runner', version: '1.6.2'

  • groupId: androidx.test
  • artifactId: runner
  • version: 1.6.2

Artifact androidx.test:runner:1.6.2 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.test:runner com.android.support.test:runner

Overview

Contains input arguments passed to the instrumentation test runner.

Summary

Fields
public final java.util.List<java.lang.String>annotations

public final java.util.List<ApplicationLifecycleCallback>appListeners

public static final java.lang.StringARGUMENT_USE_TEST_STORAGE_SERVICE

public final java.lang.ClassLoaderclassLoader

public final java.util.Set<java.lang.String>classpathToScan

public final booleancodeCoverage

public final java.lang.StringcodeCoveragePath

public final booleandebug

public final intdelayInMillis

public final booleandisableAnalytics

public final java.util.List<Filter>filters

public final java.util.List<RunListener>listeners

public final booleanlistTestsForOrchestrator

public final booleanlogOnly

public final booleannewRunListenerMode

public final java.util.List<java.lang.String>notAnnotations

public final java.util.List<java.lang.String>notTestPackages

public final java.util.List<RunnerArgs.TestArg>notTests

public final intnumShards

public final java.lang.StringorchestratorService

public final RunnerArgs.TestArgremoteMethod

public final java.util.List<java.lang.Class>runnerBuilderClasses

public final java.util.List<ScreenCaptureProcessor>screenCaptureProcessors

public final intshardIndex

public final java.lang.StringshellExecBinderKey

public final booleansuiteAssignment

public final java.lang.StringtargetProcess

public final java.lang.StringtestDiscoveryService

public final java.util.List<java.lang.String>testPackages

public final booleantestPlatformMigration

public final java.lang.StringtestRunEventsService

public final java.util.List<RunnerArgs.TestArg>tests

public final java.lang.StringtestSize

public final java.lang.StringtestsRegEx

public final longtestTimeout

public final booleanuseTestStorageService

Methods
public static longparseTestTimeout(Bundle args)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final java.lang.String ARGUMENT_USE_TEST_STORAGE_SERVICE

public final boolean debug

public final boolean suiteAssignment

public final boolean codeCoverage

public final java.lang.String codeCoveragePath

public final int delayInMillis

public final boolean logOnly

public final java.util.List<java.lang.String> testPackages

public final java.util.List<java.lang.String> notTestPackages

public final java.lang.String testSize

public final java.util.List<java.lang.String> annotations

public final java.util.List<java.lang.String> notAnnotations

public final long testTimeout

public final java.util.List<RunListener> listeners

public final java.util.List<Filter> filters

public final java.util.List<java.lang.Class> runnerBuilderClasses

public final java.util.List<RunnerArgs.TestArg> tests

public final java.util.List<RunnerArgs.TestArg> notTests

public final int numShards

public final int shardIndex

public final boolean disableAnalytics

public final java.util.List<ApplicationLifecycleCallback> appListeners

public final java.lang.ClassLoader classLoader

public final java.util.Set<java.lang.String> classpathToScan

public final RunnerArgs.TestArg remoteMethod

public final java.lang.String targetProcess

public final java.util.List<ScreenCaptureProcessor> screenCaptureProcessors

public final java.lang.String orchestratorService

public final boolean listTestsForOrchestrator

public final java.lang.String testDiscoveryService

public final java.lang.String testRunEventsService

public final boolean useTestStorageService

public final java.lang.String shellExecBinderKey

public final boolean newRunListenerMode

public final java.lang.String testsRegEx

public final boolean testPlatformMigration

Methods

public static long parseTestTimeout(Bundle args)

Source

/*
 * 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.app.Instrumentation;
import android.content.pm.InstrumentationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.test.platform.io.PlatformTestStorage;
import androidx.test.platform.io.PlatformTestStorageRegistry;
import androidx.test.runner.lifecycle.ApplicationLifecycleCallback;
import androidx.test.runner.screenshot.ScreenCaptureProcessor;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.notification.RunListener;
import org.junit.runners.model.RunnerBuilder;

/** Contains input arguments passed to the instrumentation test runner. */
public class RunnerArgs {
  private static final String LOG_TAG = "RunnerArgs";

  // constants for supported instrumentation arguments
  static final String ARGUMENT_TEST_CLASS = "class";
  static final String ARGUMENT_CLASSPATH_TO_SCAN = "classpathToScan";
  static final String ARGUMENT_NOT_TEST_CLASS = "notClass";
  static final String ARGUMENT_TEST_SIZE = "size";
  static final String ARGUMENT_LOG_ONLY = "log";
  static final String ARGUMENT_ANNOTATION = "annotation";
  static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation";
  static final String ARGUMENT_NUM_SHARDS = "numShards";
  static final String ARGUMENT_SHARD_INDEX = "shardIndex";
  static final String ARGUMENT_DELAY_IN_MILLIS = "delay_msec";
  static final String ARGUMENT_COVERAGE = "coverage";
  static final String ARGUMENT_COVERAGE_PATH = "coverageFile";
  static final String ARGUMENT_SUITE_ASSIGNMENT = "suiteAssignment";
  static final String ARGUMENT_DEBUG = "debug";
  static final String ARGUMENT_LISTENER = "listener";
  static final String ARGUMENT_FILTER = "filter";
  static final String ARGUMENT_RUNNER_BUILDER = "runnerBuilder";
  static final String ARGUMENT_TEST_PACKAGE = "package";
  static final String ARGUMENT_NOT_TEST_PACKAGE = "notPackage";
  static final String ARGUMENT_TIMEOUT = "timeout_msec";
  static final String ARGUMENT_TEST_FILE = "testFile";
  static final String ARGUMENT_NOT_TEST_FILE = "notTestFile";

  static final String ARGUMENT_DISABLE_ANALYTICS = "disableAnalytics";
  static final String ARGUMENT_APP_LISTENER = "appListener";
  static final String ARGUMENT_CLASS_LOADER = "classLoader";
  static final String ARGUMENT_REMOTE_INIT_METHOD = "remoteMethod";
  static final String ARGUMENT_TARGET_PROCESS = "targetProcess";
  static final String ARGUMENT_SCREENSHOT_PROCESSORS = "screenCaptureProcessors";
  static final String ARGUMENT_ORCHESTRATOR_SERVICE = "orchestratorService";
  static final String ARGUMENT_LIST_TESTS_FOR_ORCHESTRATOR = "listTestsForOrchestrator";
  static final String ARGUMENT_ORCHESTRATOR_DISCOVERY_SERVICE = "testDiscoveryService";
  static final String ARGUMENT_ORCHESTRATOR_RUN_EVENTS_SERVICE = "testRunEventsService";
  // WARNING: DO NOT USE THIS FLAG
  // This is a temporary flag to enable a migration to a new test platform event system. This will
  // be removed in April 2021. Depending on this flag is discouraged and dependents will not be
  // supported.
  static final String ARGUMENT_TEST_PLATFORM_MIGRATION = "temporary_testPlatformMigration";
  // Specifies whether the `androidx.test.services` service is installed on the device.
  // Supposed to be used by the test infrastructure only.
  public static final String ARGUMENT_USE_TEST_STORAGE_SERVICE = "useTestStorageService";
  static final String ARGUMENT_SHELL_EXEC_BINDER_KEY = "shellExecBinderKey";
  static final String ARGUMENT_RUN_LISTENER_NEW_ORDER = "newRunListenerMode";
  static final String ARGUMENT_TESTS_REGEX = "tests_regex";

  // used to separate multiple fully-qualified test case class names
  private static final String CLASS_SEPARATOR = ",";
  // used to separate classpath entries
  private static final String CLASSPATH_SEPARATOR = ":";
  // used to separate fully-qualified test case class name, and one of its methods
  private static final char METHOD_SEPARATOR = '#';

  public final boolean debug;
  public final boolean suiteAssignment;
  public final boolean codeCoverage;
  public final String codeCoveragePath;
  public final int delayInMillis;
  public final boolean logOnly;
  public final List<String> testPackages;
  public final List<String> notTestPackages;
  public final String testSize;
  public final List<String> annotations;
  public final List<String> notAnnotations;
  public final long testTimeout;
  public final List<RunListener> listeners;
  public final List<Filter> filters;
  public final List<Class<? extends RunnerBuilder>> runnerBuilderClasses;
  public final List<TestArg> tests;
  public final List<TestArg> notTests;
  public final int numShards;
  public final int shardIndex;
  public final boolean disableAnalytics;
  public final List<ApplicationLifecycleCallback> appListeners;
  public final ClassLoader classLoader;
  public final Set<String> classpathToScan;
  public final TestArg remoteMethod;
  public final String targetProcess;
  public final List<ScreenCaptureProcessor> screenCaptureProcessors;
  public final String orchestratorService;
  public final boolean listTestsForOrchestrator;
  public final String testDiscoveryService;
  public final String testRunEventsService;
  public final boolean useTestStorageService;
  public final String shellExecBinderKey;
  public final boolean newRunListenerMode;
  public final String testsRegEx;
  public final boolean testPlatformMigration;

  public static long parseTestTimeout(Bundle args) {
    return Builder.parseUnsignedLong(args.getString(ARGUMENT_TIMEOUT), ARGUMENT_TIMEOUT);
  }

  /** Encapsulates a test class and optional method. */
  public static class TestArg {
    public final String testClassName;
    public final String methodName;

    TestArg(String className, String methodName) {
      this.testClassName = className;
      this.methodName = methodName;
    }

    TestArg(String className) {
      this(className, null);
    }

    @Override
    public String toString() {
      return methodName != null ? testClassName + METHOD_SEPARATOR + methodName : testClassName;
    }
  }

  /** Encapsulates a list of test args and a list of package args found in a test file. */
  private static final class TestFileArgs {
    private final List<TestArg> tests = new ArrayList<>();
    private final List<String> packages = new ArrayList<>();
  }

  private RunnerArgs(Builder builder) {
    this.debug = builder.debug;
    this.suiteAssignment = builder.suiteAssignment;
    this.codeCoverage = builder.codeCoverage;
    this.codeCoveragePath = builder.codeCoveragePath;
    this.delayInMillis = builder.delayInMillis;
    this.logOnly = builder.logOnly;
    this.testPackages = builder.testPackages;
    this.notTestPackages = builder.notTestPackages;
    this.testSize = builder.testSize;
    this.annotations = Collections.unmodifiableList(builder.annotations);
    this.notAnnotations = Collections.unmodifiableList(builder.notAnnotations);
    this.testTimeout = builder.testTimeout;
    this.listeners = Collections.unmodifiableList(builder.listeners);
    this.filters = Collections.unmodifiableList(builder.filters);
    this.runnerBuilderClasses = Collections.unmodifiableList(builder.runnerBuilderClasses);
    this.tests = Collections.unmodifiableList(builder.tests);
    this.notTests = Collections.unmodifiableList(builder.notTests);
    this.numShards = builder.numShards;
    this.shardIndex = builder.shardIndex;
    this.disableAnalytics = builder.disableAnalytics;
    this.appListeners = Collections.unmodifiableList(builder.appListeners);
    this.classLoader = builder.classLoader;
    this.classpathToScan = builder.classpathToScan;
    this.remoteMethod = builder.remoteMethod;
    this.orchestratorService = builder.orchestratorService;
    this.listTestsForOrchestrator = builder.listTestsForOrchestrator;
    this.testDiscoveryService = builder.testDiscoveryService;
    this.testRunEventsService = builder.testRunEventsService;
    this.useTestStorageService = builder.useTestStorageService;
    this.screenCaptureProcessors = Collections.unmodifiableList(builder.screenCaptureProcessors);
    this.targetProcess = builder.targetProcess;
    this.shellExecBinderKey = builder.shellExecBinderKey;
    this.newRunListenerMode = builder.newRunListenerMode;
    this.testsRegEx = builder.testsRegEx;
    this.testPlatformMigration = builder.testPlatformMigration;
  }

  /** Builder for {@link RunnerArgs}. */
  public static class Builder {
    private boolean debug = false;
    private boolean suiteAssignment = false;
    private boolean codeCoverage = false;
    private String codeCoveragePath = null;
    private int delayInMillis = -1;
    private boolean logOnly = false;
    private List<String> testPackages = new ArrayList<>();
    private List<String> notTestPackages = new ArrayList<>();
    private String testSize = null;
    private final List<String> annotations = new ArrayList<>();
    private final List<String> notAnnotations = new ArrayList<>();
    private long testTimeout = -1;
    private List<RunListener> listeners = new ArrayList<RunListener>();
    private List<Filter> filters = new ArrayList<>();
    private List<Class<? extends RunnerBuilder>> runnerBuilderClasses = new ArrayList<>();
    private List<TestArg> tests = new ArrayList<>();
    private List<TestArg> notTests = new ArrayList<>();
    private int numShards = 0;
    private int shardIndex = 0;
    private boolean disableAnalytics = false;
    private List<ApplicationLifecycleCallback> appListeners =
        new ArrayList<ApplicationLifecycleCallback>();
    private ClassLoader classLoader = null;
    private Set<String> classpathToScan = new HashSet<>();
    private TestArg remoteMethod = null;
    private String orchestratorService = null;
    private boolean listTestsForOrchestrator = false;
    private String testDiscoveryService = null;
    private String testRunEventsService = null;
    private boolean useTestStorageService = false;
    private String targetProcess = null;
    private List<ScreenCaptureProcessor> screenCaptureProcessors = new ArrayList<>();
    public String shellExecBinderKey;
    private boolean newRunListenerMode = false;
    private String testsRegEx = null;
    private boolean testPlatformMigration = false;
    private final PlatformTestStorage testStorage;

    public Builder() {
      this(PlatformTestStorageRegistry.getInstance());
    }

    Builder(PlatformTestStorage testStorage) {
      this.testStorage = testStorage;
    }

    /**
     * Populate the arg data from the given Bundle.
     *
     * <p>Note: This will override any manifest-provided args
     */
    public Builder fromBundle(Instrumentation instr, Bundle bundle) {
      this.debug = parseBoolean(bundle.getString(ARGUMENT_DEBUG));
      this.useTestStorageService =
          parseBoolean(bundle.getString(ARGUMENT_USE_TEST_STORAGE_SERVICE));
      this.delayInMillis =
          parseUnsignedInt(bundle.get(ARGUMENT_DELAY_IN_MILLIS), ARGUMENT_DELAY_IN_MILLIS);
      // parse test class args
      this.tests.addAll(parseTestClasses(bundle.getString(ARGUMENT_TEST_CLASS)));
      this.notTests.addAll(parseTestClasses(bundle.getString(ARGUMENT_NOT_TEST_CLASS)));
      // parse test package args
      this.testPackages.addAll(parseTestPackages(bundle.getString(ARGUMENT_TEST_PACKAGE)));
      this.notTestPackages.addAll(parseTestPackages(bundle.getString(ARGUMENT_NOT_TEST_PACKAGE)));
      // parse test file args, which may include class and package args
      TestFileArgs testFileArgs =
          parseTestFile(instr, useTestStorageService, bundle.getString(ARGUMENT_TEST_FILE));
      this.tests.addAll(testFileArgs.tests);
      this.testPackages.addAll(testFileArgs.packages);
      TestFileArgs notTestFileArgs =
          parseTestFile(instr, useTestStorageService, bundle.getString(ARGUMENT_NOT_TEST_FILE));
      this.notTests.addAll(notTestFileArgs.tests);
      this.notTestPackages.addAll(notTestFileArgs.packages);
      this.listeners.addAll(
          parseLoadAndInstantiateClasses(
              bundle.getString(ARGUMENT_LISTENER), RunListener.class, null));
      this.filters.addAll(
          parseLoadAndInstantiateClasses(bundle.getString(ARGUMENT_FILTER), Filter.class, bundle));
      this.runnerBuilderClasses.addAll(
          parseAndLoadClasses(bundle.getString(ARGUMENT_RUNNER_BUILDER), RunnerBuilder.class));
      this.testSize = bundle.getString(ARGUMENT_TEST_SIZE);
      this.annotations.addAll(parseStrings(bundle.getString(ARGUMENT_ANNOTATION)));
      this.notAnnotations.addAll(parseStrings(bundle.getString(ARGUMENT_NOT_ANNOTATION)));
      this.testTimeout = parseTestTimeout(bundle);
      this.numShards = parseUnsignedInt(bundle.get(ARGUMENT_NUM_SHARDS), ARGUMENT_NUM_SHARDS);
      this.shardIndex = parseUnsignedInt(bundle.get(ARGUMENT_SHARD_INDEX), ARGUMENT_SHARD_INDEX);
      this.logOnly = parseBoolean(bundle.getString(ARGUMENT_LOG_ONLY));
      this.disableAnalytics = parseBoolean(bundle.getString(ARGUMENT_DISABLE_ANALYTICS));
      this.appListeners.addAll(
          parseLoadAndInstantiateClasses(
              bundle.getString(ARGUMENT_APP_LISTENER), ApplicationLifecycleCallback.class, null));
      this.codeCoverage = parseBoolean(bundle.getString(ARGUMENT_COVERAGE));
      this.codeCoveragePath = bundle.getString(ARGUMENT_COVERAGE_PATH);
      this.suiteAssignment = parseBoolean(bundle.getString(ARGUMENT_SUITE_ASSIGNMENT));
      this.classLoader =
          parseLoadAndInstantiateClass(bundle.getString(ARGUMENT_CLASS_LOADER), ClassLoader.class);
      this.classpathToScan = parseClasspath(bundle.getString(ARGUMENT_CLASSPATH_TO_SCAN));
      if (bundle.containsKey(ARGUMENT_REMOTE_INIT_METHOD)) {
        this.remoteMethod = parseTestClass(bundle.getString(ARGUMENT_REMOTE_INIT_METHOD));
      }
      this.orchestratorService = bundle.getString(ARGUMENT_ORCHESTRATOR_SERVICE);
      this.listTestsForOrchestrator =
          parseBoolean(bundle.getString(ARGUMENT_LIST_TESTS_FOR_ORCHESTRATOR));
      this.testDiscoveryService = bundle.getString(ARGUMENT_ORCHESTRATOR_DISCOVERY_SERVICE);
      this.testRunEventsService = bundle.getString(ARGUMENT_ORCHESTRATOR_RUN_EVENTS_SERVICE);
      this.targetProcess = bundle.getString(ARGUMENT_TARGET_PROCESS);
      this.screenCaptureProcessors.addAll(
          parseLoadAndInstantiateClasses(
              bundle.getString(ARGUMENT_SCREENSHOT_PROCESSORS),
              ScreenCaptureProcessor.class,
              null));
      this.shellExecBinderKey = bundle.getString(ARGUMENT_SHELL_EXEC_BINDER_KEY);
      this.newRunListenerMode = parseBoolean(bundle.getString(ARGUMENT_RUN_LISTENER_NEW_ORDER));
      this.testsRegEx = bundle.getString(ARGUMENT_TESTS_REGEX);
      this.testPlatformMigration = parseBoolean(bundle.getString(ARGUMENT_TEST_PLATFORM_MIGRATION));
      return this;
    }

    private TestFileArgs parseTestFile(
        Instrumentation instr, boolean useStorageService, String filePath) {
      if (filePath == null) {
        return new TestFileArgs();
      }

      if (useStorageService) {
        String localFilePath = filePath.startsWith("/") ? filePath.substring(1) : filePath;
        try (BufferedReader reader =
            new BufferedReader(new InputStreamReader(testStorage.openInputFile(localFilePath)))) {
          return parseFromFileStream(reader);
        } catch (IOException e) {
          Log.w(
              LOG_TAG,
              String.format(
                  "Could not read test file from TestStorage %s. "
                      + "Attempting to read from local file system",
                  filePath),
              e);
          // fall through
        }
      }

      try (BufferedReader reader = openFile(instr, filePath)) {
        return parseFromFileStream(reader);
      } catch (IOException e) {
        throw new IllegalArgumentException("Could not read test file " + filePath, e);
      }
    }

    /** Populate the arg data from the instrumentation:metadata attribute in Manifest. */
    public Builder fromManifest(Instrumentation instr) {
      PackageManager pm = instr.getContext().getPackageManager();
      try {
        InstrumentationInfo instrInfo =
            pm.getInstrumentationInfo(instr.getComponentName(), PackageManager.GET_META_DATA);
        Bundle b = instrInfo.metaData;
        if (b == null) {
          // metadata not present - skip
          return this;
        }
        // parse the metadata using same key names
        return fromBundle(instr, b);
      } catch (PackageManager.NameNotFoundException e) {
        // should never happen
        Log.wtf(LOG_TAG, String.format("Could not find component %s", instr.getComponentName()));
      }
      return this;
    }

    /**
     * Utility method to split String element data in CSV format into a List.
     *
     * @return empty list if null input, otherwise list of strings
     */
    private static List<String> parseStrings(String value) {
      if (value == null) {
        return Collections.emptyList();
      }
      return Arrays.asList(value.split(","));
    }

    /**
     * Parse boolean value from a String.
     *
     * @return the boolean value, false on null input
     */
    private static boolean parseBoolean(String booleanValue) {
      return booleanValue != null && Boolean.parseBoolean(booleanValue);
    }

    /**
     * Parse int from given value - except either int or string.
     *
     * @return the value, -1 if not found
     * @throws NumberFormatException if value is negative or not a number
     */
    private static int parseUnsignedInt(Object value, String name) {
      if (value != null) {
        int intValue = Integer.parseInt(value.toString());
        if (intValue < 0) {
          throw new NumberFormatException(name + " can not be negative");
        }

        return intValue;
      }
      return -1;
    }

    /**
     * Parse long from given value - except either Long or String.
     *
     * @return the value, -1 if not found
     * @throws NumberFormatException if value is negative or not a number
     */
    private static long parseUnsignedLong(Object value, String name) {
      if (value != null) {
        long longValue = Long.parseLong(value.toString());
        if (longValue < 0) {
          throw new NumberFormatException(name + " can not be negative");
        }
        return longValue;
      }
      return -1;
    }

    /**
     * Parse test package data from given CSV data in the following format:
     * com.android.foo,com.android.bar,...
     *
     * @return list of package names, empty list if input is null
     */
    private static List<String> parseTestPackages(String packagesArg) {
      List<String> packages = new ArrayList<>();
      if (packagesArg != null) {
        for (String packageName : packagesArg.split(CLASS_SEPARATOR)) {
          packages.add(packageName);
        }
      }
      return packages;
    }

    /**
     * Parse test class and method data from given CSV data in following format:
     * com.TestClass1#method1,com.TestClass2,...
     *
     * @return list of {@link TestArg} data, empty list if input is null
     */
    private List<TestArg> parseTestClasses(String classesArg) {
      return ClassesArgTokenizer.parse(classesArg);
    }

    /**
     * Parse classpath in the following format: {@code
     * /foo/class1.dex:/foo/class2.dex:/bar/class1.dex:...}
     *
     * @param classpath
     * @return {@link Set} of paths, empty list if input is {@code null}
     */
    private static Set<String> parseClasspath(String classpath) {
      if (classpath == null || classpath.isEmpty()) {
        return new HashSet<>();
      }
      return new HashSet<>(Arrays.asList(classpath.split(CLASSPATH_SEPARATOR, -1)));
    }

    /**
     * Parse an individual test class and optionally method from given string.
     *
     * <p>Expected format: com.TestClass1[#method1]
     */
    private static TestArg parseTestClass(String testClassName) {
      if (TextUtils.isEmpty(testClassName)) {
        return null;
      }
      int methodSeparatorIndex = testClassName.indexOf(METHOD_SEPARATOR);
      if (methodSeparatorIndex > 0) {
        String testMethodName = testClassName.substring(methodSeparatorIndex + 1);
        testClassName = testClassName.substring(0, methodSeparatorIndex);
        return new TestArg(testClassName, testMethodName);
      } else {
        return new TestArg(testClassName);
      }
    }

    private TestFileArgs parseFromFileStream(BufferedReader reader) throws IOException {
      TestFileArgs args = new TestFileArgs();
        String line;
        while ((line = reader.readLine()) != null) {
          if (isClassOrMethod(line)) {
            args.tests.add(parseTestClass(line));
          } else {
            // validate and parse test package
            args.packages.addAll(parseTestPackages(line));
          }
        }
      return args;
    }

    private BufferedReader openFile(Instrumentation instr, String filePath) throws IOException {
      // If we are running as an instant app, then read the file through the shell
      // since the APK is in the targetSandboxVersion="2" with restrictive SELinux
      // policy which prevents reading from /data/local.
      final boolean isInstantApp =
          Build.VERSION.SDK_INT >= 26 && instr.getContext().getPackageManager().isInstantApp();
      return new BufferedReader(
          isInstantApp
              ? new InputStreamReader(
                  new AutoCloseInputStream(
                      instr.getUiAutomation().executeShellCommand("cat " + filePath)))
              : new FileReader(new File(filePath)));
    }

    /**
     * Determine whether line from test file represents a test class or method, as opposed to
     * package name.
     *
     * @param line string containing either an individual test class/method or a package name
     * @return true if line contains an individual test class or method
     */
    @VisibleForTesting
    static boolean isClassOrMethod(String line) {
      for (int i = 0; i < line.length(); i++) {
        char c = line.charAt(i);
        if (c == '#' || Character.isUpperCase(c)) {
          return true;
        }
      }
      return false;
    }

    /**
     * Create a set of objects given a CSV string of full class names and type.
     *
     * @return the List of objects or empty list on null input
     */
    private <T> List<T> parseLoadAndInstantiateClasses(
        String classString, Class<T> type, Bundle bundle) {
      List<T> objects = new ArrayList<T>();
      if (classString != null) {
        for (String className : classString.split(CLASS_SEPARATOR)) {
          loadClassByNameInstantiateAndAdd(objects, className, type, bundle);
        }
      }
      return objects;
    }

    /**
     * Create an object of the given full class name.
     *
     * @return the object instance or null on null input
     */
    private <T> T parseLoadAndInstantiateClass(String classString, Class<T> type) {
      List<T> classLoaders = parseLoadAndInstantiateClasses(classString, type, null);
      if (!classLoaders.isEmpty()) {
        if (classLoaders.size() > 1) {
          throw new IllegalArgumentException(
              String.format("Expected 1 class loader, %d given", classLoaders.size()));
        }
        return classLoaders.get(0);
      }
      return null;
    }

    /**
     * Load class by supplied name, instantiate and add object to supplied list.
     *
     * <p>No effect if input is null or empty.
     *
     * @param objects the List to add to
     * @param className the fully qualified class name
     * @param bundle The bundle to pass to the constructor, null if no bundle is to be passed.
     * @throws IllegalArgumentException if listener cannot be loaded
     */
    private <T> void loadClassByNameInstantiateAndAdd(
        List<T> objects, String className, Class<T> type, Bundle bundle) {
      if (className == null || className.length() == 0) {
        return;
      }
      try {
        @SuppressWarnings("unchecked")
        final Class<? extends T> klass = (Class<? extends T>) Class.forName(className);
        Constructor<? extends T> constructor;
        Object[] arguments;

        // Look for the default constructor first to ensure backwards compatibility with
        // previous code.
        try {
          constructor = klass.getConstructor();
          arguments = new Object[0];
        } catch (NoSuchMethodException nsme1) {
          // Cannot find a default constructor so if a bundle is supplied then look for
          // one that takes a Bundle.
          if (bundle != null) {
            try {
              constructor = klass.getConstructor(Bundle.class);
              arguments = new Object[] {bundle};
            } catch (NoSuchMethodException nsme2) {
              // Could not find a constructor that takes a bundle so rethrow the
              // original exception, remembering to record that this exception was
              // suppressed.
              nsme2.initCause(nsme1);
              throw nsme2;
            }
          } else {
            // Rethrow exception as no bundle was provided.
            throw nsme1;
          }
        }
        constructor.setAccessible(true);
        @SuppressWarnings("unchecked")
        final T instance = constructor.newInstance(arguments);
        objects.add(instance);
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Could not find extra class " + className);
      } catch (NoSuchMethodException e) {
        throw new IllegalArgumentException(
            "Must have no argument constructor for class " + className);
      } catch (ClassCastException e) {
        throw new IllegalArgumentException(className + " does not extend " + type.getName());
      } catch (InstantiationException e) {
        throw new IllegalArgumentException("Failed to create: " + className, e);
      } catch (InvocationTargetException e) {
        throw new IllegalArgumentException("Failed to create: " + className, e);
      } catch (IllegalAccessException e) {
        throw new IllegalArgumentException("Failed to create listener: " + className, e);
      }
    }

    /**
     * Create a set of classes given a CSV string of full class names and type.
     *
     * @return the List of classes or empty list on null input
     */
    private <T> List<Class<? extends T>> parseAndLoadClasses(String classString, Class<T> type) {
      List<Class<? extends T>> classes = new ArrayList<>();
      if (classString != null) {
        for (String className : classString.split(CLASS_SEPARATOR)) {
          loadClassByNameAndAdd(classes, className, type);
        }
      }
      return classes;
    }

    /**
     * Load class by supplied name and add to the supplied list.
     *
     * <p>No effect if input is null or empty.
     *
     * @param classes the List to add to
     * @param type the required ancestor of the class
     * @param className the fully qualified class name
     * @throws IllegalArgumentException if listener cannot be loaded
     */
    private <T> void loadClassByNameAndAdd(
        List<Class<? extends T>> classes, String className, Class<T> type) {
      if (null == className || className.length() == 0) {
        return;
      }
      try {
        Class<?> klass = Class.forName(className);
        if (!type.isAssignableFrom(klass)) {
          throw new IllegalArgumentException(className + " does not extend " + type.getName());
        }
        @SuppressWarnings("unchecked")
        Class<? extends T> castClass = (Class<? extends T>) klass;
        classes.add(castClass);
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Could not find extra class " + className);
      } catch (ClassCastException e) {
        throw new IllegalArgumentException(className + " does not extend " + type.getName());
      }
    }

    public RunnerArgs build() {
      return new RunnerArgs(this);
    }
  }

}