Merge "Snap for 6686656 from d09c30b940da8ebed4b9218f5aff2232f9a90d84 to sdk-release" into sdk-release
diff --git a/global_configuration/com/android/tradefed/config/GlobalConfiguration.java b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
index 9d6c7a9..4e09a9b 100644
--- a/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
@@ -94,7 +94,7 @@
 
     // Configurations to be passed to subprocess: Typical object that are representing the host
     // level and the subprocess should follow too.
-    private static final String[] CONFIGS_FOR_SUBPROCESS_WHITE_LIST =
+    private static final String[] CONFIGS_FOR_SUBPROCESS_ALLOW_LIST =
             new String[] {
                 DEVICE_MANAGER_TYPE_NAME,
                 KEY_STORE_TYPE_NAME,
@@ -777,13 +777,13 @@
 
     /** {@inheritDoc} */
     @Override
-    public File cloneConfigWithFilter(String... whitelistConfigs) throws IOException {
-        return cloneConfigWithFilter(new HashSet<>(), whitelistConfigs);
+    public File cloneConfigWithFilter(String... allowlistConfigs) throws IOException {
+        return cloneConfigWithFilter(new HashSet<>(), allowlistConfigs);
     }
 
     /** {@inheritDoc} */
     @Override
-    public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... whitelistConfigs)
+    public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... allowlistConfigs)
             throws IOException {
         IConfigurationFactory configFactory = getConfigurationFactory();
         IGlobalConfiguration copy = null;
@@ -799,10 +799,10 @@
         File filteredGlobalConfig = FileUtil.createTempFile("filtered_global_config", ".config");
         KXmlSerializer serializer = ConfigurationUtil.createSerializer(filteredGlobalConfig);
         serializer.startTag(null, ConfigurationUtil.CONFIGURATION_NAME);
-        if (whitelistConfigs == null || whitelistConfigs.length == 0) {
-            whitelistConfigs = CONFIGS_FOR_SUBPROCESS_WHITE_LIST;
+        if (allowlistConfigs == null || allowlistConfigs.length == 0) {
+            allowlistConfigs = CONFIGS_FOR_SUBPROCESS_ALLOW_LIST;
         }
-        for (String config : whitelistConfigs) {
+        for (String config : allowlistConfigs) {
             Object configObj = copy.getConfigurationObject(config);
             if (configObj == null) {
                 CLog.d("Object '%s' was not found in global config.", config);
diff --git a/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
index 78096da..1107f5f 100644
--- a/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
@@ -295,7 +295,7 @@
     public void validateOptions() throws ConfigurationException;
 
     /**
-     * Filter the GlobalConfiguration based on a white list and output to an XML file.
+     * Filter the GlobalConfiguration based on a allowed list and output to an XML file.
      *
      * <p>For example, for following configuration:
      * {@code
@@ -318,24 +318,24 @@
      * </xml>
      * }
      *
-     * @param whitelistConfigs a {@link String} array of configs to be included in the new XML file.
+     * @param allowlistConfigs a {@link String} array of configs to be included in the new XML file.
      *     If it's set to <code>null<code/>, a default list should be used.
      * @return the File containing the new filtered global config.
      * @throws IOException
      */
-    public File cloneConfigWithFilter(String... whitelistConfigs) throws IOException;
+    public File cloneConfigWithFilter(String... allowlistConfigs) throws IOException;
 
     /**
      * Filter the GlobalConfiguration based on a white list and output to an XML file.
      * @see #cloneConfigWithFilter(String...)
      *
      * @param exclusionPatterns The pattern of class name to exclude from the dump.
-     * @param whitelistConfigs a {@link String} array of configs to be included in the new XML file.
+     * @param allowlistConfigs a {@link String} array of configs to be included in the new XML file.
      *     If it's set to <code>null<code/>, a default list should be used.
      * @return the File containing the new filtered global config.
      * @throws IOException
      */
-    public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... whitelistConfigs)
+    public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... allowlistConfigs)
             throws IOException;
 
     /**
diff --git a/proto/test_record.proto b/proto/test_record.proto
index 2e558eb..22ba8e7 100644
--- a/proto/test_record.proto
+++ b/proto/test_record.proto
@@ -120,6 +120,13 @@
   NOT_EXECUTED = 30;
   // System under test became unavailable and never came back available again.
   LOST_SYSTEM_UNDER_TEST = 35;
+  // Represent an error caused by an unmet dependency that the current infra
+  // depends on. For example: Unfound resources, Device error, Hardware issue
+  // (lab host, device wear), Underlying tools
+  DEPENDENCY_ISSUE = 40;
+  // Represent an error caused by the input from the end user. For example:
+  // Unexpected option combination, Configuration error, Bad flags
+  CUSTOMER_ISSUE = 41;
 }
 
 // A context to DebugInfo that allows to optionally specify some debugging context.
diff --git a/res/config/suite/test_mapping_suite.xml b/res/config/suite/test_mapping_suite.xml
index 8496121..4753255 100644
--- a/res/config/suite/test_mapping_suite.xml
+++ b/res/config/suite/test_mapping_suite.xml
@@ -37,6 +37,8 @@
     <!-- Tell all HostTests to exclude certain annotations -->
     <option name="test-arg" value="com.android.tradefed.testtype.HostTest:exclude-annotation:android.platform.test.annotations.RestrictedBuildTest" />
     <option name="test-arg" value="com.android.compatibility.common.tradefed.testtype.JarHostTest:exclude-annotation:android.platform.test.annotations.RestrictedBuildTest" />
+    <option name="test-arg" value="com.android.tradefed.testtype.HostTest:exclude-annotation:android.platform.test.annotations.AppModeInstant" />
+    <option name="test-arg" value="com.android.compatibility.common.tradefed.testtype.JarHostTest:exclude-annotation:android.platform.test.annotations.AppModeInstant" />
 
     <!-- Force GTest to report binary name in results -->
     <option name="test-arg" value="com.android.tradefed.testtype.GTest:prepend-filename:true" />
diff --git a/src/com/android/tradefed/build/BootstrapBuildProvider.java b/src/com/android/tradefed/build/BootstrapBuildProvider.java
index 33aaa86..383a30c 100644
--- a/src/com/android/tradefed/build/BootstrapBuildProvider.java
+++ b/src/com/android/tradefed/build/BootstrapBuildProvider.java
@@ -33,6 +33,9 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * A {@link IDeviceBuildProvider} that bootstraps build info from the test device
@@ -80,6 +83,13 @@
     @Option(name="tests-dir", description="Path to top directory of expanded tests zip")
     private File mTestsDir = null;
 
+    @Option(
+            name = "extra-file",
+            description =
+                    "The extra file to be added to the Build Provider. "
+                            + "Can be repeated. For example --extra-file file_key_1=/path/to/file")
+    private Map<String, File> mExtraFiles = new LinkedHashMap<>();
+
     @Override
     public IBuildInfo getBuild() throws BuildRetrievalError {
         throw new UnsupportedOperationException("Call getBuild(ITestDevice)");
@@ -93,6 +103,7 @@
     public IBuildInfo getBuild(ITestDevice device) throws BuildRetrievalError,
             DeviceNotAvailableException {
         IBuildInfo info = new DeviceBuildInfo(mBuildId, mBuildTargetName);
+        addFiles(info, mExtraFiles);
         info.setProperties(BuildInfoProperties.DO_NOT_COPY_ON_SHARDING);
         if (!(device.getIDevice() instanceof StubDevice)) {
             if (!device.waitForDeviceShell(mShellAvailableTimeout * 1000)) {
@@ -141,6 +152,18 @@
         return info;
     }
 
+    /**
+     * Add file to build info.
+     *
+     * @param buildInfo the {@link IBuildInfo} the build info
+     * @param fileMaps the {@link Map} of file_key and file object to be added to the buildInfo
+     */
+    private void addFiles(IBuildInfo buildInfo, Map<String, File> fileMaps) {
+        for (final Entry<String, File> entry : fileMaps.entrySet()) {
+            buildInfo.setFile(entry.getKey(), entry.getValue(), "0");
+        }
+    }
+
     @VisibleForTesting
     ExecutionFiles getInvocationFiles() {
         return CurrentInvocation.getInvocationFiles();
diff --git a/src/com/android/tradefed/build/DependenciesResolver.java b/src/com/android/tradefed/build/DependenciesResolver.java
new file mode 100644
index 0000000..66ccd89
--- /dev/null
+++ b/src/com/android/tradefed/build/DependenciesResolver.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 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 com.android.tradefed.build;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.dependency.TestDependencyResolver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.ExecutionFiles;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.CurrentInvocation;
+import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.testtype.IInvocationContextReceiver;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/** A new type of provider that allows to get all the dependencies for a test. */
+public class DependenciesResolver
+        implements IBuildProvider, IDeviceBuildProvider, IInvocationContextReceiver {
+
+    @Option(name = "build-id", description = "build id to supply.")
+    private String mBuildId = "0";
+
+    @Option(name = "branch", description = "build branch name to supply.")
+    private String mBranch = null;
+
+    @Option(name = "build-flavor", description = "build flavor name to supply.")
+    private String mBuildFlavor = null;
+
+    @Option(name = "build-os", description = "build os name to supply.")
+    private String mBuildOs = "linux";
+
+    @Option(name = "dependency", description = "The set of dependency to provide for the test")
+    private Set<File> mDependencies = new LinkedHashSet<>();
+
+    // TODO(b/157936948): Remove those three options when they are no longer injected
+    @Option(name = "hostname")
+    private String mHostName = null;
+
+    @Option(name = "protocol")
+    private String mProtocol = null;
+
+    @Option(name = "use-build-api ")
+    private boolean mUseBuildApi = true;
+
+    private File mTestsDir;
+    private IInvocationContext mInvocationContext;
+
+    @Override
+    public IBuildInfo getBuild(ITestDevice device)
+            throws BuildRetrievalError, DeviceNotAvailableException {
+        IDeviceBuildInfo build =
+                new DeviceBuildInfo(
+                        mBuildId, String.format("%s-%s-%s", mBranch, mBuildOs, mBuildFlavor));
+        build.setBuildBranch(mBranch);
+        build.setBuildFlavor(mBuildFlavor);
+        for (File dependency : mDependencies) {
+            File f =
+                    TestDependencyResolver.resolveDependencyFromContext(
+                            dependency, build, mInvocationContext);
+            if (f != null) {
+                // TODO: Have way to make named-dependencies
+                getInvocationFiles().put(f.getName(), f);
+                build.setFile(f.getName(), f, "1");
+            }
+        }
+        // Create a tests dir if there are none
+        if (build.getTestsDir() == null) {
+            try {
+                mTestsDir =
+                        FileUtil.createTempDir(
+                                "bootstrap-test-dir",
+                                CurrentInvocation.getInfo(InvocationInfo.WORK_FOLDER));
+            } catch (IOException e) {
+                throw new BuildRetrievalError(
+                        e.getMessage(), e, InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
+            }
+            build.setTestsDir(mTestsDir, "1");
+        }
+        return build;
+    }
+
+    @Override
+    public IBuildInfo getBuild() throws BuildRetrievalError {
+        throw new IllegalArgumentException("Should not be called");
+    }
+
+    @Override
+    public void cleanUp(IBuildInfo info) {
+        info.cleanUp();
+    }
+
+    @Override
+    public void setInvocationContext(IInvocationContext invocationContext) {
+        mInvocationContext = invocationContext;
+    }
+
+    @VisibleForTesting
+    public final Set<File> getDependencies() {
+        return mDependencies;
+    }
+
+    @VisibleForTesting
+    ExecutionFiles getInvocationFiles() {
+        return CurrentInvocation.getInvocationFiles();
+    }
+}
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 7c13735..708b0d2 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -28,6 +28,8 @@
 import com.android.tradefed.command.remote.RemoteClient;
 import com.android.tradefed.command.remote.RemoteException;
 import com.android.tradefed.command.remote.RemoteManager;
+import com.android.tradefed.config.ArgsOptionParser;
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
@@ -40,6 +42,7 @@
 import com.android.tradefed.config.RetryConfigurationFactory;
 import com.android.tradefed.config.SandboxConfigurationFactory;
 import com.android.tradefed.config.proxy.ProxyConfiguration;
+import com.android.tradefed.config.proxy.TradefedDelegator;
 import com.android.tradefed.device.DeviceAllocationState;
 import com.android.tradefed.device.DeviceManager;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -64,6 +67,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ResultForwarder;
+import com.android.tradefed.result.suite.SuiteResultReporter;
 import com.android.tradefed.sandbox.ISandbox;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.suite.retry.RetryRescheduler;
@@ -82,6 +86,7 @@
 import com.android.tradefed.util.keystore.KeyStoreException;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 
 import java.io.File;
 import java.io.IOException;
@@ -1213,6 +1218,32 @@
     }
 
     private IConfiguration createConfiguration(String[] args) throws ConfigurationException {
+        TradefedDelegator delegator = new TradefedDelegator();
+        ArgsOptionParser argsParser = new ArgsOptionParser(delegator);
+        List<String> argsList = new ArrayList<>(Arrays.asList(args));
+        argsList.remove(0);
+        argsParser.parseBestEffort(argsList, true);
+        if (delegator.shouldUseDelegation()) {
+            String[] argsWithoutDelegation = TradefedDelegator.clearCommandline(args);
+            delegator.setCommandLine(argsWithoutDelegation);
+            CLog.d(
+                    "Using commandline arguments as starting command: %s",
+                    Arrays.asList(argsWithoutDelegation));
+            IConfiguration config =
+                    ((ConfigurationFactory) getConfigFactory())
+                            .createPartialConfigurationFromArgs(
+                                    argsWithoutDelegation,
+                                    getKeyStoreClient(),
+                                    ImmutableSet.of(
+                                            Configuration.DEVICE_REQUIREMENTS_TYPE_NAME,
+                                            Configuration.LOGGER_TYPE_NAME,
+                                            Configuration.LOG_SAVER_TYPE_NAME,
+                                            Configuration.RESULT_REPORTER_TYPE_NAME));
+            config.setConfigurationObject(TradefedDelegator.DELEGATE_OBJECT, delegator);
+            setDelegateLevelReporting(config);
+            return config;
+        }
+
         // check if the command should be sandboxed
         if (isCommandSandboxed(args)) {
             // Create an sandboxed configuration based on the sandbox of the scheduler.
@@ -1236,6 +1267,20 @@
         return config;
     }
 
+    private void setDelegateLevelReporting(IConfiguration config) {
+        List<ITestInvocationListener> delegateReporters = new ArrayList<>();
+        // For debugging in the console, add a printer
+        delegateReporters.add(new SuiteResultReporter());
+        for (ITestInvocationListener listener : config.getTestInvocationListeners()) {
+            // Add infra reporter if configured.
+            if ("com.google.android.tradefed.result.teststorage.ResultReporter"
+                    .equals(listener.getClass().getCanonicalName())) {
+                delegateReporters.add(listener);
+            }
+        }
+        config.setTestInvocationListeners(delegateReporters);
+    }
+
     private boolean internalAddCommand(String[] args, String cmdFilePath)
             throws ConfigurationException {
         assertStarted();
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index b4fce56..e84fa3f 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -681,6 +681,24 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void safeInjectOptionValues(List<OptionDef> optionDefs) throws ConfigurationException {
+        OptionSetter optionSetter = createOptionSetter();
+        for (OptionDef optionDef : optionDefs) {
+            try {
+                internalInjectOptionValue(
+                        optionSetter,
+                        optionDef.name,
+                        optionDef.key,
+                        optionDef.value,
+                        optionDef.source);
+            } catch (ConfigurationException e) {
+                // Ignoring
+            }
+        }
+    }
+
     /**
      * Creates a shallow copy of this object.
      */
@@ -1126,6 +1144,21 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<String> setBestEffortOptionsFromCommandLineArgs(
+            List<String> listArgs, IKeyStoreClient keyStoreClient) throws ConfigurationException {
+        // We get all the objects except the one describing the Configuration itself which does not
+        // allow passing its option via command line.
+        ArgsOptionParser parser =
+                new ArgsOptionParser(
+                        getAllConfigurationObjects(CONFIGURATION_DESCRIPTION_TYPE_NAME, true));
+        if (keyStoreClient != null) {
+            parser.setKeyStore(keyStoreClient);
+        }
+        return parser.parseBestEffort(listArgs, /* Force continue */ true);
+    }
+
     /**
      * Outputs a command line usage help text for this configuration to given
      * printStream.
diff --git a/src/com/android/tradefed/config/ConfigurationDef.java b/src/com/android/tradefed/config/ConfigurationDef.java
index 2774b38..d7ec6c2 100644
--- a/src/com/android/tradefed/config/ConfigurationDef.java
+++ b/src/com/android/tradefed/config/ConfigurationDef.java
@@ -26,6 +26,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -65,6 +66,7 @@
     }
 
     private boolean mMultiDeviceMode = false;
+    private boolean mFilteredObjects = false;
     private Map<String, Boolean> mExpectedDevices = new LinkedHashMap<>();
     private static final Pattern MULTI_PATTERN = Pattern.compile("(.*)(:)(.*)");
     public static final String DEFAULT_DEVICE_NAME = "DEFAULT_DEVICE";
@@ -183,6 +185,20 @@
      * @throws ConfigurationException if configuration could not be created
      */
     public IConfiguration createConfiguration() throws ConfigurationException {
+        return createConfiguration(null);
+    }
+
+    /**
+     * Creates a configuration from the info stored in this definition, and populates its fields
+     * with the provided option values.
+     *
+     * @param allowedObjects the set of TF objects that we will create out of the full configuration
+     * @return the created {@link IConfiguration}
+     * @throws ConfigurationException if configuration could not be created
+     */
+    public IConfiguration createConfiguration(Set<String> allowedObjects)
+            throws ConfigurationException {
+        mFilteredObjects = false;
         IConfiguration config = new Configuration(getName(), getDescription());
         List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
         IDeviceConfiguration defaultDeviceConfig =
@@ -246,6 +262,11 @@
             boolean shouldAddToFlatConfig = true;
 
             for (ConfigObjectDef configDef : objClassEntry.getValue()) {
+                if (allowedObjects != null && !allowedObjects.contains(objClassEntry.getKey())) {
+                    CLog.d("Skipping creation of %s", objClassEntry.getKey());
+                    mFilteredObjects = true;
+                    continue;
+                }
                 Object configObject = null;
                 try {
                     configObject = createObject(objClassEntry.getKey(), configDef.mClassName);
@@ -343,7 +364,13 @@
 
     protected void injectOptions(IConfiguration config, List<OptionDef> optionList)
             throws ConfigurationException {
-        config.injectOptionValues(optionList);
+        if (mFilteredObjects) {
+            // If we filtered out some objects, some options might not be injectable anymore, so
+            // we switch to safe inject to avoid errors due to the filtering.
+            config.safeInjectOptionValues(optionList);
+        } else {
+            config.injectOptionValues(optionList);
+        }
     }
 
     /**
diff --git a/src/com/android/tradefed/config/ConfigurationFactory.java b/src/com/android/tradefed/config/ConfigurationFactory.java
index c682694..7e29821 100644
--- a/src/com/android/tradefed/config/ConfigurationFactory.java
+++ b/src/com/android/tradefed/config/ConfigurationFactory.java
@@ -422,7 +422,7 @@
                     break;
                 case ".tf_yaml":
                     ConfigurationYamlParser yamlParser = new ConfigurationYamlParser();
-                    yamlParser.parse(def, name, bufStream);
+                    yamlParser.parse(def, name, bufStream, false);
                     break;
                 default:
                     throw new ConfigurationException(
@@ -520,11 +520,13 @@
         if (arrayArgs.length == 0) {
             throw new ConfigurationException("Configuration to run was not specified");
         }
+
         List<String> listArgs = new ArrayList<String>(arrayArgs.length);
         // FIXME: Update parsing to not care about arg order.
         String[] reorderedArrayArgs = reorderArgs(arrayArgs);
         IConfiguration config =
-                internalCreateConfigurationFromArgs(reorderedArrayArgs, listArgs, keyStoreClient);
+                internalCreateConfigurationFromArgs(
+                        reorderedArrayArgs, listArgs, keyStoreClient, null);
         config.setCommandLine(arrayArgs);
         if (listArgs.contains("--" + CommandOptions.DRY_RUN_OPTION)
                 || listArgs.contains("--" + CommandOptions.NOISY_DRY_RUN_OPTION)) {
@@ -549,23 +551,45 @@
         return config;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public IConfiguration createPartialConfigurationFromArgs(
+            String[] arrayArgs, IKeyStoreClient keyStoreClient, Set<String> allowedObjects)
+            throws ConfigurationException {
+        if (arrayArgs.length == 0) {
+            throw new ConfigurationException("Configuration to run was not specified");
+        }
+
+        List<String> listArgs = new ArrayList<String>(arrayArgs.length);
+        String[] reorderedArrayArgs = reorderArgs(arrayArgs);
+        IConfiguration config =
+                internalCreateConfigurationFromArgs(
+                        reorderedArrayArgs, listArgs, keyStoreClient, allowedObjects);
+        config.setCommandLine(arrayArgs);
+        List<String> leftOver =
+                config.setBestEffortOptionsFromCommandLineArgs(listArgs, keyStoreClient);
+        CLog.d("Non-applied arguments: %s", leftOver);
+        return config;
+    }
+
     /**
      * Creates a {@link Configuration} from the name given in arguments.
-     * <p/>
-     * Note will not populate configuration with values from options
      *
-     * @param arrayArgs the full list of command line arguments, including the
-     *            config name
-     * @param optionArgsRef an empty list, that will be populated with the
-     *            option arguments left to be interpreted
-     * @param keyStoreClient {@link IKeyStoreClient} keystore client to use if
-     *            any.
-     * @return An {@link IConfiguration} object representing the configuration
-     *         that was loaded
+     * <p>Note will not populate configuration with values from options
+     *
+     * @param arrayArgs the full list of command line arguments, including the config name
+     * @param optionArgsRef an empty list, that will be populated with the option arguments left to
+     *     be interpreted
+     * @param keyStoreClient {@link IKeyStoreClient} keystore client to use if any.
+     * @param allowedObjects config object that are allowed to be created.
+     * @return An {@link IConfiguration} object representing the configuration that was loaded
      * @throws ConfigurationException
      */
-    private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
-            List<String> optionArgsRef, IKeyStoreClient keyStoreClient)
+    private IConfiguration internalCreateConfigurationFromArgs(
+            String[] arrayArgs,
+            List<String> optionArgsRef,
+            IKeyStoreClient keyStoreClient,
+            Set<String> allowedObjects)
             throws ConfigurationException {
         final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs));
         // first arg is config name
@@ -586,7 +610,7 @@
             throw new ConfigurationException(
                     String.format("Unused template:map parameters: %s", uniqueMap.toString()));
         }
-        return configDef.createConfiguration();
+        return configDef.createConfiguration(allowedObjects);
     }
 
     private Map<String, String> extractTemplates(
@@ -616,6 +640,14 @@
                     }
                 }
                 return parserSettings.templateMap.getUniqueMap();
+            case ".tf_yaml":
+                // We parse the arguments but don't support template for YAML
+                final ArgsOptionParser allArgsParser = new ArgsOptionParser();
+                if (keyStoreClient != null) {
+                    allArgsParser.setKeyStore(keyStoreClient);
+                }
+                optionArgsRef.addAll(allArgsParser.parseBestEffort(listArgs));
+                return new HashMap<>();
             default:
                 return new HashMap<>();
         }
@@ -792,8 +824,9 @@
     @Override
     public void printHelpForConfig(String[] args, boolean importantOnly, PrintStream out) {
         try {
-            IConfiguration config = internalCreateConfigurationFromArgs(args,
-                    new ArrayList<String>(args.length), null);
+            IConfiguration config =
+                    internalCreateConfigurationFromArgs(
+                            args, new ArrayList<String>(args.length), null, null);
             config.printCommandUsage(importantOnly, out);
         } catch (ConfigurationException e) {
             // config must not be specified. Print generic help
diff --git a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
index 63254cf..9bfc06c 100644
--- a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.logger.CurrentInvocation;
 import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
+import com.android.tradefed.invoker.logger.InvocationLocal;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.util.FileUtil;
@@ -30,6 +31,7 @@
 import com.android.tradefed.util.ZipUtil2;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 
 import java.io.File;
 import java.io.IOException;
@@ -67,8 +69,26 @@
     // Query key for requesting a download to be optional, so if it fails we don't replace it.
     public static final String OPTIONAL_KEY = "optional";
 
+    /**
+     * Loads file resolvers using a dedicated {@link ServiceFileResolverLoader} that is scoped to
+     * each invocation.
+     */
+    // TODO(hzalek): Store a DynamicRemoteFileResolver instance per invocation to avoid locals.
     private static final FileResolverLoader DEFAULT_FILE_RESOLVER_LOADER =
-            new ServiceFileResolverLoader();
+            new FileResolverLoader() {
+                private final InvocationLocal<FileResolverLoader> mInvocationLoader =
+                        new InvocationLocal<FileResolverLoader>() {
+                            @Override
+                            protected FileResolverLoader initialValue() {
+                                return new ServiceFileResolverLoader();
+                            }
+                        };
+
+                @Override
+                public IRemoteFileResolver load(String scheme, Map<String, String> config) {
+                    return mInvocationLoader.get().load(scheme, config);
+                }
+            };
 
     private final FileResolverLoader mFileResolverLoader;
 
@@ -286,8 +306,6 @@
                             "Failed to parse the remote zip file path: %s", remoteZipFilePath),
                     e);
         }
-        IRemoteFileResolver resolver = getResolver(protocol);
-
         queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
         if (includeFilters != null) {
             queryArgs.put("include_filters", String.join(";", includeFilters));
@@ -297,6 +315,7 @@
         }
         // Downloaded individual files should be saved to destDir, return value is not needed.
         try {
+            IRemoteFileResolver resolver = getResolver(protocol);
             resolver.setPrimaryDevice(mDevice);
             resolver.resolveRemoteFiles(new File(remoteZipFilePath), queryArgs);
         } catch (BuildRetrievalError e) {
@@ -304,14 +323,20 @@
                 CLog.d(
                         "Failed to partially download '%s' but marked optional so skipping: %s",
                         remoteZipFilePath, e.getMessage());
-            } else {
-                throw e;
+                return;
             }
+
+            throw e;
         }
     }
 
