Blueberry: add pts-bot Tradefed class

Test: make tradefed-all
Change-Id: I180397223cd56bca8dbeadb14ac8d1fd034bc401
diff --git a/test_framework/com/android/tradefed/testtype/blueberry/OWNERS b/test_framework/com/android/tradefed/testtype/blueberry/OWNERS
new file mode 100644
index 0000000..eec6a5c
--- /dev/null
+++ b/test_framework/com/android/tradefed/testtype/blueberry/OWNERS
@@ -0,0 +1,2 @@
+girardier@google.com
+licorne@google.com
diff --git a/test_framework/com/android/tradefed/testtype/blueberry/PtsBotTest.java b/test_framework/com/android/tradefed/testtype/blueberry/PtsBotTest.java
new file mode 100644
index 0000000..7367c6e
--- /dev/null
+++ b/test_framework/com/android/tradefed/testtype/blueberry/PtsBotTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2018 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.blueberry;
+
+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.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Run PTS-bot tests. PTS-bot is a complete automation of the Bluetooth Profile Tuning Suite, which
+ * is the testing tool provided by the Bluetooth standard to run Bluetooth Host certification tests
+ * (see
+ * https://www.bluetooth.com/develop-with-bluetooth/qualification-listing/qualification-test-tools/profile-tuning-suite/).
+ */
+public class PtsBotTest implements IRemoteTest {
+
+    private static final int BLUEBERRY_SERVER_PORT = 8999;
+    private static final int HCI_ROOTCANAL_PORT_CUTTLEFISH = 7300;
+    private static final int HCI_ROOTCANAL_PORT = 6211;
+    private static final int HCI_PROXY_PORT = 1234;
+
+    @Option(name = "mmi2grpc", description = "mmi2grpc python module path.")
+    private File mmi2grpc = null;
+
+    @Option(
+            name = "tests-config-file",
+            description = "Tests config file.",
+            importance = Importance.ALWAYS)
+    private File testsConfigFile = null;
+
+    @Option(name = "profile", description = "Profile to be tested.", importance = Importance.ALWAYS)
+    private Set<String> profiles = new HashSet<>();
+
+    @Option(
+            name = "physical",
+            description = "Run PTS-bot with a physical Bluetooth communication.",
+            importance = Importance.ALWAYS)
+    private boolean physical = false;
+
+    private int hciPort;
+
+    @Override
+    public void run(TestInformation testInfo, ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+
+        // If tests config file cannot be found using full path, search in
+        // dependencies.
+        if (!testsConfigFile.exists()) {
+            try {
+                testsConfigFile = testInfo.getDependencyFile(testsConfigFile.getName(), false);
+            } catch (FileNotFoundException e) {
+                throw new RuntimeException("Tests config file does not exist");
+            }
+        }
+
+        CLog.i("Tests config file: %s", testsConfigFile.getPath());
+        CLog.i("Profiles to be tested: %s", profiles);
+
+        ITestDevice testDevice = testInfo.getDevice();
+
+        // Forward Blueberry Server port.
+        adbForwardPort(testDevice, BLUEBERRY_SERVER_PORT);
+
+        if (!physical) {
+            // Check product type to determine Root Canal port.
+            hciPort = HCI_ROOTCANAL_PORT_CUTTLEFISH;
+            if (!testDevice.getProductType().equals("cutf")) {
+                hciPort = HCI_ROOTCANAL_PORT;
+
+                // Forward Root Canal port.
+                adbForwardPort(testDevice, hciPort);
+            }
+        } else {
+            hciPort = HCI_PROXY_PORT;
+        }
+
+        CLog.i("HCI port: %s", hciPort);
+
+        // Run tests.
+        for (String profile : profiles) {
+            runPtsBotTestsForProfile(profile, listener);
+        }
+
+        // Remove forwarded ports.
+        adbForwardRemovePort(testDevice, BLUEBERRY_SERVER_PORT);
+        if (!physical && !testDevice.getProductType().equals("cutf")) {
+            adbForwardRemovePort(testDevice, HCI_ROOTCANAL_PORT);
+        }
+    }
+
+    private String[] listPtsBotTestsForProfile(String profile) {
+        try {
+            ProcessBuilder processBuilder =
+                    new ProcessBuilder(
+                            "pts-bot", "-c", testsConfigFile.getPath(), "--list", profile);
+
+            CLog.i("Running command: %s", String.join(" ", processBuilder.command()));
+            Process process = processBuilder.start();
+
+            BufferedReader stdInput =
+                    new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+            String line =
+                    stdInput.lines().filter(l -> l.startsWith("Tests:")).findFirst().orElse(null);
+            stdInput.close();
+
+            if (line != null) {
+                String[] tests =
+                        line.substring(line.indexOf("[") + 1, line.indexOf("]"))
+                                .replaceAll("\"", "")
+                                .split(", ");
+                return tests;
+            }
+
+        } catch (IOException e) {
+            CLog.e(e);
+            CLog.e("Cannot run pts-bot, make sure it is properly installed");
+        }
+
+        CLog.e("No tests have been found in tests config file");
+        return null;
+    }
+
+    private void runPtsBotTestsForProfile(String profile, ITestInvocationListener listener) {
+        String[] tests = listPtsBotTestsForProfile(profile);
+        if (tests == null || tests.length == 0) {
+            CLog.e("Cannot run PTS-bot for %s, no tests found", profile);
+            return;
+        } else {
+            CLog.i("Available tests for %s: [%s]", profile, String.join(", ", tests));
+        }
+        Map<String, String> runMetrics = new HashMap<>();
+
+        listener.testRunStarted(profile, tests.length);
+        long startTimestamp = System.currentTimeMillis();
+        for (int i = 0; i < tests.length; i++) {
+            runPtsBotTest(profile, tests[i], listener);
+        }
+        long endTimestamp = System.currentTimeMillis();
+        listener.testRunEnded(endTimestamp - startTimestamp, runMetrics);
+    }
+
+    private boolean runPtsBotTest(
+            String profile, String testName, ITestInvocationListener listener) {
+        TestDescription testDescription = new TestDescription(profile, testName);
+        boolean success = false;
+
+        listener.testStarted(testDescription);
+        CLog.i(testName);
+        try {
+
+            ProcessBuilder processBuilder;
+            processBuilder =
+                    new ProcessBuilder(
+                            "pts-bot",
+                            "-c",
+                            testsConfigFile.getPath(),
+                            "--hci",
+                            String.valueOf(hciPort),
+                            testName);
+
+            if (mmi2grpc.exists()) {
+                // Add mmi2grpc python module path to process builder environment.
+                Map<String, String> env = processBuilder.environment();
+                env.put("PYTHONPATH", mmi2grpc.getPath());
+            }
+
+            CLog.i("Running command: %s", String.join(" ", processBuilder.command()));
+            Process process = processBuilder.start();
+            // Note: there is no need to implement a timeout here since it is handled in pts-bot.
+
+            BufferedReader stdInput =
+                    new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+            BufferedReader stdError =
+                    new BufferedReader(new InputStreamReader(process.getErrorStream()));
+
+            Optional<String> lastLine =
+                    stdInput.lines().peek(CLog::i).reduce((last, value) -> value);
+            // Last line is providing success information.
+            success =
+                    lastLine.map(
+                                    (line) -> {
+                                        try {
+                                            return Integer.parseInt(
+                                                            line.split(", ")[1].substring(0, 1))
+                                                    == 1;
+                                        } catch (Exception e) {
+                                            CLog.e("Failed to parse success");
+                                            return false;
+                                        }
+                                    })
+                            .orElse(false);
+            stdInput.close();
+
+            stdError.lines().forEach(CLog::e);
+            stdError.close();
+
+        } catch (Exception e) {
+            CLog.e(e);
+            CLog.e("Cannot run pts-bot, make sure it is properly installed");
+        }
+
+        if (!success) {
+            listener.testFailed(testDescription, "Unknown");
+        }
+
+        listener.testEnded(testDescription, Collections.emptyMap());
+
+        return success;
+    }
+
+    private void adbForwardPort(ITestDevice testDevice, int port)
+            throws DeviceNotAvailableException {
+        testDevice.executeAdbCommand(
+                1000L, "forward", String.format("tcp:%s", port), String.format("tcp:%s", port));
+    }
+
+    private void adbForwardRemovePort(ITestDevice testDevice, int port)
+            throws DeviceNotAvailableException {
+        testDevice.executeAdbCommand(1000L, "forward", "--remove", String.format("tcp:%s", port));
+    }
+}