/* * Copyright (C) 2010 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.invoker; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log.LogLevel; import com.android.tradefed.build.BuildRetrievalError; import com.android.tradefed.build.IBuildInfo; import com.android.tradefed.build.IBuildProvider; import com.android.tradefed.build.IDeviceBuildProvider; import com.android.tradefed.command.CommandRunner.ExitCode; import com.android.tradefed.config.ConfigurationDef; import com.android.tradefed.config.ConfigurationException; import com.android.tradefed.config.ConfigurationFactory; import com.android.tradefed.config.GlobalConfiguration; import com.android.tradefed.config.IConfiguration; import com.android.tradefed.config.IConfigurationFactory; import com.android.tradefed.config.IDeviceConfiguration; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.DeviceUnresponsiveException; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.device.ITestDevice.RecoveryMode; import com.android.tradefed.device.StubDevice; import com.android.tradefed.device.TestDeviceState; import com.android.tradefed.log.ILeveledLogOutput; import com.android.tradefed.log.ILogRegistry; import com.android.tradefed.log.LogRegistry; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.AggregatingProfilerListener; import com.android.tradefed.result.ITestInvocationListener; import com.android.tradefed.result.ITestLoggerReceiver; import com.android.tradefed.result.InputStreamSource; import com.android.tradefed.result.LogDataType; import com.android.tradefed.result.LogSaverResultForwarder; import com.android.tradefed.result.ResultForwarder; import com.android.tradefed.suite.checker.ISystemStatusCheckerReceiver; import com.android.tradefed.targetprep.BuildError; import com.android.tradefed.targetprep.DeviceFailedToBootError; import com.android.tradefed.targetprep.IHostCleaner; import com.android.tradefed.targetprep.ITargetCleaner; import com.android.tradefed.targetprep.ITargetPreparer; import com.android.tradefed.targetprep.TargetSetupError; import com.android.tradefed.targetprep.multi.IMultiTargetPreparer; import com.android.tradefed.testtype.IBuildReceiver; import com.android.tradefed.testtype.IDeviceTest; import com.android.tradefed.testtype.IInvocationContextReceiver; import com.android.tradefed.testtype.IMultiDeviceTest; import com.android.tradefed.testtype.IRemoteTest; import com.android.tradefed.testtype.IResumableTest; import com.android.tradefed.testtype.IRetriableTest; import com.android.tradefed.testtype.IStrictShardableTest; import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.QuotationAwareTokenizer; import com.android.tradefed.util.RunInterruptedException; import com.android.tradefed.util.RunUtil; import com.android.tradefed.util.StreamUtil; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ListIterator; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * Default implementation of {@link ITestInvocation}. *
* Loads major objects based on {@link IConfiguration} * - retrieves build * - prepares target * - runs tests * - reports results */ public class TestInvocation implements ITestInvocation { static final String TRADEFED_LOG_NAME = "host_log"; static final String DEVICE_LOG_NAME_PREFIX = "device_logcat_"; static final String EMULATOR_LOG_NAME_PREFIX = "emulator_log_"; static final String BUILD_ERROR_BUGREPORT_NAME = "build_error_bugreport"; static final String DEVICE_UNRESPONSIVE_BUGREPORT_NAME = "device_unresponsive_bugreport"; static final String INVOCATION_ENDED_BUGREPORT_NAME = "invocation_ended_bugreport"; static final String TARGET_SETUP_ERROR_BUGREPORT_NAME = "target_setup_error_bugreport"; static final String BATT_TAG = "[battery level]"; public enum Stage { ERROR("error"), SETUP("setup"), TEST("test"), TEARDOWN("teardown"); private final String mName; Stage(String name) { mName = name; } public String getName() { return mName; } } private String mStatus = "(not invoked)"; private boolean mStopRequested = false; /** * A {@link ResultForwarder} for forwarding resumed invocations. * * It filters the invocationStarted event for the resumed invocation, and sums the invocation * elapsed time */ private static class ResumeResultForwarder extends ResultForwarder { long mCurrentElapsedTime; /** * @param listeners */ public ResumeResultForwarder(ListIf a shard count is greater than 1, it will simply create configs for each shard by
* setting shard indices and reschedule them. If a shard count is not set,it would fallback to
* {@link ShardHelper#legacyShardConfig}.
*
* @param config the current {@link IConfiguration}.
* @param context the {@link IInvocationContext} holding the info of the tests.
* @param rescheduler the {@link IRescheduler}
* @return true if test was sharded. Otherwise return false
*/
private boolean shardConfig(
IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
if (config.getCommandOptions().getShardIndex() != null) {
// The config is already for a single shard.
return false;
}
mStatus = "sharding";
if (config.getCommandOptions().getShardCount() == null) {
return ShardHelper.legacyShardConfig(config, context, rescheduler);
}
// Schedules shard configs.
int shardCount = config.getCommandOptions().getShardCount();
for (int i = 0; i < config.getCommandOptions().getShardCount(); i++) {
IConfiguration shardConfig = null;
// Create a deep copy of the configuration.
try {
shardConfig = getConfigFactory().createConfigurationFromArgs(
QuotationAwareTokenizer.tokenizeLine(config.getCommandLine()));
} catch (ConfigurationException e) {
// This must not happen.
throw new RuntimeException("failed to deep copy a configuration", e);
}
ShardHelper.cloneBuildInfos(config, shardConfig, context);
shardConfig.getCommandOptions().setShardCount(shardCount);
shardConfig.getCommandOptions().setShardIndex(i);
rescheduler.scheduleConfig(shardConfig);
}
return true;
}
/**
* Factory method for getting a reference to the {@link IConfigurationFactory}
*
* @return the {@link IConfigurationFactory} to use
*/
protected IConfigurationFactory getConfigFactory() {
return ConfigurationFactory.getInstance();
}
/**
* Update the {@link IBuildInfo} with additional info from the {@link IConfiguration}.
*
* @param info the {@link IBuildInfo}
* @param config the {@link IConfiguration}
*/
private void updateBuild(IBuildInfo info, IConfiguration config) {
if (config.getCommandLine() != null) {
// TODO: obfuscate the password if any.
info.addBuildAttribute("command_line_args", config.getCommandLine());
}
if (config.getCommandOptions().getShardCount() != null) {
info.addBuildAttribute("shard_count",
config.getCommandOptions().getShardCount().toString());
}
if (config.getCommandOptions().getShardIndex() != null) {
info.addBuildAttribute("shard_index",
config.getCommandOptions().getShardIndex().toString());
}
// TODO: update all the configs to only use test-tag from CommandOption and not build
// providers.
// When CommandOption is set, it overrides any test-tag from build_providers
if (!"stub".equals(config.getCommandOptions().getTestTag())) {
info.setTestTag(getTestTag(config));
} else if (info.getTestTag() == null || info.getTestTag().isEmpty()) {
// We ensure that that a default test-tag is always available.
info.setTestTag("stub");
} else {
CLog.w("Using the test-tag from the build_provider. Consider updating your config to"
+ " have no alias/namespace in front of test-tag.");
}
}
/**
* Update the {@link IInvocationContext} with additional info from the {@link IConfiguration}.
*
* @param context the {@link IInvocationContext}
* @param config the {@link IConfiguration}
*/
private void updateInvocationContext(IInvocationContext context, IConfiguration config) {
// TODO: Once reporting on context is done, only set context attributes
if (config.getCommandLine() != null) {
// TODO: obfuscate the password if any.
context.addInvocationAttribute("command_line_args", config.getCommandLine());
}
if (config.getCommandOptions().getShardCount() != null) {
context.addInvocationAttribute("shard_count",
config.getCommandOptions().getShardCount().toString());
}
if (config.getCommandOptions().getShardIndex() != null) {
context.addInvocationAttribute("shard_index",
config.getCommandOptions().getShardIndex().toString());
}
context.setTestTag(getTestTag(config));
}
/**
* Helper to create the test tag from the configuration.
*/
private String getTestTag(IConfiguration config) {
String testTag = config.getCommandOptions().getTestTag();
if (config.getCommandOptions().getTestTagSuffix() != null) {
testTag = String.format("%s-%s", testTag,
config.getCommandOptions().getTestTagSuffix());
}
return testTag;
}
/**
* Updates the {@link IConfiguration} to run a single shard if a shard index is set.
*
* @see IStrictShardableTest
*
* @param config the {@link IConfiguration}.
*/
private void updateConfigIfSharded(IConfiguration config) {
if (config.getCommandOptions().getShardIndex() == null) {
return;
}
int shardCount = config.getCommandOptions().getShardCount();
int shardIndex = config.getCommandOptions().getShardIndex();
Listtrue
if invocation was resumed successfully
*/
private boolean resume(IConfiguration config, IInvocationContext context,
IRescheduler rescheduler, long elapsedTime) {
for (IRemoteTest test : config.getTests()) {
if (test instanceof IResumableTest) {
IResumableTest resumeTest = (IResumableTest)test;
if (resumeTest.isResumable()) {
// resume this config if any test is resumable
IConfiguration resumeConfig = config.clone();
// reuse the same build for the resumed invocation
ShardHelper.cloneBuildInfos(resumeConfig, resumeConfig, context);
// create a result forwarder, to prevent sending two invocationStarted events
resumeConfig.setTestInvocationListener(new ResumeResultForwarder(
config.getTestInvocationListeners(), elapsedTime));
resumeConfig.setLogOutput(config.getLogOutput().clone());
resumeConfig.setCommandOptions(config.getCommandOptions().clone());
boolean canReschedule = rescheduler.scheduleConfig(resumeConfig);
if (!canReschedule) {
CLog.i("Cannot reschedule resumed config for build. Cleaning up build.");
for (String deviceName : context.getDeviceConfigNames()) {
resumeConfig.getDeviceConfigByName(deviceName).getBuildProvider()
.cleanUp(context.getBuildInfo(deviceName));
}
}
// FIXME: is it a bug to return from here, when we may not have completed the
// FIXME: config.getTests iteration?
return canReschedule;
}
}
}
return false;
}
private void reportFailure(Throwable exception, ITestInvocationListener listener,
IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
listener.invocationFailed(exception);
if (!(exception instanceof BuildError) && !(exception.getCause() instanceof BuildError)) {
for (String deviceName : context.getDeviceConfigNames()) {
config.getDeviceConfigByName(deviceName)
.getBuildProvider()
.buildNotTested(context.getBuildInfo(deviceName));
}
rescheduleTest(config, rescheduler);
}
}
private void rescheduleTest(IConfiguration config, IRescheduler rescheduler) {
for (IRemoteTest test : config.getTests()) {
if (!config.getCommandOptions().isLoopMode() && test instanceof IRetriableTest &&
((IRetriableTest) test).isRetriable()) {
rescheduler.rescheduleCommand();
return;
}
}
}
private void reportLogs(ITestDevice device, ITestInvocationListener listener, Stage stage) {
InputStreamSource logcatSource = null;
InputStreamSource emulatorOutput = null;
try {
// only get logcat if we have an actual device available to avoid empty logs.
if (device != null && !(device.getIDevice() instanceof StubDevice)) {
logcatSource = device.getLogcat();
device.clearLogcat();
if (device.getIDevice() != null && device.getIDevice().isEmulator()) {
emulatorOutput = device.getEmulatorOutput();
// TODO: Clear the emulator log
}
}
if (logcatSource != null) {
String name = getDeviceLogName(stage);
listener.testLog(name, LogDataType.LOGCAT, logcatSource);
}
if (emulatorOutput != null) {
String name = getEmulatorLogName(stage);
listener.testLog(name, LogDataType.TEXT, emulatorOutput);
}
} finally {
// Clean up after our ISSen
StreamUtil.cancel(logcatSource);
StreamUtil.cancel(emulatorOutput);
}
}
private void reportHostLog(ITestInvocationListener listener, ILeveledLogOutput logger) {
InputStreamSource globalLogSource = logger.getLog();
listener.testLog(TRADEFED_LOG_NAME, LogDataType.TEXT, globalLogSource);
globalLogSource.cancel();
}
private void takeBugreport(ITestDevice device, ITestInvocationListener listener,
String bugreportName, boolean useBugreportz) {
if (device == null) {
return;
}
if (device.getIDevice() instanceof StubDevice) {
return;
}
if (useBugreportz) {
// logBugreport will report a regular bugreport if bugreportz is not supported.
device.logBugreport(String.format("%s_%s", bugreportName, device.getSerialNumber()),
listener);
} else {
InputStreamSource bugreport = device.getBugreport();
try {
if (bugreport != null) {
listener.testLog(String.format("%s_%s", bugreportName,
device.getSerialNumber()), LogDataType.BUGREPORT, bugreport);
} else {
CLog.w("Error when collecting bugreport for device '%s'",
device.getSerialNumber());
}
} finally {
StreamUtil.cancel(bugreport);
}
}
}
/**
* Gets the {@link ILogRegistry} to use.
*
* Exposed for unit testing.
*/
ILogRegistry getLogRegistry() {
return LogRegistry.getLogRegistry();
}
/**
* Utility method to fetch the default {@link IRunUtil} singleton
*
* Exposed for unit testing.
*/
IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
/**
* Runs the test.
*
* @param context the {@link IInvocationContext} to run tests on
* @param config the {@link IConfiguration} to run
* @param listener the {@link ITestInvocationListener} of test results
* @throws DeviceNotAvailableException
*/
private void runTests(IInvocationContext context, IConfiguration config,
ITestInvocationListener listener) throws DeviceNotAvailableException {
for (IRemoteTest test : config.getTests()) {
// For compatibility of those receivers, they are assumed to be single device alloc.
if (test instanceof IDeviceTest) {
((IDeviceTest)test).setDevice(context.getDevices().get(0));
}
if (test instanceof IBuildReceiver) {
((IBuildReceiver)test).setBuild(context.getBuildInfo(
context.getDevices().get(0)));
}
if (test instanceof ISystemStatusCheckerReceiver) {
((ISystemStatusCheckerReceiver) test).setSystemStatusChecker(
config.getSystemStatusCheckers());
}
// TODO: consider adding receivers for only the list of ITestDevice and IBuildInfo.
if (test instanceof IMultiDeviceTest) {
((IMultiDeviceTest)test).setDeviceInfos(context.getDeviceBuildMap());
}
if (test instanceof IInvocationContextReceiver) {
((IInvocationContextReceiver)test).setInvocationContext(context);
}
test.run(listener);
}
}
@Override
public String toString() {
return mStatus;
}
private void logDeviceBatteryLevel(IInvocationContext context, String event) {
for (ITestDevice testDevice : context.getDevices()) {
if (testDevice == null) {
return;
}
IDevice device = testDevice.getIDevice();
if (device == null) {
return;
}
try {
CLog.v("%s - %s - %d%%", BATT_TAG, event,
device.getBattery(500, TimeUnit.MILLISECONDS).get());
return;
} catch (InterruptedException | ExecutionException e) {
// fall through
}
CLog.v("Failed to get battery level");
}
}
/**
* {@inheritDoc}
*/
@Override
public void invoke(
IInvocationContext context, IConfiguration config, IRescheduler rescheduler,
ITestInvocationListener... extraListeners)
throws DeviceNotAvailableException, Throwable {
List