-    protected IRemoteFileResolver getResolver(String protocol) {
+    private IRemoteFileResolver getResolver(String protocol) throws BuildRetrievalError {
+        try {
         return mFileResolverLoader.load(protocol, mExtraArgs);
+        } catch (ResolverLoadingException e) {
+            throw new BuildRetrievalError(
+                    String.format("Could not load resolver for protocol %s", protocol), e);
+        }
     }
 
     @VisibleForTesting
@@ -357,28 +382,26 @@
             CLog.e(e);
             return null;
         }
-        IRemoteFileResolver resolver = getResolver(protocol);
-        if (resolver != null) {
-            try {
-                CLog.d(
-                        "Considering option '%s' with path: '%s' for download.",
-                        option.name(), path);
-                // Overrides query args
-                query.putAll(mExtraArgs);
-                resolver.setPrimaryDevice(mDevice);
-                return resolver.resolveRemoteFiles(fileToResolve, query);
-            } catch (BuildRetrievalError e) {
-                if (isOptional(query)) {
-                    CLog.d(
-                            "Failed to resolve '%s' but marked optional so skipping: %s",
-                            fileToResolve, e.getMessage());
-                } else {
-                    throw e;
-                }
+
+        try {
+            IRemoteFileResolver resolver = getResolver(protocol);
+            if (resolver == null) {
+                return null;
             }
+
+            CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
+            resolver.setPrimaryDevice(mDevice);
+            return resolver.resolveRemoteFiles(fileToResolve, query);
+        } catch (BuildRetrievalError e) {
+            if (isOptional(query)) {
+                CLog.d(
+                        "Failed to resolve '%s' but marked optional so skipping: %s",
+                        fileToResolve, e.getMessage());
+                return null;
+            }
+
+            throw e;
         }
-        // Not a remote file
-        return null;
     }
 
     /**
@@ -415,18 +438,46 @@
          * @param scheme the URI scheme that the loaded resolver is expected to handle.
          * @param config a map of all dynamic resolver configuration key-value pairs specified by
          *     the 'dynamic-resolver-args' TF command-line flag.
+         * @throws ResolverLoadingException if the resolver that handles the specified scheme cannot
+         *     be loaded and/or initialized.
          */
+        @Nullable
         IRemoteFileResolver load(String scheme, Map<String, String> config);
     }
 
+    /** Exception thrown if a resolver cannot be loaded or initialized. */
+    @VisibleForTesting
+    static final class ResolverLoadingException extends RuntimeException {
+        public ResolverLoadingException(@Nullable String message) {
+            super(message);
+        }
+
+        public ResolverLoadingException(@Nullable Throwable cause) {
+            super(cause);
+        }
+
+        public ResolverLoadingException(@Nullable String message, @Nullable Throwable cause) {
+            super(message, cause);
+        }
+    }
+
     /**
      * Loads and caches file resolvers using the service loading facility.
      *
      * <p>This implementation uses the service loading facility to find and cache available
      * resolvers on the first call to {@code load}.
      *
+     * <p>Any {@link Option}-annotated fields defined in loaded resolvers are initialized from the
+     * provided key-value pairs using the standard TF option-setting mechanism. Resolvers can define
+     * options that themselves require resolution as long as it causes no cycles during
+     * initialization.
+     *
+     * <p>Resolvers are loaded eagerly using ServiceLoader but have their options initialized only
+     * when first used. This avoids exceptions due to missing options in resolvers that are
+     * available on the class path but never used to load any files.
+     *
      * <p>This implementation is thread-safe and ensures that any loaded resolvers are loaded at
-     * most once per instance unless an exception is thrown.
+     * most once per instance.
      */
     @ThreadSafe
     @VisibleForTesting
@@ -436,7 +487,7 @@
         private final Supplier<ClassLoader> mClassLoaderSupplier;
 
         @GuardedBy("this")
