blob: 527d55f2c64160a01fe4519ca16186a50042b235 [file] [log] [blame]
/*
* Copyright (C) 2016 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;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.IFolderBuildInfo;
import com.android.tradefed.command.CommandOptions;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.proxy.AutomatedReporters;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.error.HarnessRuntimeException;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.RemoteInvocationExecution;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.result.proto.StreamProtoReceiver;
import com.android.tradefed.result.proto.StreamProtoResultReporter;
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.IRunUtil.EnvPriority;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.SubprocessExceptionParser;
import com.android.tradefed.util.SubprocessTestResultsParser;
import com.android.tradefed.util.SystemUtil;
import com.android.tradefed.util.TimeUtil;
import com.android.tradefed.util.UniqueMultiMap;
import org.junit.Assert;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* A {@link IRemoteTest} for running tests against a separate TF installation.
*
* <p>Launches an external java process to run the tests. Used for running the TF unit or functional
* tests continuously.
*/
public abstract class SubprocessTfLauncher
implements IBuildReceiver, IInvocationContextReceiver, IRemoteTest, IConfigurationReceiver {
/** The tag that will be passed to the TF subprocess to differentiate it */
public static final String SUBPROCESS_TAG_NAME = "subprocess";
public static final String PARENT_PROC_TAG_NAME = "parentprocess";
/** Env. variable that affects adb selection. */
public static final String ANDROID_SERIAL_VAR = "ANDROID_SERIAL";
@Option(name = "max-run-time", description =
"The maximum time to allow for a TF test run.", isTimeVal = true)
private long mMaxTfRunTime = 20 * 60 * 1000;
@Option(name = "remote-debug", description =
"Start the TF java process in remote debug mode.")
private boolean mRemoteDebug = false;
@Option(name = "config-name", description = "The config that runs the TF tests")
private String mConfigName;
@Option(
name = "local-sharding-mode",
description =
"If sharding is requested, allow the launcher to run with local sharding.")
private boolean mLocalShardingMode = false;
@Option(name = "use-event-streaming", description = "Use a socket to receive results as they"
+ "arrived instead of using a temporary file and parsing at the end.")
private boolean mEventStreaming = true;
@Option(
name = "use-proto-reporting",
description = "Use a proto result reporter for the results from the subprocess.")
private boolean mUseProtoReporting = false;
@Option(name = "sub-global-config", description = "The global config name to pass to the"
+ "sub process, can be local or from jar resources. Be careful of conflicts with "
+ "parent process.")
private String mGlobalConfig = null;
@Option(
name = "inject-invocation-data",
description = "Pass the invocation-data to the subprocess if enabled.")
private boolean mInjectInvocationData = true;
@Option(name = "ignore-test-log", description = "Only rely on logAssociation for logs.")
private boolean mIgnoreTestLog = true;
@Option(
name = "disable-stderr-test",
description = "Whether or not to disable the stderr validation check."
)
private boolean mDisableStderrTest = false;
@Option(
name = "disable-add-opens",
description = "Whether or not to add the java add-opens flags"
)
private boolean mDisableJavaOpens = false;
@Option(name = "add-opens", description = "Whether or not to add the java add-opens flags")
private Set<String> mAddOpens =
new LinkedHashSet<>(
Arrays.asList(
"java.base/java.nio",
"java.base/sun.reflect.annotation",
"java.base/java.io"));
// Temp global configuration filtered from the parent process.
private String mFilteredGlobalConfig = null;
private static final List<String> TRADEFED_JARS =
new ArrayList<>(
Arrays.asList(
// Loganalysis
"loganalysis.jar",
"loganalysis-tests.jar",
// Aosp Tf jars
"tradefed.jar",
"tradefed-test-framework.jar",
"tradefed-tests.jar",
// libs
"tools-common-prebuilt.jar",
// jar in older branches
"tf-prod-tests.jar",
"tf-prod-metatests.jar",
// Aosp contrib jars
"tradefed-contrib.jar",
"tf-contrib-tests.jar",
// Google Tf jars
"google-tf-prod-tests.jar",
"google-tf-prod-metatests.jar",
"google-tradefed.jar",
"google-tradefed-tests.jar",
// Google contrib jars
"google-tradefed-contrib.jar",
// Older jar required for coverage tests
"jack-jacoco-reporter.jar",
"emmalib.jar"));
/** Timeout to wait for the events received from subprocess to finish being processed.*/
private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000;
protected IRunUtil mRunUtil = new RunUtil();
protected IBuildInfo mBuildInfo = null;
// Temp directory to run the TF process.
protected File mTmpDir = null;
// List of command line arguments to run the TF process.
protected List<String> mCmdArgs = null;
// The absolute path to the build's root directory.
protected String mRootDir = null;
protected IConfiguration mConfig;
private IInvocationContext mContext;
@Override
public void setInvocationContext(IInvocationContext invocationContext) {
mContext = invocationContext;
}
@Override
public void setConfiguration(IConfiguration configuration) {
mConfig = configuration;
}
/**
* Set use-event-streaming.
*
* Exposed for unit testing.
*/
protected void setEventStreaming(boolean eventStreaming) {
mEventStreaming = eventStreaming;
}
/**
* Set IRunUtil.
*
* Exposed for unit testing.
*/
protected void setRunUtil(IRunUtil runUtil) {
mRunUtil = runUtil;
}
/** Returns the {@link IRunUtil} that will be used for the subprocess command. */
protected IRunUtil getRunUtil() {
return mRunUtil;
}
/**
* Setup before running the test.
*/
protected void preRun() {
Assert.assertNotNull(mBuildInfo);
Assert.assertNotNull(mConfigName);
IFolderBuildInfo tfBuild = (IFolderBuildInfo) mBuildInfo;
File rootDirFile = tfBuild.getRootDir();
mRootDir = rootDirFile.getAbsolutePath();
String jarClasspath = "";
List<String> paths = new ArrayList<>();
for (String jar : TRADEFED_JARS) {
File f = FileUtil.findFile(rootDirFile, jar);
if (f != null && f.exists()) {
paths.add(f.getAbsolutePath());
}
}
jarClasspath = String.join(":", paths);
mCmdArgs = new ArrayList<String>();
mCmdArgs.add(SystemUtil.getRunningJavaBinaryPath().getAbsolutePath());
try {
mTmpDir = FileUtil.createTempDir("subprocess-" + tfBuild.getBuildId());
mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mTmpDir.getAbsolutePath()));
} catch (IOException e) {
CLog.e(e);
throw new RuntimeException(e);
}
addJavaArguments(mCmdArgs);
if (mRemoteDebug) {
mCmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10088");
}
// This prevent the illegal reflective access warnings by allowing some packages.
if (!mDisableJavaOpens) {
for (String modulePackage : mAddOpens) {
mCmdArgs.add("--add-opens=" + modulePackage + "=ALL-UNNAMED");
}
}
mCmdArgs.add("-cp");
mCmdArgs.add(jarClasspath);
mCmdArgs.add("com.android.tradefed.command.CommandRunner");
mCmdArgs.add(mConfigName);
Integer shardCount = mConfig.getCommandOptions().getShardCount();
if (mLocalShardingMode && shardCount != null & shardCount > 1) {
mCmdArgs.add("--shard-count");
mCmdArgs.add(Integer.toString(shardCount));
}
// clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
mRunUtil.unsetEnvVariable(ANDROID_SERIAL_VAR);
mRunUtil.unsetEnvVariable(RemoteInvocationExecution.START_FEATURE_SERVER);
for (String variable : AutomatedReporters.REPORTER_MAPPING) {
mRunUtil.unsetEnvVariable(variable);
}
if (mGlobalConfig == null) {
// If the global configuration is not set in option, create a filtered global
// configuration for subprocess to use.
try {
File filteredGlobalConfig =
GlobalConfiguration.getInstance().cloneConfigWithFilter();
mFilteredGlobalConfig = filteredGlobalConfig.getAbsolutePath();
mGlobalConfig = mFilteredGlobalConfig;
} catch (IOException e) {
CLog.e("Failed to create filtered global configuration");
CLog.e(e);
}
}
if (mGlobalConfig != null) {
// We allow overriding this global config and then set it for the subprocess.
mRunUtil.setEnvVariablePriority(EnvPriority.SET);
mRunUtil.setEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig);
}
}
/**
* Allow to add extra java parameters to the subprocess invocation.
*
* @param args the current list of arguments to which we need to add the extra ones.
*/
protected void addJavaArguments(List<String> args) {}
/**
* Actions to take after the TF test is finished.
*
* @param listener the original {@link ITestInvocationListener} where to report results.
* @param exception True if exception was raised inside the test.
* @param elapsedTime the time taken to run the tests.
*/
protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) {}
/** Pipe to the subprocess the invocation-data so that it can use them if needed. */
private void addInvocationData() {
if (!mInjectInvocationData) {
return;
}
UniqueMultiMap<String, String> data = mConfig.getCommandOptions().getInvocationData();
for (String key : data.keySet()) {
for (String value : data.get(key)) {
mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
mCmdArgs.add(key);
mCmdArgs.add(value);
}
}
// Finally add one last more to tag the subprocess
mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
mCmdArgs.add(SUBPROCESS_TAG_NAME);
mCmdArgs.add("true");
// Tag the parent invocation
mBuildInfo.addBuildAttribute(PARENT_PROC_TAG_NAME, "true");
}
/** {@inheritDoc} */
@Override
public void run(TestInformation testInfo, ITestInvocationListener listener)
throws DeviceNotAvailableException {
preRun();
addInvocationData();
File stdoutFile = null;
File stderrFile = null;
File eventFile = null;
SubprocessTestResultsParser eventParser = null;
StreamProtoReceiver protoReceiver = null;
FileOutputStream stdout = null;
FileOutputStream stderr = null;
boolean exception = false;
long startTime = 0L;
long elapsedTime = -1L;
try {
stdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log");
stderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log");
stderr = new FileOutputStream(stderrFile);
stdout = new FileOutputStream(stdoutFile);
if (mUseProtoReporting) {
protoReceiver = new StreamProtoReceiver(listener, mContext, false, false);
mCmdArgs.add("--" + StreamProtoResultReporter.PROTO_REPORT_PORT_OPTION);
mCmdArgs.add(Integer.toString(protoReceiver.getSocketServerPort()));
} else {
eventParser = new SubprocessTestResultsParser(listener, mEventStreaming, mContext);
if (mEventStreaming) {
mCmdArgs.add("--subprocess-report-port");
mCmdArgs.add(Integer.toString(eventParser.getSocketServerPort()));
} else {
eventFile = FileUtil.createTempFile("event_subprocess_", ".log");
mCmdArgs.add("--subprocess-report-file");
mCmdArgs.add(eventFile.getAbsolutePath());
}
eventParser.setIgnoreTestLog(mIgnoreTestLog);
}
startTime = System.currentTimeMillis();
CommandResult result = mRunUtil.runTimedCmd(mMaxTfRunTime, stdout,
stderr, mCmdArgs.toArray(new String[0]));
if (eventParser != null) {
if (eventParser.getStartTime() != null) {
startTime = eventParser.getStartTime();
}
elapsedTime = System.currentTimeMillis() - startTime;
// We possibly allow for a little more time if the thread is still processing
// events.
if (!eventParser.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
elapsedTime = -1L;
throw new RuntimeException(
String.format(
"Event receiver thread did not complete:" + "\n%s",
FileUtil.readStringFromFile(stderrFile)));
}
} else if (protoReceiver != null) {
if (!protoReceiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
elapsedTime = -1L;
throw new RuntimeException(
String.format(
"Event receiver thread did not complete:" + "\n%s",
FileUtil.readStringFromFile(stderrFile)));
}
protoReceiver.completeModuleEvents();
}
if (result.getStatus().equals(CommandStatus.SUCCESS)) {
CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId());
testCleanStdErr(stderrFile, listener);
} else {
CLog.w("Failed ran TF tests for build %s, status %s",
mBuildInfo.getBuildId(), result.getStatus());
CLog.v(
"TF tests output:\nstdout:\n%s\nstderr:\n%s",
result.getStdout(), result.getStderr());
exception = true;
String errMessage = null;
if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
errMessage = String.format("Timeout after %s",
TimeUtil.formatElapsedTime(mMaxTfRunTime));
throw new HarnessRuntimeException(
String.format(
"%s Tests subprocess failed due to:\n%s\n",
mConfigName, errMessage),
InfraErrorIdentifier.INVOCATION_TIMEOUT);
} else if (eventParser != null && !eventParser.reportedInvocationFailed()) {
SubprocessExceptionParser.handleStderrException(result);
}
}
} catch (IOException e) {
exception = true;
throw new RuntimeException(e);
} finally {
StreamUtil.close(stdout);
StreamUtil.close(stderr);
logAndCleanFile(stdoutFile, listener);
logAndCleanFile(stderrFile, listener);
if (eventFile != null) {
eventParser.parseFile(eventFile);
logAndCleanFile(eventFile, listener);
}
StreamUtil.close(eventParser);
StreamUtil.close(protoReceiver);
postRun(listener, exception, elapsedTime);
if (mTmpDir != null) {
FileUtil.recursiveDelete(mTmpDir);
}
if (mFilteredGlobalConfig != null) {
FileUtil.deleteFile(new File(mFilteredGlobalConfig));
}
}
}
/**
* Log the content of given file to listener, then remove the file.
*
* @param fileToExport the {@link File} pointing to the file to log.
* @param listener the {@link ITestInvocationListener} where to report the test.
*/
private void logAndCleanFile(File fileToExport, ITestInvocationListener listener) {
if (fileToExport == null)
return;
try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport)) {
listener.testLog(fileToExport.getName(), LogDataType.TEXT, inputStream);
}
FileUtil.deleteFile(fileToExport);
}
/**
* {@inheritDoc}
*/
@Override
public void setBuild(IBuildInfo buildInfo) {
mBuildInfo = buildInfo;
}
/**
* Extra test to ensure no abnormal logging is made to stderr when all the tests pass.
*
* @param stdErrFile the stderr log file of the subprocess.
* @param listener the {@link ITestInvocationListener} where to report the test.
*/
private void testCleanStdErr(File stdErrFile, ITestInvocationListener listener)
throws IOException {
if (mDisableStderrTest) {
return;
}
listener.testRunStarted("StdErr", 1);
TestDescription tid = new TestDescription("stderr-test", "checkIsEmpty");
listener.testStarted(tid);
if (!FileUtil.readStringFromFile(stdErrFile).isEmpty()) {
String trace =
String.format(
"Found some output in stderr:\n%s",
FileUtil.readStringFromFile(stdErrFile));
listener.testFailed(tid, FailureDescription.create(trace));
}
listener.testEnded(tid, new HashMap<String, Metric>());
listener.testRunEnded(0, new HashMap<String, Metric>());
}
}