blob: b0687bdc4f10a8a35de10eb4b540538ab47c0ee9 [file]
/*
* Copyright (C) 2025 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.
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* Utility class for interacting with Android emulators during tests.
*
* <p>This class provides methods to execute Android Debug Bridge (ADB) commands against a specific
* emulator instance. It abstracts the underlying process execution and handles command output.
*/
public class EmulatorTestUtils {
/**
* The path to the prebuilt Android Debug Bridge (ADB) executable. The path is determined
* dynamically based on the host operating system.
*/
private static final String ADB =
String.format("prebuilts/studio/sdk/%s/platform-tools/adb", getOsDirName());
/**
* Executes an ADB command against a specific emulator instance.
*
* @param port The port number of the target emulator (e.g., 5554).
* @param cmd The ADB command and its arguments to execute (e.g., "shell", "getprop",
* "ro.boot.completed").
* @return The standard output from the executed command, with leading/trailing whitespace
* removed.
* @throws Exception if the command fails to execute or returns a non-zero exit code.
*/
public static String adb(int port, String... cmd) throws Exception {
String[] adbCommands =
Stream.concat(Stream.of(ADB, "-s", "emulator-" + port), Arrays.stream(cmd))
.toArray(String[]::new);
return exec(adbCommands);
}
/**
* Executes a system command using {@link ProcessBuilder}.
*
* <p>This method creates a new process, captures its standard output, and waits for completion.
* The process's standard error is merged with its standard output. Output is read on a separate
* thread to prevent buffer-related deadlocks.
*
* @param cmd The command and its arguments to execute.
* @return The combined standard output and standard error from the command, with
* leading/trailing whitespace removed.
* @throws Exception if the process returns a non-zero exit code, containing the exit code and
* the captured output.
*/
private static String exec(String... cmd) throws Exception {
ProcessBuilder pb = new ProcessBuilder().command(cmd);
pb.redirectErrorStream(true); // Merge stdout and stderr into a single stream.
StringBuilder returnValue = new StringBuilder();
Process process = pb.start();
// A dedicated thread reads the process's output stream.
// This prevents the process buffer from filling up, which could cause a deadlock.
Thread readerThread =
new Thread(
() -> {
try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
returnValue.append(line).append("\n");
}
} catch (IOException e) {
// This can happen if the process is terminated abruptly. It's
// generally
// safe to ignore as the process is ending anyway.
}
});
readerThread.start();
try {
// Wait for the external process to complete.
int exitCode = process.waitFor();
// Wait for the reader thread to complete.
readerThread.join();
if (exitCode != 0) {
throw new Exception(
String.format(
"%s exited with code: %d, output = %s",
String.join(" ", cmd), exitCode, returnValue.toString().trim()));
}
} finally {
// Ensure the reader thread is cleaned up if it's still running.
readerThread.interrupt();
}
return returnValue.toString().trim();
}
/**
* Determines the appropriate directory name for the SDK path based on the host operating
* system.
*
* @return The OS-specific directory name ("darwin", "windows", or "linux").
* @throws RuntimeException if the operating system is not Mac, Windows, or Linux.
*/
private static String getOsDirName() {
String os = System.getProperty("os.name");
if (os.startsWith("Mac OS")) {
return "darwin";
} else if (os.startsWith("Windows")) {
return "windows";
} else if (os.startsWith("Linux")) {
return "linux";
} else {
throw new RuntimeException("Unsupported os.name: " + os);
}
}
}