blob: e8bbd9219836f85691a576159ca13628fab5fb69 [file] [log] [blame]
/*
* Copyright (C) 2022 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.observatory;
import com.android.tradefed.config.ArgsOptionParser;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.invoker.tracing.CloseableTraceScope;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StringEscapeUtils;
import com.android.tradefed.util.SystemUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A class for test launcher to call the TradeFed jar that packaged in the test suite to discover
* test modules.
*
* <p>TestDiscoveryInvoker will take {@link IConfiguration} and the test root directory from the
* launch control provider to make the launch control provider to invoke the workflow to use the
* config to query the packaged TradeFed jar file in the test suite root directory to retrieve test
* module names.
*/
public class TestDiscoveryInvoker {
private final IConfiguration mConfiguration;
private final String mDefaultConfigName;
private final File mRootDir;
private final IRunUtil mRunUtil = new RunUtil();
private final boolean mHasConfigFallback;
private final boolean mUseCurrentTradefed;
private File mTestDir;
public static final String TRADEFED_OBSERVATORY_ENTRY_PATH =
TestDiscoveryExecutor.class.getName();
public static final String TEST_DEPENDENCIES_LIST_KEY = "TestDependencies";
public static final String TEST_MODULES_LIST_KEY = "TestModules";
public static final String PARTIAL_FALLBACK_KEY = "PartialFallback";
public static final String TEST_DIRECTORY_ENV_VARIABLE_KEY =
"TF_TEST_DISCOVERY_USE_TEST_DIRECTORY";
public static final String ROOT_DIRECTORY_ENV_VARIABLE_KEY =
"ROOT_TEST_DISCOVERY_USE_TEST_DIRECTORY";
public static final String OUTPUT_FILE = "DISCOVERY_OUTPUT_FILE";
@VisibleForTesting
IRunUtil getRunUtil() {
return mRunUtil;
}
@VisibleForTesting
String getJava() {
return SystemUtil.getRunningJavaBinaryPath().getAbsolutePath();
}
@VisibleForTesting
File createOutputFile() throws IOException {
return FileUtil.createTempFile("discovery-output", ".txt");
}
public File getTestDir() {
return mTestDir;
}
public void setTestDir(File testDir) {
mTestDir = testDir;
}
/** Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration} and root directory. */
public TestDiscoveryInvoker(IConfiguration config, File rootDir) {
this(config, null, rootDir);
}
/**
* Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration}, test launcher's
* default config name and root directory.
*/
public TestDiscoveryInvoker(IConfiguration config, String defaultConfigName, File rootDir) {
this(config, defaultConfigName, rootDir, false, false);
}
/**
* Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration}, test launcher's
* default config name, root directory and if fallback is required.
*/
public TestDiscoveryInvoker(
IConfiguration config,
String defaultConfigName,
File rootDir,
boolean hasConfigFallback,
boolean useCurrentTradefed) {
mConfiguration = config;
mDefaultConfigName = defaultConfigName;
mRootDir = rootDir;
mTestDir = null;
mHasConfigFallback = hasConfigFallback;
mUseCurrentTradefed = useCurrentTradefed;
}
/**
* Retrieve a map of xTS test dependency names - categorized by either test modules or other
* test dependencies.
*
* @return A map of test dependencies which grouped by TEST_MODULES_LIST_KEY and
* TEST_DEPENDENCIES_LIST_KEY.
* @throws IOException
* @throws JSONException
* @throws ConfigurationException
* @throws TestDiscoveryException
*/
public Map<String, List<String>> discoverTestDependencies()
throws IOException, JSONException, ConfigurationException, TestDiscoveryException {
File outputFile = createOutputFile();
try (CloseableTraceScope ignored = new CloseableTraceScope("discoverTestDependencies")) {
Map<String, List<String>> dependencies = new HashMap<>();
// Build the classpath base on test root directory which should contain all the jars
String classPath = buildXtsClasspath(mRootDir);
// Build command line args to query the tradefed.jar in the root directory
List<String> args = buildJavaCmdForXtsDiscovery(classPath);
String[] subprocessArgs = args.toArray(new String[args.size()]);
if (mHasConfigFallback) {
getRunUtil()
.setEnvVariable(
ROOT_DIRECTORY_ENV_VARIABLE_KEY, mRootDir.getAbsolutePath());
}
getRunUtil().setEnvVariable(OUTPUT_FILE, outputFile.getAbsolutePath());
CommandResult res = getRunUtil().runTimedCmd(60000, subprocessArgs);
if (res.getExitCode() != 0 || !res.getStatus().equals(CommandStatus.SUCCESS)) {
DiscoveryExitCode exitCode = null;
if (res.getExitCode() != null) {
for (DiscoveryExitCode code : DiscoveryExitCode.values()) {
if (code.exitCode() == res.getExitCode()) {
exitCode = code;
}
}
}
throw new TestDiscoveryException(
String.format(
"Tradefed observatory error, unable to discover test module names."
+ " command used: %s error: %s",
Joiner.on(" ").join(subprocessArgs), res.getStderr()),
null,
exitCode);
}
String stdout = res.getStdout();
CLog.i(String.format("Tradefed Observatory returned in stdout: %s", stdout));
String result = FileUtil.readStringFromFile(outputFile);
CLog.i("output file content: %s", result);
// For backward compatibility
try {
new JSONObject(result);
} catch (JSONException e) {
CLog.w("Output file was incorrect. Try falling back stdout");
result = stdout;
}
List<String> testModules = parseTestDiscoveryOutput(result, TEST_MODULES_LIST_KEY);
if (!testModules.isEmpty()) {
dependencies.put(TEST_MODULES_LIST_KEY, testModules);
}
List<String> testDependencies =
parseTestDiscoveryOutput(result, TEST_DEPENDENCIES_LIST_KEY);
if (!testDependencies.isEmpty()) {
dependencies.put(TEST_DEPENDENCIES_LIST_KEY, testDependencies);
}
String partialFallback = parsePartialFallback(result);
if (partialFallback != null) {
dependencies.put(PARTIAL_FALLBACK_KEY, Arrays.asList(partialFallback));
}
return dependencies;
} finally {
FileUtil.deleteFile(outputFile);
}
}
/**
* Retrieve a map of test mapping test module names.
*
* @return A map of test module names which grouped by TEST_MODULES_LIST_KEY.
* @throws IOException
* @throws JSONException
* @throws ConfigurationException
* @throws TestDiscoveryException
*/
public Map<String, List<String>> discoverTestMappingDependencies()
throws IOException, JSONException, ConfigurationException, TestDiscoveryException {
File outputFile = createOutputFile();
try (CloseableTraceScope ignored =
new CloseableTraceScope("discoverTestMappingDependencies")) {
Map<String, List<String>> dependencies = new HashMap<>();
// Build the classpath base on the working directory
String classPath = buildTestMappingClasspath(mRootDir);
// Build command line args to query the tradefed.jar in the working directory
List<String> args = buildJavaCmdForTestMappingDiscovery(classPath);
String[] subprocessArgs = args.toArray(new String[args.size()]);
// Pass the test directory path to subprocess by environment variable
if (mTestDir != null) {
getRunUtil()
.setEnvVariable(
TEST_DIRECTORY_ENV_VARIABLE_KEY, mTestDir.getAbsolutePath());
}
if (mHasConfigFallback) {
getRunUtil()
.setEnvVariable(
ROOT_DIRECTORY_ENV_VARIABLE_KEY, mRootDir.getAbsolutePath());
}
getRunUtil().setEnvVariable(OUTPUT_FILE, outputFile.getAbsolutePath());
CommandResult res = getRunUtil().runTimedCmd(60000, subprocessArgs);
if (res.getExitCode() != 0 || !res.getStatus().equals(CommandStatus.SUCCESS)) {
throw new TestDiscoveryException(
String.format(
"Tradefed observatory error, unable to discover test module names."
+ " command used: %s error: %s",
Joiner.on(" ").join(subprocessArgs), res.getStderr()),
null);
}
String stdout = res.getStdout();
CLog.i(String.format("Tradefed Observatory returned in stdout:\n %s", stdout));
String result = FileUtil.readStringFromFile(outputFile);
List<String> testModules = parseTestDiscoveryOutput(result, TEST_MODULES_LIST_KEY);
if (!testModules.isEmpty()) {
dependencies.put(TEST_MODULES_LIST_KEY, testModules);
}
String partialFallback = parsePartialFallback(result);
if (partialFallback != null) {
dependencies.put(PARTIAL_FALLBACK_KEY, Arrays.asList(partialFallback));
}
return dependencies;
} finally {
FileUtil.deleteFile(outputFile);
}
}
/**
* Build java cmd for invoking a subprocess to discover test mapping test module names.
*
* @return A list of java command args.
*/
private List<String> buildJavaCmdForTestMappingDiscovery(String classpath) {
List<String> fullCommandLineArgs =
new ArrayList<String>(
Arrays.asList(
QuotationAwareTokenizer.tokenizeLine(
mConfiguration.getCommandLine())));
List<String> args = new ArrayList<>();
args.add(getJava());
args.add("-cp");
args.add(classpath);
args.add(TRADEFED_OBSERVATORY_ENTRY_PATH);
// Delete invocation data from args which test discovery don't need
int i = 0;
while (i < fullCommandLineArgs.size()) {
if (fullCommandLineArgs.get(i).equals("--invocation-data")) {
i = i + 2;
} else {
args.add(fullCommandLineArgs.get(i));
i = i + 1;
}
}
return args;
}
/**
* Build java cmd for invoking a subprocess to discover XTS test module names.
*
* @return A list of java command args.
* @throws ConfigurationException
*/
private List<String> buildJavaCmdForXtsDiscovery(String classpath)
throws ConfigurationException {
List<String> fullCommandLineArgs =
new ArrayList<String>(
Arrays.asList(
QuotationAwareTokenizer.tokenizeLine(
mConfiguration.getCommandLine())));
// first arg is config name
final String testLauncherConfigName = fullCommandLineArgs.remove(0);
final ConfigurationCtsParserSettings ctsParserSettings =
new ConfigurationCtsParserSettings();
ArgsOptionParser ctsOptionParser = null;
ctsOptionParser = new ArgsOptionParser(ctsParserSettings);
// Parse to collect all values of --cts-params as well config name
ctsOptionParser.parseBestEffort(fullCommandLineArgs, true);
List<String> ctsParams = ctsParserSettings.mCtsParams;
String configName = ctsParserSettings.mConfigName;
if (configName == null) {
if (mDefaultConfigName == null) {
throw new ConfigurationException(
String.format(
"Failed to extract config-name from parent test command options,"
+ " unable to build args to invoke tradefed observatory."
+ " Parent test command options is: %s",
fullCommandLineArgs));
} else {
CLog.i(
String.format(
"No config name provided in the command args, use default config"
+ " name %s",
mDefaultConfigName));
configName = mDefaultConfigName;
}
}
List<String> args = new ArrayList<>();
args.add(getJava());
args.add("-cp");
args.add(classpath);
// Cts V2 requires CTS_ROOT to be set or VTS_ROOT for vts run
args.add(
String.format(
"-D%s=%s", ctsParserSettings.mRootdirVar, mRootDir.getAbsolutePath()));
args.add(TRADEFED_OBSERVATORY_ENTRY_PATH);
args.add(configName);
// Tokenize args to be passed to CtsTest/XtsTest
args.addAll(StringEscapeUtils.paramsToArgs(ctsParams));
return args;
}
/**
* Build the classpath string based on jars in the sandbox's working directory.
*
* @return A string of classpaths.
* @throws IOException
*/
private String buildTestMappingClasspath(File workingDir) throws IOException {
List<String> classpathList = new ArrayList<>();
if (!workingDir.exists()) {
throw new FileNotFoundException("Couldn't find the build directory");
}
if (workingDir.listFiles().length == 0) {
throw new FileNotFoundException(
String.format(
"Could not find any files under %s", workingDir.getAbsolutePath()));
}
for (File toolsFile : workingDir.listFiles()) {
if (toolsFile.getName().endsWith(".jar")) {
classpathList.add(toolsFile.getAbsolutePath());
}
}
Collections.sort(classpathList);
if (mUseCurrentTradefed) {
classpathList.add(getCurrentClassPath());
}
return Joiner.on(":").join(classpathList);
}
private String getCurrentClassPath() {
return System.getProperty("java.class.path");
}
/**
* Build the classpath string based on jars in the XTS test root directory's tools folder.
*
* @return A string of classpaths.
* @throws IOException
*/
private String buildXtsClasspath(File ctsRoot) throws IOException {
List<File> classpathList = new ArrayList<>();
if (!ctsRoot.exists()) {
throw new FileNotFoundException("Couldn't find the build directory: " + ctsRoot);
}
// Safe to assume single dir from extracted zip
if (ctsRoot.list().length != 1) {
throw new RuntimeException(
"List of sub directory does not contain only one item "
+ "current list is:"
+ Arrays.toString(ctsRoot.list()));
}
String mainDirName = ctsRoot.list()[0];
// Jar files from the downloaded cts/xts
File jarCtsPath = new File(new File(ctsRoot, mainDirName), "tools");
if (jarCtsPath.listFiles().length == 0) {
throw new FileNotFoundException(
String.format(
"Could not find any files under %s", jarCtsPath.getAbsolutePath()));
}
for (File toolsFile : jarCtsPath.listFiles()) {
if (toolsFile.getName().endsWith(".jar")) {
classpathList.add(toolsFile);
}
}
Collections.sort(classpathList);
return Joiner.on(":").join(classpathList);
}
/**
* Parse test module names from the tradefed observatory's output JSON string.
*
* @param discoveryOutput JSON string from test discovery
* @param dependencyListKey test dependency type
* @return A list of test module names.
* @throws JSONException
*/
private List<String> parseTestDiscoveryOutput(String discoveryOutput, String dependencyListKey)
throws JSONException {
JSONObject jsonObject = new JSONObject(discoveryOutput);
List<String> testModules = new ArrayList<>();
if (jsonObject.has(dependencyListKey)) {
JSONArray jsonArray = jsonObject.getJSONArray(dependencyListKey);
for (int i = 0; i < jsonArray.length(); i++) {
testModules.add(jsonArray.getString(i));
}
}
return testModules;
}
private String parsePartialFallback(String discoveryOutput) throws JSONException {
JSONObject jsonObject = new JSONObject(discoveryOutput);
if (jsonObject.has(PARTIAL_FALLBACK_KEY)) {
return jsonObject.getString(PARTIAL_FALLBACK_KEY);
}
return null;
}
}