-        private @Nullable ImmutableMap<String, IRemoteFileResolver> mResolvers;
+        private @Nullable LoaderState mLoaderState;
 
         ServiceFileResolverLoader() {
             mClassLoaderSupplier = () -> Thread.currentThread().getContextClassLoader();
@@ -448,12 +499,14 @@
 
         @Override
         public synchronized IRemoteFileResolver load(String scheme, Map<String, String> config) {
-            if (mResolvers != null) {
-                return mResolvers.get(scheme);
+            if (mLoaderState != null) {
+                return mLoaderState.getAndInit(scheme);
             }
 
             // We use an intermediate map because the ImmutableMap builder throws if we add multiple
-            // entries with the same key.
+            // entries with the same key. Note that we don't worry about setting any state that
+            // prevents this code from re-executing since failures loading service providers throws
+            // an Error which bubbles all the way to the top.
             Map<String, IRemoteFileResolver> resolvers = new HashMap<>();
             ServiceLoader<IRemoteFileResolver> serviceLoader =
                     ServiceLoader.load(IRemoteFileResolver.class, mClassLoaderSupplier.get());
@@ -462,8 +515,134 @@
                 resolvers.putIfAbsent(resolver.getSupportedProtocol(), resolver);
             }
 
-            mResolvers = ImmutableMap.copyOf(resolvers);
-            return resolvers.get(scheme);
+            mLoaderState = new LoaderState(resolvers, config);
+            return mLoaderState.getAndInit(scheme);
+        }
+
+        /** Stores the state of loaded file resolvers. */
+        private static final class LoaderState {
+            private final ImmutableMap<String, String> mConfig;
+            private final ImmutableMap<String, ResolverState> mState;
+
+            LoaderState(Map<String, IRemoteFileResolver> resolvers, Map<String, String> config) {
+                this.mState =
+                        ImmutableMap.copyOf(
+                                Maps.transformValues(resolvers, r -> new ResolverState(r)));
+                this.mConfig = ImmutableMap.copyOf(config);
+            }
+
+            /** Returns an initialized resolver instance for the specified scheme. */
+            @Nullable
+            IRemoteFileResolver getAndInit(String scheme) {
+                ResolverState state = mState.get(scheme);
+                if (state == null) {
+                    return null;
+                }
+
+                return state.getAndInit(this);
+            }
+
+            void resolve(IRemoteFileResolver resolver)
+                    throws ConfigurationException, BuildRetrievalError {
+                // The device isn't set when resolving dynamic options because we don't want to load
+                // device-specific configuration when initializing pseudo-static resolvers that
+                // could out-live a particular device.
+                OptionSetter setter = new OptionSetter(resolver);
+
+                for (Map.Entry<String, String> e : mConfig.entrySet()) {
+                    String name = e.getKey();
+
+                    // Note that we don't throw for options that don't exist.
+                    if (setter.fieldsForArgNoThrow(name) == null) {
+                        // TODO(hzalek): Consider throwing when the option doesn't exist and is
+                        // qualified using one of the option source's aliases.
+                        // option name uses one of
+                        // the option source's aliases
+                        continue;
+                    }
+
+                    if (setter.isMapOption(name)) {
+                        throw new ConfigurationException("Map options are not supported: " + name);
+                    }
+
+                    setter.setOptionValue(name, e.getValue());
+                }
+
+                Collection<String> missingOptions = setter.getUnsetMandatoryOptions();
+                if (!missingOptions.isEmpty()) {
+                    throw new ConfigurationException(
+                            String.format(
+                                    "Found missing mandatory options %s for resolver %s",
+                                    missingOptions, resolver.toString()));
+                }
+
+                DynamicRemoteFileResolver dynamicResolver =
+                        new DynamicRemoteFileResolver((scheme, unused) -> getAndInit(scheme));
+                dynamicResolver.addExtraArgs(mConfig);
+                setter.validateRemoteFilePath(dynamicResolver);
+            }
+
+            /** Stores the resolver and its initialization state. */
+            static final class ResolverState {
+                final IRemoteFileResolver mResolver;
+
+                /**
+                 * The initialization state where {@code null} means never initialized, {@code
+                 * false} means started, and {@code true} means done.
+                 */
+                @Nullable Boolean mDone;
+
+                /**
+                 * The exception thrown when initializing the resolver to ensure that we only do it
+                 * once.
+                 */
+                @Nullable ResolverLoadingException mException;
+
+                ResolverState(IRemoteFileResolver resolver) {
+                    this.mResolver = resolver;
+                }
+
+                IRemoteFileResolver getAndInit(LoaderState context) {
+                    if (Boolean.TRUE.equals(mDone)) {
+                        return getOrThrow();
+                    }
+
+                    if (Boolean.FALSE.equals(mDone)) {
+                        // No need to catch or store the exception since it gets thrown in the
+                        // recursive
+                        // call to the dynamic resolver as a BuildRetrievalError which we already
+                        // catch.
+                        throw new ResolverLoadingException(
+                                "Cycle detected while initializing resolver options: "
+                                        + mResolver.toString());
+                    }
+
+                    CLog.i("Initializing file resolver options: %s", mResolver);
+                    mDone = Boolean.FALSE;
+
+                    try {
+                        context.resolve(mResolver);
+                    } catch (BuildRetrievalError | ConfigurationException e) {
+                        mException =
+                                new ResolverLoadingException(
+                                        "Could not initialize resolver options: "
+                                                + mResolver.toString(),
+                                        e);
+                        throw mException;
+                    } finally {
+                        mDone = Boolean.TRUE;
+                    }
+
+                    return mResolver;
+                }
+
+                private IRemoteFileResolver getOrThrow() {
+                    if (mException != null) {
+                        throw mException;
+                    }
+                    return mResolver;
+                }
+            }
         }
     }
 }
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index 128b887..1c4e714 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -257,6 +257,17 @@
     public void injectOptionValues(List<OptionDef> optionDefs) throws ConfigurationException;
 
     /**
+     * Inject multiple option values into the set of configuration objects without throwing if one
+     * of the option cannot be applied.
+     *
+     * <p>Useful to inject many option values at once after creating a new object.
+     *
+     * @param optionDefs a list of option defs to inject
+     * @throws ConfigurationException if failed to create the {@link OptionSetter}
+     */
+    public void safeInjectOptionValues(List<OptionDef> optionDefs) throws ConfigurationException;
+
+    /**
      * Create a shallow copy of this object.
      *
      * @return a {link IConfiguration} copy
@@ -492,6 +503,19 @@
             throws ConfigurationException;
 
     /**
+     * Set the config {@link Option} fields with given set of command line arguments using a best
+     * effort approach.
+     *
+     * <p>See {@link ArgsOptionParser} for expected format
+     *
+     * @param listArgs the command line arguments
+     * @param keyStoreClient {@link IKeyStoreClient} to use.
+     * @return the unconsumed arguments
+     */
+    public List<String> setBestEffortOptionsFromCommandLineArgs(
+            List<String> listArgs, IKeyStoreClient keyStoreClient) throws ConfigurationException;
+
+    /**
      * Outputs a command line usage help text for this configuration to given printStream.
      *
      * @param importantOnly if <code>true</code> only print help for the important options
diff --git a/src/com/android/tradefed/config/IConfigurationFactory.java b/src/com/android/tradefed/config/IConfigurationFactory.java
index 0cda174..08b74b5 100644
--- a/src/com/android/tradefed/config/IConfigurationFactory.java
+++ b/src/com/android/tradefed/config/IConfigurationFactory.java
@@ -20,6 +20,7 @@
 
 import java.io.PrintStream;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Factory for creating {@link IConfiguration}s
@@ -76,6 +77,20 @@
             IKeyStoreClient keyStoreClient) throws ConfigurationException;
 
     /**
+     * Create a configuration that only contains a set of selected objects.
+     *
+     * @param arrayArgs The command line arguments
+     * @param keyStoreClient A {@link IKeyStoreClient} which is used to obtain sensitive info in the
+     *     args.
+     * @param allowedObjects The set of allowed objects to be created
+     * @return The loaded {@link IConfiguration}.
+     * @throws ConfigurationException if configuration could not be loaded
+     */
+    public IConfiguration createPartialConfigurationFromArgs(
+            String[] arrayArgs, IKeyStoreClient keyStoreClient, Set<String> allowedObjects)
+            throws ConfigurationException;
+
+    /**
      * Create a {@link IGlobalConfiguration} from command line arguments.
      * <p/>
      * Expected format is "CONFIG [options]", where CONFIG is the built-in configuration name or
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index d562cc0..49d0bec 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -328,14 +328,22 @@
     }
 
     private OptionFieldsForName fieldsForArg(String name) throws ConfigurationException {
-        OptionFieldsForName fields = mOptionMap.get(name);
-        if (fields == null || fields.size() == 0) {
+        OptionFieldsForName fields = fieldsForArgNoThrow(name);
+        if (fields == null) {
             throw new ConfigurationException(String.format("Could not find option with name %s",
                     name));
         }
         return fields;
     }
 
+    OptionFieldsForName fieldsForArgNoThrow(String name) throws ConfigurationException {
+        OptionFieldsForName fields = mOptionMap.get(name);
+        if (fields == null || fields.size() == 0) {
+            return null;
+        }
+        return fields;
+    }
+
     /**
      * Returns a string describing the type of the field with given name.
      *
diff --git a/src/com/android/tradefed/config/proxy/AutomatedReporters.java b/src/com/android/tradefed/config/proxy/AutomatedReporters.java
new file mode 100644
index 0000000..57f7d53
--- /dev/null
+++ b/src/com/android/tradefed/config/proxy/AutomatedReporters.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 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 com.android.tradefed.config.proxy;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.proto.StreamProtoResultReporter;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+
+/**
+ * Class that defines the mapping from Tradefed automated reporters.
+ *
+ * <p>TODO: Formalize how to expose the list of supported automation.
+ */
+public class AutomatedReporters {
+
+    private static final String PROTO_REPORTING_PORT = "PROTO_REPORTING_PORT";
+    private static final ImmutableSet<String> REPORTER_MAPPING =
+            ImmutableSet.of(PROTO_REPORTING_PORT);
+
+    /**
+     * Complete the listeners based on the environment.
+     *
+     * @param configuration The configuration to complete
+     */
+    public void applyAutomatedReporters(IConfiguration configuration) {
+        for (String key : REPORTER_MAPPING) {
+            String envValue = getEnv(key);
+            if (envValue == null) {
+                continue;
+            }
+            switch (key) {
+                case PROTO_REPORTING_PORT:
+                    StreamProtoResultReporter reporter = new StreamProtoResultReporter();
+                    try {
+                        reporter.setProtoReportPort(Integer.parseInt(envValue));
+                        List<ITestInvocationListener> listeners =
+                                configuration.getTestInvocationListeners();
+                        listeners.add(reporter);
+                        configuration.setTestInvocationListeners(listeners);
+                    } catch (NumberFormatException e) {
+                        CLog.e(e);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    protected String getEnv(String key) {
+        return System.getenv(key);
+    }
+}
diff --git a/src/com/android/tradefed/config/proxy/TradefedDelegator.java b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
new file mode 100644
index 0000000..d50c8a1
--- /dev/null
+++ b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2020 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 com.android.tradefed.config.proxy;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+
+import com.google.common.base.Joiner;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Objects that helps delegating the invocation to another Tradefed binary. */
+public class TradefedDelegator {
+
+    /** The object reference in the configuration. */
+    public static final String DELEGATE_OBJECT = "DELEGATE";
+
+    private static final String DELETEGATED_OPTION_NAME = "delegated-tf";
+
+    @Option(
+            name = DELETEGATED_OPTION_NAME,
+            description =
+                    "Points to the root dir of another Tradefed binary that will be used to drive the invocation")
+    private File mDelegatedTfRootDir;
+
+    private String[] mCommandLine = null;
+
+    /** Whether or not trigger the delegation logic. */
+    public boolean shouldUseDelegation() {
+        return mDelegatedTfRootDir != null;
+    }
+
+    /** Returns the directory of a Tradefed binary. */
+    public File getTfRootDir() {
+        return mDelegatedTfRootDir;
+    }
+
+    /** Creates the classpath out of the jars in the directory. */
+    public String createClasspath() {
+        List<File> jars =
+                Arrays.asList(
+                        mDelegatedTfRootDir.listFiles(
+                                new FileFilter() {
+                                    @Override
+                                    public boolean accept(File pathname) {
+                                        return pathname.getName().endsWith(".jar");
+                                    }
+                                }));
+        return Joiner.on(":").join(jars);
+    }
+
+    public void setCommandLine(String[] command) {
+        mCommandLine = command;
+    }
+
+    public String[] getCommandLine() {
+        return mCommandLine;
+    }
+
+    public static String[] clearCommandline(String[] originalCommand)
+            throws ConfigurationException {
+        List<String> argsList = new ArrayList<>(Arrays.asList(originalCommand));
+        try {
+            while (argsList.contains("--" + DELETEGATED_OPTION_NAME)) {
+                int index = argsList.indexOf("--" + DELETEGATED_OPTION_NAME);
+                if (index != -1) {
+                    argsList.remove(index + 1);
+                    argsList.remove(index);
+                }
+            }
+        } catch (RuntimeException e) {
+            throw new ConfigurationException(e.getMessage(), e);
+        }
+        return argsList.toArray(new String[0]);
+    }
+}
diff --git a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
index 9ac47bd..1aef60c 100644
--- a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
+++ b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
@@ -46,6 +46,7 @@
     private static final List<String> REQUIRED_KEYS =
             ImmutableList.of(DESCRIPTION_KEY, DEPENDENCIES_KEY, TESTS_KEY);
     private Set<String> mSeenKeys = new HashSet<>();
+    private boolean mCreatedAsModule = false;
 
     /**
      * Main entry point of the parser to parse a given YAML file into Trade Federation objects.
@@ -53,9 +54,15 @@
      * @param configDef
      * @param source
      * @param yamlInput
+     * @param createdAsModule
      */
-    public void parse(ConfigurationDef configDef, String source, InputStream yamlInput)
+    public void parse(
+            ConfigurationDef configDef,
+            String source,
+            InputStream yamlInput,
+            boolean createdAsModule)
             throws ConfigurationException {
+        mCreatedAsModule = createdAsModule;
         // We don't support multi-device in YAML
         configDef.setMultiDeviceMode(false);
         Yaml yaml = new Yaml();
@@ -105,7 +112,10 @@
 
         // Add default configured objects
         LoaderConfiguration loadConfiguration = new LoaderConfiguration();
-        loadConfiguration.setConfigurationDef(configDef).addDependencies(dependencyFiles);
+        loadConfiguration
+                .setConfigurationDef(configDef)
+                .addDependencies(dependencyFiles)
+                .setCreatedAsModule(mCreatedAsModule);
         ServiceLoader<IDefaultObjectLoader> serviceLoader =
                 ServiceLoader.load(IDefaultObjectLoader.class);
         for (IDefaultObjectLoader loader : serviceLoader) {
diff --git a/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java b/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java
index b4b4501..284e44f 100644
--- a/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java
+++ b/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java
@@ -33,6 +33,7 @@
     public class LoaderConfiguration {
 
         private ConfigurationDef mConfigDef;
+        private boolean mCreatedAsModule = false;
         private Set<String> mDependencies = new LinkedHashSet<>();
 
         public LoaderConfiguration setConfigurationDef(ConfigurationDef configDef) {
@@ -50,6 +51,11 @@
             return this;
         }
 
+        public LoaderConfiguration setCreatedAsModule(boolean createdAsModule) {
+            mCreatedAsModule = createdAsModule;
+            return this;
+        }
+
         public ConfigurationDef getConfigDef() {
             return mConfigDef;
         }
@@ -57,5 +63,9 @@
         public Set<String> getDependencies() {
             return mDependencies;
         }
+
+        public boolean createdAsModule() {
+            return mCreatedAsModule;
+        }
     }
 }
diff --git a/src/com/android/tradefed/config/yaml/OpenObjectLoader.java b/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
index 839ee9b..af7e11b 100644
--- a/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
+++ b/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
@@ -15,8 +15,10 @@
  */
 package com.android.tradefed.config.yaml;
 
-import com.android.tradefed.build.BootstrapBuildProvider;
+import com.android.tradefed.build.DependenciesResolver;
 import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.log.FileLogger;
 import com.android.tradefed.result.suite.SuiteResultReporter;
 
 /** Loader for the default objects available in AOSP. */
@@ -24,16 +26,46 @@
 
     @Override
     public void addDefaultObjects(LoaderConfiguration loadConfiguration) {
+        // Only add the objects below if it's created as a stand alone configuration, in suite as
+        // a module, it will be resolving object from the top level suite.
+        if (loadConfiguration.createdAsModule()) {
+            return;
+        }
+        // Logger
+        loadConfiguration
+                .getConfigDef()
+                .addConfigObjectDef(Configuration.LOGGER_TYPE_NAME, FileLogger.class.getName());
+        // Result Reporters
         loadConfiguration
                 .getConfigDef()
                 .addConfigObjectDef(
                         Configuration.RESULT_REPORTER_TYPE_NAME,
                         SuiteResultReporter.class.getName());
-        // TODO: Replace by a provider that can handle both local and remote
-        loadConfiguration
-                .getConfigDef()
-                .addConfigObjectDef(
-                        Configuration.BUILD_PROVIDER_TYPE_NAME,
-                        BootstrapBuildProvider.class.getName());
+        // Build
+        int classCount =
+                loadConfiguration
+                        .getConfigDef()
+                        .addConfigObjectDef(
+                                Configuration.BUILD_PROVIDER_TYPE_NAME,
+                                DependenciesResolver.class.getName());
+        // Set all the dependencies on the provider
+        for (String depencency : loadConfiguration.getDependencies()) {
+            String optionName =
+                    String.format(
+                            "%s%c%d%c%s",
+                            DependenciesResolver.class.getName(),
+                            OptionSetter.NAMESPACE_SEPARATOR,
+                            classCount,
+                            OptionSetter.NAMESPACE_SEPARATOR,
+                            "dependency");
+            loadConfiguration
+                    .getConfigDef()
+                    .addOptionDef(
+                            optionName,
+                            null,
+                            depencency,
+                            loadConfiguration.getConfigDef().getName(),
+                            Configuration.BUILD_PROVIDER_TYPE_NAME);
+        }
     }
 }
diff --git a/src/com/android/tradefed/dependency/TestDependencyResolver.java b/src/com/android/tradefed/dependency/TestDependencyResolver.java
new file mode 100644
index 0000000..5034b09
--- /dev/null
+++ b/src/com/android/tradefed/dependency/TestDependencyResolver.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 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 com.android.tradefed.dependency;
+
+import com.android.tradefed.build.BuildRetrievalError;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.invoker.IInvocationContext;
+
+import java.io.File;
+
+/** Helper to resolve dependencies if needed. */
+public class TestDependencyResolver {
+
+    /**
+     * Resolve a single dependency based on some context.
+     *
+     * @param dependency The dependency to be resolved.
+     * @param build The current build information being created.
+     * @param context The invocation context.
+     * @return The resolved dependency or null if unresolved.
+     * @throws BuildRetrievalError
+     */
+    public static File resolveDependencyFromContext(
+            File dependency, IBuildInfo build, IInvocationContext context)
+            throws BuildRetrievalError {
+        if (dependency.exists()) {
+            return dependency;
+        }
+        // TODO: Implement the core logic
+        return null;
+    }
+}
diff --git a/src/com/android/tradefed/device/NativeDeviceStateMonitor.java b/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
index 5d55bb7..ebe4526 100644
--- a/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
+++ b/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.device.IDeviceManager.IFastbootListener;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunInterruptedException;
 import com.android.tradefed.util.RunUtil;
 
 import java.io.IOException;
@@ -432,7 +433,7 @@
             } catch (InterruptedException e) {
                 CLog.w("wait for device bootloader state update interrupted");
                 CLog.w(e);
-                throw new RuntimeException(e);
+                throw new RunInterruptedException(e);
             } finally {
                 mMgr.removeFastbootListener(listener);
             }
@@ -455,7 +456,7 @@
             } catch (InterruptedException e) {
                 CLog.w("wait for device state interrupted");
                 CLog.w(e);
-                throw new RuntimeException(e);
+                throw new RunInterruptedException(e);
             } finally {
                 removeDeviceStateListener(listener);
             }
diff --git a/src/com/android/tradefed/device/WaitDeviceRecovery.java b/src/com/android/tradefed/device/WaitDeviceRecovery.java
index 635f1fb..80a8f7d 100644
--- a/src/com/android/tradefed/device/WaitDeviceRecovery.java
+++ b/src/com/android/tradefed/device/WaitDeviceRecovery.java
@@ -231,9 +231,10 @@
             }
         }
         // If no reboot was done, waitForDeviceAvailable has already been checked.
-        throw new DeviceUnresponsiveException(String.format(
-                "Device %s is online but unresponsive", monitor.getSerialNumber()),
-                monitor.getSerialNumber());
+        throw new DeviceUnresponsiveException(
+                String.format("Device %s is online but unresponsive", monitor.getSerialNumber()),
+                monitor.getSerialNumber(),
+                DeviceErrorIdentifier.DEVICE_UNRESPONSIVE);
     }
 
     /**
diff --git a/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
new file mode 100644
index 0000000..c4c9697
--- /dev/null
+++ b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2020 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 com.android.tradefed.invoker;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.proxy.TradefedDelegator;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.error.HarnessRuntimeException;
+import com.android.tradefed.invoker.TestInvocation.Stage;
+import com.android.tradefed.log.ITestLogger;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.proto.StreamProtoReceiver;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.IRunUtil.EnvPriority;
+import com.android.tradefed.util.RunInterruptedException;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.SystemUtil;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** {@link InvocationExecution} which delegate the execution to another Tradefed binary. */
+public class DelegatedInvocationExecution extends InvocationExecution {
+
+    /** Timeout to wait for the events received from subprocess to finish being processed. */
+    private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000;
+
+    private File mTmpDelegatedDir = null;
+    private File mGlobalConfig = null;
+    // Output reporting
+    private File mStdoutFile = null;
+    private File mStderrFile = null;
+    private OutputStream mStderr = null;
+    private OutputStream mStdout = null;
+
+    @Override
+    public void reportLogs(ITestDevice device, ITestLogger logger, Stage stage) {
+        // Do nothing
+    }
+
+    @Override
+    public boolean shardConfig(
+            IConfiguration config,
+            TestInformation testInfo,
+            IRescheduler rescheduler,
+            ITestLogger logger) {
+        return false;
+    }
+
+    @Override
+    public void doSetup(TestInformation testInfo, IConfiguration config, ITestLogger listener)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        // Do nothing
+    }
+
+    @Override
+    public void doTeardown(
+            TestInformation testInfo,
+            IConfiguration config,
+            ITestLogger logger,
+            Throwable exception)
+            throws Throwable {
+        // Do nothing
+    }
+
+    @Override
+    public void runTests(
+            TestInformation info, IConfiguration config, ITestInvocationListener listener)
+            throws Throwable {
+        // Dump the delegated config for debugging
+        File dumpConfig = FileUtil.createTempFile("delegated-config", ".xml");
+        try (PrintWriter pw = new PrintWriter(dumpConfig)) {
+            config.dumpXml(pw);
+        }
+        logAndCleanFile(dumpConfig, LogDataType.XML, listener);
+
+        if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) == null) {
+            throw new ConfigurationException(
+                    "Delegate object should not be null in DelegatedInvocation");
+        }
+        TradefedDelegator delegator =
+                (TradefedDelegator)
+                        config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT);
+        List<String> commandLine = new ArrayList<>();
+        commandLine.add(SystemUtil.getRunningJavaBinaryPath().getAbsolutePath());
+        mTmpDelegatedDir = FileUtil.createTempDir("delegated-invocation");
+        commandLine.add(String.format("-Djava.io.tmpdir=%s", mTmpDelegatedDir.getAbsolutePath()));
+        commandLine.add("-cp");
+        // Add classpath
+        commandLine.add(delegator.createClasspath());
+        commandLine.add("com.android.tradefed.command.CommandRunner");
+        // Add command line
+        commandLine.addAll(Arrays.asList(delegator.getCommandLine()));
+
+        try (StreamProtoReceiver receiver = createReceiver(listener, info.getContext())) {
+            mStdoutFile = FileUtil.createTempFile("stdout_delegate_", ".log", mTmpDelegatedDir);
+            mStderrFile = FileUtil.createTempFile("stderr_delegate_", ".log", mTmpDelegatedDir);
+            mStderr = new FileOutputStream(mStderrFile);
+            mStdout = new FileOutputStream(mStdoutFile);
+            IRunUtil runUtil = createRunUtil(receiver.getSocketServerPort());
+            CommandResult result = null;
+            RuntimeException runtimeException = null;
+            try {
+                result =
+                        runUtil.runTimedCmd(
+                                config.getCommandOptions().getInvocationTimeout(),
+                                mStdout,
+                                mStderr,
+                                commandLine.toArray(new String[0]));
+            } catch (RuntimeException e) {
+                runtimeException = e;
+            }
+            if (!receiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
+                throw new RuntimeException(
+                        String.format(
+                                "Event receiver thread did not complete:\n%s",
+                                FileUtil.readStringFromFile(mStderrFile)));
+            }
+            receiver.completeModuleEvents();
+            if (runtimeException != null) {
+                if (runtimeException instanceof RunInterruptedException) {
+                    throw runtimeException;
+                }
+                throw new HarnessRuntimeException(
+                        runtimeException.getMessage(),
+                        runtimeException,
+                        InfraErrorIdentifier.UNDETERMINED);
+            }
+            if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
+                throw new HarnessRuntimeException(
+                        "Delegated invocation timed out.", InfraErrorIdentifier.UNDETERMINED);
+            }
+        } finally {
+            StreamUtil.close(mStderr);
+            StreamUtil.close(mStdout);
+            logAndCleanFile(mStdoutFile, LogDataType.TEXT, listener);
+            logAndCleanFile(mStderrFile, LogDataType.TEXT, listener);
+            logAndCleanFile(mGlobalConfig, LogDataType.XML, listener);
+        }
+    }
+
+    @Override
+    public void doCleanUp(IInvocationContext context, IConfiguration config, Throwable exception) {
+        super.doCleanUp(context, config, exception);
+        FileUtil.recursiveDelete(mTmpDelegatedDir);
+        FileUtil.deleteFile(mGlobalConfig);
+    }
+
+    private IRunUtil createRunUtil(int port) throws IOException {
+        IRunUtil runUtil = new RunUtil();
+        // Handle the global configs for the subprocess
+        runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
+        runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
+        runUtil.setEnvVariablePriority(EnvPriority.SET);
+        mGlobalConfig = createGlobalConfig();
+        runUtil.setEnvVariable(
+                GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig.getAbsolutePath());
+        runUtil.setEnvVariable("PROTO_REPORTING_PORT", Integer.toString(port));
+        return runUtil;
+    }
+
+    private StreamProtoReceiver createReceiver(
+            ITestInvocationListener listener, IInvocationContext mainContext) throws IOException {
+        StreamProtoReceiver receiver =
+                new StreamProtoReceiver(
+                        listener, mainContext, false, false, /* report logs */ false, "");
+        return receiver;
+    }
+
+    private File createGlobalConfig() throws IOException {
+        String[] configList =
+                new String[] {
+                    GlobalConfiguration.DEVICE_MANAGER_TYPE_NAME,
+                    GlobalConfiguration.KEY_STORE_TYPE_NAME,
+                    GlobalConfiguration.HOST_OPTIONS_TYPE_NAME,
+                    GlobalConfiguration.SANDBOX_FACTORY_TYPE_NAME,
+                    "android-build"
+                };
+        File filteredGlobalConfig =
+                GlobalConfiguration.getInstance().cloneConfigWithFilter(configList);
+        return filteredGlobalConfig;
+    }
+
+    /**
+     * Log the content of given file to listener, then remove the file.
+     *
+     * @param fileToExport the {@link File} pointing to the file to log.
+     * @param type the {@link LogDataType} of the data
+     * @param listener the {@link ITestInvocationListener} where to report the test.
+     */
+    private void logAndCleanFile(
+            File fileToExport, LogDataType type, ITestInvocationListener listener) {
+        if (fileToExport == null) return;
+
+        try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport, true)) {
+            listener.testLog(fileToExport.getName(), type, inputStream);
+        }
+    }
+}
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 0c32e14..2927f19 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -26,6 +26,8 @@
 import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.proxy.AutomatedReporters;
