Merge "Ensure the new testFailed interface is aggregated properly"
diff --git a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
index d7843d9..a87a79e 100644
--- a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
@@ -359,8 +359,12 @@
                             info,
                             options,
                             runUtil);
-            mRemoteConsoleStdErr = FileUtil.readStringFromFile(stderr);
-            FileUtil.recursiveDelete(stderr);
+            if (stderr != null && stderr.exists()) {
+                mRemoteConsoleStdErr = FileUtil.readStringFromFile(stderr);
+                FileUtil.recursiveDelete(stderr);
+            } else {
+                mRemoteConsoleStdErr = "Failed to fetch stderr from remote.";
+            }
         }
 
         // If not result in progress are reported, parse the full results at the end.
diff --git a/src/com/android/tradefed/util/TestFilterHelper.java b/src/com/android/tradefed/util/TestFilterHelper.java
index 892a0ce..84ea0f3 100644
--- a/src/com/android/tradefed/util/TestFilterHelper.java
+++ b/src/com/android/tradefed/util/TestFilterHelper.java
@@ -253,35 +253,40 @@
     public boolean shouldRun(Description desc, List<File> extraJars) {
         // We need to build the packageName for a description object
         Class<?> classObj = null;
+        URLClassLoader cl = null;
         try {
-            List<URL> urlList = new ArrayList<>();
-            for (File f : extraJars) {
-                urlList.add(f.toURI().toURL());
+            try {
+                List<URL> urlList = new ArrayList<>();
+                for (File f : extraJars) {
+                    urlList.add(f.toURI().toURL());
+                }
+                cl = URLClassLoader.newInstance(urlList.toArray(new URL[0]));
+                classObj = cl.loadClass(desc.getClassName());
+            } catch (MalformedURLException | ClassNotFoundException e) {
+                throw new IllegalArgumentException(
+                        String.format("Could not load Test class %s", classObj), e);
             }
-            URLClassLoader cl = URLClassLoader.newInstance(urlList.toArray(new URL[0]));
-            classObj = cl.loadClass(desc.getClassName());
-        } catch (MalformedURLException | ClassNotFoundException e) {
-            throw new IllegalArgumentException(String.format("Could not load Test class %s",
-                    classObj), e);
-        }
-        // If class is explicitly annotated to be excluded, exclude it.
-        if (isExcluded(Arrays.asList(classObj.getAnnotations()))) {
-            return false;
-        }
-        String packageName = classObj.getPackage().getName();
 
-        String className = desc.getClassName();
-        String methodName = String.format("%s#%s", className, desc.getMethodName());
-        if (!shouldRunFilter(packageName, className, methodName)) {
-            return false;
+            // If class is explicitly annotated to be excluded, exclude it.
+            if (isExcluded(Arrays.asList(classObj.getAnnotations()))) {
+                return false;
+            }
+            String packageName = classObj.getPackage().getName();
+            String className = desc.getClassName();
+            String methodName = String.format("%s#%s", className, desc.getMethodName());
+            if (!shouldRunFilter(packageName, className, methodName)) {
+                return false;
+            }
+            if (!shouldTestRun(desc)) {
+                return false;
+            }
+            return mIncludeFilters.isEmpty()
+                    || mIncludeFilters.contains(methodName)
+                    || mIncludeFilters.contains(className)
+                    || mIncludeFilters.contains(packageName);
+        } finally {
+            StreamUtil.close(cl);
         }
-        if (!shouldTestRun(desc)) {
-            return false;
-        }
-        return mIncludeFilters.isEmpty()
-                || mIncludeFilters.contains(methodName)
-                || mIncludeFilters.contains(className)
-                || mIncludeFilters.contains(packageName);
     }
 
     /**
diff --git a/src/com/android/tradefed/util/TestLoader.java b/src/com/android/tradefed/util/TestLoader.java
index 848d2cd..962cc9f 100644
--- a/src/com/android/tradefed/util/TestLoader.java
+++ b/src/com/android/tradefed/util/TestLoader.java
@@ -55,8 +55,12 @@
             Set<String> classNames =
                     scanner.getEntriesFromJar(testJarFile, new ExternalClassNameFilter()).keySet();
 
-            ClassLoader jarClassLoader = buildJarClassLoader(testJarFile, dependentJars);
-            return loadTests(classNames, jarClassLoader);
+            URLClassLoader jarClassLoader = buildJarClassLoader(testJarFile, dependentJars);
+            try {
+                return loadTests(classNames, jarClassLoader);
+            } finally {
+                jarClassLoader.close();
+            }
         } catch (IOException e) {
             Log.e(LOG_TAG, String.format("IOException when loading test classes from jar %s",
                     testJarFile.getAbsolutePath()));
@@ -65,7 +69,7 @@
         return null;
     }
 
-    private ClassLoader buildJarClassLoader(File jarFile, Collection<File> dependentJars)
+    private URLClassLoader buildJarClassLoader(File jarFile, Collection<File> dependentJars)
             throws MalformedURLException {
         URL[] urls = new URL[dependentJars.size() + 1];
         urls[0] = jarFile.toURI().toURL();
diff --git a/test_framework/com/android/tradefed/testtype/HostTest.java b/test_framework/com/android/tradefed/testtype/HostTest.java
index f9aad4b..476c43b 100644
--- a/test_framework/com/android/tradefed/testtype/HostTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostTest.java
@@ -181,6 +181,8 @@
     private boolean mSkipTestClassCheck = false;
 
     private List<Object> mTestMethods;
+    private List<Class<?>> mLoadedClasses = new ArrayList<>();
+    private List<URLClassLoader> mOpenClassLoaders = new ArrayList<>();
 
     // Initialized as -1 to indicate that this value needs to be recalculated
     // when test count is requested.
@@ -508,38 +510,46 @@
         mFilterHelper.addAllExcludeAnnotation(mExcludeAnnotations);
 
         try {
-            List<Class<?>> classes = getClasses();
-            if (!mSkipTestClassCheck) {
-                if (classes.isEmpty()) {
-                    throw new IllegalArgumentException("No '--class' option was specified.");
+            try {
+                List<Class<?>> classes = getClasses();
+                if (!mSkipTestClassCheck) {
+                    if (classes.isEmpty()) {
+                        throw new IllegalArgumentException("No '--class' option was specified.");
+                    }
                 }
+                if (mMethodName != null && classes.size() > 1) {
+                    throw new IllegalArgumentException(
+                            String.format(
+                                    "'--method' only supports one '--class' name. Multiple were "
+                                            + "given: '%s'",
+                                    classes));
+                }
+            } catch (IllegalArgumentException e) {
+                listener.testRunStarted(this.getClass().getCanonicalName(), 0);
+                FailureDescription failureDescription =
+                        FailureDescription.create(StreamUtil.getStackTrace(e));
+                failureDescription.setFailureStatus(FailureStatus.TEST_FAILURE);
+                listener.testRunFailed(failureDescription);
+                listener.testRunEnded(0L, new HashMap<String, Metric>());
+                throw e;
             }
-            if (mMethodName != null && classes.size() > 1) {
-                throw new IllegalArgumentException(
-                        String.format(
-                                "'--method' only supports one '--class' name. Multiple were "
-                                        + "given: '%s'",
-                                classes));
-            }
-        } catch (IllegalArgumentException e) {
-            listener.testRunStarted(this.getClass().getCanonicalName(), 0);
-            FailureDescription failureDescription =
-                    FailureDescription.create(StreamUtil.getStackTrace(e));
-            failureDescription.setFailureStatus(FailureStatus.TEST_FAILURE);
-            listener.testRunFailed(failureDescription);
-            listener.testRunEnded(0L, new HashMap<String, Metric>());
-            throw e;
-        }
 
-        // Add a pretty logger to the events to mark clearly start/end of test cases.
-        if (mEnableHostDeviceLogs) {
-            PrettyTestEventLogger logger = new PrettyTestEventLogger(mTestInfo.getDevices());
-            listener = new ResultForwarder(logger, listener);
-        }
-        if (mTestMethods != null) {
-            runTestCases(listener);
-        } else {
-            runTestClasses(listener);
+            // Add a pretty logger to the events to mark clearly start/end of test cases.
+            if (mEnableHostDeviceLogs) {
+                PrettyTestEventLogger logger = new PrettyTestEventLogger(mTestInfo.getDevices());
+                listener = new ResultForwarder(logger, listener);
+            }
+            if (mTestMethods != null) {
+                runTestCases(listener);
+            } else {
+                runTestClasses(listener);
+            }
+        } finally {
+            mLoadedClasses.clear();
+            for (URLClassLoader cl : mOpenClassLoaders) {
+                StreamUtil.close(cl);
+            }
+            mOpenClassLoaders.clear();
         }
     }
 
@@ -885,9 +895,12 @@
     }
 
     protected final List<Class<?>> getClasses() throws IllegalArgumentException {
+        if (!mLoadedClasses.isEmpty()) {
+            return mLoadedClasses;
+        }
         // Use a set to avoid repeat between filters and jar search
         Set<String> classNames = new HashSet<>();
-        List<Class<?>> classes = new ArrayList<>();
+        List<Class<?>> classes = mLoadedClasses;
         for (String className : mClasses) {
             if (classNames.contains(className)) {
                 continue;
@@ -910,18 +923,21 @@
                                 .getUniqueMap()
                                 .get(ModuleDefinition.MODULE_NAME);
                 if (moduleName != null) {
+                    URLClassLoader cl = null;
                     try {
                         File f = getJarFile(moduleName + ".jar", mTestInfo);
                         URL[] urls = {f.toURI().toURL()};
-                        URLClassLoader cl = URLClassLoader.newInstance(urls);
+                        cl = URLClassLoader.newInstance(urls);
                         mJUnit4JarFiles.add(f);
                         Class<?> cls = cl.loadClass(className);
                         classes.add(cls);
                         classNames.add(className);
                         initialError = null;
+                        mOpenClassLoaders.add(cl);
                     } catch (FileNotFoundException
                             | MalformedURLException
                             | ClassNotFoundException fallbackSearch) {
+                        StreamUtil.close(cl);
                         CLog.e(
                                 "Fallback search for a jar containing '%s' didn't work."
                                         + "Consider using --jar option directly instead of using --class",
@@ -933,6 +949,7 @@
                 throw initialError;
             }
         }
+        URLClassLoader cl = null;
         // Inspect for the jar files
         for (String jarName : mJars) {
             JarFile jarFile = null;
@@ -941,8 +958,9 @@
                 jarFile = new JarFile(file);
                 Enumeration<JarEntry> e = jarFile.entries();
                 URL[] urls = {file.toURI().toURL()};
-                URLClassLoader cl = URLClassLoader.newInstance(urls);
+                cl = URLClassLoader.newInstance(urls);
                 mJUnit4JarFiles.add(file);
+                mOpenClassLoaders.add(cl);
 
                 while (e.hasMoreElements()) {
                     JarEntry je = e.nextElement();
@@ -1191,56 +1209,63 @@
         }
         mTestInfo = testInfo;
         List<IRemoteTest> listTests = new ArrayList<>();
-        List<Class<?>> classes = getClasses();
-        if (classes.isEmpty()) {
-            throw new IllegalArgumentException("Missing Test class name");
-        }
-        if (mMethodName != null && classes.size() > 1) {
-            throw new IllegalArgumentException("Method name given with multiple test classes");
-        }
-        List<? extends Object> testObjects;
-        if (shardUnitIsMethod()) {
-            testObjects = getTestMethods();
-        } else {
-            testObjects = classes;
-            // ignore shardCount when shard unit is class;
-            // simply shard by the number of classes
-            shardCount = testObjects.size();
-        }
-        if (testObjects.size() == 1) {
-            return null;
-        }
-        int i = 0;
-        int numTotalTestCases = countTestCases();
-        for (Object testObj : testObjects) {
-            Class<?> classObj = Class.class.isInstance(testObj) ? (Class<?>)testObj : null;
-            HostTest test;
-            if (i >= listTests.size()) {
-                test = createHostTest(classObj);
-                test.mRuntimeHint = 0;
-                // Carry over non-annotation filters to shards.
-                test.addAllExcludeFilters(mFilterHelper.getExcludeFilters());
-                test.addAllIncludeFilters(mFilterHelper.getIncludeFilters());
-                listTests.add(test);
+        try {
+            List<Class<?>> classes = getClasses();
+            if (classes.isEmpty()) {
+                throw new IllegalArgumentException("Missing Test class name");
             }
-            test = (HostTest) listTests.get(i);
-            Collection<? extends Object> subTests;
-            if (classObj != null) {
-                test.addClassName(classObj.getName());
-                subTests = test.mClasses;
+            if (mMethodName != null && classes.size() > 1) {
+                throw new IllegalArgumentException("Method name given with multiple test classes");
+            }
+            List<? extends Object> testObjects;
+            if (shardUnitIsMethod()) {
+                testObjects = getTestMethods();
             } else {
-                test.addTestMethod(testObj);
-                subTests = test.mTestMethods;
+                testObjects = classes;
+                // ignore shardCount when shard unit is class;
+                // simply shard by the number of classes
+                shardCount = testObjects.size();
             }
-            if (numTotalTestCases == 0) {
-                // In case there is no tests left
-                test.mRuntimeHint = 0L;
-            } else {
-                test.mRuntimeHint = mRuntimeHint * subTests.size() / numTotalTestCases;
+            if (testObjects.size() == 1) {
+                return null;
             }
-            i = (i + 1) % shardCount;
+            int i = 0;
+            int numTotalTestCases = countTestCases();
+            for (Object testObj : testObjects) {
+                Class<?> classObj = Class.class.isInstance(testObj) ? (Class<?>) testObj : null;
+                HostTest test;
+                if (i >= listTests.size()) {
+                    test = createHostTest(classObj);
+                    test.mRuntimeHint = 0;
+                    // Carry over non-annotation filters to shards.
+                    test.addAllExcludeFilters(mFilterHelper.getExcludeFilters());
+                    test.addAllIncludeFilters(mFilterHelper.getIncludeFilters());
+                    listTests.add(test);
+                }
+                test = (HostTest) listTests.get(i);
+                Collection<? extends Object> subTests;
+                if (classObj != null) {
+                    test.addClassName(classObj.getName());
+                    subTests = test.mClasses;
+                } else {
+                    test.addTestMethod(testObj);
+                    subTests = test.mTestMethods;
+                }
+                if (numTotalTestCases == 0) {
+                    // In case there is no tests left
+                    test.mRuntimeHint = 0L;
+                } else {
+                    test.mRuntimeHint = mRuntimeHint * subTests.size() / numTotalTestCases;
+                }
+                i = (i + 1) % shardCount;
+            }
+        } finally {
+            mLoadedClasses.clear();
+            for (URLClassLoader cl : mOpenClassLoaders) {
+                StreamUtil.close(cl);
+            }
+            mOpenClassLoaders.clear();
         }
-
         return listTests;
     }
 
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index e6b9a16..074a9bb 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
@@ -29,6 +29,7 @@
 import com.android.tradefed.testtype.IRuntimeHintProvider;
 import com.android.tradefed.testtype.IShardableTest;
 import com.android.tradefed.testtype.ITestCollector;
+import com.android.tradefed.testtype.ITestFilterReceiver;
 import com.android.tradefed.util.StreamUtil;
 
 import java.io.File;
@@ -37,21 +38,41 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /** Base class for executable style of tests. For example: binaries, shell scripts. */
 public abstract class ExecutableBaseTest
-        implements IRemoteTest, IRuntimeHintProvider, ITestCollector, IShardableTest, IAbiReceiver {
+        implements IRemoteTest,
+                IRuntimeHintProvider,
+                ITestCollector,
+                IShardableTest,
+                IAbiReceiver,
+                ITestFilterReceiver {
 
     public static final String NO_BINARY_ERROR = "Binary %s does not exist.";
 
+    @Option(
+            name = "per-binary-timeout",
+            isTimeVal = true,
+            description = "Timeout applied to each binary for their execution.")
+    private long mTimeoutPerBinaryMs = 5 * 60 * 1000L;
+
     @Option(name = "binary", description = "Path to the binary to be run. Can be repeated.")
     private List<String> mBinaryPaths = new ArrayList<>();
 
     @Option(
-        name = "collect-tests-only",
-        description = "Only dry-run through the tests, do not actually run them."
-    )
+            name = "test-command-line",
+            description = "The test commands of each test names.",
+            requiredForRerun = true)
+    private Map<String, String> mTestCommands = new LinkedHashMap<>();
+
+    @Option(
+            name = "collect-tests-only",
+            description = "Only dry-run through the tests, do not actually run them.")
     private boolean mCollectTestsOnly = false;
 
     @Option(
@@ -63,22 +84,79 @@
 
     private IAbi mAbi;
     private TestInformation mTestInfo;
+    private Set<String> mIncludeFilters = new LinkedHashSet<>();
+    private Set<String> mExcludeFilters = new LinkedHashSet<>();
+
+    /** @return the timeout applied to each binary for their execution. */
+    protected long getTimeoutPerBinaryMs() {
+        return mTimeoutPerBinaryMs;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addIncludeFilter(String filter) {
+        mIncludeFilters.add(filter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addExcludeFilter(String filter) {
+        mExcludeFilters.add(filter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAllIncludeFilters(Set<String> filters) {
+        mIncludeFilters.addAll(filters);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAllExcludeFilters(Set<String> filters) {
+        mExcludeFilters.addAll(filters);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearIncludeFilters() {
+        mIncludeFilters.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearExcludeFilters() {
+        mExcludeFilters.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<String> getIncludeFilters() {
+        return mIncludeFilters;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<String> getExcludeFilters() {
+        return mExcludeFilters;
+    }
 
     @Override
-    public final void run(TestInformation testInfo, ITestInvocationListener listener)
+    public void run(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         mTestInfo = testInfo;
-        for (String binary : mBinaryPaths) {
-            String path = findBinary(binary);
+        Map<String, String> testCommands = getAllTestCommands();
+        for (String testName : testCommands.keySet()) {
+            String cmd = testCommands.get(testName);
+            String path = findBinary(cmd);
+            TestDescription description = new TestDescription(testName, testName);
+            if (shouldSkipCurrentTest(description)) continue;
             if (path == null) {
-                listener.testRunStarted(new File(binary).getName(), 0);
-                listener.testRunFailed(String.format(NO_BINARY_ERROR, binary));
+                listener.testRunStarted(testName, 0);
+                listener.testRunFailed(String.format(NO_BINARY_ERROR, cmd));
                 listener.testRunEnded(0L, new HashMap<String, Metric>());
             } else {
-                listener.testRunStarted(new File(path).getName(), 1);
+                listener.testRunStarted(testName, 1);
                 long startTimeMs = System.currentTimeMillis();
-                TestDescription description =
-                        new TestDescription(new File(path).getName(), new File(path).getName());
                 listener.testStarted(description);
                 try {
                     if (!mCollectTestsOnly) {
@@ -99,12 +177,33 @@
     }
 
     /**
+     * Check if current test should be skipped.
+     *
+     * @param description The test in progress.
+     * @return true if the test should be skipped.
+     */
+    private boolean shouldSkipCurrentTest(TestDescription description) {
+        // Force to skip any test not listed in include filters, or listed in exclude filters.
+        // exclude filters have highest priority.
+        String testName = description.getTestName();
+        if (mExcludeFilters.contains(testName)
+                || mExcludeFilters.contains(description.toString())) {
+            return true;
+        }
+        if (!mIncludeFilters.isEmpty()) {
+            return !mIncludeFilters.contains(testName)
+                    && !mExcludeFilters.contains(description.toString());
+        }
+        return false;
+    }
+
+    /**
      * Search for the binary to be able to run it.
      *
      * @param binary the path of the binary or simply the binary name.
      * @return The path to the binary, or null if not found.
      */
-    public abstract String findBinary(String binary);
+    public abstract String findBinary(String binary) throws DeviceNotAvailableException;
 
     /**
      * Actually run the binary at the given path.
@@ -180,4 +279,17 @@
         }
         return shard;
     }
+
+    /**
+     * Convert mBinaryPaths to mTestCommands for consistency.
+     *
+     * @return a Map{@link LinkedHashMap}<String, String> of testCommands.
+     */
+    private Map<String, String> getAllTestCommands() {
+        Map<String, String> testCommands = new LinkedHashMap<>(mTestCommands);
+        for (String binary : mBinaryPaths) {
+            testCommands.put(new File(binary).getName(), binary);
+        }
+        return testCommands;
+    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
index 7c3c738..3d44499 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
@@ -56,13 +56,6 @@
     private static final String LOG_STDERR_TAG = "-binary-stderr-";
 
     @Option(
-        name = "per-binary-timeout",
-        isTimeVal = true,
-        description = "Timeout applied to each binary for their execution."
-    )
-    private long mTimeoutPerBinaryMs = 5 * 60 * 1000L;
-
-    @Option(
         name = "relative-path-execution",
         description =
                 "Some scripts assume a relative location to their tests file, this allows to"
@@ -135,7 +128,7 @@
                 FileOutputStream stderrStream = new FileOutputStream(stderr); ) {
             CommandResult res =
                     runUtil.runTimedCmd(
-                            mTimeoutPerBinaryMs,
+                            getTimeoutPerBinaryMs(),
                             stdoutStream,
                             stderrStream,
                             command.toArray(new String[0]));
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
new file mode 100644
index 0000000..85e7ae8
--- /dev/null
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.testtype.binary;
+
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test runner for executable running on the target. The runner implements {@link IDeviceTest} since
+ * the binary run on a device.
+ */
+@OptionClass(alias = "executable-target-test")
+public class ExecutableTargetTest extends ExecutableBaseTest implements IDeviceTest {
+
+    private ITestDevice mDevice = null;
+
+    /** {@inheritDoc} */
+    @Override
+    public void setDevice(ITestDevice device) {
+        mDevice = device;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    @Override
+    public String findBinary(String binary) throws DeviceNotAvailableException {
+        for (String path : binary.split(" ")) {
+            if (mDevice.isExecutable(path)) return binary;
+        }
+        return null;
+    }
+
+    @Override
+    public void runBinary(
+            String binaryPath, ITestInvocationListener listener, TestDescription description)
+            throws DeviceNotAvailableException, IOException {
+        if (mDevice == null) {
+            throw new IllegalArgumentException("Device has not been set");
+        }
+        CommandResult result =
+                mDevice.executeShellV2Command(
+                        binaryPath, getTimeoutPerBinaryMs(), TimeUnit.MILLISECONDS);
+        checkCommandResult(result, listener, description);
+    }
+
+    /**
+     * Check the result of the test command.
+     *
+     * @param result test result of the command {@link CommandResult}
+     * @param listener the {@link ITestInvocationListener}
+     * @param description The test in progress.
+     */
+    protected void checkCommandResult(
+            CommandResult result, ITestInvocationListener listener, TestDescription description) {
+        if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
+            String error_message;
+            error_message =
+                    String.format(
+                            "binary returned non-zero. Exit code: %d, stderr: %s, stdout: %s",
+                            result.getExitCode(), result.getStderr(), result.getStdout());
+            listener.testFailed(description, error_message);
+        }
+    }
+}
diff --git a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
index 1d23bfe..91893ab 100644
--- a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
@@ -178,15 +178,18 @@
     public final void run(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         mTestInfo = testInfo;
-        File hostTestDir = mTestInfo.executionFiles().get(FilesKey.HOST_TESTS_DIRECTORY);
-        if (hostTestDir.exists()) {
-            File libDir = new File(hostTestDir, "lib");
+        File testDir = mTestInfo.executionFiles().get(FilesKey.HOST_TESTS_DIRECTORY);
+        if (testDir == null || !testDir.exists()) {
+            testDir = mTestInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY);
+        }
+        if (testDir != null && testDir.exists()) {
+            File libDir = new File(testDir, "lib");
             List<String> ldLibraryPath = new ArrayList<>();
             if (libDir.exists()) {
                 ldLibraryPath.add(libDir.getAbsolutePath());
             }
 
-            File lib64Dir = new File(hostTestDir, "lib64");
+            File lib64Dir = new File(testDir, "lib64");
             if (lib64Dir.exists()) {
                 ldLibraryPath.add(lib64Dir.getAbsolutePath());
             }
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 3cd8928..ad894a9 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -280,6 +280,7 @@
 import com.android.tradefed.testtype.PythonUnitTestRunnerTest;
 import com.android.tradefed.testtype.TfTestLauncherTest;
 import com.android.tradefed.testtype.binary.ExecutableHostTestTest;
+import com.android.tradefed.testtype.binary.ExecutableTargetTestTest;
 import com.android.tradefed.testtype.host.CoverageMeasurementForwarderTest;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4TestTest;
 import com.android.tradefed.testtype.junit4.DeviceParameterizedRunnerTest;
@@ -750,6 +751,7 @@
 
     // testtype/binary
     ExecutableHostTestTest.class,
+    ExecutableTargetTestTest.class,
 
     // testtype/junit4
     BaseHostJUnit4TestTest.class,
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
new file mode 100644
index 0000000..69bbd96
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
@@ -0,0 +1,306 @@
+/*
+ * 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.testtype.binary;
+
+import static org.mockito.ArgumentMatchers.eq;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.metrics.proto.MetricMeasurement;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.util.CommandResult;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+
+/** Unit tests for {@link com.android.tradefed.testtype.binary.ExecutableTargetTest}. */
+@RunWith(JUnit4.class)
+public class ExecutableTargetTestTest {
+    private final String testName1 = "testName1";
+    private final String testCmd1 = "cmd1";
+    private final String testName2 = "testName2";
+    private final String testCmd2 = "cmd2";
+    private final String testName3 = "testName3";
+    private final String testCmd3 = "cmd3";
+    private static final String NO_BINARY_ERROR = "Binary %s does not exist.";
+    private static final String ERROR_MESSAGE = "binary returned non-zero exit code.";
+
+    private ITestInvocationListener mListener = null;
+    private ITestDevice mMockITestDevice = null;
+    private ExecutableTargetTest mExecutableTargetTest;
+
+    private TestInformation mTestInfo;
+
+    /** Helper to initialize the various EasyMocks we'll need. */
+    @Before
+    public void setUp() throws Exception {
+        mListener = Mockito.mock(ITestInvocationListener.class);
+        mMockITestDevice = Mockito.mock(ITestDevice.class);
+        InvocationContext context = new InvocationContext();
+        context.addAllocatedDevice("device", mMockITestDevice);
+        mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
+        mTestInfo = TestInformation.newBuilder().build();
+    }
+
+    /** Test the run method for a couple commands and success */
+    @Test
+    public void testRun_cmdSuccess() throws DeviceNotAvailableException, ConfigurationException {
+        mExecutableTargetTest =
+                new ExecutableTargetTest() {
+                    @Override
+                    public String findBinary(String binary) {
+                        return binary;
+                    }
+
+                    @Override
+                    protected void checkCommandResult(
+                            CommandResult result,
+                            ITestInvocationListener listener,
+                            TestDescription description) {}
+                };
+        mExecutableTargetTest.setDevice(mMockITestDevice);
+        // Set test commands
+        OptionSetter setter = new OptionSetter(mExecutableTargetTest);
+        setter.setOptionValue("test-command-line", testName1, testCmd1);
+        setter.setOptionValue("test-command-line", testName2, testCmd2);
+        mExecutableTargetTest.run(mTestInfo, mListener);
+        // run cmd1 test
+        TestDescription testDescription = new TestDescription(testName1, testName1);
+        Mockito.verify(mListener, Mockito.times(1)).testRunStarted(eq(testName1), eq(1));
+        Mockito.verify(mListener, Mockito.times(1)).testStarted(Mockito.eq(testDescription));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testEnded(
+                        Mockito.eq(testDescription),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        // run cmd2 test
+        TestDescription testDescription2 = new TestDescription(testName2, testName2);
+        Mockito.verify(mListener, Mockito.times(1)).testRunStarted(eq(testName2), eq(1));
+        Mockito.verify(mListener, Mockito.times(1)).testStarted(Mockito.eq(testDescription2));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testEnded(
+                        Mockito.eq(testDescription2),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        Mockito.verify(mListener, Mockito.times(2))
+                .testRunEnded(
+                        Mockito.anyLong(),
+                        Mockito.<HashMap<String, MetricMeasurement.Metric>>any());
+    }
+
+    /** Test the run method for a couple commands but binary path not found. */
+    @Test
+    public void testRun_pathNotExist() throws DeviceNotAvailableException, ConfigurationException {
+        mExecutableTargetTest =
+                new ExecutableTargetTest() {
+                    @Override
+                    public String findBinary(String binary) {
+                        return null;
+                    }
+
+                    @Override
+                    protected void checkCommandResult(
+                            CommandResult result,
+                            ITestInvocationListener listener,
+                            TestDescription description) {}
+                };
+        mExecutableTargetTest.setDevice(mMockITestDevice);
+        // Set test commands
+        OptionSetter setter = new OptionSetter(mExecutableTargetTest);
+        setter.setOptionValue("test-command-line", testName1, testCmd1);
+        setter.setOptionValue("test-command-line", testName2, testCmd2);
+        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));
+        // 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));
+        Mockito.verify(mListener, Mockito.times(2))
+                .testRunEnded(
+                        Mockito.anyLong(),
+                        Mockito.<HashMap<String, MetricMeasurement.Metric>>any());
+    }
+
+    /** Test the run method for a couple commands and commands failed. */
+    @Test
+    public void testRun_cmdFailed() throws DeviceNotAvailableException, ConfigurationException {
+        mExecutableTargetTest =
+                new ExecutableTargetTest() {
+                    @Override
+                    public String findBinary(String binary) {
+                        return binary;
+                    }
+
+                    @Override
+                    protected void checkCommandResult(
+                            CommandResult result,
+                            ITestInvocationListener listener,
+                            TestDescription description) {
+                        listener.testFailed(description, ERROR_MESSAGE);
+                    }
+                };
+        mExecutableTargetTest.setDevice(mMockITestDevice);
+        // Set test commands
+        OptionSetter setter = new OptionSetter(mExecutableTargetTest);
+        setter.setOptionValue("test-command-line", testName1, testCmd1);
+        setter.setOptionValue("test-command-line", testName2, testCmd2);
+        mExecutableTargetTest.run(mTestInfo, mListener);
+        // run cmd1 test
+        TestDescription testDescription = new TestDescription(testName1, testName1);
+        Mockito.verify(mListener, Mockito.times(1)).testRunStarted(eq(testName1), eq(1));
+        Mockito.verify(mListener, Mockito.times(1)).testStarted(Mockito.eq(testDescription));
+        Mockito.verify(mListener, Mockito.times(1)).testFailed(testDescription, ERROR_MESSAGE);
+        Mockito.verify(mListener, Mockito.times(1))
+                .testEnded(
+                        Mockito.eq(testDescription),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        // run cmd2 test
+        TestDescription testDescription2 = new TestDescription(testName2, testName2);
+        Mockito.verify(mListener, Mockito.times(1)).testRunStarted(eq(testName2), eq(1));
+        Mockito.verify(mListener, Mockito.times(1)).testStarted(Mockito.eq(testDescription2));
+        Mockito.verify(mListener, Mockito.times(1)).testFailed(testDescription2, ERROR_MESSAGE);
+        Mockito.verify(mListener, Mockito.times(1))
+                .testEnded(
+                        Mockito.eq(testDescription2),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        Mockito.verify(mListener, Mockito.times(2))
+                .testRunEnded(
+                        Mockito.anyLong(),
+                        Mockito.<HashMap<String, MetricMeasurement.Metric>>any());
+    }
+
+    /** Test the run method for a couple commands with ExcludeFilters */
+    @Test
+    public void testRun_addExcludeFilter()
+            throws DeviceNotAvailableException, ConfigurationException {
+        mExecutableTargetTest =
+                new ExecutableTargetTest() {
+                    @Override
+                    public String findBinary(String binary) {
+                        return binary;
+                    }
+
+                    @Override
+                    protected void checkCommandResult(
+                            CommandResult result,
+                            ITestInvocationListener listener,
+                            TestDescription description) {}
+                };
+        mExecutableTargetTest.setDevice(mMockITestDevice);
+        // 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);
+        TestDescription testDescription = new TestDescription(testName1, testName1);
+        TestDescription testDescription2 = new TestDescription(testName2, testName2);
+        TestDescription testDescription3 = new TestDescription(testName3, testName3);
+        mExecutableTargetTest.addExcludeFilter(testName1);
+        mExecutableTargetTest.addExcludeFilter(testDescription3.toString());
+        mExecutableTargetTest.run(mTestInfo, mListener);
+        // testName1 should NOT run.
+        Mockito.verify(mListener, Mockito.never()).testRunStarted(eq(testName1), eq(1));
+        Mockito.verify(mListener, Mockito.never()).testStarted(Mockito.eq(testDescription));
+        Mockito.verify(mListener, Mockito.never())
+                .testEnded(
+                        Mockito.eq(testDescription),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        // run cmd2 test
+        Mockito.verify(mListener, Mockito.times(1)).testRunStarted(eq(testName2), eq(1));
+        Mockito.verify(mListener, Mockito.times(1)).testStarted(Mockito.eq(testDescription2));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testEnded(
+                        Mockito.eq(testDescription2),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testRunEnded(
+                        Mockito.anyLong(),
+                        Mockito.<HashMap<String, MetricMeasurement.Metric>>any());
+        // testName3 should NOT run.
+        Mockito.verify(mListener, Mockito.never()).testRunStarted(eq(testName3), eq(1));
+        Mockito.verify(mListener, Mockito.never()).testStarted(Mockito.eq(testDescription3));
+        Mockito.verify(mListener, Mockito.never())
+                .testEnded(
+                        Mockito.eq(testDescription3),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+    }
+
+    /** Test the run method for a couple commands with IncludeFilter */
+    @Test
+    public void testRun_addIncludeFilter()
+            throws DeviceNotAvailableException, ConfigurationException {
+        mExecutableTargetTest =
+                new ExecutableTargetTest() {
+                    @Override
+                    public String findBinary(String binary) {
+                        return binary;
+                    }
+
+                    @Override
+                    protected void checkCommandResult(
+                            CommandResult result,
+                            ITestInvocationListener listener,
+                            TestDescription description) {}
+                };
+        mExecutableTargetTest.setDevice(mMockITestDevice);
+        // 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);
+        TestDescription testDescription = new TestDescription(testName1, testName1);
+        TestDescription testDescription2 = new TestDescription(testName2, testName2);
+        TestDescription testDescription3 = new TestDescription(testName3, testName3);
+        mExecutableTargetTest.addIncludeFilter(testName2);
+        mExecutableTargetTest.run(mTestInfo, mListener);
+        // testName1 should NOT run.
+        Mockito.verify(mListener, Mockito.never()).testRunStarted(eq(testName1), eq(1));
+        Mockito.verify(mListener, Mockito.never()).testStarted(Mockito.eq(testDescription));
+        Mockito.verify(mListener, Mockito.never())
+                .testEnded(
+                        Mockito.eq(testDescription),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        // run cmd2 test
+        Mockito.verify(mListener, Mockito.times(1)).testRunStarted(eq(testName2), eq(1));
+        Mockito.verify(mListener, Mockito.times(1)).testStarted(Mockito.eq(testDescription2));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testEnded(
+                        Mockito.eq(testDescription2),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testRunEnded(
+                        Mockito.anyLong(),
+                        Mockito.<HashMap<String, MetricMeasurement.Metric>>any());
+        // testName3 should NOT run.
+        Mockito.verify(mListener, Mockito.never()).testRunStarted(eq(testName3), eq(1));
+        Mockito.verify(mListener, Mockito.never()).testStarted(Mockito.eq(testDescription3));
+        Mockito.verify(mListener, Mockito.never())
+                .testEnded(
+                        Mockito.eq(testDescription3),
+                        Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index 8cf07d0..b0f8d09 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -348,7 +348,7 @@
 
     /** Test running the python tests when shared lib is available in HOST_TESTS_DIRECTORY. */
     @Test
-    public void testRun_withSharedLib() throws Exception {
+    public void testRun_withSharedLibInHostTestsDir() throws Exception {
         File hostTestsDir = FileUtil.createTempDir("host-test-cases");
         mTestInfo.executionFiles().put(FilesKey.HOST_TESTS_DIRECTORY, hostTestsDir);
         File binary = FileUtil.createTempFile("python-dir", "", hostTestsDir);
@@ -391,6 +391,51 @@
         }
     }
 
+    /** Test running the python tests when shared lib is available in TESTS_DIRECTORY. */
+    @Test
+    public void testRun_withSharedLib() throws Exception {
+        File testsDir = FileUtil.createTempDir("host-test-cases");
+        mTestInfo.executionFiles().put(FilesKey.TESTS_DIRECTORY, testsDir);
+        File binary = FileUtil.createTempFile("python-dir", "", testsDir);
+        File lib = new File(testsDir, "lib");
+        lib.mkdirs();
+        File lib64 = new File(testsDir, "lib64");
+        lib64.mkdirs();
+
+        try {
+            OptionSetter setter = new OptionSetter(mTest);
+            setter.setOptionValue("python-binaries", binary.getAbsolutePath());
+            mMockRunUtil.setEnvVariable(
+                    PythonBinaryHostTest.LD_LIBRARY_PATH,
+                    lib.getAbsolutePath() + ":" + lib64.getAbsolutePath());
+            expectedAdbPath(mFakeAdb);
+
+            CommandResult res = new CommandResult();
+            res.setStatus(CommandStatus.SUCCESS);
+            res.setStderr("TEST_RUN_STARTED {\"testCount\": 5, \"runName\": \"TestSuite\"}");
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmd(
+                                    EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
+                    .andReturn(res);
+            mMockListener.testRunStarted(
+                    EasyMock.eq(binary.getName()),
+                    EasyMock.eq(5),
+                    EasyMock.eq(0),
+                    EasyMock.anyLong());
+            mMockListener.testLog(
+                    EasyMock.eq(binary.getName() + "-stderr"),
+                    EasyMock.eq(LogDataType.TEXT),
+                    EasyMock.anyObject());
+            EasyMock.expect(mMockDevice.getIDevice()).andReturn(new StubDevice("serial"));
+
+            EasyMock.replay(mMockRunUtil, mMockBuildInfo, mMockListener, mMockDevice);
+            mTest.run(mTestInfo, mMockListener);
+            EasyMock.verify(mMockRunUtil, mMockBuildInfo, mMockListener, mMockDevice);
+        } finally {
+            FileUtil.recursiveDelete(testsDir);
+        }
+    }
+
     /**
      * If the binary returns an exception status, we should throw a runtime exception since
      * something went wrong with the binary setup.