Search --class into a possible module

Modules have the name of their build target so it's possible
for a jar to be present which would contain the class.

Test: unit tests
Bug: 153893318
Change-Id: I72bdfb3b1a5f19e41b0f2f58d90f3a07eb60b2ce
diff --git a/test_framework/com/android/tradefed/testtype/HostTest.java b/test_framework/com/android/tradefed/testtype/HostTest.java
index 51e9acf..f9aad4b 100644
--- a/test_framework/com/android/tradefed/testtype/HostTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostTest.java
@@ -39,6 +39,7 @@
 import com.android.tradefed.testtype.host.PrettyTestEventLogger;
 import com.android.tradefed.testtype.junit4.CarryDnaeError;
 import com.android.tradefed.testtype.junit4.JUnit4ResultForwarder;
+import com.android.tradefed.testtype.suite.ModuleDefinition;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.JUnit4TestFilter;
 import com.android.tradefed.util.StreamUtil;
@@ -68,6 +69,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.ArrayDeque;
@@ -890,12 +892,45 @@
             if (classNames.contains(className)) {
                 continue;
             }
+            IllegalArgumentException initialError = null;
             try {
                 classes.add(Class.forName(className, true, getClassLoader()));
                 classNames.add(className);
             } catch (ClassNotFoundException e) {
-                throw new IllegalArgumentException(String.format("Could not load Test class %s",
-                        className), e);
+                initialError =
+                        new IllegalArgumentException(
+                                String.format("Could not load Test class %s", className), e);
+            }
+            if (initialError != null) {
+                // Fallback search a jar for the module under tests if any.
+                String moduleName =
+                        mTestInfo
+                                .getContext()
+                                .getAttributes()
+                                .getUniqueMap()
+                                .get(ModuleDefinition.MODULE_NAME);
+                if (moduleName != null) {
+                    try {
+                        File f = getJarFile(moduleName + ".jar", mTestInfo);
+                        URL[] urls = {f.toURI().toURL()};
+                        URLClassLoader cl = URLClassLoader.newInstance(urls);
+                        mJUnit4JarFiles.add(f);
+                        Class<?> cls = cl.loadClass(className);
+                        classes.add(cls);
+                        classNames.add(className);
+                        initialError = null;
+                    } catch (FileNotFoundException
+                            | MalformedURLException
+                            | ClassNotFoundException fallbackSearch) {
+                        CLog.e(
+                                "Fallback search for a jar containing '%s' didn't work."
+                                        + "Consider using --jar option directly instead of using --class",
+                                className);
+                    }
+                }
+            }
+            if (initialError != null) {
+                throw initialError;
             }
         }
         // Inspect for the jar files
diff --git a/tests/src/com/android/tradefed/testtype/JarHostTestTest.java b/tests/src/com/android/tradefed/testtype/JarHostTestTest.java
index f8fbc1a..e7f1bca 100644
--- a/tests/src/com/android/tradefed/testtype/JarHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/JarHostTestTest.java
@@ -33,6 +33,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
+import com.android.tradefed.testtype.suite.ModuleDefinition;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
@@ -69,18 +70,6 @@
     private File mTestDir = null;
     private ITestInvocationListener mListener;
 
-    /** More testable version of {@link HostTest} */
-    public static class HostTestable extends HostTest {
-
-        public static File mTestDir;
-
-        public HostTestable() {}
-
-        public HostTestable(File testDir) {
-            mTestDir = testDir;
-        }
-    }
-
     @Before
     public void setUp() throws Exception {
         mTest = new HostTest();
@@ -145,7 +134,7 @@
     @Test
     public void testSplit_withJar() throws Exception {
         File testJar = getJarResource(TEST_JAR1, mTestDir);
-        mTest = new HostTestLoader(mTestDir, testJar);
+        mTest = new HostTestLoader(testJar);
         mTest.setBuild(mStubBuildInfo);
         ITestDevice device = EasyMock.createNiceMock(ITestDevice.class);
         mTest.setDevice(device);
@@ -225,7 +214,7 @@
     @Test
     public void testSplit_countWithFilter() throws Exception {
         File testJar = getJarResource(TEST_JAR1, mTestDir);
-        mTest = new HostTestLoader(mTestDir, testJar);
+        mTest = new HostTestLoader(testJar);
         mTest.setBuild(mStubBuildInfo);
         ITestDevice device = EasyMock.createNiceMock(ITestDevice.class);
         mTest.setDevice(device);
@@ -242,14 +231,13 @@
     /**
      * Testable version of {@link HostTest} that allows adding jar to classpath for testing purpose.
      */
-    public static class HostTestLoader extends HostTestable {
+    public static class HostTestLoader extends HostTest {
 
         private static File mTestJar;
 
         public HostTestLoader() {}
 
-        public HostTestLoader(File testDir, File jar) {
-            super(testDir);
+        public HostTestLoader(File jar) {
             mTestJar = jar;
         }
 
@@ -326,7 +314,7 @@
     @Test
     public void testRunWithJar() throws Exception {
         File testJar = getJarResource(TEST_JAR2, mTestDir);
-        mTest = new HostTestLoader(mTestDir, testJar);
+        mTest = new HostTest();
         mTest.setBuild(mStubBuildInfo);
         ITestDevice device = EasyMock.createNiceMock(ITestDevice.class);
         mTest.setDevice(device);
@@ -352,4 +340,38 @@
         mTest.run(mTestInfo, mListener);
         EasyMock.verify(mListener);
     }
+
+    @Test
+    public void testRunWithClassFromExternalJar() throws Exception {
+        File testJar = getJarResource(TEST_JAR2, mTestDir);
+        mTestInfo
+                .getContext()
+                .addInvocationAttribute(
+                        ModuleDefinition.MODULE_NAME, FileUtil.getBaseName(testJar.getName()));
+        mTest = new HostTest();
+        mTest.setBuild(mStubBuildInfo);
+        ITestDevice device = EasyMock.createNiceMock(ITestDevice.class);
+        mTest.setDevice(device);
+        OptionSetter setter = new OptionSetter(mTest);
+        setter.setOptionValue("enable-pretty-logs", "false");
+        setter.setOptionValue("class", "com.android.tradefed.JUnit4TfUnitTest");
+        // full class count without sharding
+        mTest.setTestInformation(mTestInfo);
+        assertEquals(2, mTest.countTestCases());
+
+        mListener.testRunStarted("com.android.tradefed.JUnit4TfUnitTest", 2);
+        TestDescription testOne =
+                new TestDescription("com.android.tradefed.JUnit4TfUnitTest", "testOne");
+        TestDescription testTwo =
+                new TestDescription("com.android.tradefed.JUnit4TfUnitTest", "testTwo");
+        mListener.testStarted(testOne);
+        mListener.testEnded(EasyMock.eq(testOne), EasyMock.<HashMap<String, Metric>>anyObject());
+        mListener.testStarted(testTwo);
+        mListener.testEnded(EasyMock.eq(testTwo), EasyMock.<HashMap<String, Metric>>anyObject());
+        mListener.testRunEnded(EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
+
+        EasyMock.replay(mListener);
+        mTest.run(mTestInfo, mListener);
+        EasyMock.verify(mListener);
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index f9cf4e1..b3f918e 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -532,7 +532,7 @@
                                 eq("--test-output-file"),
                                 capture(testOutputFilePath)))
                 .andStubAnswer(
-                        new IAnswer<>() {
+                        new IAnswer<CommandResult>() {
                             @Override
                             public CommandResult answer() {
                                 try {