+import com.android.tradefed.config.proxy.TradefedDelegator;
 import com.android.tradefed.device.DeviceManager;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
@@ -119,6 +121,7 @@
 
     public static final String TRADEFED_LOG_NAME = "host_log";
     public static final String TRADEFED_END_HOST_LOG = "end_host_log";
+    private static final String TRADEFED_DELEGATED_LOG_NAME = "delegated_parent_log";
     /** Suffix used on host_log for the part before sharding occurs. */
     static final String BEFORE_SHARDING_SUFFIX = "_before_sharding";
     static final String DEVICE_LOG_NAME_PREFIX = "device_logcat_";
@@ -152,6 +155,7 @@
         PARENT_SANDBOX,
         SANDBOX,
         REMOTE_INVOCATION,
+        DELEGATED_INVOCATION
     }
 
     private String mStatus = "(not invoked)";
@@ -275,7 +279,11 @@
         } catch (RunInterruptedException e) {
             exception = e;
             CLog.w("Invocation interrupted");
-            reportFailure(createFailureFromException(e, FailureStatus.UNSET), listener);
+            // if a stop cause was set, the interruption is most likely due to the invocation being
+            // cancelled
+            if (mStopCause == null) {
+                reportFailure(createFailureFromException(e, FailureStatus.UNSET), listener);
+            }
         } catch (AssertionError e) {
             exception = e;
             CLog.e("Caught AssertionError while running invocation: %s", e.toString());
@@ -488,7 +496,11 @@
     }
 
     private void reportHostLog(ITestInvocationListener listener, IConfiguration config) {
-        reportHostLog(listener, config, TRADEFED_LOG_NAME);
+        String name = TRADEFED_LOG_NAME;
+        if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+            name = TRADEFED_DELEGATED_LOG_NAME;
+        }
+        reportHostLog(listener, config, name);
     }
 
     private void reportHostLog(
@@ -703,6 +715,9 @@
             IRescheduler rescheduler,
             ITestInvocationListener... extraListeners)
             throws DeviceNotAvailableException, Throwable {
+        // Handle the automated reporting
+        applyAutomatedReporters(config);
+
         for (ITestInvocationListener listener : extraListeners) {
             if (listener instanceof IScheduledInvocationListener) {
                 mSchedulerListeners.add((IScheduledInvocationListener) listener);
@@ -780,6 +795,9 @@
         if (context.getDevices().get(0) instanceof ManagedRemoteDevice) {
             mode = RunMode.REMOTE_INVOCATION;
         }
+        if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+            mode = RunMode.DELEGATED_INVOCATION;
+        }
         IInvocationExecution invocationPath = createInvocationExec(mode);
         updateInvocationContext(context, config);
 
@@ -907,7 +925,8 @@
             if (!deviceInit) {
                 startInvocation(config, context, listener);
             }
-            if (config.getTests() == null || config.getTests().isEmpty()) {
+            if (!RunMode.DELEGATED_INVOCATION.equals(mode)
+                    && (config.getTests() == null || config.getTests().isEmpty())) {
                 CLog.e("No tests to run");
                 if (deviceInit) {
                     // If we did an early setup, do the tear down.
@@ -1003,6 +1022,8 @@
                 return new SandboxedInvocationExecution();
             case REMOTE_INVOCATION:
                 return new RemoteInvocationExecution();
+            case DELEGATED_INVOCATION:
+                return new DelegatedInvocationExecution();
             default:
                 return new InvocationExecution();
         }
@@ -1015,6 +1036,12 @@
         PrettyPrintDelimiter.printStageDelimiter(message);
     }
 
+    @VisibleForTesting
+    protected void applyAutomatedReporters(IConfiguration config) {
+        AutomatedReporters autoReport = new AutomatedReporters();
+        autoReport.applyAutomatedReporters(config);
+    }
+
     private void logExecuteShellCommand(List<ITestDevice> devices, ITestLogger logger) {
         for (ITestDevice device : devices) {
             if (!(device instanceof NativeDevice)) {
diff --git a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
index 5fe2ae5..6da0c2d 100644
--- a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
+++ b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.util.StreamUtil;
 
@@ -103,6 +104,9 @@
             errorMessage = extractCrashAndAddToMessage(errorMessage, mLastStartTime);
         }
         error.setErrorMessage(errorMessage);
+        if (isCrash(errorMessage)) {
+            error.setErrorIdentifier(DeviceErrorIdentifier.INSTRUMENATION_CRASH);
+        }
         super.testRunFailed(error);
     }
 
@@ -114,14 +118,17 @@
 
     /** Attempt to extract the crash from the logcat if the test was seen as started. */
     private String extractCrashAndAddToMessage(String errorMessage, Long startTime) {
-        if ((errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE))
-                && startTime != null) {
+        if (isCrash(errorMessage) && startTime != null) {
             mLogcatItem = extractLogcat(mDevice, startTime);
             errorMessage = addJavaCrashToString(mLogcatItem, errorMessage);
         }
         return errorMessage;
     }
 
+    private boolean isCrash(String errorMessage) {
+        return errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE);
+    }
+
     /**
      * Extract a formatted object from the logcat snippet.
      *
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index a481cc9..7a4c3ef 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -69,6 +69,8 @@
      * invocation scope we should not report it again.
      */
     private boolean mReportInvocation = false;
+    /** In some cases we do not need to forward the logs. */
+    private boolean mReportLogs = true;
     /** Prefix that will be added to the files logged through the parser. */
     private String mFilePrefix;
     /** The context from the invocation in progress, not the proto one. */
@@ -115,6 +117,11 @@
         mQuietParsing = quiet;
     }
 
+    /** Sets whether or not we should report the logs. */
+    public void setReportLogs(boolean reportLogs) {
+        mReportLogs = reportLogs;
+    }
+
     /**
      * Main entry function that takes the finalized completed proto and replay its results.
      *
@@ -318,6 +325,7 @@
                     } catch (IOException e) {
                         CLog.e("Failed to deserialize the invocation exception:");
                         CLog.e(e);
+                        failure.setCause(new RuntimeException(failure.getErrorMessage()));
                     }
                 }
             }
@@ -500,6 +508,9 @@
         if (!(mListener instanceof ILogSaverListener)) {
             return;
         }
+        if (!mReportLogs) {
+            return;
+        }
         ILogSaverListener logger = (ILogSaverListener) mListener;
         for (Entry<String, Any> entry : proto.getArtifactsMap().entrySet()) {
             try {
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index f7119f3..5e70f63 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -483,6 +483,10 @@
 
     @Override
     public final void logAssociation(String dataName, LogFile logFile) {
+        if (mLatestChild == null || mLatestChild.isEmpty()) {
+            CLog.w("Skip logging '%s' logAssociation called out of sequence.", dataName);
+            return;
+        }
         TestRecord.Builder current = mLatestChild.peek();
         Map<String, Any> fullmap = new HashMap<>();
         fullmap.putAll(current.getArtifactsMap());
diff --git a/src/com/android/tradefed/result/proto/StreamProtoReceiver.java b/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
index f674906..ab49396 100644
--- a/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
+++ b/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
@@ -83,7 +83,7 @@
             boolean reportInvocation,
             boolean quietParsing)
             throws IOException {
-        this(listener, mainContext, reportInvocation, quietParsing, "subprocess-");
+        this(listener, mainContext, reportInvocation, quietParsing, true, "subprocess-");
     }
 
     /**
@@ -102,8 +102,30 @@
             boolean quietParsing,
             String logNamePrefix)
             throws IOException {
+        this(listener, mainContext, reportInvocation, quietParsing, true, logNamePrefix);
+    }
+
+    /**
+     * Ctor.
+     *
+     * @param listener the {@link ITestInvocationListener} where to report the results.
+     * @param reportInvocation Whether or not to report the invocation level events.
+     * @param quietParsing Whether or not to let the parser log debug information.
+     * @param reportLogs Whether or not to report the logs
+     * @param logNamePrefix The prefix for file logged through the parser.
+     * @throws IOException
+     */
+    public StreamProtoReceiver(
+            ITestInvocationListener listener,
+            IInvocationContext mainContext,
+            boolean reportInvocation,
+            boolean quietParsing,
+            boolean reportLogs,
+            String logNamePrefix)
+            throws IOException {
         mListener = listener;
         mParser = new ProtoResultParser(mListener, mainContext, reportInvocation, logNamePrefix);
+        mParser.setReportLogs(reportLogs);
         mParser.setQuiet(quietParsing);
         mEventReceiver = new EventReceiverThread();
         mEventReceiver.start();
diff --git a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
index 23d605d..a4a564f 100644
--- a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
@@ -38,6 +38,14 @@
     private Socket mReportSocket = null;
     private boolean mPrintedMessage = false;
 
+    public void setProtoReportPort(Integer portValue) {
+        mReportPort = portValue;
+    }
+
+    public Integer getProtoReportPort() {
+        return mReportPort;
+    }
+
     @Override
     public void processStartInvocation(
             TestRecord invocationStartRecord, IInvocationContext context) {
diff --git a/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java b/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
index dbd521f..9c8bd22 100644
--- a/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
+++ b/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
@@ -17,7 +17,9 @@
 
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.error.IHarnessException;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.suite.retry.ResultsPlayer;
@@ -68,11 +70,23 @@
 
     @Override
     public void invocationFailed(Throwable cause) {
+        FailureDescription description =
+                FailureDescription.create(cause.getMessage()).setCause(cause);
+        if (cause instanceof IHarnessException) {
+            description.setErrorIdentifier(((IHarnessException) cause).getErrorId());
+        }
+        invocationFailed(description);
+    }
+
+    @Override
+    public void invocationFailed(FailureDescription failure) {
         // Some exception indicate a harness level issue, the tests result cannot be trusted at
         // that point so we should skip the reporting.
-        if (cause instanceof TargetSetupError
-                || cause instanceof RuntimeException
-                || cause instanceof OutOfMemoryError) {
+        Throwable cause = failure.getCause();
+        if (cause != null
+                && (cause instanceof TargetSetupError
+                        || cause instanceof RuntimeException
+                        || cause instanceof OutOfMemoryError)) {
             mTestHarnessError = cause;
         }
         super.invocationFailed(cause);
diff --git a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
index 06592b2..772075b 100644
--- a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
+++ b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
@@ -34,6 +34,7 @@
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.common.base.Strings;
+import com.google.common.xml.XmlEscapers;
 import com.google.gson.Gson;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -707,7 +708,6 @@
 
     @VisibleForTesting
     static String sanitizeXmlContent(String s) {
-        // Replace c++ \0 null since Serializer doesn't handle it.
-        return s.replace("\0", "[Null]");
+        return XmlEscapers.xmlContentEscaper().escape(s);
     }
 }
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index 110230e..71b13a4 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -124,8 +124,17 @@
         long timeout = config.getCommandOptions().getInvocationTimeout();
         // Allow interruption, subprocess should handle signals itself
         mRunUtil.allowInterrupt(true);
-        CommandResult result =
-                mRunUtil.runTimedCmd(timeout, mStdout, mStderr, mCmdArgs.toArray(new String[0]));
+        CommandResult result = null;
+        try {
+            result =
+                    mRunUtil.runTimedCmd(
+                            timeout, mStdout, mStderr, mCmdArgs.toArray(new String[0]));
+        } catch (RuntimeException interrupted) {
+            CLog.e("Sandbox runtimedCmd threw an exception");
+            CLog.e(interrupted);
+            result = new CommandResult(CommandStatus.EXCEPTION);
+            result.setStdout(StreamUtil.getStackTrace(interrupted));
+        }
 
         boolean failedStatus = false;
         String stderrText;
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index a5f6316..2db82ba 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.util.BinaryState;
 import com.android.tradefed.util.MultiMap;
 
@@ -918,7 +919,8 @@
                     String.format(
                             "Failed to connect to wifi network %s on %s",
                             mWifiSsid, device.getSerialNumber()),
-                    device.getDeviceDescriptor());
+                    device.getDeviceDescriptor(),
+                    InfraErrorIdentifier.WIFI_FAILED_CONNECT);
         }
     }
 
diff --git a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
index 3e32e33..aa912a8 100644
--- a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
+++ b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
@@ -134,7 +134,14 @@
                 charge = runTest(testInfo, listener);
                 elapsedTimeMs = getCurrentTimeMs() - elapsedTimeMs;
             } catch (DeviceNotAvailableException e) {
-                FailureDescription failure = FailureDescription.create(e.getMessage()).setCause(e);
+                FailureDescription failure =
+                        FailureDescription.create(e.getMessage())
+                                .setCause(e)
+                                .setErrorIdentifier(e.getErrorId())
+                                .setOrigin(e.getOrigin());
+                if (e.getErrorId() != null) {
+                    failure.setFailureStatus(e.getErrorId().status());
+                }
                 listener.testRunFailed(failure);
                 throw e;
             } finally {
diff --git a/src/com/android/tradefed/testtype/UsbResetTest.java b/src/com/android/tradefed/testtype/UsbResetTest.java
index 8e659b9..12c0915 100644
--- a/src/com/android/tradefed/testtype/UsbResetTest.java
+++ b/src/com/android/tradefed/testtype/UsbResetTest.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
 
@@ -46,7 +47,8 @@
                 if (usbDevice == null) {
                     throw new DeviceNotAvailableException(
                             String.format("Device '%s' not found during USB reset.", serial),
-                            serial);
+                            serial,
+                            DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
                 } else {
                     CLog.d("Resetting USB port for device '%s'", serial);
                     usbDevice.reset();
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index c0f0b31..5f00853 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -303,7 +303,10 @@
             // TODO: See if it's possible to report IReportNotExecuted
             CLog.e("Run in progress was not completed due to:");
             CLog.e(dnae);
-            runListener.testRunFailed(createFromException(dnae));
+            // If it already was marked as failure do not remark it.
+            if (!mMainGranularRunListener.hasLastAttemptFailed()) {
+                runListener.testRunFailed(createFromException(dnae));
+            }
             // Device Not Available Exception are rethrown.
             throw dnae;
         } finally {
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index bd33a30..0372eb7 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -58,6 +58,7 @@
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.result.error.ErrorIdentifier;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.retry.IRetryDecision;
 import com.android.tradefed.retry.RetryStatistics;
@@ -687,14 +688,19 @@
                     String.format(
                             "Module %s only ran %d out of %d expected tests.",
                             getId(), numResults, totalExpectedTests);
-            runFailureMessages.add(FailureDescription.create(error));
+            FailureDescription mismatch =
+                    FailureDescription.create(error)
+                            .setFailureStatus(FailureStatus.TEST_FAILURE)
+                            .setErrorIdentifier(InfraErrorIdentifier.EXPECTED_TESTS_MISMATCH);
+            runFailureMessages.add(mismatch);
             CLog.e(error);
         }
 
         if (tearDownException != null) {
             FailureDescription failure =
                     CurrentInvocation.createFailure(
-                            StreamUtil.getStackTrace(tearDownException), null);
+                                    StreamUtil.getStackTrace(tearDownException), null)
+                            .setCause(tearDownException);
             runFailureMessages.add(failure);
         }
         // If there is any errors report them all at once
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 85f0a43..26a7f4a 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -341,7 +341,9 @@
                         }
                         String fullId =
                                 String.format("%s[%s]", baseId, param.getParameterIdentifier());
-                        if (shouldRunParameterized(baseId, fullId, mForcedParameter)) {
+                        String nameWithParam =
+                                String.format("%s[%s]", name, param.getParameterIdentifier());
+                        if (shouldRunParameterized(baseId, fullId, nameWithParam, mForcedParameter)) {
                             IConfiguration paramConfig =
                                     mConfigFactory.createConfigurationFromArgs(pathArg);
                             // Mark the parameter in the metadata
@@ -351,7 +353,7 @@
                                             ConfigurationDescriptor.ACTIVE_PARAMETER_KEY,
                                             param.getParameterIdentifier());
                             param.addParameterSpecificConfig(paramConfig);
-                            setUpConfig(name, baseId, fullId, paramConfig, abi);
+                            setUpConfig(name, nameWithParam, baseId, fullId, paramConfig, abi);
                             param.applySetup(paramConfig);
                             toRun.put(fullId, paramConfig);
                         }
@@ -367,7 +369,8 @@
                     // If we find any parameterized combination for mainline modules.
                     for (String param : mainlineParams) {
                         String fullId = String.format("%s[%s]", baseId, param);
-                        if (!shouldRunParameterized(baseId, fullId, null)) {
+                        String nameWithParam = String.format("%s[%s]", name, param);
+                        if (!shouldRunParameterized(baseId, fullId, nameWithParam, null)) {
                             continue;
                         }
                         // Create mainline handler for each defined mainline parameter.
@@ -385,7 +388,7 @@
                                 .addMetadata(
                                         ITestSuite.ACTIVE_MAINLINE_PARAMETER_KEY,
                                         param);
-                        setUpConfig(name, baseId, fullId, paramConfig, abi);
+                        setUpConfig(name, nameWithParam, baseId, fullId, paramConfig, abi);
                         handler.applySetup(paramConfig);
                         toRun.put(fullId, paramConfig);
                     }
@@ -399,7 +402,9 @@
                 }
                 if (shouldRunModule(baseId)) {
                     // Always add the base regular configuration to the execution.
-                    setUpConfig(name, baseId, baseId, config, abi);
+                    // Do not pass the nameWithParam in because it would cause the module args be
+                    // injected into config twice if we pass nameWithParam using name.
+                    setUpConfig(name, null, baseId, baseId, config, abi);
                     toRun.put(baseId, config);
                 }
             }
