Snap for 5606075 from 1bc59c39bf6a116bfa7f1da5587f883e19bdb65b to qt-c2f2-release

Change-Id: Ib3e8c9e400bcfd4ecb20aecaa77996e4a127fea4
diff --git a/res/config/build/kernel-image-check.xml b/res/config/build/kernel-image-check.xml
new file mode 100644
index 0000000..73043c9
--- /dev/null
+++ b/res/config/build/kernel-image-check.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Runs host-based kernel image check tests">
+    <option name="null-device" value="true" />
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="com.android.build.tests.KernelImageCheck" />
+    </test>
+</configuration>
diff --git a/res/config/example/multi-devices.xml b/res/config/example/multi-devices.xml
new file mode 100644
index 0000000..2c556e6
--- /dev/null
+++ b/res/config/example/multi-devices.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="A simple multi-devices example in Tradefed">
+    <option name="test-tag" value="multi-devices-example" />
+
+    <device name="device1">
+        <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+            <option name="disable" value="true" />
+        </target_preparer>
+    </device>
+
+    <device name="device2">
+        <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+            <option name="disable" value="true" />
+        </target_preparer>
+    </device>
+    <multi_target_preparer class="com.android.tradefed.targetprep.multi.HelloWorldMultiTargetPreparer" />
+
+    <test class="com.android.tradefed.HelloWorldMultiDevices" />
+
+    <logger class="com.android.tradefed.log.FileLogger">
+        <option name="log-level" value="verbose" />
+        <option name="log-level-display" value="verbose" />
+    </logger>
+    <result_reporter class="com.android.tradefed.result.ConsoleResultReporter" />
+
+</configuration>
diff --git a/res/config/google/example/BT-discovery-sl4a.xml b/res/config/google/example/BT-discovery-sl4a.xml
new file mode 100644
index 0000000..20f8aa0
--- /dev/null
+++ b/res/config/google/example/BT-discovery-sl4a.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Bluetooth discovery test to test sl4a with multi-devices">
+
+    <device name="dut">
+        <build_provider class="com.android.tradefed.build.BootstrapBuildProvider" />
+        <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+            <option name="test-file-name" value="sl4a.apk" />
+        </target_preparer>
+    </device>
+
+    <device name="discoverer">
+        <build_provider class="com.android.tradefed.build.BootstrapBuildProvider" />
+        <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+            <option name="test-file-name" value="sl4a.apk" />
+        </target_preparer>
+    </device>
+
+    <logger class="com.android.tradefed.log.FileLogger">
+        <option name="log-level" value="VERBOSE" />
+        <option name="log-level-display" value="VERBOSE" />
+    </logger>
+
+    <test class="com.android.tradefed.Sl4aBluetoothDiscovery" />
+
+    <result_reporter class="com.android.tradefed.result.ConsoleResultReporter" />
+</configuration>
diff --git a/res/config/uicd/uiconductor-commandlineaction-sample.xml b/res/config/uicd/uiconductor-commandlineaction-sample.xml
new file mode 100644
index 0000000..df8f3ae
--- /dev/null
+++ b/res/config/uicd/uiconductor-commandlineaction-sample.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Runs the ui conductor test">
+
+  <!--These are the dependency apks that needs to be installed in the devices to run uicd successfully-->
+  <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+
+    <!--option apk-path can be repeated with apks that are needed for the device specific tests-->
+    <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-test-v1.0.2.apk" />
+    <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-v1.0.2.apk" />
+  </target_preparer>
+
+  <!--Provide the test related information here-->
+  <test class="com.android.uicd.tests.UiConductorTest" >
+
+    <!--This is the required uicd-cli jar for the uicd-tests to run-->
+    <option name="uicd-cli-jar" value="gs://uicd-deps/cli/uicd-commandline.jar" />
+
+    <!--mode of the tests. Can be SINGLE, MULTIDEVICE or PLAYALL. But only one at a time.-->
+    <option name="play-mode" value="SINGLE" />
+
+    <!--test file's identifier key and path which can be a local path or a GCS path(as below)-->
+    <option name="uicd-test" key="nuwa_python_test" value="gs://uicd-samples/tests/nuwa_python_test" />
+
+    <!--Path of the executable that is referenced in command line action-->
+    <option name="commandline-action-executable" value="gs://uicd-samples/executables/basic_nuwa_script" />
+  </test>
+
+  <logger class="com.android.tradefed.log.FileLogger" >
+    <option name="log-level-display" value="debug" />
+  </logger>
+
+
+</configuration>
\ No newline at end of file
diff --git a/res/config/uicd/uiconductor-globalvariable-sample.xml b/res/config/uicd/uiconductor-globalvariable-sample.xml
new file mode 100644
index 0000000..13277db
--- /dev/null
+++ b/res/config/uicd/uiconductor-globalvariable-sample.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Runs the ui conductor test">
+
+  <!--These are the dependency apks that needs to be installed in the devices to run uicd successfully-->
+  <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+    <!--option apk-path can be repeated with apks that are needed for the device specific tests-->
+    <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-test-v1.0.2.apk" />
+    <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-v1.0.2.apk" />
+  </target_preparer>
+
+  <!--Provide the test related information here-->
+  <test class="com.android.uicd.tests.UiConductorTest" >
+
+    <!--This is the required uicd-cli jar for the uicd-tests to run-->
+    <option name="uicd-cli-jar" value="gs://uicd-deps/cli/uicd-commandline.jar" />
+
+    <!--Mode of the tests. Can be SINGLE, MULTIDEVICE or PLAYALL. But only one at a time.-->
+    <option name="play-mode" value="SINGLE" />
+
+    <!--Global variables for a particular tests are identified through the xml keys-->
+    <!--Global variables are key value pairs separated by "=" in the xml values as shown below-->
+    <!--Multiple global variables can be repeated as shown below-->
+    <option name="global-variables" key="dummytest1" value="uicd_key1=value1Fordummytest1" />
+    <option name="global-variables" key="dummytest1" value="uicd_key2=value2Fordummytest1" />
+
+    <!--Global variable pairs can be grouped together separated by ","-->
+    <option name="global-variables" key="dummytest2" value="uicd_key1=value1Fordummytest2,uicd_key2=value2Fordummytest2" />
+
+    <!--test file's identifier key and path which can be a local path or a GCS path(as below)-->
+    <!--Global variables for dummytest1 is resolved as "uicd_key1=value1Fordummytest1,uicd_key2=value2Fordummytest1"-->
+    <option name="uicd-test" key="dummytest1" value="gs://uicd-samples/tests/tests_subdir1/dummytest1" />
+
+    <!--Global variables for dummytest2 is resolved as "uicd_key1=value1Fordummytest2,uicd_key2=value2Fordummytest2"-->
+    <option name="uicd-test" key="dummytest2" value="gs://uicd-samples/tests/tests_subdir1/tests_subdir2/dummytest2" />
+
+    <!--No global variables for hellothisworks-->
+    <option name="uicd-test" key="hellothisworks" value="gs://uicd-samples/tests/hellothisworks" />
+  </test>
+
+  <logger class="com.android.tradefed.log.FileLogger" >
+    <option name="log-level-display" value="debug" />
+  </logger>
+</configuration>
\ No newline at end of file
diff --git a/res/config/uicd/uiconductor-multidevice-sample.xml b/res/config/uicd/uiconductor-multidevice-sample.xml
new file mode 100644
index 0000000..070dafb
--- /dev/null
+++ b/res/config/uicd/uiconductor-multidevice-sample.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Runs the ui conductor multi-device test">
+
+  <!-- Each device configuration -->
+  <device name="device1">
+
+    <!--These are the dependency apks that needs to be installed in the devices to run uicd successfully-->
+    <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+
+      <!--option apk-path can be repeated with apks that are needed for the device specific tests-->
+      <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-test-v1.0.2.apk" />
+      <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-v1.0.2.apk" />
+    </target_preparer>
+  </device>
+
+  <!-- A device tag can be completely empty if it doesn't require any setup -->
+  <device name="device2">
+
+    <!--These are the dependency apks that needs to be installed in the devices to run uicd successfully-->
+    <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+
+      <!--option apk-path can be repeated with apks that are needed for the device specific tests-->
+      <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-test-v1.0.2.apk" />
+      <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-v1.0.2.apk" />
+    </target_preparer>
+  </device>
+
+  <!--Provide the test related information here-->
+  <test class="com.android.uicd.tests.UiConductorTest" >
+
+    <!--This is the required uicd-cli jar for the uicd-tests to run-->
+    <option name="uicd-cli-jar" value="gs://uicd-deps/cli/uicd-commandline.jar" />
+
+    <!--mode of the tests. Can be SINGLE, MULTIDEVICE or PLAYALL. But only one at a time.-->
+    <option name="play-mode" value="MULTIDEVICE" />
+
+    <!--Global variables for a particular tests are identified through the keys-->
+    <!--Global variables are key value pairs separated by "=" as shown below-->
+    <option name="global-variables" key="tracker" value="uicd_key1=value1Fortracker" />
+
+    <!--test file's identifier key and path which can be a local path or a GCS path(as below)-->
+    <option name="uicd-test" key="tracker" value="gs://uicd-samples/tests/tracker-1.2.1" />
+  </test>
+
+  <logger class="com.android.tradefed.log.FileLogger" >
+    <option name="log-level-display" value="debug" />
+  </logger>
+
+
+</configuration>
\ No newline at end of file
diff --git a/res/config/uicd/uiconductor-sample.xml b/res/config/uicd/uiconductor-sample.xml
new file mode 100644
index 0000000..f2e87cb
--- /dev/null
+++ b/res/config/uicd/uiconductor-sample.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Runs the ui conductor test">
+
+  <!--These are the dependency apks that needs to be installed in the devices to run uicd successfully-->
+  <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+
+    <!--option apk-path can be repeated with apks that are needed for the device specific tests-->
+    <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-test-v1.0.2.apk" />
+    <option name="apk-path" value="gs://uicd-deps/deps/apks/uicd-xmldumper-server-v1.0.2.apk" />
+  </target_preparer>
+
+  <!--Provide the test related information here-->
+  <test class="com.android.uicd.tests.UiConductorTest" >
+
+    <!--This is the required uicd-cli jar for the uicd-tests to run-->
+    <option name="uicd-cli-jar" value="gs://uicd-deps/cli/uicd-commandline.jar" />
+
+    <!--mode of the tests. Can be SINGLE, MULTIDEVICE or PLAYALL. But only one at a time.-->
+    <option name="play-mode" value="SINGLE" />
+
+    <!--test file's identifier key and path which can be a local path or a GCS path(as below)-->
+    <option name="uicd-test" key="dummytest1" value="gs://uicd-samples/tests/tests_subdir1/dummytest1" />
+    <option name="uicd-test" key="dummytest2" value="gs://uicd-samples/tests/tests_subdir1/tests_subdir2/dummytest2" />
+    <option name="uicd-test" key="hellothisworks" value="gs://uicd-samples/tests/hellothisworks" />
+  </test>
+
+  <logger class="com.android.tradefed.log.FileLogger" >
+    <option name="log-level-display" value="debug" />
+  </logger>
+
+
+</configuration>
\ No newline at end of file
diff --git a/src/com/android/build/tests/KernelImageCheck.java b/src/com/android/build/tests/KernelImageCheck.java
new file mode 100644
index 0000000..b4d5248
--- /dev/null
+++ b/src/com/android/build/tests/KernelImageCheck.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2019 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.build.tests;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+
+/** A device-less test that test kernel image */
+@OptionClass(alias = "kernel-image-check")
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class KernelImageCheck extends BaseHostJUnit4Test {
+
+    private static final String KERNEL_IMAGE_NAME = "vmlinux";
+    private static final int CMD_TIMEOUT = 1000000;
+
+    @Option(
+        name = "kernel-image-check-tool",
+        description = "The file path of kernel image check tool (mandatory)",
+        mandatory = true
+    )
+    private File mKernelImageCheckTool = null;
+
+    @Option(
+        name = "kernel-image-name",
+        description = "The file name of the kernel image. Default: vmlinux"
+    )
+    private String mKernelImageName = KERNEL_IMAGE_NAME;
+
+    @Option(
+        name = "kernel-image-alt-path",
+        description = "The kernel image alternative path string"
+    )
+    private String mKernelImageAltPath = null;
+
+    @Option(
+        name = "kernel-abi-file",
+        description = "The file path of kernel ABI file",
+        mandatory = true
+    )
+    private File mKernelAbiFile = null;
+
+    private IBuildInfo mBuildInfo;
+    private File mKernelImageFile = null;
+
+    @Before
+    public void setUp() throws Exception {
+        if (!mKernelImageCheckTool.exists()) {
+            throw new IOException("Cannot find kernel image tool at: " + mKernelImageCheckTool);
+        }
+        if (!mKernelAbiFile.exists()) {
+            throw new IOException("Cannot find kernel ABI representation at: " + mKernelAbiFile);
+        }
+        // First try to get kernel image from BuildInfo
+        mKernelImageFile = getBuild().getFile(mKernelImageName);
+        if (mKernelImageFile == null || !mKernelImageFile.exists()) {
+            // Then check within alternative path.
+            File imageDir = new File(mKernelImageAltPath);
+            if (imageDir.isDirectory()) {
+                mKernelImageFile = new File(imageDir, mKernelImageName);
+            }
+        }
+
+        if (mKernelImageFile == null || !mKernelImageFile.exists()) {
+            throw new RuntimeException("Cannot find kernel image file: " + mKernelImageName);
+        }
+    }
+
+    /** Test that kernel ABI is not different from the given ABI representation */
+    @Test
+    public void test_stable_abi() throws Exception {
+        // Generate kernel ABI
+        String[] cmd =
+                new String[] {
+                    mKernelImageCheckTool.getAbsolutePath() + "/abidw",
+                    "--linux-tree",
+                    mKernelImageFile.getParent(),
+                    "--out-file",
+                    "abi-new.out"
+                };
+        CommandResult result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, cmd);
+        CLog.i("Result stdout: %s", result.getStdout());
+        // TODO: differentiate non-zero exit codes.
+        if (result.getExitCode() != 0) {
+            CLog.e("Result stderr: %s", result.getStderr());
+            CLog.e("Result exit code: %d", result.getExitCode());
+        }
+        assertEquals(CommandStatus.SUCCESS, result.getStatus());
+
+        // Diff kernel ABI with the given ABI file
+        cmd =
+                new String[] {
+                    mKernelImageCheckTool.getAbsolutePath() + "/abidiff",
+                    "abi-new.out",
+                    mKernelAbiFile.getAbsolutePath()
+                };
+        result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, cmd);
+        CLog.i("Result stdout: %s", result.getStdout());
+        if (result.getExitCode() != 0) {
+            CLog.e("Result stderr: %s", result.getStderr());
+            CLog.e("Result exit code: %d", result.getExitCode());
+        }
+        assertEquals(CommandStatus.SUCCESS, result.getStatus());
+    }
+}
diff --git a/src/com/android/tradefed/presubmit/TestMappingsValidation.java b/src/com/android/tradefed/presubmit/TestMappingsValidation.java
index d9912f4..408f136 100644
--- a/src/com/android/tradefed/presubmit/TestMappingsValidation.java
+++ b/src/com/android/tradefed/presubmit/TestMappingsValidation.java
@@ -31,6 +31,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -69,6 +70,9 @@
     private static final String LOCAL_COMPATIBILITY_SUITES = "compatibility_suites";
     private static final String GENERAL_TESTS = "general-tests";
     private static final String DEVICE_TESTS = "device-tests";
