/*
* 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.Log.LogLevel;
import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.ExistingBuildProvider;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.IDeviceBuildProvider;
import com.android.tradefed.build.IDeviceConfigBuildProvider;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.ITestDevice;
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.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.InvocationSummaryHelper;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.targetprep.BuildError;
import com.android.tradefed.targetprep.ITargetCleaner;
import com.android.tradefed.targetprep.ITargetPreparer;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.IResumableTest;
import com.android.tradefed.testtype.IRetriableTest;
import com.android.tradefed.testtype.IShardableTest;
import junit.framework.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Default implementation of {@link ITestInvocation}.
* <p/>
* 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";
public static final String DEVICE_LOG_NAME = "device_logcat";
static final String BUILD_ERROR_BUGREPORT_NAME = "build_error_bugreport";
static final String DEVICE_UNRESPONSIVE_BUGREPORT_NAME = "device_unresponsive_bugreport";
private String mStatus = "(not invoked)";
private static boolean isRepeat = false;
/**
* A {@link ResultForwarder} for forwarding resumed invocations.
* <p/>
* 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(List<ITestInvocationListener> listeners, long currentElapsedTime) {
super(listeners);
mCurrentElapsedTime = currentElapsedTime;
}
@Override
public void invocationStarted(IBuildInfo buildInfo) {
// ignore
}
@Override
public void invocationEnded(long newElapsedTime) {
super.invocationEnded(mCurrentElapsedTime + newElapsedTime);
}
}
/**
* {@inheritDoc}
*/
@Override
public void invoke(ITestDevice device, IConfiguration config, IRescheduler rescheduler) throws DeviceNotAvailableException, Throwable {
try {
mStatus = "fetching build";
config.getLogOutput().init();
getLogRegistry().registerLogger(config.getLogOutput());
IBuildInfo info = null;
if (config.getBuildProvider() instanceof IDeviceBuildProvider) {
info = ((IDeviceBuildProvider) config.getBuildProvider()).getBuild(device);
} else if (config.getBuildProvider() instanceof IDeviceConfigBuildProvider) {
// 调用config获得cts.xml文件中的<build_provider>标签中对应的类,然后通过调用getBuild得到IBuildInfo对象
info = ((IDeviceConfigBuildProvider) config.getBuildProvider()).getBuild(device, config);
} else {
info = config.getBuildProvider().getBuild();
}
if (info != null) {
// System.out.println(String.format("setup: %s tearDown: %s",
// config.getCommandOptions().isNeedPrepare(),
// config.getCommandOptions().isNeedTearDown()));
CLog.logAndDisplay(LogLevel.INFO,
String.format("setup: %s tearDown: %s", config.getCommandOptions().isNeedPrepare(), config.getCommandOptions().isNeedTearDown()));
// 获取<test>配置项里的测试选项,并注入到info中
injectBuild(info, config.getTests());
if (shardConfig(config, info, rescheduler)) {
CLog.i("Invocation for %s has been sharded, rescheduling", device.getSerialNumber());
} else {
device.setRecovery(config.getDeviceRecovery());
// 准备刷机,启动case
performInvocation(config, device, info, rescheduler);
// exit here, depend on performInvocation to deregister
// logger
return;
}
} else {
mStatus = "(no build to test)";
CLog.d("No build to test");
rescheduleTest(config, rescheduler);
}
} catch (BuildRetrievalError e) {
CLog.e(e);
/*
* because this is BuildRetrievalError, so do not generate test
* result // report an empty invocation, so this error is sent to
* listeners startInvocation(config, device, e.getBuildInfo()); //
* don't want to use #reportFailure, since that will call
* buildNotTested for (ITestInvocationListener listener :
* config.getTestInvocationListeners()) {
* listener.invocationFailed(e); } reportLogs(device,
* config.getTestInvocationListeners(), config.getLogOutput());
* InvocationSummaryHelper.reportInvocationEnded(
* config.getTestInvocationListeners(), 0); return;
*/
} catch (IOException e) {
CLog.e(e);
}
// save current log contents to global log
getLogRegistry().dumpToGlobalLog(config.getLogOutput());
getLogRegistry().unregisterLogger();
config.getLogOutput().closeLog();
}
/**
* Pass the build to any {@link IBuildReceiver} tests
*
* @param buildInfo
* @param tests
*/
private void injectBuild(IBuildInfo buildInfo, List<IRemoteTest> tests) {
for (IRemoteTest test : tests) {
if (test instanceof IBuildReceiver) {
((IBuildReceiver) test).setBuild(buildInfo);
}
}
}
/**
* Attempt to shard the configuration into sub-configurations, to be
* re-scheduled to run on multiple resources in parallel.
* <p/>
* A successful shard action renders the current config empty, and
* invocation should not proceed.
*
* @see IShardableTest
* @see IRescheduler
*
* @param config
* the current {@link IConfiguration}.
* @param info
* the {@link IBuildInfo} to test
* @param rescheduler
* the {@link IRescheduler}
* @return true if test was sharded. Otherwise return <code>false</code>
*/
private boolean shardConfig(IConfiguration config, IBuildInfo info, IRescheduler rescheduler) {
mStatus = "sharding";
List<IRemoteTest> shardableTests = new ArrayList<IRemoteTest>();
boolean isSharded = false;
for (IRemoteTest test : config.getTests()) {
isSharded |= shardTest(shardableTests, test);
}
if (isSharded) {
ShardMasterResultForwarder resultCollector = new ShardMasterResultForwarder(config.getTestInvocationListeners(), shardableTests.size());
ShardListener origConfigListener = new ShardListener(resultCollector);
config.setTestInvocationListener(origConfigListener);
// report invocation started using original buildinfo
resultCollector.invocationStarted(info);
for (IRemoteTest testShard : shardableTests) {
CLog.i("Rescheduling sharded config...");
IConfiguration shardConfig = config.clone();
shardConfig.setTest(testShard);
shardConfig.setBuildProvider(new ExistingBuildProvider(info.clone(), config.getBuildProvider()));
shardConfig.setTestInvocationListener(new ShardListener(resultCollector));
shardConfig.setLogOutput(config.getLogOutput().clone());
shardConfig.setCommandOptions(config.getCommandOptions().clone());
// use the same {@link ITargetPreparer}, {@link IDeviceRecovery}
// etc as original
// config
rescheduler.scheduleConfig(shardConfig);
}
// clean up original build
config.getBuildProvider().cleanUp(info);
return true;
}
return false;
}
/**
* Attempt to shard given {@link IRemoteTest}.
*
* @param shardableTests
* the list of {@link IRemoteTest}s to add to
* @param test
* the {@link Test} to shard
* @return <code>true</code> if test was sharded
*/
private boolean shardTest(List<IRemoteTest> shardableTests, IRemoteTest test) {
boolean isSharded = false;
if (test instanceof IShardableTest) {
IShardableTest shardableTest = (IShardableTest) test;
Collection<IRemoteTest> shards = shardableTest.split();
if (shards != null) {
shardableTests.addAll(shards);
isSharded = true;
}
}
if (!isSharded) {
shardableTests.add(test);
}
return isSharded;
}
/**
* Display a log message informing the user of a invocation being started.
*
* @param info
* the {@link IBuildInfo}
* @param device
* the {@link ITestDevice}
*/
private void logStartInvocation(IBuildInfo info, ITestDevice device) {
StringBuilder msg = new StringBuilder("Starting invocation for '");
msg.append(info.getTestTag());
msg.append("'");
if (!info.getBuildId().equals(IBuildInfo.UNKNOWN_BUILD_ID)) {
msg.append(" on build ");
msg.append(getBuildDescription(info));
}
for (String buildAttr : info.getBuildAttributes().values()) {
msg.append(" ");
msg.append(buildAttr);
}
msg.append(" on device ");
msg.append(device.getSerialNumber());
CLog.logAndDisplay(LogLevel.INFO, msg.toString());
mStatus = String.format("running %s on build %s", info.getTestTag(), getBuildDescription(info));
}
/**
* Returns a user-friendly description of the build
*
* @param info
* @return
*/
private String getBuildDescription(IBuildInfo info) {
return String.format("'%s'", buildSpacedString(info.getBuildBranch(), info.getBuildFlavor(), info.getBuildId()));
}
/**
* Helper method for adding space delimited sequence of strings. Will ignore
* null segments
*/
private String buildSpacedString(String... segments) {
StringBuilder sb = new StringBuilder();
for (String s : segments) {
if (s != null) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(s);
}
}
return sb.toString();
}
/**
* Performs the invocation
*
* @param config
* the {@link IConfiguration}
* @param device
* the {@link ITestDevice} to use. May be <code>null</code>
* @param info
* the {@link IBuildInfo}
*/
private void performInvocation(IConfiguration config, ITestDevice device, IBuildInfo info, IRescheduler rescheduler) throws Throwable {
boolean resumed = false;
long startTime = System.currentTimeMillis();
long elapsedTime = -1;
info.setDeviceSerial(device.getSerialNumber());
startInvocation(config, device, info);
try {
device.setOptions(config.getDeviceOptions());
// 准备build和跑case
prepareAndRun(config, device, info, rescheduler);
} catch (BuildError e) {
CLog.w("Build %s failed on device %s. Reason: %s", info.getBuildId(), device.getSerialNumber(), e.toString());
takeBugreport(device, config.getTestInvocationListeners(), BUILD_ERROR_BUGREPORT_NAME);
reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
} catch (TargetSetupError e) {
CLog.e("Caught exception while running invocation");
CLog.e(e);
reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
// maybe test device if offline, check it
device.waitForDeviceOnline();
} catch (DeviceNotAvailableException e) {
// log a warning here so its captured before reportLogs is called
CLog.w("Invocation did not complete due to device %s becoming not available. " + "Reason: %s", device.getSerialNumber(), e.getMessage());
if ((e instanceof DeviceUnresponsiveException) && TestDeviceState.ONLINE.equals(device.getDeviceState())) {
// under certain cases it might still be possible to grab a
// bugreport
takeBugreport(device, config.getTestInvocationListeners(), DEVICE_UNRESPONSIVE_BUGREPORT_NAME);
}
resumed = resume(config, info, rescheduler, System.currentTimeMillis() - startTime);
if (!resumed) {
reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
} else {
CLog.i("Rescheduled failed invocation for resume");
}
throw e;
} catch (RuntimeException e) {
// log a warning here so its captured before reportLogs is called
CLog.w("Unexpected exception when running invocation: %s", e.toString());
reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
throw e;
} catch (AssertionError e) {
CLog.w("Caught AssertionError while running invocation: ", e.toString());
reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
} finally {
mStatus = "done running tests";
try {
//
reportLogs(device, config.getTestInvocationListeners(), config.getLogOutput());
elapsedTime = System.currentTimeMillis() - startTime;
if (!resumed) {
// 发送报告
InvocationSummaryHelper.reportInvocationEnded(config.getTestInvocationListeners(), elapsedTime);
}
} finally {
config.getBuildProvider().cleanUp(info);
}
}
}
/**
* Do setup, run the tests, then call tearDown
*/
private void prepareAndRun(IConfiguration config, ITestDevice device, IBuildInfo info, IRescheduler rescheduler) throws Throwable {
// use the JUnit3 logic for handling exceptions when running tests
Throwable exception = null;
try {
//
if (config.getCommandOptions().isNeedPrepare()&&!isRepeat) {
doSetup(config, device, info);
//下次启动的时候,不再刷机
isRepeat = true;
}else{
CLog.logAndDisplay(LogLevel.DEBUG, String.format("No need to flash,derect to run case"));
}
// 跑case
runTests(device, info, config, rescheduler);
} catch (Throwable running) {
exception = running;
} finally {
try {
if (config.getCommandOptions().isNeedTearDown()) {
doTeardown(config, device, info, exception);
}
} catch (Throwable tearingDown) {
if (exception == null) {
exception = tearingDown;
}
}
}
if (exception != null) {
throw exception;
}
}
private void doSetup(IConfiguration config, ITestDevice device, IBuildInfo info) throws TargetSetupError, BuildError, DeviceNotAvailableException {
for (ITargetPreparer preparer : config.getTargetPreparers()) {
preparer.setUp(device, info);
}
}
private void doTeardown(IConfiguration config, ITestDevice device, IBuildInfo info, Throwable exception) throws DeviceNotAvailableException {
for (ITargetPreparer preparer : config.getTargetPreparers()) {
// Note: adjusted indentation below for legibility. If preparer is
// an
// ITargetCleaner and we didn't hit DeviceNotAvailableException,
// then...
if (preparer instanceof ITargetCleaner && !(exception != null && exception instanceof DeviceNotAvailableException)) {
ITargetCleaner cleaner = (ITargetCleaner) preparer;
cleaner.tearDown(device, info, exception);
}
}
}
/**
* Starts the invocation.
* <p/>
* Starts logging, and informs listeners that invocation has been started.
*
* @param config
* @param device
* @param info
*/
private void startInvocation(IConfiguration config, ITestDevice device, IBuildInfo info) {
logStartInvocation(info, device);
for (ITestInvocationListener listener : config.getTestInvocationListeners()) {
try {
listener.invocationStarted(info);
} catch (RuntimeException e) {
// don't let one listener leave the invocation in a bad state
CLog.e("Caught runtime exception from ITestInvocationListener");
CLog.e(e);
}
}
}
/**
* Attempt to reschedule the failed invocation to resume where it left off.
* <p/>
*
* @see IResumableTest
*
* @param config
* @return <code>true</code> if invocation was resumed successfully
*/
private boolean resume(IConfiguration config, IBuildInfo info, 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
IBuildInfo clonedBuild = info.clone();
resumeConfig.setBuildProvider(new ExistingBuildProvider(clonedBuild, config.getBuildProvider()));
// 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 %s. Cleaning up build.", info.getBuildId());
resumeConfig.getBuildProvider().cleanUp(clonedBuild);
}
// 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, List<ITestInvocationListener> listeners, IConfiguration config, IBuildInfo info, IRescheduler rescheduler) {
for (ITestInvocationListener listener : listeners) {
listener.invocationFailed(exception);
}
if (!(exception instanceof BuildError)) {
config.getBuildProvider().buildNotTested(info);
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, List<ITestInvocationListener> listeners, ILeveledLogOutput logger) {
InputStreamSource logcatSource = null;
InputStreamSource globalLogSource = logger.getLog();
if (device != null) {
logcatSource = device.getLogcat();
}
for (ITestInvocationListener listener : listeners) {
if (logcatSource != null) {
listener.testLog(DEVICE_LOG_NAME, LogDataType.TEXT, logcatSource);
}
listener.testLog(TRADEFED_LOG_NAME, LogDataType.TEXT, globalLogSource);
}
// Clean up after our ISSen
if (logcatSource != null) {
logcatSource.cancel();
}
globalLogSource.cancel();
// once tradefed log is reported, all further log calls for this
// invocation can get lost
// unregister logger so future log calls get directed to the tradefed
// global log
getLogRegistry().dumpToGlobalLog(logger);
getLogRegistry().unregisterLogger();
logger.closeLog();
}
private void takeBugreport(ITestDevice device, List<ITestInvocationListener> listeners, String bugreportName) {
if (device == null) {
return;
}
InputStreamSource bugreport = device.getBugreport();
try {
for (ITestInvocationListener listener : listeners) {
listener.testLog(bugreportName, LogDataType.TEXT, bugreport);
}
} finally {
bugreport.cancel();
}
}
/**
* Gets the {@link ILogRegistry} to use.
* <p/>
* Exposed for unit testing.
*/
ILogRegistry getLogRegistry() {
return LogRegistry.getLogRegistry();
}
/**
* Runs the test.
*
* @param device
* the {@link ITestDevice} to run tests on
* @param buildInfo
* the {@link BuildInfo} describing the build target
* @param config
* the {@link IConfiguration} to run
* @param rescheduler
* the {@link IRescheduler} used to reschedule the test.
* @throws DeviceNotAvailableException
*/
private void runTests(ITestDevice device, IBuildInfo buildInfo, IConfiguration config, IRescheduler rescheduler) throws DeviceNotAvailableException {
List<ITestInvocationListener> listeners = config.getTestInvocationListeners();
for (IRemoteTest test : config.getTests()) {
if (test instanceof IDeviceTest) {
((IDeviceTest) test).setDevice(device);
}
test.run(new ResultForwarder(listeners));
}
}
@Override
public String toString() {
return mStatus;
}
}