@@ -476,10 +481,14 @@
      * in including its parameterization variant.
      */
     private boolean shouldRunParameterized(
-            String baseModuleId, String parameterModuleId, IModuleParameter forcedModuleParameter) {
+            String baseModuleId,
+            String parameterModuleId,
+            String nameWithParam,
+            IModuleParameter forcedModuleParameter) {
         // Explicitly excluded
         List<SuiteTestFilter> excluded = getFilterList(mExcludeFilters, parameterModuleId);
-        if (containsModuleExclude(excluded)) {
+        List<SuiteTestFilter> excludedParam = getFilterList(mExcludeFilters, nameWithParam);
+        if (containsModuleExclude(excluded) || containsModuleExclude(excludedParam)) {
             return false;
         }
 
@@ -492,7 +501,8 @@
         }
         // Explicitly included
         List<SuiteTestFilter> included = getFilterList(mIncludeFilters, parameterModuleId);
-        if (mIncludeAll || !included.isEmpty()) {
+        List<SuiteTestFilter> includedParam = getFilterList(mIncludeFilters, nameWithParam);
+        if (mIncludeAll || !included.isEmpty() || !includedParam.isEmpty()) {
             return true;
         }
         return false;
@@ -748,18 +758,23 @@
      * Setup the options for the module configuration.
      *
      * @param name The base name of the module
+     * @param nameWithParam The id of the parameterized mainline module (module name + parameters)
      * @param id The base id name of the module.
      * @param fullId The full id of the module (usually abi + module name + parameters)
      * @param config The module configuration.
      * @param abi The abi of the module.
      * @throws ConfigurationException
      */
-    private void setUpConfig(String name, String id, String fullId, IConfiguration config, IAbi abi)
-            throws ConfigurationException {
+    private void setUpConfig(String name, String nameWithParam, String id, String fullId,
+            IConfiguration config, IAbi abi)
+        throws ConfigurationException {
         List<OptionDef> optionsToInject = new ArrayList<>();
         if (mModuleOptions.containsKey(name)) {
             optionsToInject.addAll(mModuleOptions.get(name));
         }
+        if (nameWithParam != null && mModuleOptions.containsKey(nameWithParam)) {
+            optionsToInject.addAll(mModuleOptions.get(nameWithParam));
+        }
         if (mModuleOptions.containsKey(id)) {
             optionsToInject.addAll(mModuleOptions.get(id));
         }
@@ -801,6 +816,7 @@
         config.validateOptions();
     }
 
+
     /** Whether or not the base configuration should be created for all abis or not. */
     private boolean shouldCreateMultiAbiForBase(List<IModuleParameter> params) {
         for (IModuleParameter param : params) {
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index 587f24e..da1741f 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -157,6 +157,11 @@
                 testNames.add(testInfo.getName());
             }
             setIncludeFilter(testNames);
+            // With include filters being set, the test no longer needs group and path settings.
+            // Clear the settings to avoid conflict when the test is running in a shard.
+            mTestGroup = null;
+            mTestMappingPaths.clear();
+            mUseTestMappingPath = false;
         }
 
         // load all the configurations with include-filter injected.
@@ -186,6 +191,21 @@
         return testConfigs;
     }
 
+    @VisibleForTesting
+    String getTestGroup() {
+        return mTestGroup;
+    }
+
+    @VisibleForTesting
+    List<String> getTestMappingPaths() {
+        return mTestMappingPaths;
+    }
+
+    @VisibleForTesting
+    boolean getUseTestMappingPath() {
+        return mUseTestMappingPath;
+    }
+
     /**
      * Create individual tests with test infos for a module.
      *
diff --git a/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
index 8d6031b..2594d7f 100644
--- a/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 
 /**
  * Target preparer that performs "adb root" or "adb unroot" based on option "force-root".
@@ -79,7 +80,8 @@
     private void throwOrLog(String message, DeviceDescriptor deviceDescriptor)
             throws TargetSetupError {
         if (mThrowOnError) {
-            throw new TargetSetupError(message, deviceDescriptor);
+            throw new TargetSetupError(
+                    message, deviceDescriptor, DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
         } else {
             CLog.w(message + " " + deviceDescriptor);
         }
diff --git a/test_framework/com/android/tradefed/targetprep/WifiPreparer.java b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
index 1959a27..1bfc635 100644
--- a/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 
 /**
  * A {@link ITargetPreparer} that configures wifi on the device if necessary.
@@ -77,8 +78,12 @@
 
         InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.WIFI_AP_NAME, mWifiNetwork);
         if (!device.connectToWifiNetworkIfNeeded(mWifiNetwork, mWifiPsk)) {
-            throw new TargetSetupError(String.format("Failed to connect to wifi network %s on %s",
-                    mWifiNetwork, device.getSerialNumber()), device.getDeviceDescriptor());
+            throw new TargetSetupError(
+                    String.format(
+                            "Failed to connect to wifi network %s on %s",
+                            mWifiNetwork, device.getSerialNumber()),
+                    device.getDeviceDescriptor(),
+                    InfraErrorIdentifier.WIFI_FAILED_CONNECT);
         }
         if (mMonitorNetwork) {
             device.enableNetworkMonitor();
diff --git a/test_framework/com/android/tradefed/testtype/ArtRunTest.java b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
index 1b16316..a982104 100644
--- a/test_framework/com/android/tradefed/testtype/ArtRunTest.java
+++ b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -31,6 +32,7 @@
 import com.android.tradefed.util.FileUtil;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -141,8 +143,8 @@
             // Check the output producted by the test.
             if (output != null) {
                 try {
-                    File expectedFile =
-                            testInfo.getDependencyFile("expected.txt", /* targetFirst */ false);
+                    File expectedFile = getDependencyFileFromRunTestDir(testInfo, "expected.txt");
+                    CLog.i("Found expected output for run-test %s: %s", mRunTestName, expectedFile);
                     String expected = FileUtil.readStringFromFile(expectedFile);
                     if (!output.equals(expected)) {
                         String error = String.format("'%s' instead of '%s'", output, expected);
@@ -174,4 +176,24 @@
     protected CollectingOutputReceiver createTestOutputReceiver() {
         return new CollectingOutputReceiver();
     }
+
+    /** Search for a dependency/artifact file in the run-test's directory. */
+    protected File getDependencyFileFromRunTestDir(TestInformation testInfo, String fileName)
+            throws FileNotFoundException {
+        File testsDir = testInfo.executionFiles().get(FilesKey.TARGET_TESTS_DIRECTORY);
+        if (testsDir == null || !testsDir.exists()) {
+            throw new FileNotFoundException(
+                    String.format(
+                            "Could not find target tests directory for test %s.", mRunTestName));
+        }
+        File runTestDir = new File(testsDir, mRunTestName);
+        File file = FileUtil.findFile(runTestDir, fileName);
+        if (file == null) {
+            throw new FileNotFoundException(
+                    String.format(
+                            "Could not find an artifact file associated with %s in directory %s.",
+                            fileName, runTestDir));
+        }
+        return file;
+    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index ed238fe..94e4aa6 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.testtype.binary;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -30,6 +31,8 @@
 import com.android.tradefed.testtype.IShardableTest;
 import com.android.tradefed.testtype.ITestCollector;
 import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.util.StreamUtil;
 
 import java.io.File;
@@ -87,6 +90,16 @@
     private Set<String> mIncludeFilters = new LinkedHashSet<>();
     private Set<String> mExcludeFilters = new LinkedHashSet<>();
 
+    /**
+     * Get test commands.
+     *
+     * @return the test commands.
+     */
+    @VisibleForTesting
+    Map<String, String> getTestCommands() {
+        return mTestCommands;
+    }
+
     /** @return the timeout applied to each binary for their execution. */
     protected long getTimeoutPerBinaryMs() {
         return mTimeoutPerBinaryMs;
@@ -152,7 +165,12 @@
             if (shouldSkipCurrentTest(description)) continue;
             if (path == null) {
                 listener.testRunStarted(testName, 0);
-                listener.testRunFailed(String.format(NO_BINARY_ERROR, cmd));
+                FailureDescription failure =
+                        FailureDescription.create(
+                                        String.format(NO_BINARY_ERROR, cmd),
+                                        FailureStatus.TEST_FAILURE)
+                                .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                listener.testRunFailed(failure);
                 listener.testRunEnded(0L, new HashMap<String, Metric>());
             } else {
                 listener.testRunStarted(testName, 1);
@@ -247,26 +265,44 @@
     /** {@inheritDoc} */
     @Override
     public final Collection<IRemoteTest> split() {
-        if (mBinaryPaths.size() <= 2) {
+        int testCount = mBinaryPaths.size() + mTestCommands.size();
+        if (testCount <= 2) {
             return null;
         }
         Collection<IRemoteTest> tests = new ArrayList<>();
         for (String path : mBinaryPaths) {
-            tests.add(getTestShard(path));
+            tests.add(getTestShard(path, null, null));
+        }
+        Map<String, String> testCommands = new LinkedHashMap<>(mTestCommands);
+        for (String testName : testCommands.keySet()) {
+            String cmd = testCommands.get(testName);
+            tests.add(getTestShard(null, testName, cmd));
         }
         return tests;
     }
 
-    private IRemoteTest getTestShard(String path) {
+    /**
+     * Get a testShard of ExecutableBaseTest.
+     *
+     * @param binaryPath the binary path for ExecutableHostTest.
+     * @param testName the test name for ExecutableTargetTest.
+     * @param cmd the test command for ExecutableTargetTest.
+     * @return a shard{@link IRemoteTest} of ExecutableBaseTest{@link ExecutableBaseTest}
+     */
+    private IRemoteTest getTestShard(String binaryPath, String testName, String cmd) {
         ExecutableBaseTest shard = null;
         try {
             shard = this.getClass().getDeclaredConstructor().newInstance();
             OptionCopier.copyOptionsNoThrow(this, shard);
-            // We approximate the runtime of each shard to be equal since we can't know.
-            shard.mRuntimeHintMs = mRuntimeHintMs / shard.mBinaryPaths.size();
-            // Set one binary per shard
             shard.mBinaryPaths.clear();
-            shard.mBinaryPaths.add(path);
+            shard.mTestCommands.clear();
+            if (binaryPath != null) {
+                // Set one binary per shard
+                shard.mBinaryPaths.add(binaryPath);
+            } else if (testName != null && cmd != null) {
+                // Set one test command per shard
+                shard.mTestCommands.put(testName, cmd);
+            }
         } catch (InstantiationException
                 | IllegalAccessException
                 | InvocationTargetException
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
index 3d44499..50492f1 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
@@ -29,6 +29,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.util.CommandResult;
@@ -158,8 +159,14 @@
             try {
                 getTestInfo().getDevice().waitForDeviceAvailable();
             } catch (DeviceNotAvailableException e) {
-                listener.testRunFailed(
-                        String.format("Device became unavailable after %s.", binaryPath));
+                FailureDescription failure =
+                        FailureDescription.create(
+                                        String.format(
+                                                "Device became unavailable after %s.", binaryPath),
+                                        FailureStatus.LOST_SYSTEM_UNDER_TEST)
+                                .setErrorIdentifier(DeviceErrorIdentifier.DEVICE_UNAVAILABLE)
+                                .setCause(e);
+                listener.testRunFailed(failure);
                 throw e;
             }
         }
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
index 85e7ae8..181117c 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
@@ -18,8 +18,10 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -84,7 +86,10 @@
                     String.format(
                             "binary returned non-zero. Exit code: %d, stderr: %s, stdout: %s",
                             result.getExitCode(), result.getStderr(), result.getStdout());
-            listener.testFailed(description, error_message);
+            listener.testFailed(
+                    description,
+                    FailureDescription.create(error_message)
+                            .setFailureStatus(FailureStatus.TEST_FAILURE));
         }
     }
 }
diff --git a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
index 91893ab..b871688 100644
--- a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
@@ -26,9 +26,11 @@
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.ResultForwarder;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
@@ -73,6 +75,7 @@
     @VisibleForTesting static final String USE_TEST_OUTPUT_FILE_OPTION = "use-test-output-file";
     static final String TEST_OUTPUT_FILE_FLAG = "test-output-file";
 
+    private static final String PYTHON_LOG_STDOUT_FORMAT = "%s-stdout";
     private static final String PYTHON_LOG_STDERR_FORMAT = "%s-stderr";
     private static final String PYTHON_LOG_TEST_OUTPUT_FORMAT = "%s-test-output";
 
@@ -316,7 +319,13 @@
                             + "%s\nstderr:%s",
                     result.getStdout(), result.getStderr());
         }
-
+        if (result.getStdout() != null) {
+            try (InputStreamSource data =
+                    new ByteArrayInputStreamSource(result.getStdout().getBytes())) {
+                listener.testLog(
+                        String.format(PYTHON_LOG_STDOUT_FORMAT, runName), LogDataType.TEXT, data);
+            }
+        }
         File stderrFile = null;
         try {
             // Note that we still log stderr when parsing results from a test-written output file
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
index bd4df58..42187c5 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
@@ -24,9 +24,11 @@
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -130,8 +132,9 @@
         commandLine.add(file.getAbsolutePath());
         CLog.d("Run single Rust File: " + file.getAbsolutePath());
 
-        // Add all the other options
+        // Add all the other options and include/exclude filters.
         commandLine.addAll(mTestOptions);
+        addFiltersToArgs(commandLine);
 
         List<String> listCommandLine = new ArrayList<>(commandLine);
         listCommandLine.add("--list");
@@ -162,11 +165,15 @@
                 getRunUtil().runTimedCmd(mTestTimeout, commandLine.toArray(new String[0]));
         long testTimeMs = System.currentTimeMillis() - startTimeMs;
         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
-            listener.testRunFailed("Fail to run: " + file.getAbsolutePath());
-            CLog.e(
-                    "Something went wrong when running the rust binary:\nstdout: "
-                            + "%s\nstderr:%s",
-                    result.getStdout(), result.getStderr());
+            String message =
+                    String.format(
+                            "Something went wrong when running the rust binary:Exit Code: %s"
+                                    + "\nstdout: %s\nstderr: %s",
+                            result.getExitCode(), result.getStdout(), result.getStderr());
+            FailureDescription failure =
+                    FailureDescription.create(message, FailureStatus.TEST_FAILURE);
+            listener.testRunFailed(failure);
+            CLog.e(message);
         }
 
         File resultFile = null;
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java b/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
index 019e4b8..a35b722 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
@@ -32,9 +32,9 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.NativeCodeCoverageListener;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.NativeCodeCoverageFlusher;
 
 import java.io.File;
@@ -88,8 +88,6 @@
         return mTestModule;
     }
 
-    // TODO(chh): implement test filter
-
     /**
      * Gets the path where tests live on the device.
      *
@@ -166,6 +164,7 @@
         } else {
             cmd = fullPath;
         }
+        cmd = addFiltersToCommand(cmd);
 
         int testCount = 0;
         try {
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java b/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
index a4c479e..aa84c00 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
@@ -18,6 +18,7 @@
 import com.android.ddmlib.IShellOutputReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
@@ -50,7 +51,12 @@
             isTimeVal = true)
     protected long mTestTimeout = 20 * 1000L; // milliseconds
 
+    @Option(
+            name = "include-filter",
+            description = "A substr filter of the test names to run; only the first one is used.")
     private Set<String> mIncludeFilters = new LinkedHashSet<>();
+
+    @Option(name = "exclude-filter", description = "A substr filter of the test names to skip.")
     private Set<String> mExcludeFilters = new LinkedHashSet<>();
 
     // A wrapper that can be redefined in unit tests to create a (mocked) result parser.
@@ -88,10 +94,6 @@
         return testCount;
     }
 
-    // TODO(b/145607401): make rust test runners accept filters
-    // Now the following are just dummy methods,
-    // to shut off run-time warning about not implementing ITestFilterReceiver.
-
     /** {@inheritDoc} */
     @Override
     public void addIncludeFilter(String filter) {
@@ -139,4 +141,32 @@
     public Set<String> getExcludeFilters() {
         return mExcludeFilters;
     }
+
+    private void checkMultipleIncludeFilters() {
+        if (mIncludeFilters.size() > 1) {
+            CLog.e("Found multiple include filters; all except the 1st are ignored.");
+        }
+    }
+
+    protected void addFiltersToArgs(List<String> args) {
+        checkMultipleIncludeFilters();
+        for (String s : mIncludeFilters) {
+            args.add(s);
+        }
+        for (String s : mExcludeFilters) {
+            args.add("--skip");
+            args.add(s);
+        }
+    }
+
+    protected String addFiltersToCommand(String cmd) {
+        if (!mIncludeFilters.isEmpty()) {
+            checkMultipleIncludeFilters();
+            cmd += " " + String.join(" ", mIncludeFilters);
+        }
+        if (!mExcludeFilters.isEmpty()) {
+            cmd += " --skip " + String.join(" --skip ", mExcludeFilters);
+        }
+        return cmd;
+    }
 }
diff --git a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java b/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
index 5541719..c3ef3a5 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
+++ b/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
@@ -30,11 +30,14 @@
     SHELL_COMMAND_ERROR(30_100, FailureStatus.UNSET),
     DEVICE_UNEXPECTED_RESPONSE(30_101, FailureStatus.UNSET),
 
+    INSTRUMENATION_CRASH(30_200, FailureStatus.UNSET),
+
     FAILED_TO_LAUNCH_GCE(30_500, FailureStatus.LOST_SYSTEM_UNDER_TEST),
     FAILED_TO_CONNECT_TO_GCE(30_501, FailureStatus.LOST_SYSTEM_UNDER_TEST),
     ERROR_AFTER_FLASHING(30_502, FailureStatus.LOST_SYSTEM_UNDER_TEST),
 
-    DEVICE_UNAVAILABLE(30_750, FailureStatus.LOST_SYSTEM_UNDER_TEST);
+    DEVICE_UNAVAILABLE(30_750, FailureStatus.LOST_SYSTEM_UNDER_TEST),
+    DEVICE_UNRESPONSIVE(30_751, FailureStatus.LOST_SYSTEM_UNDER_TEST);
 
     private final long code;
     private final FailureStatus status;
diff --git a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java b/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
index 1f13121..3001231 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
+++ b/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
@@ -32,6 +32,12 @@
     ARTIFACT_UNSUPPORTED_PATH(10_502, FailureStatus.INFRA_FAILURE),
     ARTIFACT_DOWNLOAD_ERROR(10_503, FailureStatus.INFRA_FAILURE),
 
+    // 11_001 - 11_500: environment issues: For example: lab wifi
+    WIFI_FAILED_CONNECT(11_001, FailureStatus.UNSET), // TODO: switch to dependency_issue
+
+    // 12_000 - 12_100: Test issues detected by infra
+    EXPECTED_TESTS_MISMATCH(12_000, FailureStatus.TEST_FAILURE),
+
     UNDETERMINED(20_000, FailureStatus.UNSET);
 
     private final long code;
diff --git a/tests/res/testconfigs/yaml/test-config.tf_yaml b/tests/res/testconfigs/yaml/test-config.tf_yaml
index d547953..eb89137 100644
--- a/tests/res/testconfigs/yaml/test-config.tf_yaml
+++ b/tests/res/testconfigs/yaml/test-config.tf_yaml
@@ -1,5 +1,4 @@
 description: "Human friendly description of the test"
-infra_run: false
 
 dependencies:
    - apks: ["test.apk", "test2.apk"]
diff --git a/tests/res/testdata/test_mapping_with_mainline b/tests/res/testdata/test_mapping_with_mainline
index 8275f16..8e6afc8 100644
--- a/tests/res/testdata/test_mapping_with_mainline
+++ b/tests/res/testdata/test_mapping_with_mainline
@@ -17,7 +17,12 @@
       ]
     },
     {
-      "name": "test[mod1.apk+mod2.apk]"
+      "name": "test[mod1.apk+mod2.apk]",
+      "options": [
+        {
+          "exclude-annotation": "test-annotation"
+        }
+      ]
     }
   ]
 }
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 3262fff..832cc950 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -62,6 +62,7 @@
 import com.android.tradefed.config.SandboxConfigurationFactoryTest;
 import com.android.tradefed.config.gcs.GCSConfigurationFactoryTest;
 import com.android.tradefed.config.gcs.GCSConfigurationServerTest;
+import com.android.tradefed.config.proxy.AutomatedReportersTest;
 import com.android.tradefed.config.remote.GcsRemoteFileResolverTest;
 import com.android.tradefed.config.remote.HttpRemoteFileResolverTest;
 import com.android.tradefed.config.remote.LocalFileResolverTest;
@@ -480,6 +481,9 @@
     GCSConfigurationServerTest.class,
     GCSConfigurationFactoryTest.class,
 
+    // config.proxy
+    AutomatedReportersTest.class,
+
     // config.remote
     GcsRemoteFileResolverTest.class,
     HttpRemoteFileResolverTest.class,
diff --git a/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java b/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java
index 53167f8..a1f2e59 100644
--- a/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java
+++ b/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java
@@ -22,8 +22,10 @@
 import com.android.ddmlib.IDevice;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.invoker.ExecutionFiles;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.util.FileUtil;
 
 import org.easymock.EasyMock;
 import org.junit.Before;
@@ -31,6 +33,8 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.File;
+
 /** Unit tests for {@link BootstrapBuildProvider}. */
 @RunWith(JUnit4.class)
 public class BootstrapBuildProviderTest {
@@ -71,6 +75,34 @@
         }
     }
 