+    // Only Check the tests with group in presubmit or postsubmit.
+    private static final Set<String> TEST_GROUPS_TO_VALIDATE =
+            new HashSet<>(Arrays.asList("presubmit", "postsubmit"));
 
     private File testMappingsDir = null;
     private IDeviceBuildInfo deviceBuildInfo = null;
@@ -115,8 +119,12 @@
     public void testTestSuiteSetting() throws JSONException {
         List<String> errors = new ArrayList<>();
         for (String testGroup : allTests.keySet()) {
+            if (!TEST_GROUPS_TO_VALIDATE.contains(testGroup)) {
+                CLog.d("Skip checking tests with group: %s", testGroup);
+                continue;
+            }
             for (TestInfo testInfo : allTests.get(testGroup)) {
-                if (!validateSuiteSetting(testInfo.getName())) {
+                if (!validateSuiteSetting(testInfo.getName(), testInfo.getKeywords())) {
                     errors.add(
                             String.format(
                                     "Missing test_suite setting for test: %s, test group: %s, " +
@@ -242,11 +250,17 @@
      * Validate if the name exists in module-info.json and with the correct suite setting.
      *
      * @param name A {@code String} name of the test.
+     * @param keywords A {@code Set<String>} keywords of the test.
      * @return true if name exists in module-info.json and matches either "general-tests" or
-     *     "device-tests".
+     *     "device-tests", or name doesn't exist but has keywords attribute set.
      */
-    private boolean validateSuiteSetting(String name) throws JSONException {
+    private boolean validateSuiteSetting(String name, Set<String> keywords) throws JSONException {
         if (!moduleInfo.has(name)) {
+            if (!keywords.isEmpty()) {
+                CLog.d("Test Module: %s can't be found in module-info.json, but it has " +
+                        "keyword setting. Ignore checking...", name);
+                return true;
+            }
             CLog.w("Test Module: %s can't be found in module-info.json.", name);
             return false;
         }
diff --git a/src/com/android/uicd/tests/UiConductorTest.java b/src/com/android/uicd/tests/UiConductorTest.java
new file mode 100644
index 0000000..4fa9ebe
--- /dev/null
+++ b/src/com/android/uicd/tests/UiConductorTest.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 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.uicd.tests;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+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.TestDescription;
+import com.android.tradefed.testtype.IMultiDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.RunUtil;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * The class enables user to run their pre-recorded UICD tests on tradefed. Go to
+ * https://github.com/google/android-uiconductor/releases/tag/v0.1.1 to download the uicd_cli.tar.gz
+ * and extract the jar and apks required for the tests. Please look at the sample xmls in
+ * res/config/uicd to configure your tests.
+ */
+public class UiConductorTest implements IMultiDeviceTest, IRemoteTest {
+
+    @Option(
+        name = "uicd-cli-jar",
+        description = "The cli jar that runs the user provided tests in commandline",
+        importance = Importance.IF_UNSET
+    )
+    private File cliJar;
+
+    @Option(
+        name = "commandline-action-executable",
+        description =
+                "the filesystem path of the binaries that are ran through command line actions on UICD. Can be repeated.",
+        importance = Importance.IF_UNSET
+    )
+    private Collection<File> binaries = new ArrayList<File>();
+
+    @Option(
+        name = "global-variables",
+        description = "Global variable (uicd_key1=value1,uicd_key2=value2)",
+        importance = Importance.ALWAYS
+    )
+    private MultiMap<String, String> globalVariables = new MultiMap<>();
+
+    @Option(
+        name = "play-mode",
+        description = "Play Mode (SINGLE|MULTIDEVICE|PLAYALL).",
+        importance = Importance.ALWAYS
+    )
+    private String playMode = "SINGLE";
+
+    @Option(name = "test-name", description = "Name of the test.", importance = Importance.ALWAYS)
+    private String testName = "Your test results are here";
+
+    // Same key can have multiple test files because global-variables can be referenced using the
+    // that particular key and shared across different tests.
+    // Refer res/config/uicd/uiconductor-globalvariable-sample.xml for more information.
+    @Option(
+        name = "uicd-test",
+        description =
+                "the filesystem path of the json test files or directory of multiple json test files that needs to be run on devices. Can be repeated.",
+        importance = Importance.IF_UNSET
+    )
+    private MultiMap<String, File> uicdTest = new MultiMap<>();
+
+    @Option(
+        name = "test-timeout",
+        description = "Time out for each test.",
+        importance = Importance.IF_UNSET
+    )
+    private int testTimeout = 1800000;
+
+    private static final String BINARY_RELATIVE_PATH = "binary";
+
+    private static final String OUTPUT_RELATIVE_PATH = "output";
+
+    private static final String TESTS_RELATIVE_PATH = "tests";
+
+    private static final String RESULTS_RELATIVE_PATH = "result";
+
+    private static final String OPTION_SYMBOL = "-";
+    private static final String INPUT_OPTION_SHORT_NAME = "i";
+    private static final String OUTPUT_OPTION_SHORT_NAME = "o";
+    private static final String DEVICES_OPTION_SHORT_NAME = "d";
+    private static final String MODE_OPTION_SHORT_NAME = "m";
+    private static final String GLOBAL_VARIABLE_OPTION_SHORT_NAME = "g";
+
+    private static final String CHILDRENRESULT_ATTRIBUTE = "childrenResult";
+    private static final String PLAYSTATUS_ATTRIBUTE = "playStatus";
+    private static final String VALIDATIONDETAILS_ATTRIBUTE = "validationDetails";
+
+    private static final String EXECUTABLE = "u+x";
+
+    private static String baseFilePath = System.getenv("HOME") + "/tmp/uicd-on-tf";
+
+    Map<ITestDevice, IBuildInfo> deviceInfos;
+
+    @Override
+    public void setDeviceInfos(Map<ITestDevice, IBuildInfo> deviceInfos) {
+        this.deviceInfos = deviceInfos;
+    }
+
+    @Override
+    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+        CLog.i("Starting the UIConductor tests:\n");
+        String runId = UUID.randomUUID().toString();
+        baseFilePath = Paths.get(baseFilePath, runId).toString();
+        String jarFileDir = Paths.get(baseFilePath, BINARY_RELATIVE_PATH).toString();
+        String testFilesDir = Paths.get(baseFilePath, TESTS_RELATIVE_PATH).toString();
+        String binaryFilesDir = Paths.get(baseFilePath).toString();
+        File jarFile;
+        MultiMap<String, File> copiedTestFileMap = new MultiMap<>();
+        if (cliJar == null || !cliJar.exists()) {
+            CLog.e("Unable to fetch provided binary.\n");
+            return;
+        }
+        try {
+            jarFile = copyFile(cliJar.getAbsolutePath(), jarFileDir);
+            FileUtil.chmod(jarFile, EXECUTABLE);
+
+            for (Map.Entry<String, File> testFileOrDirEntry : uicdTest.entries()) {
+                copiedTestFileMap.putAll(
+                        copyFile(
+                                testFileOrDirEntry.getKey(),
+                                testFileOrDirEntry.getValue().getAbsolutePath(),
+                                testFilesDir));
+            }
+
+            for (File binaryFile : binaries) {
+                File binary = copyFile(binaryFile.getAbsolutePath(), binaryFilesDir);
+                FileUtil.chmod(binary, EXECUTABLE);
+            }
+        } catch (IOException ex) {
+            throw new DeviceNotAvailableException(ex.getMessage());
+        }
+
+        RunUtil rUtil = new RunUtil();
+        rUtil.setWorkingDir(new File(baseFilePath));
+        long runStartTime = System.currentTimeMillis();
+        listener.testRunStarted(testName, copiedTestFileMap.values().size());
+        for (Map.Entry<String, File> testFileEntry : copiedTestFileMap.entries()) {
+            runTest(
+                    listener,
+                    rUtil,
+                    jarFile,
+                    testFileEntry.getKey(),
+                    testFileEntry.getValue().getName());
+        }
+
+        listener.testRunEnded(
+                System.currentTimeMillis() - runStartTime, new HashMap<String, String>());
+        FileUtil.recursiveDelete(new File(baseFilePath));
+        CLog.i("Finishing the ui conductor tests\n");
+    }
+
+    public void runTest(
+            ITestInvocationListener listener,
+            RunUtil rUtil,
+            File jarFile,
+            String key,
+            String testFileName) {
+        TestDescription testDesc =
+                new TestDescription(this.getClass().getSimpleName(), testFileName);
+        listener.testStarted(testDesc, System.currentTimeMillis());
+
+        String testId = UUID.randomUUID().toString();
+        CommandResult cmndRes =
+                rUtil.runTimedCmd(testTimeout, getCommand(jarFile, testFileName, testId, key));
+        logInfo(testId, "STD", cmndRes.getStdout());
+        logInfo(testId, "ERR", cmndRes.getStderr());
+
+        File resultsFile =
+                new File(
+                        Paths.get(
+                                        baseFilePath,
+                                        OUTPUT_RELATIVE_PATH,
+                                        testId,
+                                        RESULTS_RELATIVE_PATH,
+                                        "action_execution_result")
+                                .toString());
+
+        if (resultsFile.exists()) {
+            try {
+                String content = FileUtil.readStringFromFile(resultsFile);
+                JSONObject result = new JSONObject(content);
+                List<String> errors = new ArrayList<>();
+                errors = parseResult(errors, result);
+                if (!errors.isEmpty()) {
+                    listener.testFailed(testDesc, errors.get(0));
+                    CLog.i("Test %s failed due to following errors: \n", testDesc.getTestName());
+                    for (String error : errors) {
+                        CLog.i(error + "\n");
+                    }
+                }
+            } catch (IOException | JSONException e) {
+                CLog.e(e);
+            }
+            String testResultFileName = testFileName + "_action_execution_result";
+            try (InputStreamSource iSSource = new FileInputStreamSource(resultsFile)) {
+                listener.testLog(testResultFileName, LogDataType.TEXT, iSSource);
+            }
+        }
+        listener.testEnded(testDesc, System.currentTimeMillis(), new HashMap<String, String>());
+    }
+
+    private void logInfo(String testId, String cmdOutputType, String content) {
+        CLog.i(
+                "==========================="
+                        + cmdOutputType
+                        + " logs for "
+                        + testId
+                        + " starts===========================\n");
+        CLog.i(content);
+        CLog.i(
+                "==========================="
+                        + cmdOutputType
+                        + " logs for "
+                        + testId
+                        + " ends===========================\n");
+    }
+
+    private List<String> parseResult(List<String> errors, JSONObject result) throws JSONException {
+
+        if (result != null) {
+            if (result.has(CHILDRENRESULT_ATTRIBUTE)) {
+                JSONArray childResults = result.getJSONArray(CHILDRENRESULT_ATTRIBUTE);
+                for (int i = 0; i < childResults.length(); i++) {
+                    errors = parseResult(errors, childResults.getJSONObject(i));
+                }
+            }
+
+            if (result.has(PLAYSTATUS_ATTRIBUTE)
+                    && result.getString(PLAYSTATUS_ATTRIBUTE).equalsIgnoreCase("FAIL")) {
+                if (result.has(VALIDATIONDETAILS_ATTRIBUTE)) {
+                    errors.add(result.getString(VALIDATIONDETAILS_ATTRIBUTE));
+                }
+            }
+        }
+        return errors;
+    }
+
+    private File copyFile(String srcFilePath, String destDirPath) throws IOException {
+        File srcFile = new File(srcFilePath);
+        File destDir = new File(destDirPath);
+        if (srcFile.isDirectory()) {
+            for (File file : srcFile.listFiles()) {
+                copyFile(file.getAbsolutePath(), Paths.get(destDirPath, file.getName()).toString());
+            }
+        }
+        if (!destDir.isDirectory() && !destDir.mkdirs()) {
+            throw new IOException(
+                    String.format("Could not create directory %s", destDir.getAbsolutePath()));
+        }
+        File destFile = new File(Paths.get(destDir.toString(), srcFile.getName()).toString());
+        FileUtil.copyFile(srcFile, destFile);
+        return destFile;
+    }
+
+    // copy file to destDirPath while maintaining a map of key that refers to that src file
+    private MultiMap<String, File> copyFile(String key, String srcFilePath, String destDirPath)
+            throws IOException {
+        MultiMap<String, File> copiedTestFileMap = new MultiMap<>();
+        File srcFile = new File(srcFilePath);
+        File destDir = new File(destDirPath);
+        if (srcFile.isDirectory()) {
+            for (File file : srcFile.listFiles()) {
+                copiedTestFileMap.putAll(
+                        copyFile(
+                                key,
+                                file.getAbsolutePath(),
+                                Paths.get(destDirPath, file.getName()).toString()));
+            }
+        }
+        if (!destDir.isDirectory() && !destDir.mkdirs()) {
+            throw new IOException(
+                    String.format("Could not create directory %s", destDir.getAbsolutePath()));
+        }
+        if (srcFile.isFile()) {
+            File destFile = new File(Paths.get(destDir.toString(), srcFile.getName()).toString());
+            FileUtil.copyFile(srcFile, destFile);
+            copiedTestFileMap.put(key, destFile);
+        }
+        return copiedTestFileMap;
+    }
+
+    private String getTestFilesArgsForUicdBin(String testFilesDir, String filename) {
+        return (!testFilesDir.isEmpty() && !filename.isEmpty())
+                ? Paths.get(testFilesDir, filename).toString()
+                : "";
+    }
+
+    private String getOutFilesArgsForUicdBin(String outFilesDir) {
+        return !outFilesDir.isEmpty() ? outFilesDir : "";
+    }
+
+    private String getPlaymodeArgForUicdBin() {
+        return !playMode.isEmpty() ? playMode : "";
+    }
+
+    private String getDevIdsArgsForUicdBin() {
+        List<String> devIds = new ArrayList<>();
+        for (ITestDevice device : deviceInfos.keySet()) {
+            devIds.add(device.getSerialNumber());
+        }
+        return String.join(",", devIds);
+    }
+
+    private String[] getCommand(File jarFile, String testFileName, String testId, String key) {
+        List<String> command = new ArrayList<>();
+        command.add("java");
+        command.add("-jar");
+        command.add(jarFile.getAbsolutePath());
+        if (!getTestFilesArgsForUicdBin(TESTS_RELATIVE_PATH, testFileName).isEmpty()) {
+            command.add(OPTION_SYMBOL + INPUT_OPTION_SHORT_NAME);
+            command.add(getTestFilesArgsForUicdBin(TESTS_RELATIVE_PATH, testFileName));
+        }
+        if (!getOutFilesArgsForUicdBin(OUTPUT_RELATIVE_PATH + "/" + testId).isEmpty()) {
+            command.add(OPTION_SYMBOL + OUTPUT_OPTION_SHORT_NAME);
+            command.add(getOutFilesArgsForUicdBin(OUTPUT_RELATIVE_PATH + "/" + testId));
+        }
+        if (!getPlaymodeArgForUicdBin().isEmpty()) {
+            command.add(OPTION_SYMBOL + MODE_OPTION_SHORT_NAME);
+            command.add(getPlaymodeArgForUicdBin());
+        }
+        if (!getDevIdsArgsForUicdBin().isEmpty()) {
+            command.add(OPTION_SYMBOL + DEVICES_OPTION_SHORT_NAME);
+            command.add(getDevIdsArgsForUicdBin());
+        }
+        if (globalVariables.containsKey(key)) {
+            command.add(OPTION_SYMBOL + GLOBAL_VARIABLE_OPTION_SHORT_NAME);
+            command.add(String.join(",", globalVariables.get(key)));
+        }
+        return command.toArray(new String[] {});
+    }
+}