blob: 5f0c31434a3104440eba894f30db409683f195a9 [file] [log] [blame]
/*
* Copyright (C) 2017 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.shard;
import com.android.annotations.VisibleForTesting;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Configuration;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.cloud.NestedRemoteDevice;
import com.android.tradefed.device.metric.IMetricCollector;
import com.android.tradefed.device.metric.IMetricCollectorReceiver;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.invoker.shard.token.ITokenProvider;
import com.android.tradefed.invoker.shard.token.ITokenRequest;
import com.android.tradefed.invoker.shard.token.TokenProperty;
import com.android.tradefed.invoker.shard.token.TokenProviderHelper;
import com.android.tradefed.log.ILogRegistry;
import com.android.tradefed.log.ILogRegistry.EventType;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogRegistry;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.suite.checker.ISystemStatusChecker;
import com.android.tradefed.suite.checker.ISystemStatusCheckerReceiver;
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.IReportNotExecuted;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.TimeUtil;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
/**
* Tests wrapper that allow to execute all the tests of a pool of tests. Tests can be shared by
* another {@link TestsPoolPoller} so synchronization is required.
*
* <p>TODO: Add handling for token module/tests.
*/
public final class TestsPoolPoller
implements IRemoteTest,
IConfigurationReceiver,
IMultiDeviceTest,
ISystemStatusCheckerReceiver,
IMetricCollectorReceiver {
private static final long WAIT_RECOVERY_TIME = 15 * 60 * 1000;
private Collection<IRemoteTest> mGenericPool;
private Collection<ITokenRequest> mTokenPool;
private CountDownLatch mTracker;
private Set<ITokenRequest> mRejectedToken;
private TestInformation mTestInfo;
private Map<ITestDevice, IBuildInfo> mDeviceInfos;
private IConfiguration mConfig;
private List<ISystemStatusChecker> mSystemStatusCheckers;
private List<IMetricCollector> mCollectors;
private ILogRegistry mRegistry = null;
/**
* Ctor where the pool of {@link IRemoteTest} is provided.
*
* @param tests {@link IRemoteTest}s pool of all tests.
* @param tracker a {@link CountDownLatch} shared to get the number of running poller.
*/
public TestsPoolPoller(Collection<IRemoteTest> tests, CountDownLatch tracker) {
mGenericPool = tests;
mTracker = tracker;
}
public TestsPoolPoller(
Collection<IRemoteTest> tests,
Collection<ITokenRequest> tokenTests,
CountDownLatch tracker) {
this(tests, tracker);
mTokenPool = tokenTests;
mRejectedToken = Sets.newConcurrentHashSet();
}
/** Returns the first {@link IRemoteTest} from the pool or null if none remaining. */
IRemoteTest poll() {
return poll(false);
}
/** Returns the first {@link IRemoteTest} from the pool or null if none remaining. */
private IRemoteTest poll(boolean reportNotExecuted) {
if (mTokenPool != null) {
synchronized (mTokenPool) {
if (!mTokenPool.isEmpty()) {
Iterator<ITokenRequest> itr = mTokenPool.iterator();
while (itr.hasNext()) {
ITokenRequest test = itr.next();
if (reportNotExecuted) {
// Return to report not executed tests, regardless of if they can
// actually execute or not.
mRejectedToken.remove(test);
mTokenPool.remove(test);
return test;
}
if (mRejectedToken.contains(test)) {
// If the poller already rejected the tests once, do not re-evaluate.
continue;
}
Set<TokenProperty> tokens = test.getRequiredTokens();
if (tokens == null || tokens.isEmpty() || isSupported(tokens)) {
// No Token can run anywhere, or supported can run
mTokenPool.remove(test);
mRejectedToken.remove(test);
return test;
}
// Track as rejected
mRejectedToken.add(test);
}
}
}
}
synchronized (mGenericPool) {
if (mGenericPool.isEmpty()) {
return null;
}
IRemoteTest test = mGenericPool.iterator().next();
mGenericPool.remove(test);
return test;
}
}
private ITokenRequest pollRejectedTokenModule() {
if (mTokenPool == null) {
return null;
}
synchronized (mTokenPool) {
if (mRejectedToken.isEmpty()) {
return null;
}
ITokenRequest test = mRejectedToken.iterator().next();
mRejectedToken.remove(test);
mTokenPool.remove(test);
return test;
}
}
/** {@inheritDoc} */
@Override
public void run(TestInformation info, ITestInvocationListener listener)
throws DeviceNotAvailableException {
mTestInfo = info;
try {
ITestInvocationListener listenerWithCollectors = listener;
for (IMetricCollector collector : mCollectors) {
listenerWithCollectors = collector.init(info.getContext(), listenerWithCollectors);
}
while (true) {
IRemoteTest test = poll();
if (test == null) {
return;
}
if (test instanceof IBuildReceiver) {
((IBuildReceiver) test).setBuild(info.getBuildInfo());
}
if (test instanceof IDeviceTest) {
((IDeviceTest) test).setDevice(info.getDevice());
}
if (test instanceof IInvocationContextReceiver) {
((IInvocationContextReceiver) test).setInvocationContext(info.getContext());
}
if (test instanceof IMultiDeviceTest) {
((IMultiDeviceTest) test).setDeviceInfos(mDeviceInfos);
}
if (test instanceof ISystemStatusCheckerReceiver) {
((ISystemStatusCheckerReceiver) test)
.setSystemStatusChecker(mSystemStatusCheckers);
}
IConfiguration validationConfig = new Configuration("validation", "validation");
try {
// At this point only the <test> object needs to be validated for options, this
// ensures that the object is fine before running it.
validationConfig.setTest(test);
validationConfig.validateOptions();
validationConfig.resolveDynamicOptions();
// Set the configuration after the validation, otherwise we override the config
// available to the test.
if (test instanceof IConfigurationReceiver) {
((IConfigurationReceiver) test).setConfiguration(mConfig);
}
// Run the test itself and prevent random exception from stopping the poller.
if (test instanceof IMetricCollectorReceiver) {
((IMetricCollectorReceiver) test).setMetricCollectors(mCollectors);
// If test can receive collectors then let it handle the how to set them up
test.run(info, listener);
} else {
test.run(info, listenerWithCollectors);
}
} catch (RuntimeException e) {
CLog.e(
"Caught an Exception in a test: %s. Proceeding to next test.",
test.getClass());
CLog.e(e);
} catch (DeviceUnresponsiveException due) {
// being able to catch a DeviceUnresponsiveException here implies that recovery
// was successful, and test execution should proceed to next test.
CLog.w(
"Ignored DeviceUnresponsiveException because recovery was "
+ "successful, proceeding with next test. Stack trace:");
CLog.w(due);
CLog.w("Proceeding to the next test.");
} catch (DeviceNotAvailableException dnae) {
HandleDeviceNotAvailable(listener, dnae, test);
} catch (ConfigurationException | BuildRetrievalError e) {
CLog.w(
"Failed to validate the @options of test: %s. Proceeding to next test.",
test.getClass());
CLog.w(e);
} finally {
validationConfig.cleanConfigurationData();
}
}
} finally {
mTracker.countDown();
if (mTracker.getCount() == 0) {
// If the last poller is also disconnected we want to know about the tests that
// did not execute.
reportNotExecuted(listener);
}
}
}
/**
* Helper to wait for the device to maybe come back online, in that case we reboot it to refresh
* the state and proceed with execution.
*/
void HandleDeviceNotAvailable(
ITestLogger logger, DeviceNotAvailableException originalException, IRemoteTest test)
throws DeviceNotAvailableException {
ITestDevice device = mTestInfo.getDevice();
try {
if (device instanceof NestedRemoteDevice) {
// If it's not the last device, reset it.
if (((NestedRemoteDevice) device)
.resetVirtualDevice(
logger, mTestInfo.getBuildInfo(), /* Collect the logs */ true)) {
CLog.d("Successful virtual device reset.");
return;
}
// Original exception will be thrown below
CLog.e("Virtual device %s reset failed.", device.getSerialNumber());
} else if (mTracker.getCount() > 1) {
CLog.d(
"Wait %s for device to maybe come back online.",
TimeUtil.formatElapsedTime(WAIT_RECOVERY_TIME));
device.waitForDeviceAvailable(WAIT_RECOVERY_TIME);
device.reboot();
CLog.d("TestPoller was recovered after %s went offline", device.getSerialNumber());
return;
}
} catch (DeviceNotAvailableException e) {
// ignore this exception
}
// We catch and rethrow in order to log that the poller associated with the device
// that went offline is terminating.
CLog.e(
"Test %s threw DeviceNotAvailableException. Test poller associated with "
+ "device %s is terminating.",
test.getClass(), device.getSerialNumber());
// Log an event to track more easily the failure
logDeviceEvent(
EventType.SHARD_POLLER_EARLY_TERMINATION,
device.getSerialNumber(),
originalException);
throw originalException;
}
/** Go through the remaining IRemoteTest and report them as not executed. */
private void reportNotExecuted(ITestInvocationListener listener) {
// Report non-executed token test first
ITokenRequest tokenTest = pollRejectedTokenModule();
while (tokenTest != null) {
if (tokenTest instanceof IReportNotExecuted) {
String message =
String.format(
"Test did not run. No token '%s' matching it on any device.",
tokenTest.getRequiredTokens());
((IReportNotExecuted) tokenTest).reportNotExecuted(listener, message);
} else {
CLog.e(
"Could not report not executed tests from %s.",
tokenTest.getClass().getCanonicalName());
}
tokenTest = pollRejectedTokenModule();
}
// Report all remaining test
IRemoteTest test = poll(true);
while (test != null) {
if (test instanceof IReportNotExecuted) {
((IReportNotExecuted) test).reportNotExecuted(listener);
} else {
CLog.e(
"Could not report not executed tests from %s.",
test.getClass().getCanonicalName());
}
test = poll(true);
}
}
/** Helper to log the device events. */
private void logDeviceEvent(EventType event, String serial, Throwable t) {
Map<String, String> args = new HashMap<>();
args.put("serial", serial);
args.put("trace", StreamUtil.getStackTrace(t));
getLogRegistry().logEvent(LogLevel.DEBUG, event, args);
}
private ILogRegistry getLogRegistry() {
if (mRegistry != null) {
return mRegistry;
}
return LogRegistry.getLogRegistry();
}
private boolean isSupported(Set<TokenProperty> requiredTokens) {
for (TokenProperty prop : requiredTokens) {
ITokenProvider provider = TokenProviderHelper.getTokenProvider(prop);
if (provider == null) {
CLog.e("No provider for token %s", prop);
return false;
}
if (!provider.hasToken(mTestInfo.getDevice(), prop)) {
return false;
}
}
return true;
}
@VisibleForTesting
public void setLogRegistry(ILogRegistry registry) {
mRegistry = registry;
}
@Override
public void setDeviceInfos(Map<ITestDevice, IBuildInfo> deviceInfos) {
mDeviceInfos = deviceInfos;
}
@Override
public void setConfiguration(IConfiguration configuration) {
mConfig = configuration;
}
@Override
public void setSystemStatusChecker(List<ISystemStatusChecker> systemCheckers) {
mSystemStatusCheckers = systemCheckers;
}
@Override
public void setMetricCollectors(List<IMetricCollector> collectors) {
mCollectors = collectors;
}
/** Get a copy of the pool of token tests. For testing only. */
@VisibleForTesting
List<ITokenRequest> getTokenPool() {
if (mTokenPool == null) {
return null;
}
synchronized (mTokenPool) {
return new ArrayList<>(mTokenPool);
}
}
}