+    @Test
+    public void testGetBuild_add_extra_file() throws Exception {
+        EasyMock.expect(mMockDevice.getBuildId()).andReturn("5");
+        EasyMock.expect(mMockDevice.getIDevice()).andReturn(EasyMock.createMock(IDevice.class));
+        EasyMock.expect(mMockDevice.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
+        EasyMock.expect(mMockDevice.getProperty(EasyMock.anyObject())).andStubReturn("property");
+        EasyMock.expect(mMockDevice.getProductVariant()).andStubReturn("variant");
+        EasyMock.expect(mMockDevice.getBuildFlavor()).andStubReturn("flavor");
+        EasyMock.expect(mMockDevice.getBuildAlias()).andStubReturn("alias");
+        EasyMock.replay(mMockDevice);
+        OptionSetter setter = new OptionSetter(mProvider);
+        File tmpDir = FileUtil.createTempDir("tmp");
+        File file_1 = new File(tmpDir, "sys.img");
+        setter.setOptionValue("extra-file", "file_1", file_1.getAbsolutePath());
+        IBuildInfo res = mProvider.getBuild(mMockDevice);
+        assertNotNull(res);
+        try {
+            assertTrue(res instanceof IDeviceBuildInfo);
+            // Ensure tests dir is never null
+            assertTrue(((IDeviceBuildInfo) res).getTestsDir() != null);
+            assertEquals(((IDeviceBuildInfo) res).getFile("file_1"), file_1);
+            EasyMock.verify(mMockDevice);
+        } finally {
+            mProvider.cleanUp(res);
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
     /**
      * Test that when using the provider with a StubDevice information that cannot be queried are
      * stubbed.
diff --git a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
index 53e268c..ffa3255 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -16,6 +16,9 @@
 package com.android.tradefed.config;
 
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IBuildProvider;
+import com.android.tradefed.build.IDeviceBuildProvider;
 import com.android.tradefed.build.LocalDeviceBuildProvider;
 import com.android.tradefed.config.ConfigurationDef.ConfigObjectDef;
 import com.android.tradefed.config.ConfigurationFactory.ConfigId;
@@ -1801,6 +1804,28 @@
         }
     }
 
+    /** Test that a YAML config command line parse correctly. */
+    public void testCreateConfigurationFromArgs_yaml() throws Exception {
+        IConfiguration config =
+                mFactory.createConfigurationFromArgs(
+                        new String[] {
+                            "yaml/test-config.tf_yaml",
+                            "--build-id",
+                            "5",
+                            "--build-flavor",
+                            "test",
+                            "--branch",
+                            "main"
+                        });
+        assertNotNull(config);
+        IBuildProvider provider = config.getBuildProvider();
+        assertTrue(provider instanceof IDeviceBuildProvider);
+        IBuildInfo info = ((IDeviceBuildProvider) provider).getBuild(null);
+        assertEquals("5", info.getBuildId());
+        assertEquals("test", info.getBuildFlavor());
+        assertEquals("main", info.getBuildBranch());
+    }
+
     private static String getClassName(String name) {
         // -6 because of .class
         return name.substring(0, name.length() - 6).replace('/', '.');
diff --git a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
index cf20f8f..eded0ac 100644
--- a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
@@ -28,6 +28,7 @@
 import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.StubBuildProvider;
 import com.android.tradefed.config.DynamicRemoteFileResolver.FileResolverLoader;
+import com.android.tradefed.config.DynamicRemoteFileResolver.ResolverLoadingException;
 import com.android.tradefed.config.DynamicRemoteFileResolver.ServiceFileResolverLoader;
 import com.android.tradefed.config.remote.GcsRemoteFileResolver;
 import com.android.tradefed.config.remote.IRemoteFileResolver;
@@ -168,34 +169,6 @@
         EasyMock.verify(mMockResolver);
     }
 
-    @Test
-    public void testResolveWithQuery_overrides() throws Exception {
-        RemoteFileOption object = new RemoteFileOption();
-        OptionSetter setter = new OptionSetter(object);
-
-        File fake = temporaryFolder.newFile();
-
-        setter.setOptionValue("remote-file", "gs://fake/path?key=value");
-        assertEquals("gs:/fake/path?key=value", object.remoteFile.getPath());
-
-        Map<String, String> testMap = new HashMap<>();
-        testMap.put("key", "override" /* The args value is overriden*/);
-        EasyMock.expect(
-                        mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path")), EasyMock.eq(testMap)))
-                .andReturn(fake);
-        EasyMock.replay(mMockResolver);
-        Map<String, String> extraArgs = new HashMap<>();
-        extraArgs.put("key", "override");
-        mResolver.addExtraArgs(extraArgs);
-        Set<File> downloadedFile = setter.validateRemoteFilePath(mResolver);
-        assertEquals(1, downloadedFile.size());
-        File downloaded = downloadedFile.iterator().next();
-        // The file has been replaced by the downloaded one.
-        assertEquals(downloaded.getAbsolutePath(), object.remoteFile.getAbsolutePath());
-        EasyMock.verify(mMockResolver);
-    }
-
     /** Test to make sure that a dynamic download marked as "optional" does not throw */
     @Test
     public void testResolveOptional() throws Exception {
@@ -661,6 +634,210 @@
     }
 
     @Test
+    public void resolverLoader_throwsIfMissingMandatoryOption() throws Exception {
+        ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = ResolverWithOptions.minimalConfig();
+        config.remove(ResolverWithOptions.MANDATORY_OPTION_FQN);
+
+        try {
+            loader.load(ResolverWithOptions.PROTOCOL, config);
+            fail();
+        } catch (ResolverLoadingException expected) {
+            assertThat(expected)
+                    .hasCauseThat()
+                    .hasMessageThat()
+                    .contains(ResolverWithOptions.MANDATORY_OPTION_NAME);
+        }
+    }
+
+    @Test
+    public void resolverLoader_setsNonMandatoryOption() throws Exception {
+        String optionValue = "value";
+        ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = ResolverWithOptions.minimalConfig();
+        config.put(ResolverWithOptions.NON_MANDATORY_OPTION_FQN, optionValue);
+
+        IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+        assertThat(((ResolverWithOptions) resolver).nonMandatoryOption).isEqualTo(optionValue);
+    }
+
+    @Test
+    public void resolverLoader_throwsForMapOption() throws Exception {
+        ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = ResolverWithOptions.minimalConfig();
+        config.put(ResolverWithOptions.MAP_OPTION_FQN, "key=value");
+
+        try {
+            loader.load(ResolverWithOptions.PROTOCOL, config);
+            fail();
+        } catch (ResolverLoadingException expected) {
+            assertThat(expected)
+                    .hasCauseThat()
+                    .hasMessageThat()
+                    .contains(ResolverWithOptions.MAP_OPTION_FQN);
+            assertThat(expected).hasCauseThat().hasMessageThat().contains("not supported");
+        }
+    }
+
+    @Test
+    public void resolverLoader_resolvesFileOption() throws Exception {
+        File file = temporaryFolder.newFile();
+        ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = ResolverWithOptions.minimalConfig();
+        config.put(ResolverWithOptions.MANDATORY_OPTION_FQN, file.toURI().toString());
+
+        IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+        assertThat(((ResolverWithOptions) resolver).mandatoryOption).isEqualTo(file);
+    }
+
+    @Test
+    public void resolverLoader_doesNotResolveFileOptionWithUnsupportedScheme() throws Exception {
+        ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = ResolverWithOptions.minimalConfig();
+        config.put(ResolverWithOptions.MANDATORY_OPTION_FQN, "missing://tmp");
+
+        IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+        assertThat(((ResolverWithOptions) resolver).mandatoryOption.toString())
+                .contains("missing:");
+    }
+
+    @Test
+    public void resolverLoader_resolvesResolverFileOption() throws Exception {
+        File f = new File("/tmp/a-file");
+        ClassLoader classLoader =
+                classLoaderWithProviders(
+                        ResolverWithOptions.class
+                                .getName(), // Contains a file option resolved by the next.
+                        AnotherResolverWithOptions.class.getName());
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = new HashMap<>();
+        config.putAll(ResolverWithOptions.minimalConfig());
+        config.putAll(AnotherResolverWithOptions.minimalConfig());
+        config.put(
+                ResolverWithOptions.MANDATORY_OPTION_FQN,
+                AnotherResolverWithOptions.PROTOCOL + "://a-file");
+        config.put(AnotherResolverWithOptions.MANDATORY_OPTION_FQN, f.toString());
+
+        IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+        assertThat(((ResolverWithOptions) resolver).mandatoryOption).isEqualTo(f);
+    }
+
+    @Test
+    public void resolverLoader_throwsOnInitializationCycles() throws Exception {
+        ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = ResolverWithOptions.minimalConfig();
+        config.put(
+                ResolverWithOptions.MANDATORY_OPTION_FQN,
+                ResolverWithOptions.PROTOCOL + "://a-file");
+
+        try {
+            loader.load(ResolverWithOptions.PROTOCOL, config);
+            fail();
+        } catch (ResolverLoadingException expected) {
+            assertThat(expected)
+                    .hasCauseThat()
+                    .hasCauseThat()
+                    .hasMessageThat()
+                    .contains("Cycle detected");
+        }
+    }
+
+    @Test
+    public void resolverLoader_onlyInitializesOptionsOfUsedResolvers() throws Exception {
+        File f = new File("/tmp/a-file");
+        ClassLoader classLoader =
+                classLoaderWithProviders(
+                        ResolverWithOptions.class.getName(),
+                        AnotherResolverWithOptions.class
+                                .getName()); // Requires option but never used.
+        FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+        Map<String, String> config = new HashMap<>();
+        config.putAll(ResolverWithOptions.minimalConfig());
+        config.put(ResolverWithOptions.MANDATORY_OPTION_FQN, f.toString());
+
+        IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+        assertThat(((ResolverWithOptions) resolver).mandatoryOption).isEqualTo(f);
+    }
+
+    public static final class ResolverWithOptions implements IRemoteFileResolver {
+        static final String PROTOCOL = "rwo";
+        static final String MANDATORY_OPTION_NAME = "mandatory";
+        static final String MANDATORY_OPTION_FQN =
+                optionFqn(ResolverWithOptions.class, MANDATORY_OPTION_NAME);
+        static final String NON_MANDATORY_OPTION_NAME = "nonMandatory";
+        static final String NON_MANDATORY_OPTION_FQN =
+                optionFqn(ResolverWithOptions.class, NON_MANDATORY_OPTION_NAME);
+        static final String MAP_OPTION_NAME = "map";
+        static final String MAP_OPTION_FQN = optionFqn(ResolverWithOptions.class, MAP_OPTION_NAME);
+
+        static Map<String, String> minimalConfig() {
+            Map<String, String> config = new HashMap<>();
+            config.put(MANDATORY_OPTION_FQN, "anything");
+            return config;
+        }
+
+        @Option(name = MANDATORY_OPTION_NAME, mandatory = true)
+        private File mandatoryOption;
+
+        @Option(name = NON_MANDATORY_OPTION_NAME)
+        private String nonMandatoryOption;
+
+        @Option(name = MAP_OPTION_NAME)
+        private Map<String, String> mapOption;
+
+        @Override
+        public String getSupportedProtocol() {
+            return PROTOCOL;
+        }
+
+        @Override
+        public File resolveRemoteFiles(File consideredFile, Map<String, String> queryArgs) {
+            return mandatoryOption;
+        }
+    }
+
+    public static final class AnotherResolverWithOptions implements IRemoteFileResolver {
+        static final String PROTOCOL = "arwo";
+        static final String MANDATORY_OPTION_NAME = "mandatory";
+        static final String MANDATORY_OPTION_FQN =
+                optionFqn(AnotherResolverWithOptions.class, MANDATORY_OPTION_NAME);
+
+        static Map<String, String> minimalConfig() {
+            Map<String, String> config = new HashMap<>();
+            config.put(MANDATORY_OPTION_FQN, "anything");
+            return config;
+        }
+
+        @Option(name = MANDATORY_OPTION_NAME, mandatory = true)
+        private File mandatoryOption;
+
+        @Override
+        public String getSupportedProtocol() {
+            return PROTOCOL;
+        }
+
+        @Override
+        public File resolveRemoteFiles(File consideredFile, Map<String, String> queryArgs) {
+            return mandatoryOption;
+        }
+    }
+
+    private static String optionFqn(Class<?> cls, String name) {
+        return cls.getName() + ":" + name;
+    }
+
+    @Test
     public void testMultiDevices() throws Exception {
         IConfiguration configuration = new Configuration("test", "test");
 
diff --git a/tests/src/com/android/tradefed/config/proxy/AutomatedReportersTest.java b/tests/src/com/android/tradefed/config/proxy/AutomatedReportersTest.java
new file mode 100644
index 0000000..9e3684f
--- /dev/null
+++ b/tests/src/com/android/tradefed/config/proxy/AutomatedReportersTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 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 com.android.tradefed.config.proxy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.result.proto.StreamProtoResultReporter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AutomatedReporters}. */
+@RunWith(JUnit4.class)
+public class AutomatedReportersTest {
+
+    private AutomatedReporters mReporter =
+            new AutomatedReporters() {
+                @Override
+                protected String getEnv(String key) {
+                    if (key.equals("PROTO_REPORTING_PORT")) {
+                        return "8888";
+                    }
+                    return null;
+                }
+            };
+
+    @Test
+    public void testReporting() {
+        IConfiguration config = new Configuration("name", "test");
+        assertEquals(1, config.getTestInvocationListeners().size());
+        mReporter.applyAutomatedReporters(config);
+        assertEquals(2, config.getTestInvocationListeners().size());
+        assertTrue(config.getTestInvocationListeners().get(1) instanceof StreamProtoResultReporter);
+        StreamProtoResultReporter protoReporter =
+                (StreamProtoResultReporter) config.getTestInvocationListeners().get(1);
+        assertEquals(Integer.valueOf(8888), protoReporter.getProtoReportPort());
+    }
+}
diff --git a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
index 2fbdc5f..6a1c081 100644
--- a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
+++ b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
@@ -20,7 +20,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tradefed.build.BootstrapBuildProvider;
+import com.android.tradefed.build.DependenciesResolver;
+import com.android.tradefed.build.StubBuildProvider;
 import com.android.tradefed.config.ConfigurationDef;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -57,12 +58,23 @@
     @Test
     public void testParseConfig() throws Exception {
         try (InputStream res = readFromRes(YAML_TEST_CONFIG_1)) {
-            mParser.parse(mConfigDef, "source", res);
+            mParser.parse(mConfigDef, "source", res, false);
             // Create the configuration to test the flow
             IConfiguration config = mConfigDef.createConfiguration();
             config.validateOptions();
             // build provider
-            assertTrue(config.getBuildProvider() instanceof BootstrapBuildProvider);
+            assertTrue(config.getBuildProvider() instanceof DependenciesResolver);
+            DependenciesResolver resolver = (DependenciesResolver) config.getBuildProvider();
+            assertEquals(7, resolver.getDependencies().size());
+            assertThat(resolver.getDependencies())
+                    .containsExactly(
+                            new File("test.apk"),
+                            new File("test2.apk"),
+                            new File("test1.apk"),
+                            new File("tobepushed2.txt"),
+                            new File("tobepushed.txt"),
+                            new File("file1.txt"),
+                            new File("file2.txt"));
             // Test
             assertEquals(1, config.getTests().size());
             assertTrue(config.getTests().get(0) instanceof AndroidJUnitTest);
@@ -93,6 +105,46 @@
         }
     }
 
+    @Test
+    public void testParseModule() throws Exception {
+        try (InputStream res = readFromRes(YAML_TEST_CONFIG_1)) {
+            mParser.parse(mConfigDef, "source", res, true /* createdAsModule */);
+            // Create the configuration to test the flow
+            IConfiguration config = mConfigDef.createConfiguration();
+            config.validateOptions();
+            // build provider isn't set
+            assertTrue(config.getBuildProvider() instanceof StubBuildProvider);
+            // Test
+            assertEquals(1, config.getTests().size());
+            assertTrue(config.getTests().get(0) instanceof AndroidJUnitTest);
+            assertEquals(
+                    "android.package",
+                    ((AndroidJUnitTest) config.getTests().get(0)).getPackageName());
+
+            // Dependencies
+            // apk dependencies
+            assertEquals(2, config.getTargetPreparers().size());
+            ITargetPreparer installApk = config.getTargetPreparers().get(0);
+            assertTrue(installApk instanceof SuiteApkInstaller);
+            assertThat(((SuiteApkInstaller) installApk).getTestsFileName())
+                    .containsExactly(
+                            new File("test.apk"), new File("test2.apk"), new File("test1.apk"));
+            // device file dependencies
+            ITargetPreparer pushFile = config.getTargetPreparers().get(1);
+            assertTrue(pushFile instanceof PushFilePreparer);
+            assertThat(((PushFilePreparer) pushFile).getPushSpecs(null))
+                    .containsExactly(
+                            "/sdcard/",
+                            new File("tobepushed2.txt"),
+                            "/sdcard",
+                            new File("tobepushed.txt"));
+            // Result reporters aren't set
+            List<ITestInvocationListener> listeners = config.getTestInvocationListeners();
+            // TODO: Renable when matching project is updated
+            // assertTrue(listeners.get(0) instanceof TextResultReporter);
+        }
+    }
+
     private InputStream readFromRes(String resourceFile) {
         return getClass().getResourceAsStream(resourceFile);
     }
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index 77624d5..b66ba56 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -89,6 +89,7 @@
                 .andReturn(null);
         EasyMock.expect(mMockConfig.getConfigurationObject(ShardHelper.LAST_SHARD_DETECTOR))
                 .andReturn(null);
+        EasyMock.expect(mMockConfig.getConfigurationObject("DELEGATE")).andStubReturn(null);
         mMockRescheduler = EasyMock.createMock(IRescheduler.class);
         mMockTestListener = EasyMock.createMock(ITestInvocationListener.class);
         mMockLogSaver = EasyMock.createMock(ILogSaver.class);
@@ -113,6 +114,11 @@
                     }
 
                     @Override
+                    protected void applyAutomatedReporters(IConfiguration config) {
+                        // Empty on purpose
+                    }
+
+                    @Override
                     protected void setExitCode(ExitCode code, Throwable stack) {
                         // empty on purpose
                     }
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index 2dd6327..f8c4eb2 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -309,7 +309,7 @@
 
                     @Override
                     protected void setExitCode(ExitCode code, Throwable stack) {
-                        // empty on purpose
+                        // Empty on purpose
                     }
 
                     @Override
@@ -320,7 +320,12 @@
 
                     @Override
                     public void registerExecutionFiles(ExecutionFiles executionFiles) {
-                        // Empty of purpose
+                        // Empty on purpose
+                    }
+
+                    @Override
+                    protected void applyAutomatedReporters(IConfiguration config) {
+                        // Empty on purpose
                     }
 
                     @Override
@@ -1659,6 +1664,11 @@
                     }
 
                     @Override
+                    protected void applyAutomatedReporters(IConfiguration config) {
+                        // Empty on purpose
+                    }
+
+                    @Override
                     protected void addInvocationMetric(InvocationMetricKey key, long value) {}
 
                     @Override
@@ -1732,6 +1742,11 @@
                         }
 
                         @Override
+                        protected void applyAutomatedReporters(IConfiguration config) {
+                            // Empty on purpose
+                        }
+
+                        @Override
                         protected void setExitCode(ExitCode code, Throwable stack) {
                             // empty on purpose
                         }
@@ -1839,6 +1854,11 @@
                         }
 
                         @Override
+                        protected void applyAutomatedReporters(IConfiguration config) {
+                            // Empty on purpose
+                        }
+
+                        @Override
                         InvocationScope getInvocationScope() {
                             // Avoid re-entry in the current TF invocation scope for unit tests.
                             return new InvocationScope();
diff --git a/tests/src/com/android/tradefed/presubmit/TestMappingsValidation.java b/tests/src/com/android/tradefed/presubmit/TestMappingsValidation.java
index 9f5a056..4a5ef92 100644
--- a/tests/src/com/android/tradefed/presubmit/TestMappingsValidation.java
+++ b/tests/src/com/android/tradefed/presubmit/TestMappingsValidation.java
@@ -195,7 +195,10 @@
                     continue;
                 }
                 for (String param : params) {
-                    errors.add(validateMainlineModuleConfig(param, Set.of(configName)));
+                    String error = validateMainlineModuleConfig(param, Set.of(configName));
+                    if (!Strings.isNullOrEmpty(error)){
+                        errors.add(error);
+                    }
                 }
             } catch (ConfigurationException e) {
                 errors.add(String.format("\t%s: %s", configName, e.getMessage()));
diff --git a/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java b/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
index 67a623a..647db0b 100644
--- a/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
+++ b/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
@@ -170,7 +170,7 @@
         assertXmlContainsValue(
                 content,
                 "Result/Module/TestCase/Test/Failure/StackTrace",
-                "module1 failed.\nstack\n<null>stack[Null]");
+                XmlSuiteResultFormatter.sanitizeXmlContent("module1 failed.\nstack\nstack\0"));
         // Test that we can read back the informations
         SuiteResultHolder holder = mFormatter.parseResults(mResultDir, false);
         assertEquals(holder.completeModules, mResultHolder.completeModules);
@@ -224,7 +224,7 @@
         assertXmlContainsValue(
                 content,
                 "Result/Module/TestCase/Test/Failure/StackTrace",
-                "module1 failed.\nstack\n<null>stack[Null]");
+                XmlSuiteResultFormatter.sanitizeXmlContent("module1 failed.\nstack\nstack\0"));
         // Test that we can read back the informations
         SuiteResultHolder holder = mFormatter.parseResults(mResultDir, false);
         assertEquals(holder.completeModules, mResultHolder.completeModules);
@@ -477,7 +477,7 @@
         assertXmlContainsValue(
                 content,
                 "Result/Module/TestCase/Test/Failure/StackTrace",
-                "module1 failed.\nstack\n<null>stack[Null]");
+                XmlSuiteResultFormatter.sanitizeXmlContent("module1 failed.\nstack\nstack\0"));
         // Test that we can read back the informations
         SuiteResultHolder holder = mFormatter.parseResults(mResultDir, true);
         assertEquals(holder.completeModules, mResultHolder.completeModules);
@@ -671,7 +671,7 @@
                     new TestDescription("com.class." + runName, runName + ".failed" + i);
             fakeRes.testStarted(description);
             // Include a null character \0 that is not XML supported
-            fakeRes.testFailed(description, runName + " failed.\nstack\n<null>stack\0");
+            fakeRes.testFailed(description, runName + " failed.\nstack\nstack\0");
             HashMap<String, Metric> metrics = new HashMap<String, Metric>();
             if (withMetrics) {
                 metrics.put("metric0" + i, TfMetricProtoUtil.stringToMetric("value0" + i));
diff --git a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
index 0e5b502..23d8b75 100644
--- a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
@@ -25,6 +25,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
@@ -48,6 +49,9 @@
 @RunWith(JUnit4.class)
 public class ArtRunTestTest {
 
+    // Default run-test name.
+    private static final String RUN_TEST_NAME = "run-test";
+
     private ITestInvocationListener mMockInvocationListener;
     private IAbi mMockAbi;
     private ITestDevice mMockITestDevice;
@@ -56,9 +60,9 @@
     private ArtRunTest mArtRunTest;
     private OptionSetter mSetter;
     private TestInformation mTestInfo;
-    // Test dependencies directory on host.
-    private File mTmpDepsDir;
-    // Expected output file (within the dependencies directory).
+    // Target tests directory.
+    private File mTmpTargetTestsDir;
+    // Expected output file (under the target tests directory).
     private File mTmpExpectedFile;
 
     @Before
@@ -78,19 +82,23 @@
         mArtRunTest.setDevice(mMockITestDevice);
         mSetter = new OptionSetter(mArtRunTest);
 
-        // Expected output file.
-        mTmpDepsDir = FileUtil.createTempDir("art-run-test-deps");
-        mTmpExpectedFile = new File(mTmpDepsDir, "expected.txt");
+        // Set up target tests directory and expected output file.
+        mTmpTargetTestsDir = FileUtil.createTempDir("target_testcases");
+        File runTestDir = new File(mTmpTargetTestsDir, RUN_TEST_NAME);
+        runTestDir.mkdir();
+        mTmpExpectedFile = new File(runTestDir, "expected.txt");
         FileWriter fw = new FileWriter(mTmpExpectedFile);
         fw.write("output\n");
         fw.close();
 
-        mTestInfo = TestInformation.newBuilder().setDependenciesFolder(mTmpDepsDir).build();
+        // Set the target tests directory in test information object.
+        mTestInfo = TestInformation.newBuilder().build();
+        mTestInfo.executionFiles().put(FilesKey.TARGET_TESTS_DIRECTORY, mTmpTargetTestsDir);
     }
 
     @After
     public void tearDown() {
-        FileUtil.recursiveDelete(mTmpDepsDir);
+        FileUtil.recursiveDelete(mTmpTargetTestsDir);
     }
 
     /** Helper mocking writing the output of a test command. */
@@ -143,8 +151,7 @@
     @Test
     public void testRunSingleTest_unsetClasspathOption()
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        final String runTestName = "test";
-        mSetter.setOptionValue("run-test-name", runTestName);
+        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
 
         replayMocks();
         try {
@@ -160,8 +167,7 @@
     @Test
     public void testRunSingleTest()
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        final String runTestName = "test";
-        mSetter.setOptionValue("run-test-name", runTestName);
+        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
         final String classpath = "/data/local/tmp/test/test.jar";
         mSetter.setOptionValue("classpath", classpath);
 
@@ -171,7 +177,7 @@
         String runName = "ArtRunTest_abi";
         // Beginning of test.
         mMockInvocationListener.testRunStarted(runName, 1);
-        TestDescription testId = new TestDescription(runName, runTestName);
+        TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
         mMockInvocationListener.testStarted(testId);
         String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
         // Test execution.
@@ -199,8 +205,7 @@
     @Test
     public void testRunSingleTest_unexpectedOutput()
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        final String runTestName = "test";
-        mSetter.setOptionValue("run-test-name", runTestName);
+        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
         final String classpath = "/data/local/tmp/test/test.jar";
         mSetter.setOptionValue("classpath", classpath);
 
@@ -210,7 +215,7 @@
         String runName = "ArtRunTest_abi";
         // Beginning of test.
         mMockInvocationListener.testRunStarted(runName, 1);
-        TestDescription testId = new TestDescription(runName, runTestName);
+        TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
         mMockInvocationListener.testStarted(testId);
         String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
         // Test execution.
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
index 5dae1dd..ceec73d 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
@@ -36,6 +36,8 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -97,8 +99,12 @@
         mExecutableTest.run(mTestInfo, mMockListener);
 
         verify(mMockListener, Mockito.times(1)).testRunStarted(eq("test"), eq(0));
-        verify(mMockListener, Mockito.times(1))
-                .testRunFailed(String.format(ExecutableBaseTest.NO_BINARY_ERROR, path));
+        FailureDescription failure =
+                FailureDescription.create(
+                                String.format(ExecutableBaseTest.NO_BINARY_ERROR, path),
+                                FailureStatus.TEST_FAILURE)
+                        .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+        verify(mMockListener, Mockito.times(1)).testRunFailed(failure);
         verify(mMockListener, Mockito.times(1))
                 .testRunEnded(eq(0L), Mockito.<HashMap<String, Metric>>any());
     }
@@ -184,20 +190,25 @@
             doThrow(new DeviceNotAvailableException("test", "serial"))
                     .when(mMockDevice)
                     .waitForDeviceAvailable();
+            DeviceNotAvailableException dnae = null;
             try {
                 mExecutableTest.run(mTestInfo, mMockListener);
                 fail("Should have thrown an exception.");
             } catch (DeviceNotAvailableException expected) {
                 // Expected
+                dnae = expected;
             }
 
             verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
-            verify(mMockListener, Mockito.times(1))
-                    .testRunFailed(
-                            eq(
+            FailureDescription failure =
+                    FailureDescription.create(
                                     String.format(
                                             "Device became unavailable after %s.",
-                                            tmpBinary.getAbsolutePath())));
+                                            tmpBinary.getAbsolutePath()),
+                                    FailureStatus.LOST_SYSTEM_UNDER_TEST)
+                            .setErrorIdentifier(DeviceErrorIdentifier.DEVICE_UNAVAILABLE)
+                            .setCause(dnae);
+            verify(mMockListener, Mockito.times(1)).testRunFailed(eq(failure));
             verify(mMockListener, Mockito.times(0)).testFailed(any(), (String) any());
             verify(mMockListener, Mockito.times(1))
                     .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
@@ -259,12 +270,14 @@
             mExecutableTest.run(mTestInfo, mMockListener);
 
             verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(0));
-            verify(mMockListener, Mockito.times(1))
-                    .testRunFailed(
-                            eq(
+            FailureDescription failure =
+                    FailureDescription.create(
                                     String.format(
                                             ExecutableBaseTest.NO_BINARY_ERROR,
-                                            tmpBinary.getName())));
+                                            tmpBinary.getName()),
+                                    FailureStatus.TEST_FAILURE)
+                            .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+            verify(mMockListener, Mockito.times(1)).testRunFailed(eq(failure));
             verify(mMockListener, Mockito.times(1))
                     .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
         } finally {
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
index 05cf59a..ba18dc0 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.testtype.binary;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.eq;
 
 import com.android.tradefed.config.ConfigurationException;
@@ -24,8 +25,12 @@
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.metrics.proto.MetricMeasurement;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.CommandResult;
 
 import org.junit.Before;
@@ -35,6 +40,8 @@
 import org.mockito.Mockito;
 
 import java.util.HashMap;
+import java.util.Collection;
+import java.util.Map;
 
 /** Unit tests for {@link com.android.tradefed.testtype.binary.ExecutableTargetTest}. */
 @RunWith(JUnit4.class)
@@ -133,12 +140,20 @@
         mExecutableTargetTest.run(mTestInfo, mListener);
         // run cmd1 test
         Mockito.verify(mListener, Mockito.times(0)).testRunStarted(eq(testName1), eq(1));
-        Mockito.verify(mListener, Mockito.times(1))
-                .testRunFailed(String.format(NO_BINARY_ERROR, testCmd1));
+        FailureDescription failure1 =
+                FailureDescription.create(
+                                String.format(ExecutableBaseTest.NO_BINARY_ERROR, testCmd1),
+                                FailureStatus.TEST_FAILURE)
+                        .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+        Mockito.verify(mListener, Mockito.times(1)).testRunFailed(failure1);
         // run cmd2 test
         Mockito.verify(mListener, Mockito.times(0)).testRunStarted(eq(testName2), eq(1));
-        Mockito.verify(mListener, Mockito.times(1))
-                .testRunFailed(String.format(NO_BINARY_ERROR, testCmd2));
+        FailureDescription failure2 =
+                FailureDescription.create(
+                                String.format(ExecutableBaseTest.NO_BINARY_ERROR, testCmd2),
+                                FailureStatus.TEST_FAILURE)
+                        .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+        Mockito.verify(mListener, Mockito.times(1)).testRunFailed(failure2);
         Mockito.verify(mListener, Mockito.times(2))
                 .testRunEnded(
                         Mockito.anyLong(),
@@ -358,4 +373,31 @@
                         Mockito.eq(testDescription3),
                         Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
     }
+
+    /** Test split() for sharding */
+    @Test
+    public void testShard_Split() throws DeviceNotAvailableException, ConfigurationException {
+        mExecutableTargetTest = new ExecutableTargetTest();
+        // Set test commands
+        OptionSetter setter = new OptionSetter(mExecutableTargetTest);
+        setter.setOptionValue("test-command-line", testName1, testCmd1);
+        setter.setOptionValue("test-command-line", testName2, testCmd2);
+        setter.setOptionValue("test-command-line", testName3, testCmd3);
+        // Split the shard.
+        Collection<IRemoteTest> testShards = mExecutableTargetTest.split();
+        // Test the size of the test Shard.
+        assertEquals(3, testShards.size());
+        // Test the command of each shard.
+        for (IRemoteTest test : testShards) {
+            Map<String, String> TestCommands = ((ExecutableTargetTest) test).getTestCommands();
+            String cmd1 = TestCommands.get(testName1);
+            if (cmd1 != null) assertEquals(testCmd1, cmd1);
+            String cmd2 = TestCommands.get(testName2);
+            if (cmd2 != null) assertEquals(testCmd2, cmd2);
+            String cmd3 = TestCommands.get(testName3);
+            if (cmd3 != null) assertEquals(testCmd3, cmd3);
+            // The test command should equals to one of them.
+            assertEquals(true, cmd1 != null || cmd2 != null || cmd3 != null);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
index 3b0e0f9..de69a05 100644
--- a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
@@ -43,6 +43,8 @@
 import com.android.tradefed.util.IRunUtil;
 import com.android.utils.FileUtils;
 
+import com.google.common.base.Throwables;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -67,13 +69,13 @@
     private static final String DEVICE_SERIAL = "X123SER";
     private static final long DEFAULT_TIME_OUT = 30 * 1000L;
     private static final String TEST_RESULT_FILE_NAME = "test_summary.yaml";
-    private static final String TEMP_DIR = "/tmp";
 
     private MoblyBinaryHostTest mSpyTest;
     private ITestDevice mMockDevice;
     private IRunUtil mMockRunUtil;
     private MoblyYamlResultParser mMockParser;
     private InputStream mMockSummaryInputStream;
+    private File mMoblyTestDir;
     private File mMoblyBinary; // used by python-binaries option
     private File mMoblyBinary2; // used by par-file-name option
     private DeviceBuildInfo mMockBuildInfo;
@@ -88,17 +90,15 @@
         Mockito.doReturn(mMockRunUtil).when(mSpyTest).getRunUtil();
         Mockito.doReturn(DEFAULT_TIME_OUT).when(mSpyTest).getTestTimeout();
         Mockito.doReturn("not_adb").when(mSpyTest).getAdbPath();
-        mMoblyBinary = new File(TEMP_DIR, "mobly_binary.par");
-        FileUtils.createFile(mMoblyBinary, "");
-        mMoblyBinary2 = new File(TEMP_DIR, "mobly_binary_2.par");
-        FileUtils.createFile(mMoblyBinary2, "");
+        mMoblyTestDir = FileUtil.createTempDir("mobly_tests");
+        mMoblyBinary = FileUtil.createTempFile("mobly_binary", ".par", mMoblyTestDir);
+        mMoblyBinary2 = FileUtil.createTempFile("mobly_binary_2", ".par", mMoblyTestDir);
         mSpyTest.setBuild(mMockBuildInfo);
     }
 
     @After
     public void tearDown() throws Exception {
-        FileUtils.deleteIfExists(mMoblyBinary);
-        FileUtils.deleteIfExists(mMoblyBinary2);
+        FileUtil.recursiveDelete(mMoblyTestDir);
     }
 
     @Test
@@ -142,7 +142,7 @@
     public void testRun_withParFileNameOption() throws Exception {
         OptionSetter setter = new OptionSetter(mSpyTest);
         setter.setOptionValue("par-file-name", mMoblyBinary2.getName());
-        Mockito.doReturn(new File(TEMP_DIR)).when(mMockBuildInfo).getTestsDir();
+        Mockito.doReturn(mMoblyTestDir).when(mMockBuildInfo).getTestsDir();
         File testResult = new File(mSpyTest.getLogDirAbsolutePath(), TEST_RESULT_FILE_NAME);
         Mockito.when(mMockRunUtil.runTimedCmd(anyLong(), any()))
                 .thenAnswer(
@@ -168,7 +168,7 @@
     public void testRun_withParFileNameOption_binaryNotFound() throws Exception {
         OptionSetter setter = new OptionSetter(mSpyTest);
         setter.setOptionValue("par-file-name", mMoblyBinary2.getName());
-        Mockito.doReturn(new File(TEMP_DIR)).when(mMockBuildInfo).getTestsDir();
+        Mockito.doReturn(mMoblyTestDir).when(mMockBuildInfo).getTestsDir();
         FileUtil.deleteFile(mMoblyBinary2);
 
         try {
@@ -177,6 +177,9 @@
         } catch (RuntimeException e) {
             verify(mSpyTest, never()).reportLogs(any(), any());
             assertEquals(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
                     e.getMessage(),
                     String.format("Couldn't find a par file %s", mMoblyBinary2.getName()));
         }
@@ -202,6 +205,9 @@
             fail("Should have thrown an exception");
         } catch (RuntimeException e) {
             assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
                     e.getMessage(),
                     containsString(
                             "Fail to find test summary file test_summary.yaml under directory"));
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index b0f8d09..1cb5dd1 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -122,6 +122,7 @@
 
             CommandResult res = new CommandResult();
             res.setStatus(CommandStatus.SUCCESS);
+            res.setStdout("python binary stdout.");
             res.setStderr("TEST_RUN_STARTED {\"testCount\": 5, \"runName\": \"TestSuite\"}");
             EasyMock.expect(
                             mMockRunUtil.runTimedCmd(
@@ -133,6 +134,10 @@
                     EasyMock.eq(0),
                     EasyMock.anyLong());
             mMockListener.testLog(
+                    EasyMock.eq(binary.getName() + "-stdout"),
+                    EasyMock.eq(LogDataType.TEXT),
+                    EasyMock.anyObject());
+            mMockListener.testLog(
                     EasyMock.eq(binary.getName() + "-stderr"),
                     EasyMock.eq(LogDataType.TEXT),
                     EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
index 91bb1d1..782eac0 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.util.CommandResult;
@@ -64,23 +65,34 @@
         mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
     }
 
+    private CommandResult newCommandResult(CommandStatus status, String stderr, String stdout) {
+        CommandResult res = new CommandResult();
+        res.setStatus(status);
+        res.setStderr(stderr);
+        res.setStdout(stdout);
+        return res;
+    }
+
+    private String resultCount(int pass, int fail, int ignore) {
+        return "test result: ok. " + pass + " passed; " + fail + " failed; " + ignore + " ignored;";
+    }
+
+    private CommandResult successResult(String stderr, String stdout) throws Exception {
+        return newCommandResult(CommandStatus.SUCCESS, stderr, stdout);
+    }
+
     /** Add mocked call "binary --list" to count the number of tests. */
     private void mockCountTests(File binary, int numOfTest) throws Exception {
-        CommandResult res = new CommandResult();
-        res.setStatus(CommandStatus.SUCCESS);
-        res.setStderr("");
-        res.setStdout(numOfTest + " tests, 0 benchmarks");
         EasyMock.expect(
                         mMockRunUtil.runTimedCmdSilently(
                                 EasyMock.anyLong(),
                                 EasyMock.eq(binary.getAbsolutePath()),
                                 EasyMock.eq("--list")))
-                .andReturn(res);
+                .andReturn(successResult("", numOfTest + " tests, 0 benchmarks"));
     }
 
-    /** Add mocked call to count tests and testRunStarted. */
-    private void mockTestRunStarted(File binary, int count) throws Exception {
-        mockCountTests(binary, count);
+    /** Add mocked testRunStarted call to the listener. */
+    private void mockListenerStarted(File binary, int count) throws Exception {
         mMockListener.testRunStarted(
                 EasyMock.eq(binary.getName()),
                 EasyMock.eq(count),
@@ -88,23 +100,21 @@
                 EasyMock.anyLong());
     }
 
-    /** Add mocked call to "binary" with result status, stderr, and stdout. */
-    private void mockRunTest(File binary, CommandStatus status, String stderr, String stdout)
-            throws Exception {
-        CommandResult res = new CommandResult();
-        res.setStatus(status);
-        res.setStderr(stderr);
-        res.setStdout(stdout);
-        EasyMock.expect(
-                        mMockRunUtil.runTimedCmd(
-                                EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
-                .andReturn(res);
+    /** Add mocked call to check listener log file. */
+    private void mockListenerLog(File binary) {
         mMockListener.testLog(
                 EasyMock.eq(binary.getName() + "-stderr"),
                 EasyMock.eq(LogDataType.TEXT),
                 EasyMock.anyObject());
     }
 
+    private void mockTestRunExpect(File binary, CommandResult res) throws Exception {
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
+                .andReturn(res);
+    }
+
     /** Add mocked call to testRunEnded. */
     private void mockTestRunEnded() {
         mMockListener.testRunEnded(
@@ -125,12 +135,11 @@
         try {
             OptionSetter setter = new OptionSetter(mTest);
             setter.setOptionValue("test-file", binary.getAbsolutePath());
-            mockTestRunStarted(binary, 9);
-            mockRunTest(
-                    binary,
-                    CommandStatus.SUCCESS,
-                    "",
-                    "test result: ok. 6 passed; 1 failed; 2 ignored;");
+            mockCountTests(binary, 9);
+            mockListenerStarted(binary, 9);
+            mockListenerLog(binary);
+            CommandResult res = successResult("", resultCount(6, 1, 2));
+            mockTestRunExpect(binary, res);
             mockTestRunEnded();
             callReplayRunVerify();
         } finally {
@@ -151,12 +160,11 @@
         try {
             OptionSetter setter = new OptionSetter(mTest);
             setter.setOptionValue("test-file", binary.getAbsolutePath());
-            mockTestRunStarted(binary, 9);
-            mockRunTest(
-                    binary,
-                    CommandStatus.SUCCESS,
-                    "",
-                    "test result: ok. 6 passed; 1 failed; 2 ignored;");
+            mockCountTests(binary, 9);
+            mockListenerStarted(binary, 9);
+            mockListenerLog(binary);
+            CommandResult res = successResult("", resultCount(6, 1, 2));
+            mockTestRunExpect(binary, res);
             mockTestRunEnded();
             callReplayRunVerify();
         } finally {
@@ -171,11 +179,13 @@
         try {
             OptionSetter setter = new OptionSetter(mTest);
             setter.setOptionValue("test-file", binary.getAbsolutePath());
-            mockTestRunStarted(binary, 0);
-            mockRunTest(
-                    binary, CommandStatus.EXCEPTION, "Count not execute.", "Could not execute.");
+            mockCountTests(binary, 0);
+            mockListenerStarted(binary, 0);
+            mockListenerLog(binary);
+            CommandResult res = newCommandResult(CommandStatus.EXCEPTION, "Err.", "Exception.");
+            mockTestRunExpect(binary, res);
             mMockListener.testRunFailed((String) EasyMock.anyObject());
-            mMockListener.testRunFailed((String) EasyMock.anyObject());
+            mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
             mockTestRunEnded();
             callReplayRunVerify();
         } finally {
@@ -190,10 +200,7 @@
         try {
             OptionSetter setter = new OptionSetter(mTest);
             setter.setOptionValue("test-file", binary.getAbsolutePath());
-            CommandResult listRes = new CommandResult();
-            listRes.setStatus(CommandStatus.FAILED);
-            listRes.setStderr("");
-            listRes.setStdout("");
+            CommandResult listRes = newCommandResult(CommandStatus.FAILED, "", "");
             EasyMock.expect(
                             mMockRunUtil.runTimedCmdSilently(
                                     EasyMock.anyLong(),
@@ -205,12 +212,10 @@
                     EasyMock.eq(0),
                     EasyMock.anyInt(),
                     EasyMock.anyLong());
-            mockRunTest(
-                    binary,
-                    CommandStatus.FAILED,
-                    "",
-                    "test result: ok. 6 passed; 1 failed; 2 ignored;");
-            mMockListener.testRunFailed((String) EasyMock.anyObject());
+            mockListenerLog(binary);
+            CommandResult res = newCommandResult(CommandStatus.FAILED, "", resultCount(6, 1, 2));
+            mockTestRunExpect(binary, res);
+            mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
             mockTestRunEnded();
             callReplayRunVerify();
         } finally {
@@ -225,13 +230,99 @@
         try {
             OptionSetter setter = new OptionSetter(mTest);
             setter.setOptionValue("test-file", binary.getAbsolutePath());
-            mockTestRunStarted(binary, 9);
-            mockRunTest(
-                    binary,
-                    CommandStatus.FAILED,
-                    "",
-                    "test result: ok. 6 passed; 1 failed; 2 ignored;");
-            mMockListener.testRunFailed((String) EasyMock.anyObject());
+            mockCountTests(binary, 9);
+            mockListenerStarted(binary, 9);
+            mockListenerLog(binary);
+            CommandResult res = newCommandResult(CommandStatus.FAILED, "", resultCount(6, 1, 2));
+            mockTestRunExpect(binary, res);
+            mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
+            mockTestRunEnded();
+            callReplayRunVerify();
+        } finally {
+            FileUtil.deleteFile(binary);
+        }
+    }
+
+    /** Test the exclude filtering of test methods. */
+    @Test
+    public void testExcludeFilter() throws Exception {
+        File binary = FileUtil.createTempFile("rust-dir", "");
+        try {
+            OptionSetter setter = new OptionSetter(mTest);
+            setter.setOptionValue("test-file", binary.getAbsolutePath());
+            setter.setOptionValue("exclude-filter", "NotMe");
+            setter.setOptionValue("exclude-filter", "Long");
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmdSilently(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Long"),
+                                    EasyMock.eq("--list")))
+                    .andReturn(successResult("", "9 tests, 0 benchmarks"));
+            mockListenerStarted(binary, 9);
+            mockListenerLog(binary);
+            CommandResult res = successResult("", resultCount(6, 1, 2));
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmd(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Long")))
+                    .andReturn(res);
+
+            mockTestRunEnded();
+            callReplayRunVerify();
+        } finally {
+            FileUtil.deleteFile(binary);
+        }
+    }
+
+    /** Test both include and exclude filters. */
+    @Test
+    public void testIncludeExcludeFilter() throws Exception {
+        File binary = FileUtil.createTempFile("rust-dir", "");
+        try {
+            OptionSetter setter = new OptionSetter(mTest);
+            setter.setOptionValue("test-file", binary.getAbsolutePath());
+            setter.setOptionValue("exclude-filter", "NotMe");
+            setter.setOptionValue("include-filter", "OnlyMe");
+            setter.setOptionValue("exclude-filter", "Other");
+            setter.setOptionValue("include-filter", "Me2");
+            // We always pass the include-filter before exclude-filter strings.
+            // Multiple include filters are accepted but all except the 1st are ignored.
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmdSilently(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
+                                    EasyMock.eq("OnlyMe"),
+                                    EasyMock.eq("Me2"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Other"),
+                                    EasyMock.eq("--list")))
+                    .andReturn(successResult("", "3 tests, 0 benchmarks"));
+            mockListenerStarted(binary, 3);
+
+            mockListenerLog(binary);
+            CommandResult res = successResult("", resultCount(3, 0, 0));
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmd(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
+                                    EasyMock.eq("OnlyMe"),
+                                    EasyMock.eq("Me2"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Other")))
+                    .andReturn(res);
+
             mockTestRunEnded();
             callReplayRunVerify();
         } finally {
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java b/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
index 2672314..a633ed1 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
@@ -328,11 +328,13 @@
         EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
         EasyMock.expect(
                         mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
+                                        + " /data/misc/trace/coverage.tar -T -"))
                 .andReturn("");
         EasyMock.expect(
                         mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
+                                        + " /data/misc/trace/coverage.tar -T -"))
                 .andReturn("");
         File tmpFile1 = FileUtil.createTempFile("coverage", ".tar");
         EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile1);
@@ -431,11 +433,13 @@
         EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
         EasyMock.expect(
                         mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
+                                        + " /data/misc/trace/coverage.tar -T -"))
                 .andReturn("");
         EasyMock.expect(
                         mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
+                                        + " /data/misc/trace/coverage.tar -T -"))
                 .andReturn("");
         File tmpFile1 = FileUtil.createTempFile("coverage", ".tar");
         EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile1);
@@ -479,4 +483,53 @@
         mockTestRunEnded();
         callReplayRunVerify();
     }
+
+    /**
+     * Helper function to do the actual filtering test.
+     *
+     * @param filterString The string to search for in the Mock, to verify filtering was called
+     * @throws DeviceNotAvailableException
+     */
+    private void doTestFilter(String filterString) throws DeviceNotAvailableException {
+        final String testPath = RustBinaryTest.DEFAULT_TEST_PATH;
+        final String test1 = "test1";
+        final String testPath1 = String.format("%s/%s", testPath, test1);
+        final String[] files = new String[] {test1};
+
+        // Find files
+        MockFileUtil.setMockDirContents(mMockITestDevice, testPath, test1);
+        EasyMock.expect(mMockITestDevice.doesFileExist(testPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.getChildren(testPath)).andReturn(files);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+
+        mockCountTests(testPath1 + filterString, "test1\n3 tests, 0 benchmarks\n");
+        mockTestRunStarted("test1", 3);
+        mockShellCommand(test1 + filterString);
+        mockTestRunEnded();
+        callReplayRunVerify();
+    }
+
+    /** Test the exclude-filter option. */
+    @Test
+    public void testExcludeFilter() throws Exception {
+        OptionSetter setter = new OptionSetter(mRustBinaryTest);
+        setter.setOptionValue("exclude-filter", "NotMe");
+        setter.setOptionValue("exclude-filter", "Long");
+        doTestFilter(" --skip NotMe --skip Long");
+    }
+
+    /** Test both include- and exclude-filter options. */
+    @Test
+    public void testIncludeExcludeFilter() throws Exception {
+        OptionSetter setter = new OptionSetter(mRustBinaryTest);
+        setter.setOptionValue("exclude-filter", "NotMe2");
+        setter.setOptionValue("include-filter", "OnlyMe");
+        setter.setOptionValue("exclude-filter", "other");
+        setter.setOptionValue("include-filter", "Me2");
+        // Include filters are passed before exclude filters.
+        // Multiple include filters are accepted, but all except the 1st are ignored.
+        doTestFilter(" OnlyMe Me2 --skip NotMe2 --skip other");
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
index 7d77450..4a58724 100644
--- a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
@@ -424,6 +424,37 @@
         assertEquals("armeabi-v7a", descriptor.getAbi().getName());
     }
 
+    /**
+     * Test that if the base module is excluded in full, the filters of parameterized modules are
+     * still populated with the proper filters.
+     */
+    @Test
+    public void testFilterParameterized_excludeFilter_parameter() throws Exception {
+        Map<String, List<SuiteTestFilter>> excludeFilters = new LinkedHashMap<>();
+        createInstantModuleConfig("basemodule");
+        SuiteTestFilter fullFilter = SuiteTestFilter.createFrom("armeabi-v7a basemodule[instant]");
+        excludeFilters.put("basemodule[instant]", Arrays.asList(fullFilter));
+
+        mRepo =
+                new SuiteModuleLoader(
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        excludeFilters,
+                        new ArrayList<>(),
+                        new ArrayList<>());
+        mRepo.setParameterizedModules(true);
+
+        List<String> patterns = new ArrayList<>();
+        patterns.add(".*.config");
+        patterns.add(".*.xml");
+        LinkedHashMap<String, IConfiguration> res =
+            mRepo.loadConfigsFromDirectory(
+                Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+        assertEquals(1, res.size());
+        // Full module was excluded completely
+        IConfiguration instantModule = res.get("armeabi-v7a basemodule[instant]");
+        assertNull(instantModule);
+    }
+
     @Test
     public void testFilterParameterized_includeFilter_base() throws Exception {
         Map<String, List<SuiteTestFilter>> includeFilters = new LinkedHashMap<>();
@@ -478,6 +509,36 @@
         assertNotNull(instantModule);
     }
 
+    @Test
+    public void testFilterParameterized_WithModuleArg() throws Exception {
+        List<String> moduleArgs = new ArrayList<>();
+        createInstantModuleConfig("basemodule");
+        moduleArgs.add("basemodule[instant]:exclude-annotation:test-annotation");
+
+        mRepo =
+                new SuiteModuleLoader(
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new ArrayList<>(),
+                        moduleArgs);
+        mRepo.setParameterizedModules(true);
+
+        List<String> patterns = new ArrayList<>();
+        patterns.add(".*.config");
+        patterns.add(".*.xml");
+        LinkedHashMap<String, IConfiguration> res =
+                mRepo.loadConfigsFromDirectory(
+                        Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+        assertEquals(2, res.size());
+        IConfiguration instantModule = res.get("armeabi-v7a basemodule[instant]");
+        assertNotNull(instantModule);
+        TestSuiteStub stubTest = (TestSuiteStub) instantModule.getTests().get(0);
+        assertEquals(2, stubTest.getExcludeAnnotations().size());
+        List<String> expected =
+                Arrays.asList("android.platform.test.annotations.AppModeFull", "test-annotation");
+        assertTrue(stubTest.getExcludeAnnotations().containsAll(expected));
+    }
+
     /**
      * Test that the configuration can be found if specifying specific path.
      */
@@ -759,6 +820,40 @@
 
     /**
      * Test that generate the correct IConfiguration objects based on the defined mainline modules
+     * with given module args.
+     */
+    @Test
+    public void testLoadParameterizedMainlineModule_WithModuleArgs() throws Exception {
+        List<String> moduleArgs = new ArrayList<>();
+        moduleArgs.add("basemodule[mod1.apk]:exclude-annotation:test-annotation");
+        createMainlineModuleConfig("basemodule");
+
+        mRepo =
+                new SuiteModuleLoader(
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new ArrayList<>(),
+                        moduleArgs);
+        mRepo.setInvocationContext(mContext);
+        mRepo.setMainlineParameterizedModules(true);
+
+        List<String> patterns = new ArrayList<>();
+        patterns.add(".*.config");
+        patterns.add(".*.xml");
+        LinkedHashMap<String, IConfiguration> res =
+                mRepo.loadConfigsFromDirectory(
+                        Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+        assertEquals(3, res.size());
+        IConfiguration module1 = res.get("armeabi-v7a basemodule[mod1.apk]");
+        assertNotNull(module1);
+        TestSuiteStub stubTest = (TestSuiteStub) module1.getTests().get(0);
+        assertEquals(1, stubTest.getExcludeAnnotations().size());
+        assertEquals("test-annotation", stubTest.getExcludeAnnotations().iterator().next());
+        EasyMock.verify(mMockBuildInfo);
+    }
+
+    /**
+     * Test that generate the correct IConfiguration objects based on the defined mainline modules
      * with given include-filter and exclude-filter.
      */
     @Test
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index 99d8164..07e0807 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -31,7 +31,7 @@
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.testtype.Abi;
-import com.android.tradefed.testtype.GTest;
+import com.android.tradefed.testtype.HostTest;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.testtype.IRemoteTest;
@@ -53,6 +53,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
@@ -93,7 +94,7 @@
             + "    <option name=\"config-descriptor:metadata\" key=\"mainline-param\" value=\"mod2.apk\" />"
             + "    <option name=\"config-descriptor:metadata\" key=\"mainline-param\" value=\"mod1.apk+mod2.apk\" />"
             + "    <option name=\"config-descriptor:metadata\" key=\"mainline-param\" value=\"mod1.apk+mod2.apk+mod3.apk\" />"
-            + "    <test class=\"com.android.tradefed.testtype.GTest\" />\n"
+            + "    <test class=\"com.android.tradefed.testtype.HostTest\" />\n"
             + "</configuration>";
 
     @Before
@@ -514,6 +515,58 @@
         }
     }
 
+    /**
+     * Test for {@link TestMappingSuiteRunner#loadTests()} for loading tests from test_mappings.zip
+     * and run with shard, and no test is split due to exclude-filter.
+     */
+    @Test
+    public void testLoadTests_shardNoTest() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+            ZipUtil.createZip(srcDir, zipFile);
+
+            mOptionSetter.setOptionValue("test-mapping-test-group", "postsubmit");
+            mOptionSetter.setOptionValue("test-mapping-path", srcDir.getName());
+            mOptionSetter.setOptionValue("exclude-filter", "suite/stub1");
+
+            IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+            EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+                    .andStubReturn(null);
+            EasyMock.expect(mockBuildInfo.getTestsDir())
+                    .andStubReturn(new File("non-existing-dir"));
+            EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+            mTestInfo
+                    .getContext()
+                    .addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mockBuildInfo);
+            EasyMock.replay(mockBuildInfo);
+
+            Collection<IRemoteTest> tests = mRunner.split(2, mTestInfo);
+            assertEquals(null, tests);
+            assertEquals(2, mRunner.getIncludeFilter().size());
+            assertEquals(null, mRunner.getTestGroup());
+            assertEquals(0, mRunner.getTestMappingPaths().size());
+            assertEquals(false, mRunner.getUseTestMappingPath());
+            EasyMock.verify(mockBuildInfo);
+        } finally {
+            // Clean up the static variable due to the usage of option `test-mapping-path`.
+            TestMapping.setTestMappingPaths(new ArrayList<String>());
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
     /** Test for {@link TestMappingSuiteRunner#loadTests()} to fail when no test is found. */
     @Test(expected = RuntimeException.class)
     public void testLoadTests_noTest() throws Exception {
@@ -870,14 +923,16 @@
             assertTrue(configMap.containsKey(ABI_1 + " test[mod1.apk]"));
             assertTrue(configMap.containsKey(ABI_1 + " test[mod2.apk]"));
             assertTrue(configMap.containsKey(ABI_1 + " test[mod1.apk+mod2.apk]"));
-            GTest test = (GTest) configMap.get(ABI_1 + " test[mod1.apk]").getTests().get(0);
+            HostTest test = (HostTest) configMap.get(ABI_1 + " test[mod1.apk]").getTests().get(0);
             assertTrue(test.getIncludeFilters().contains("test-filter"));
 
-            test = (GTest) configMap.get(ABI_1 + " test[mod2.apk]").getTests().get(0);
+            test = (HostTest) configMap.get(ABI_1 + " test[mod2.apk]").getTests().get(0);
             assertTrue(test.getIncludeFilters().contains("test-filter2"));
 
-            test = (GTest) configMap.get(ABI_1 + " test[mod1.apk+mod2.apk]").getTests().get(0);
+            test = (HostTest) configMap.get(ABI_1 + " test[mod1.apk+mod2.apk]").getTests().get(0);
             assertTrue(test.getIncludeFilters().isEmpty());
+            assertEquals(1, test.getExcludeAnnotations().size());
+            assertEquals("test-annotation", test.getExcludeAnnotations().iterator().next());
 
             EasyMock.verify(mockBuildInfo);
         } finally {
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index c1537ca..e23201f 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -166,6 +166,8 @@
 
             EasyMock.replay(mockBuildInfo);
 
+            // Ensure the static variable doesn't have any relative path configured.
+            TestMapping.setTestMappingPaths(new ArrayList<String>());
             Set<TestInfo> tests = TestMapping.getTests(mockBuildInfo, "presubmit", false, null);
             assertEquals(0, tests.size());