Snap for 6948038 from 7bb00bb3a1d32630492cdd4af3b203aa755d7c27 to rvc-platform-release
Change-Id: Idb15b962a2932562c43d0cfc08f3e924c9a38aae
diff --git a/device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java b/device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java
index 0e6c86e..8f432fc 100644
--- a/device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java
+++ b/device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java
@@ -196,4 +196,9 @@
*/
public void setDefaultAvailableTimeout(long timeoutMs);
+ /** Sets the fastboot mode serial number. */
+ public void setFastbootSerialNumber(String serial);
+
+ /** Gets the fastboot mode serial number. */
+ public String getFastbootSerialNumber();
}
diff --git a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
index 3ceb944..b3fdbbb 100644
--- a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
@@ -82,6 +82,9 @@
*/
public String getSerialNumber();
+ /** Returns the fastboot mode serial number. */
+ public String getFastbootSerialNumber();
+
/**
* Retrieve the given property value from the device.
*
diff --git a/global_configuration/com/android/tradefed/config/GlobalConfiguration.java b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
index 9d6c7a9..4e09a9b 100644
--- a/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
@@ -94,7 +94,7 @@
// Configurations to be passed to subprocess: Typical object that are representing the host
// level and the subprocess should follow too.
- private static final String[] CONFIGS_FOR_SUBPROCESS_WHITE_LIST =
+ private static final String[] CONFIGS_FOR_SUBPROCESS_ALLOW_LIST =
new String[] {
DEVICE_MANAGER_TYPE_NAME,
KEY_STORE_TYPE_NAME,
@@ -777,13 +777,13 @@
/** {@inheritDoc} */
@Override
- public File cloneConfigWithFilter(String... whitelistConfigs) throws IOException {
- return cloneConfigWithFilter(new HashSet<>(), whitelistConfigs);
+ public File cloneConfigWithFilter(String... allowlistConfigs) throws IOException {
+ return cloneConfigWithFilter(new HashSet<>(), allowlistConfigs);
}
/** {@inheritDoc} */
@Override
- public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... whitelistConfigs)
+ public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... allowlistConfigs)
throws IOException {
IConfigurationFactory configFactory = getConfigurationFactory();
IGlobalConfiguration copy = null;
@@ -799,10 +799,10 @@
File filteredGlobalConfig = FileUtil.createTempFile("filtered_global_config", ".config");
KXmlSerializer serializer = ConfigurationUtil.createSerializer(filteredGlobalConfig);
serializer.startTag(null, ConfigurationUtil.CONFIGURATION_NAME);
- if (whitelistConfigs == null || whitelistConfigs.length == 0) {
- whitelistConfigs = CONFIGS_FOR_SUBPROCESS_WHITE_LIST;
+ if (allowlistConfigs == null || allowlistConfigs.length == 0) {
+ allowlistConfigs = CONFIGS_FOR_SUBPROCESS_ALLOW_LIST;
}
- for (String config : whitelistConfigs) {
+ for (String config : allowlistConfigs) {
Object configObj = copy.getConfigurationObject(config);
if (configObj == null) {
CLog.d("Object '%s' was not found in global config.", config);
diff --git a/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
index 78096da..1107f5f 100644
--- a/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
@@ -295,7 +295,7 @@
public void validateOptions() throws ConfigurationException;
/**
- * Filter the GlobalConfiguration based on a white list and output to an XML file.
+ * Filter the GlobalConfiguration based on a allowed list and output to an XML file.
*
* <p>For example, for following configuration:
* {@code
@@ -318,24 +318,24 @@
* </xml>
* }
*
- * @param whitelistConfigs a {@link String} array of configs to be included in the new XML file.
+ * @param allowlistConfigs a {@link String} array of configs to be included in the new XML file.
* If it's set to <code>null<code/>, a default list should be used.
* @return the File containing the new filtered global config.
* @throws IOException
*/
- public File cloneConfigWithFilter(String... whitelistConfigs) throws IOException;
+ public File cloneConfigWithFilter(String... allowlistConfigs) throws IOException;
/**
* Filter the GlobalConfiguration based on a white list and output to an XML file.
* @see #cloneConfigWithFilter(String...)
*
* @param exclusionPatterns The pattern of class name to exclude from the dump.
- * @param whitelistConfigs a {@link String} array of configs to be included in the new XML file.
+ * @param allowlistConfigs a {@link String} array of configs to be included in the new XML file.
* If it's set to <code>null<code/>, a default list should be used.
* @return the File containing the new filtered global config.
* @throws IOException
*/
- public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... whitelistConfigs)
+ public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... allowlistConfigs)
throws IOException;
/**
diff --git a/global_configuration/com/android/tradefed/host/HostOptions.java b/global_configuration/com/android/tradefed/host/HostOptions.java
index 68aa51f..f280130 100644
--- a/global_configuration/com/android/tradefed/host/HostOptions.java
+++ b/global_configuration/com/android/tradefed/host/HostOptions.java
@@ -19,9 +19,14 @@
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.log.LogUtil.CLog;
import java.io.File;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
import java.util.ArrayList;
+import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -96,6 +101,11 @@
description = "Whether to use zip64 format in partial download.")
private boolean mUseZip64InPartialDownload = false;
+ @Option(
+ name = "use-network-interface",
+ description = "The network interface used to connect to test devices.")
+ private String mNetworkInterface = null;
+
/** {@inheritDoc} */
@Override
public Integer getConcurrentFlasherLimit() {
@@ -167,4 +177,37 @@
public boolean getUseZip64InPartialDownload() {
return mUseZip64InPartialDownload;
}
+
+ /** {@inheritDoc} */
+ @Override
+ public String getNetworkInterface() {
+ if (mNetworkInterface != null) {
+ return mNetworkInterface;
+ }
+
+ try {
+ for (Enumeration<NetworkInterface> enNetI = NetworkInterface.getNetworkInterfaces();
+ enNetI.hasMoreElements(); ) {
+ NetworkInterface netI = enNetI.nextElement();
+ if (!netI.isUp()) {
+ continue;
+ }
+ for (Enumeration<InetAddress> enumIpAddr = netI.getInetAddresses();
+ enumIpAddr.hasMoreElements(); ) {
+ InetAddress inetAddress = enumIpAddr.nextElement();
+ if (!inetAddress.isAnyLocalAddress()
+ && !inetAddress.isLinkLocalAddress()
+ && !inetAddress.isLoopbackAddress()
+ && !inetAddress.isMulticastAddress()) {
+ mNetworkInterface = netI.getName();
+ return mNetworkInterface;
+ }
+ }
+ }
+ } catch (SocketException e) {
+ CLog.w("Failed to get host's active interface");
+ CLog.w(e);
+ }
+ return null;
+ }
}
diff --git a/global_configuration/com/android/tradefed/host/IHostOptions.java b/global_configuration/com/android/tradefed/host/IHostOptions.java
index 32b8a04..3426587 100644
--- a/global_configuration/com/android/tradefed/host/IHostOptions.java
+++ b/global_configuration/com/android/tradefed/host/IHostOptions.java
@@ -73,4 +73,7 @@
/** Check if it should use the zip64 format in partial download or not. */
boolean getUseZip64InPartialDownload();
+
+ /** Returns the network interface used to connect to remote test devices. */
+ String getNetworkInterface();
}
diff --git a/proto/test_record.proto b/proto/test_record.proto
index 2e558eb..22ba8e7 100644
--- a/proto/test_record.proto
+++ b/proto/test_record.proto
@@ -120,6 +120,13 @@
NOT_EXECUTED = 30;
// System under test became unavailable and never came back available again.
LOST_SYSTEM_UNDER_TEST = 35;
+ // Represent an error caused by an unmet dependency that the current infra
+ // depends on. For example: Unfound resources, Device error, Hardware issue
+ // (lab host, device wear), Underlying tools
+ DEPENDENCY_ISSUE = 40;
+ // Represent an error caused by the input from the end user. For example:
+ // Unexpected option combination, Configuration error, Bad flags
+ CUSTOMER_ISSUE = 41;
}
// A context to DebugInfo that allows to optionally specify some debugging context.
diff --git a/src/com/android/tradefed/build/BootstrapBuildProvider.java b/src/com/android/tradefed/build/BootstrapBuildProvider.java
index 33aaa86..383a30c 100644
--- a/src/com/android/tradefed/build/BootstrapBuildProvider.java
+++ b/src/com/android/tradefed/build/BootstrapBuildProvider.java
@@ -33,6 +33,9 @@
import java.io.File;
import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
/**
* A {@link IDeviceBuildProvider} that bootstraps build info from the test device
@@ -80,6 +83,13 @@
@Option(name="tests-dir", description="Path to top directory of expanded tests zip")
private File mTestsDir = null;
+ @Option(
+ name = "extra-file",
+ description =
+ "The extra file to be added to the Build Provider. "
+ + "Can be repeated. For example --extra-file file_key_1=/path/to/file")
+ private Map<String, File> mExtraFiles = new LinkedHashMap<>();
+
@Override
public IBuildInfo getBuild() throws BuildRetrievalError {
throw new UnsupportedOperationException("Call getBuild(ITestDevice)");
@@ -93,6 +103,7 @@
public IBuildInfo getBuild(ITestDevice device) throws BuildRetrievalError,
DeviceNotAvailableException {
IBuildInfo info = new DeviceBuildInfo(mBuildId, mBuildTargetName);
+ addFiles(info, mExtraFiles);
info.setProperties(BuildInfoProperties.DO_NOT_COPY_ON_SHARDING);
if (!(device.getIDevice() instanceof StubDevice)) {
if (!device.waitForDeviceShell(mShellAvailableTimeout * 1000)) {
@@ -141,6 +152,18 @@
return info;
}
+ /**
+ * Add file to build info.
+ *
+ * @param buildInfo the {@link IBuildInfo} the build info
+ * @param fileMaps the {@link Map} of file_key and file object to be added to the buildInfo
+ */
+ private void addFiles(IBuildInfo buildInfo, Map<String, File> fileMaps) {
+ for (final Entry<String, File> entry : fileMaps.entrySet()) {
+ buildInfo.setFile(entry.getKey(), entry.getValue(), "0");
+ }
+ }
+
@VisibleForTesting
ExecutionFiles getInvocationFiles() {
return CurrentInvocation.getInvocationFiles();
diff --git a/src/com/android/tradefed/build/DependenciesResolver.java b/src/com/android/tradefed/build/DependenciesResolver.java
index d724508..66ccd89 100644
--- a/src/com/android/tradefed/build/DependenciesResolver.java
+++ b/src/com/android/tradefed/build/DependenciesResolver.java
@@ -52,6 +52,16 @@
@Option(name = "dependency", description = "The set of dependency to provide for the test")
private Set<File> mDependencies = new LinkedHashSet<>();
+ // TODO(b/157936948): Remove those three options when they are no longer injected
+ @Option(name = "hostname")
+ private String mHostName = null;
+
+ @Option(name = "protocol")
+ private String mProtocol = null;
+
+ @Option(name = "use-build-api ")
+ private boolean mUseBuildApi = true;
+
private File mTestsDir;
private IInvocationContext mInvocationContext;
@@ -61,6 +71,7 @@
IDeviceBuildInfo build =
new DeviceBuildInfo(
mBuildId, String.format("%s-%s-%s", mBranch, mBuildOs, mBuildFlavor));
+ build.setBuildBranch(mBranch);
build.setBuildFlavor(mBuildFlavor);
for (File dependency : mDependencies) {
File f =
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 7c13735..708b0d2 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -28,6 +28,8 @@
import com.android.tradefed.command.remote.RemoteClient;
import com.android.tradefed.command.remote.RemoteException;
import com.android.tradefed.command.remote.RemoteManager;
+import com.android.tradefed.config.ArgsOptionParser;
+import com.android.tradefed.config.Configuration;
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
@@ -40,6 +42,7 @@
import com.android.tradefed.config.RetryConfigurationFactory;
import com.android.tradefed.config.SandboxConfigurationFactory;
import com.android.tradefed.config.proxy.ProxyConfiguration;
+import com.android.tradefed.config.proxy.TradefedDelegator;
import com.android.tradefed.device.DeviceAllocationState;
import com.android.tradefed.device.DeviceManager;
import com.android.tradefed.device.DeviceNotAvailableException;
@@ -64,6 +67,7 @@
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.ResultForwarder;
+import com.android.tradefed.result.suite.SuiteResultReporter;
import com.android.tradefed.sandbox.ISandbox;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.suite.retry.RetryRescheduler;
@@ -82,6 +86,7 @@
import com.android.tradefed.util.keystore.KeyStoreException;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.IOException;
@@ -1213,6 +1218,32 @@
}
private IConfiguration createConfiguration(String[] args) throws ConfigurationException {
+ TradefedDelegator delegator = new TradefedDelegator();
+ ArgsOptionParser argsParser = new ArgsOptionParser(delegator);
+ List<String> argsList = new ArrayList<>(Arrays.asList(args));
+ argsList.remove(0);
+ argsParser.parseBestEffort(argsList, true);
+ if (delegator.shouldUseDelegation()) {
+ String[] argsWithoutDelegation = TradefedDelegator.clearCommandline(args);
+ delegator.setCommandLine(argsWithoutDelegation);
+ CLog.d(
+ "Using commandline arguments as starting command: %s",
+ Arrays.asList(argsWithoutDelegation));
+ IConfiguration config =
+ ((ConfigurationFactory) getConfigFactory())
+ .createPartialConfigurationFromArgs(
+ argsWithoutDelegation,
+ getKeyStoreClient(),
+ ImmutableSet.of(
+ Configuration.DEVICE_REQUIREMENTS_TYPE_NAME,
+ Configuration.LOGGER_TYPE_NAME,
+ Configuration.LOG_SAVER_TYPE_NAME,
+ Configuration.RESULT_REPORTER_TYPE_NAME));
+ config.setConfigurationObject(TradefedDelegator.DELEGATE_OBJECT, delegator);
+ setDelegateLevelReporting(config);
+ return config;
+ }
+
// check if the command should be sandboxed
if (isCommandSandboxed(args)) {
// Create an sandboxed configuration based on the sandbox of the scheduler.
@@ -1236,6 +1267,20 @@
return config;
}
+ private void setDelegateLevelReporting(IConfiguration config) {
+ List<ITestInvocationListener> delegateReporters = new ArrayList<>();
+ // For debugging in the console, add a printer
+ delegateReporters.add(new SuiteResultReporter());
+ for (ITestInvocationListener listener : config.getTestInvocationListeners()) {
+ // Add infra reporter if configured.
+ if ("com.google.android.tradefed.result.teststorage.ResultReporter"
+ .equals(listener.getClass().getCanonicalName())) {
+ delegateReporters.add(listener);
+ }
+ }
+ config.setTestInvocationListeners(delegateReporters);
+ }
+
private boolean internalAddCommand(String[] args, String cmdFilePath)
throws ConfigurationException {
assertStarted();
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index b4fce56..e84fa3f 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -681,6 +681,24 @@
}
}
+ /** {@inheritDoc} */
+ @Override
+ public void safeInjectOptionValues(List<OptionDef> optionDefs) throws ConfigurationException {
+ OptionSetter optionSetter = createOptionSetter();
+ for (OptionDef optionDef : optionDefs) {
+ try {
+ internalInjectOptionValue(
+ optionSetter,
+ optionDef.name,
+ optionDef.key,
+ optionDef.value,
+ optionDef.source);
+ } catch (ConfigurationException e) {
+ // Ignoring
+ }
+ }
+ }
+
/**
* Creates a shallow copy of this object.
*/
@@ -1126,6 +1144,21 @@
}
}
+ /** {@inheritDoc} */
+ @Override
+ public List<String> setBestEffortOptionsFromCommandLineArgs(
+ List<String> listArgs, IKeyStoreClient keyStoreClient) throws ConfigurationException {
+ // We get all the objects except the one describing the Configuration itself which does not
+ // allow passing its option via command line.
+ ArgsOptionParser parser =
+ new ArgsOptionParser(
+ getAllConfigurationObjects(CONFIGURATION_DESCRIPTION_TYPE_NAME, true));
+ if (keyStoreClient != null) {
+ parser.setKeyStore(keyStoreClient);
+ }
+ return parser.parseBestEffort(listArgs, /* Force continue */ true);
+ }
+
/**
* Outputs a command line usage help text for this configuration to given
* printStream.
diff --git a/src/com/android/tradefed/config/ConfigurationDef.java b/src/com/android/tradefed/config/ConfigurationDef.java
index 2774b38..d7ec6c2 100644
--- a/src/com/android/tradefed/config/ConfigurationDef.java
+++ b/src/com/android/tradefed/config/ConfigurationDef.java
@@ -26,6 +26,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -65,6 +66,7 @@
}
private boolean mMultiDeviceMode = false;
+ private boolean mFilteredObjects = false;
private Map<String, Boolean> mExpectedDevices = new LinkedHashMap<>();
private static final Pattern MULTI_PATTERN = Pattern.compile("(.*)(:)(.*)");
public static final String DEFAULT_DEVICE_NAME = "DEFAULT_DEVICE";
@@ -183,6 +185,20 @@
* @throws ConfigurationException if configuration could not be created
*/
public IConfiguration createConfiguration() throws ConfigurationException {
+ return createConfiguration(null);
+ }
+
+ /**
+ * Creates a configuration from the info stored in this definition, and populates its fields
+ * with the provided option values.
+ *
+ * @param allowedObjects the set of TF objects that we will create out of the full configuration
+ * @return the created {@link IConfiguration}
+ * @throws ConfigurationException if configuration could not be created
+ */
+ public IConfiguration createConfiguration(Set<String> allowedObjects)
+ throws ConfigurationException {
+ mFilteredObjects = false;
IConfiguration config = new Configuration(getName(), getDescription());
List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
IDeviceConfiguration defaultDeviceConfig =
@@ -246,6 +262,11 @@
boolean shouldAddToFlatConfig = true;
for (ConfigObjectDef configDef : objClassEntry.getValue()) {
+ if (allowedObjects != null && !allowedObjects.contains(objClassEntry.getKey())) {
+ CLog.d("Skipping creation of %s", objClassEntry.getKey());
+ mFilteredObjects = true;
+ continue;
+ }
Object configObject = null;
try {
configObject = createObject(objClassEntry.getKey(), configDef.mClassName);
@@ -343,7 +364,13 @@
protected void injectOptions(IConfiguration config, List<OptionDef> optionList)
throws ConfigurationException {
- config.injectOptionValues(optionList);
+ if (mFilteredObjects) {
+ // If we filtered out some objects, some options might not be injectable anymore, so
+ // we switch to safe inject to avoid errors due to the filtering.
+ config.safeInjectOptionValues(optionList);
+ } else {
+ config.injectOptionValues(optionList);
+ }
}
/**
diff --git a/src/com/android/tradefed/config/ConfigurationFactory.java b/src/com/android/tradefed/config/ConfigurationFactory.java
index c682694..7e29821 100644
--- a/src/com/android/tradefed/config/ConfigurationFactory.java
+++ b/src/com/android/tradefed/config/ConfigurationFactory.java
@@ -422,7 +422,7 @@
break;
case ".tf_yaml":
ConfigurationYamlParser yamlParser = new ConfigurationYamlParser();
- yamlParser.parse(def, name, bufStream);
+ yamlParser.parse(def, name, bufStream, false);
break;
default:
throw new ConfigurationException(
@@ -520,11 +520,13 @@
if (arrayArgs.length == 0) {
throw new ConfigurationException("Configuration to run was not specified");
}
+
List<String> listArgs = new ArrayList<String>(arrayArgs.length);
// FIXME: Update parsing to not care about arg order.
String[] reorderedArrayArgs = reorderArgs(arrayArgs);
IConfiguration config =
- internalCreateConfigurationFromArgs(reorderedArrayArgs, listArgs, keyStoreClient);
+ internalCreateConfigurationFromArgs(
+ reorderedArrayArgs, listArgs, keyStoreClient, null);
config.setCommandLine(arrayArgs);
if (listArgs.contains("--" + CommandOptions.DRY_RUN_OPTION)
|| listArgs.contains("--" + CommandOptions.NOISY_DRY_RUN_OPTION)) {
@@ -549,23 +551,45 @@
return config;
}
+ /** {@inheritDoc} */
+ @Override
+ public IConfiguration createPartialConfigurationFromArgs(
+ String[] arrayArgs, IKeyStoreClient keyStoreClient, Set<String> allowedObjects)
+ throws ConfigurationException {
+ if (arrayArgs.length == 0) {
+ throw new ConfigurationException("Configuration to run was not specified");
+ }
+
+ List<String> listArgs = new ArrayList<String>(arrayArgs.length);
+ String[] reorderedArrayArgs = reorderArgs(arrayArgs);
+ IConfiguration config =
+ internalCreateConfigurationFromArgs(
+ reorderedArrayArgs, listArgs, keyStoreClient, allowedObjects);
+ config.setCommandLine(arrayArgs);
+ List<String> leftOver =
+ config.setBestEffortOptionsFromCommandLineArgs(listArgs, keyStoreClient);
+ CLog.d("Non-applied arguments: %s", leftOver);
+ return config;
+ }
+
/**
* Creates a {@link Configuration} from the name given in arguments.
- * <p/>
- * Note will not populate configuration with values from options
*
- * @param arrayArgs the full list of command line arguments, including the
- * config name
- * @param optionArgsRef an empty list, that will be populated with the
- * option arguments left to be interpreted
- * @param keyStoreClient {@link IKeyStoreClient} keystore client to use if
- * any.
- * @return An {@link IConfiguration} object representing the configuration
- * that was loaded
+ * <p>Note will not populate configuration with values from options
+ *
+ * @param arrayArgs the full list of command line arguments, including the config name
+ * @param optionArgsRef an empty list, that will be populated with the option arguments left to
+ * be interpreted
+ * @param keyStoreClient {@link IKeyStoreClient} keystore client to use if any.
+ * @param allowedObjects config object that are allowed to be created.
+ * @return An {@link IConfiguration} object representing the configuration that was loaded
* @throws ConfigurationException
*/
- private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
- List<String> optionArgsRef, IKeyStoreClient keyStoreClient)
+ private IConfiguration internalCreateConfigurationFromArgs(
+ String[] arrayArgs,
+ List<String> optionArgsRef,
+ IKeyStoreClient keyStoreClient,
+ Set<String> allowedObjects)
throws ConfigurationException {
final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs));
// first arg is config name
@@ -586,7 +610,7 @@
throw new ConfigurationException(
String.format("Unused template:map parameters: %s", uniqueMap.toString()));
}
- return configDef.createConfiguration();
+ return configDef.createConfiguration(allowedObjects);
}
private Map<String, String> extractTemplates(
@@ -616,6 +640,14 @@
}
}
return parserSettings.templateMap.getUniqueMap();
+ case ".tf_yaml":
+ // We parse the arguments but don't support template for YAML
+ final ArgsOptionParser allArgsParser = new ArgsOptionParser();
+ if (keyStoreClient != null) {
+ allArgsParser.setKeyStore(keyStoreClient);
+ }
+ optionArgsRef.addAll(allArgsParser.parseBestEffort(listArgs));
+ return new HashMap<>();
default:
return new HashMap<>();
}
@@ -792,8 +824,9 @@
@Override
public void printHelpForConfig(String[] args, boolean importantOnly, PrintStream out) {
try {
- IConfiguration config = internalCreateConfigurationFromArgs(args,
- new ArrayList<String>(args.length), null);
+ IConfiguration config =
+ internalCreateConfigurationFromArgs(
+ args, new ArrayList<String>(args.length), null, null);
config.printCommandUsage(importantOnly, out);
} catch (ConfigurationException e) {
// config must not be specified. Print generic help
diff --git a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
index 63254cf..9bfc06c 100644
--- a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
@@ -22,6 +22,7 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.logger.CurrentInvocation;
import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
+import com.android.tradefed.invoker.logger.InvocationLocal;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.util.FileUtil;
@@ -30,6 +31,7 @@
import com.android.tradefed.util.ZipUtil2;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
import java.io.File;
import java.io.IOException;
@@ -67,8 +69,26 @@
// Query key for requesting a download to be optional, so if it fails we don't replace it.
public static final String OPTIONAL_KEY = "optional";
+ /**
+ * Loads file resolvers using a dedicated {@link ServiceFileResolverLoader} that is scoped to
+ * each invocation.
+ */
+ // TODO(hzalek): Store a DynamicRemoteFileResolver instance per invocation to avoid locals.
private static final FileResolverLoader DEFAULT_FILE_RESOLVER_LOADER =
- new ServiceFileResolverLoader();
+ new FileResolverLoader() {
+ private final InvocationLocal<FileResolverLoader> mInvocationLoader =
+ new InvocationLocal<FileResolverLoader>() {
+ @Override
+ protected FileResolverLoader initialValue() {
+ return new ServiceFileResolverLoader();
+ }
+ };
+
+ @Override
+ public IRemoteFileResolver load(String scheme, Map<String, String> config) {
+ return mInvocationLoader.get().load(scheme, config);
+ }
+ };
private final FileResolverLoader mFileResolverLoader;
@@ -286,8 +306,6 @@
"Failed to parse the remote zip file path: %s", remoteZipFilePath),
e);
}
- IRemoteFileResolver resolver = getResolver(protocol);
-
queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
if (includeFilters != null) {
queryArgs.put("include_filters", String.join(";", includeFilters));
@@ -297,6 +315,7 @@
}
// Downloaded individual files should be saved to destDir, return value is not needed.
try {
+ IRemoteFileResolver resolver = getResolver(protocol);
resolver.setPrimaryDevice(mDevice);
resolver.resolveRemoteFiles(new File(remoteZipFilePath), queryArgs);
} catch (BuildRetrievalError e) {
@@ -304,14 +323,20 @@
CLog.d(
"Failed to partially download '%s' but marked optional so skipping: %s",
remoteZipFilePath, e.getMessage());
- } else {
- throw e;
+ return;
}
+
+ throw e;
}
}
- protected IRemoteFileResolver getResolver(String protocol) {
+ private IRemoteFileResolver getResolver(String protocol) throws BuildRetrievalError {
+ try {
return mFileResolverLoader.load(protocol, mExtraArgs);
+ } catch (ResolverLoadingException e) {
+ throw new BuildRetrievalError(
+ String.format("Could not load resolver for protocol %s", protocol), e);
+ }
}
@VisibleForTesting
@@ -357,28 +382,26 @@
CLog.e(e);
return null;
}
- IRemoteFileResolver resolver = getResolver(protocol);
- if (resolver != null) {
- try {
- CLog.d(
- "Considering option '%s' with path: '%s' for download.",
- option.name(), path);
- // Overrides query args
- query.putAll(mExtraArgs);
- resolver.setPrimaryDevice(mDevice);
- return resolver.resolveRemoteFiles(fileToResolve, query);
- } catch (BuildRetrievalError e) {
- if (isOptional(query)) {
- CLog.d(
- "Failed to resolve '%s' but marked optional so skipping: %s",
- fileToResolve, e.getMessage());
- } else {
- throw e;
- }
+
+ try {
+ IRemoteFileResolver resolver = getResolver(protocol);
+ if (resolver == null) {
+ return null;
}
+
+ CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
+ resolver.setPrimaryDevice(mDevice);
+ return resolver.resolveRemoteFiles(fileToResolve, query);
+ } catch (BuildRetrievalError e) {
+ if (isOptional(query)) {
+ CLog.d(
+ "Failed to resolve '%s' but marked optional so skipping: %s",
+ fileToResolve, e.getMessage());
+ return null;
+ }
+
+ throw e;
}
- // Not a remote file
- return null;
}
/**
@@ -415,18 +438,46 @@
* @param scheme the URI scheme that the loaded resolver is expected to handle.
* @param config a map of all dynamic resolver configuration key-value pairs specified by
* the 'dynamic-resolver-args' TF command-line flag.
+ * @throws ResolverLoadingException if the resolver that handles the specified scheme cannot
+ * be loaded and/or initialized.
*/
+ @Nullable
IRemoteFileResolver load(String scheme, Map<String, String> config);
}
+ /** Exception thrown if a resolver cannot be loaded or initialized. */
+ @VisibleForTesting
+ static final class ResolverLoadingException extends RuntimeException {
+ public ResolverLoadingException(@Nullable String message) {
+ super(message);
+ }
+
+ public ResolverLoadingException(@Nullable Throwable cause) {
+ super(cause);
+ }
+
+ public ResolverLoadingException(@Nullable String message, @Nullable Throwable cause) {
+ super(message, cause);
+ }
+ }
+
/**
* Loads and caches file resolvers using the service loading facility.
*
* <p>This implementation uses the service loading facility to find and cache available
* resolvers on the first call to {@code load}.
*
+ * <p>Any {@link Option}-annotated fields defined in loaded resolvers are initialized from the
+ * provided key-value pairs using the standard TF option-setting mechanism. Resolvers can define
+ * options that themselves require resolution as long as it causes no cycles during
+ * initialization.
+ *
+ * <p>Resolvers are loaded eagerly using ServiceLoader but have their options initialized only
+ * when first used. This avoids exceptions due to missing options in resolvers that are
+ * available on the class path but never used to load any files.
+ *
* <p>This implementation is thread-safe and ensures that any loaded resolvers are loaded at
- * most once per instance unless an exception is thrown.
+ * most once per instance.
*/
@ThreadSafe
@VisibleForTesting
@@ -436,7 +487,7 @@
private final Supplier<ClassLoader> mClassLoaderSupplier;
@GuardedBy("this")
- private @Nullable ImmutableMap<String, IRemoteFileResolver> mResolvers;
+ private @Nullable LoaderState mLoaderState;
ServiceFileResolverLoader() {
mClassLoaderSupplier = () -> Thread.currentThread().getContextClassLoader();
@@ -448,12 +499,14 @@
@Override
public synchronized IRemoteFileResolver load(String scheme, Map<String, String> config) {
- if (mResolvers != null) {
- return mResolvers.get(scheme);
+ if (mLoaderState != null) {
+ return mLoaderState.getAndInit(scheme);
}
// We use an intermediate map because the ImmutableMap builder throws if we add multiple
- // entries with the same key.
+ // entries with the same key. Note that we don't worry about setting any state that
+ // prevents this code from re-executing since failures loading service providers throws
+ // an Error which bubbles all the way to the top.
Map<String, IRemoteFileResolver> resolvers = new HashMap<>();
ServiceLoader<IRemoteFileResolver> serviceLoader =
ServiceLoader.load(IRemoteFileResolver.class, mClassLoaderSupplier.get());
@@ -462,8 +515,134 @@
resolvers.putIfAbsent(resolver.getSupportedProtocol(), resolver);
}
- mResolvers = ImmutableMap.copyOf(resolvers);
- return resolvers.get(scheme);
+ mLoaderState = new LoaderState(resolvers, config);
+ return mLoaderState.getAndInit(scheme);
+ }
+
+ /** Stores the state of loaded file resolvers. */
+ private static final class LoaderState {
+ private final ImmutableMap<String, String> mConfig;
+ private final ImmutableMap<String, ResolverState> mState;
+
+ LoaderState(Map<String, IRemoteFileResolver> resolvers, Map<String, String> config) {
+ this.mState =
+ ImmutableMap.copyOf(
+ Maps.transformValues(resolvers, r -> new ResolverState(r)));
+ this.mConfig = ImmutableMap.copyOf(config);
+ }
+
+ /** Returns an initialized resolver instance for the specified scheme. */
+ @Nullable
+ IRemoteFileResolver getAndInit(String scheme) {
+ ResolverState state = mState.get(scheme);
+ if (state == null) {
+ return null;
+ }
+
+ return state.getAndInit(this);
+ }
+
+ void resolve(IRemoteFileResolver resolver)
+ throws ConfigurationException, BuildRetrievalError {
+ // The device isn't set when resolving dynamic options because we don't want to load
+ // device-specific configuration when initializing pseudo-static resolvers that
+ // could out-live a particular device.
+ OptionSetter setter = new OptionSetter(resolver);
+
+ for (Map.Entry<String, String> e : mConfig.entrySet()) {
+ String name = e.getKey();
+
+ // Note that we don't throw for options that don't exist.
+ if (setter.fieldsForArgNoThrow(name) == null) {
+ // TODO(hzalek): Consider throwing when the option doesn't exist and is
+ // qualified using one of the option source's aliases.
+ // option name uses one of
+ // the option source's aliases
+ continue;
+ }
+
+ if (setter.isMapOption(name)) {
+ throw new ConfigurationException("Map options are not supported: " + name);
+ }
+
+ setter.setOptionValue(name, e.getValue());
+ }
+
+ Collection<String> missingOptions = setter.getUnsetMandatoryOptions();
+ if (!missingOptions.isEmpty()) {
+ throw new ConfigurationException(
+ String.format(
+ "Found missing mandatory options %s for resolver %s",
+ missingOptions, resolver.toString()));
+ }
+
+ DynamicRemoteFileResolver dynamicResolver =
+ new DynamicRemoteFileResolver((scheme, unused) -> getAndInit(scheme));
+ dynamicResolver.addExtraArgs(mConfig);
+ setter.validateRemoteFilePath(dynamicResolver);
+ }
+
+ /** Stores the resolver and its initialization state. */
+ static final class ResolverState {
+ final IRemoteFileResolver mResolver;
+
+ /**
+ * The initialization state where {@code null} means never initialized, {@code
+ * false} means started, and {@code true} means done.
+ */
+ @Nullable Boolean mDone;
+
+ /**
+ * The exception thrown when initializing the resolver to ensure that we only do it
+ * once.
+ */
+ @Nullable ResolverLoadingException mException;
+
+ ResolverState(IRemoteFileResolver resolver) {
+ this.mResolver = resolver;
+ }
+
+ IRemoteFileResolver getAndInit(LoaderState context) {
+ if (Boolean.TRUE.equals(mDone)) {
+ return getOrThrow();
+ }
+
+ if (Boolean.FALSE.equals(mDone)) {
+ // No need to catch or store the exception since it gets thrown in the
+ // recursive
+ // call to the dynamic resolver as a BuildRetrievalError which we already
+ // catch.
+ throw new ResolverLoadingException(
+ "Cycle detected while initializing resolver options: "
+ + mResolver.toString());
+ }
+
+ CLog.i("Initializing file resolver options: %s", mResolver);
+ mDone = Boolean.FALSE;
+
+ try {
+ context.resolve(mResolver);
+ } catch (BuildRetrievalError | ConfigurationException e) {
+ mException =
+ new ResolverLoadingException(
+ "Could not initialize resolver options: "
+ + mResolver.toString(),
+ e);
+ throw mException;
+ } finally {
+ mDone = Boolean.TRUE;
+ }
+
+ return mResolver;
+ }
+
+ private IRemoteFileResolver getOrThrow() {
+ if (mException != null) {
+ throw mException;
+ }
+ return mResolver;
+ }
+ }
}
}
}
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index 128b887..1c4e714 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -257,6 +257,17 @@
public void injectOptionValues(List<OptionDef> optionDefs) throws ConfigurationException;
/**
+ * Inject multiple option values into the set of configuration objects without throwing if one
+ * of the option cannot be applied.
+ *
+ * <p>Useful to inject many option values at once after creating a new object.
+ *
+ * @param optionDefs a list of option defs to inject
+ * @throws ConfigurationException if failed to create the {@link OptionSetter}
+ */
+ public void safeInjectOptionValues(List<OptionDef> optionDefs) throws ConfigurationException;
+
+ /**
* Create a shallow copy of this object.
*
* @return a {link IConfiguration} copy
@@ -492,6 +503,19 @@
throws ConfigurationException;
/**
+ * Set the config {@link Option} fields with given set of command line arguments using a best
+ * effort approach.
+ *
+ * <p>See {@link ArgsOptionParser} for expected format
+ *
+ * @param listArgs the command line arguments
+ * @param keyStoreClient {@link IKeyStoreClient} to use.
+ * @return the unconsumed arguments
+ */
+ public List<String> setBestEffortOptionsFromCommandLineArgs(
+ List<String> listArgs, IKeyStoreClient keyStoreClient) throws ConfigurationException;
+
+ /**
* Outputs a command line usage help text for this configuration to given printStream.
*
* @param importantOnly if <code>true</code> only print help for the important options
diff --git a/src/com/android/tradefed/config/IConfigurationFactory.java b/src/com/android/tradefed/config/IConfigurationFactory.java
index 0cda174..08b74b5 100644
--- a/src/com/android/tradefed/config/IConfigurationFactory.java
+++ b/src/com/android/tradefed/config/IConfigurationFactory.java
@@ -20,6 +20,7 @@
import java.io.PrintStream;
import java.util.List;
+import java.util.Set;
/**
* Factory for creating {@link IConfiguration}s
@@ -76,6 +77,20 @@
IKeyStoreClient keyStoreClient) throws ConfigurationException;
/**
+ * Create a configuration that only contains a set of selected objects.
+ *
+ * @param arrayArgs The command line arguments
+ * @param keyStoreClient A {@link IKeyStoreClient} which is used to obtain sensitive info in the
+ * args.
+ * @param allowedObjects The set of allowed objects to be created
+ * @return The loaded {@link IConfiguration}.
+ * @throws ConfigurationException if configuration could not be loaded
+ */
+ public IConfiguration createPartialConfigurationFromArgs(
+ String[] arrayArgs, IKeyStoreClient keyStoreClient, Set<String> allowedObjects)
+ throws ConfigurationException;
+
+ /**
* Create a {@link IGlobalConfiguration} from command line arguments.
* <p/>
* Expected format is "CONFIG [options]", where CONFIG is the built-in configuration name or
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index d562cc0..49d0bec 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -328,14 +328,22 @@
}
private OptionFieldsForName fieldsForArg(String name) throws ConfigurationException {
- OptionFieldsForName fields = mOptionMap.get(name);
- if (fields == null || fields.size() == 0) {
+ OptionFieldsForName fields = fieldsForArgNoThrow(name);
+ if (fields == null) {
throw new ConfigurationException(String.format("Could not find option with name %s",
name));
}
return fields;
}
+ OptionFieldsForName fieldsForArgNoThrow(String name) throws ConfigurationException {
+ OptionFieldsForName fields = mOptionMap.get(name);
+ if (fields == null || fields.size() == 0) {
+ return null;
+ }
+ return fields;
+ }
+
/**
* Returns a string describing the type of the field with given name.
*
diff --git a/src/com/android/tradefed/config/proxy/AutomatedReporters.java b/src/com/android/tradefed/config/proxy/AutomatedReporters.java
new file mode 100644
index 0000000..57f7d53
--- /dev/null
+++ b/src/com/android/tradefed/config/proxy/AutomatedReporters.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 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.config.proxy;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.proto.StreamProtoResultReporter;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+
+/**
+ * Class that defines the mapping from Tradefed automated reporters.
+ *
+ * <p>TODO: Formalize how to expose the list of supported automation.
+ */
+public class AutomatedReporters {
+
+ private static final String PROTO_REPORTING_PORT = "PROTO_REPORTING_PORT";
+ private static final ImmutableSet<String> REPORTER_MAPPING =
+ ImmutableSet.of(PROTO_REPORTING_PORT);
+
+ /**
+ * Complete the listeners based on the environment.
+ *
+ * @param configuration The configuration to complete
+ */
+ public void applyAutomatedReporters(IConfiguration configuration) {
+ for (String key : REPORTER_MAPPING) {
+ String envValue = getEnv(key);
+ if (envValue == null) {
+ continue;
+ }
+ switch (key) {
+ case PROTO_REPORTING_PORT:
+ StreamProtoResultReporter reporter = new StreamProtoResultReporter();
+ try {
+ reporter.setProtoReportPort(Integer.parseInt(envValue));
+ List<ITestInvocationListener> listeners =
+ configuration.getTestInvocationListeners();
+ listeners.add(reporter);
+ configuration.setTestInvocationListeners(listeners);
+ } catch (NumberFormatException e) {
+ CLog.e(e);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ protected String getEnv(String key) {
+ return System.getenv(key);
+ }
+}
diff --git a/src/com/android/tradefed/config/proxy/TradefedDelegator.java b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
new file mode 100644
index 0000000..d50c8a1
--- /dev/null
+++ b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2020 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.config.proxy;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+
+import com.google.common.base.Joiner;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Objects that helps delegating the invocation to another Tradefed binary. */
+public class TradefedDelegator {
+
+ /** The object reference in the configuration. */
+ public static final String DELEGATE_OBJECT = "DELEGATE";
+
+ private static final String DELETEGATED_OPTION_NAME = "delegated-tf";
+
+ @Option(
+ name = DELETEGATED_OPTION_NAME,
+ description =
+ "Points to the root dir of another Tradefed binary that will be used to drive the invocation")
+ private File mDelegatedTfRootDir;
+
+ private String[] mCommandLine = null;
+
+ /** Whether or not trigger the delegation logic. */
+ public boolean shouldUseDelegation() {
+ return mDelegatedTfRootDir != null;
+ }
+
+ /** Returns the directory of a Tradefed binary. */
+ public File getTfRootDir() {
+ return mDelegatedTfRootDir;
+ }
+
+ /** Creates the classpath out of the jars in the directory. */
+ public String createClasspath() {
+ List<File> jars =
+ Arrays.asList(
+ mDelegatedTfRootDir.listFiles(
+ new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.getName().endsWith(".jar");
+ }
+ }));
+ return Joiner.on(":").join(jars);
+ }
+
+ public void setCommandLine(String[] command) {
+ mCommandLine = command;
+ }
+
+ public String[] getCommandLine() {
+ return mCommandLine;
+ }
+
+ public static String[] clearCommandline(String[] originalCommand)
+ throws ConfigurationException {
+ List<String> argsList = new ArrayList<>(Arrays.asList(originalCommand));
+ try {
+ while (argsList.contains("--" + DELETEGATED_OPTION_NAME)) {
+ int index = argsList.indexOf("--" + DELETEGATED_OPTION_NAME);
+ if (index != -1) {
+ argsList.remove(index + 1);
+ argsList.remove(index);
+ }
+ }
+ } catch (RuntimeException e) {
+ throw new ConfigurationException(e.getMessage(), e);
+ }
+ return argsList.toArray(new String[0]);
+ }
+}
diff --git a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
index 9ac47bd..1aef60c 100644
--- a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
+++ b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
@@ -46,6 +46,7 @@
private static final List<String> REQUIRED_KEYS =
ImmutableList.of(DESCRIPTION_KEY, DEPENDENCIES_KEY, TESTS_KEY);
private Set<String> mSeenKeys = new HashSet<>();
+ private boolean mCreatedAsModule = false;
/**
* Main entry point of the parser to parse a given YAML file into Trade Federation objects.
@@ -53,9 +54,15 @@
* @param configDef
* @param source
* @param yamlInput
+ * @param createdAsModule
*/
- public void parse(ConfigurationDef configDef, String source, InputStream yamlInput)
+ public void parse(
+ ConfigurationDef configDef,
+ String source,
+ InputStream yamlInput,
+ boolean createdAsModule)
throws ConfigurationException {
+ mCreatedAsModule = createdAsModule;
// We don't support multi-device in YAML
configDef.setMultiDeviceMode(false);
Yaml yaml = new Yaml();
@@ -105,7 +112,10 @@
// Add default configured objects
LoaderConfiguration loadConfiguration = new LoaderConfiguration();
- loadConfiguration.setConfigurationDef(configDef).addDependencies(dependencyFiles);
+ loadConfiguration
+ .setConfigurationDef(configDef)
+ .addDependencies(dependencyFiles)
+ .setCreatedAsModule(mCreatedAsModule);
ServiceLoader<IDefaultObjectLoader> serviceLoader =
ServiceLoader.load(IDefaultObjectLoader.class);
for (IDefaultObjectLoader loader : serviceLoader) {
diff --git a/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java b/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java
index b4b4501..284e44f 100644
--- a/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java
+++ b/src/com/android/tradefed/config/yaml/IDefaultObjectLoader.java
@@ -33,6 +33,7 @@
public class LoaderConfiguration {
private ConfigurationDef mConfigDef;
+ private boolean mCreatedAsModule = false;
private Set<String> mDependencies = new LinkedHashSet<>();
public LoaderConfiguration setConfigurationDef(ConfigurationDef configDef) {
@@ -50,6 +51,11 @@
return this;
}
+ public LoaderConfiguration setCreatedAsModule(boolean createdAsModule) {
+ mCreatedAsModule = createdAsModule;
+ return this;
+ }
+
public ConfigurationDef getConfigDef() {
return mConfigDef;
}
@@ -57,5 +63,9 @@
public Set<String> getDependencies() {
return mDependencies;
}
+
+ public boolean createdAsModule() {
+ return mCreatedAsModule;
+ }
}
}
diff --git a/src/com/android/tradefed/config/yaml/OpenObjectLoader.java b/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
index f5c5d06..af7e11b 100644
--- a/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
+++ b/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
@@ -18,6 +18,7 @@
import com.android.tradefed.build.DependenciesResolver;
import com.android.tradefed.config.Configuration;
import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.log.FileLogger;
import com.android.tradefed.result.suite.SuiteResultReporter;
/** Loader for the default objects available in AOSP. */
@@ -25,11 +26,22 @@
@Override
public void addDefaultObjects(LoaderConfiguration loadConfiguration) {
+ // Only add the objects below if it's created as a stand alone configuration, in suite as
+ // a module, it will be resolving object from the top level suite.
+ if (loadConfiguration.createdAsModule()) {
+ return;
+ }
+ // Logger
+ loadConfiguration
+ .getConfigDef()
+ .addConfigObjectDef(Configuration.LOGGER_TYPE_NAME, FileLogger.class.getName());
+ // Result Reporters
loadConfiguration
.getConfigDef()
.addConfigObjectDef(
Configuration.RESULT_REPORTER_TYPE_NAME,
SuiteResultReporter.class.getName());
+ // Build
int classCount =
loadConfiguration
.getConfigDef()
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index cf20f87..c43fd75 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -34,6 +34,7 @@
import com.android.tradefed.log.ILogRegistry.EventType;
import com.android.tradefed.log.LogRegistry;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
@@ -194,6 +195,8 @@
/** Flag to remember if adb bridge has been disconnected and needs to be reset * */
private boolean mAdbBridgeNeedRestart = false;
+ private Map<String, String> mMonitoringTcpFastbootDevices = new HashMap<>();
+
/**
* The DeviceManager should be retrieved from the {@link GlobalConfiguration}
*/
@@ -1016,8 +1019,10 @@
@Override
public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline)
throws DeviceNotAvailableException {
- throw new DeviceNotAvailableException("aborted test session",
- monitor.getSerialNumber());
+ throw new DeviceNotAvailableException(
+ "aborted test session",
+ monitor.getSerialNumber(),
+ InfraErrorIdentifier.INVOCATION_CANCELLED);
}
/**
@@ -1026,8 +1031,10 @@
@Override
public void recoverDeviceBootloader(IDeviceStateMonitor monitor)
throws DeviceNotAvailableException {
- throw new DeviceNotAvailableException("aborted test session",
- monitor.getSerialNumber());
+ throw new DeviceNotAvailableException(
+ "aborted test session",
+ monitor.getSerialNumber(),
+ InfraErrorIdentifier.INVOCATION_CANCELLED);
}
/**
@@ -1036,8 +1043,10 @@
@Override
public void recoverDeviceRecovery(IDeviceStateMonitor monitor)
throws DeviceNotAvailableException {
- throw new DeviceNotAvailableException("aborted test session",
- monitor.getSerialNumber());
+ throw new DeviceNotAvailableException(
+ "aborted test session",
+ monitor.getSerialNumber(),
+ InfraErrorIdentifier.INVOCATION_CANCELLED);
}
/** {@inheritDoc} */
@@ -1045,7 +1054,9 @@
public void recoverDeviceFastbootd(IDeviceStateMonitor monitor)
throws DeviceNotAvailableException {
throw new DeviceNotAvailableException(
- "aborted test session", monitor.getSerialNumber());
+ "aborted test session",
+ monitor.getSerialNumber(),
+ InfraErrorIdentifier.INVOCATION_CANCELLED);
}
}
@@ -1343,6 +1354,11 @@
final FastbootHelper fastboot = new FastbootHelper(getRunUtil(), getFastbootPath());
while (!mQuit) {
Map<String, Boolean> serialAndMode = fastboot.getBootloaderAndFastbootdDevices();
+
+ serialAndMode.putAll(
+ fastboot.getBootloaderAndFastbootdTcpDevices(
+ mMonitoringTcpFastbootDevices));
+
if (serialAndMode != null) {
// Update known bootloader devices state
Set<String> bootloader = new HashSet<>();
@@ -1582,4 +1598,10 @@
public String getAdbVersion() {
return mAdbBridge.getAdbVersion(mAdbPath);
}
+
+ /** {@inheritDoc} */
+ @Override
+ public void addMonitoringTcpFastbootDevice(String serial, String fastboot_serial) {
+ mMonitoringTcpFastbootDevices.put(serial, fastboot_serial);
+ }
}
diff --git a/src/com/android/tradefed/device/FastbootHelper.java b/src/com/android/tradefed/device/FastbootHelper.java
index 2762075..fe251fd 100644
--- a/src/com/android/tradefed/device/FastbootHelper.java
+++ b/src/com/android/tradefed/device/FastbootHelper.java
@@ -25,6 +25,7 @@
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -125,6 +126,38 @@
}
/**
+ * Returns a map of device serials and whether they are in fastbootd mode or not.
+ *
+ * @param serials a map of devices serial number and fastboot mode serial number.
+ * @return a Map of serial in bootloader or fastbootd, the boolean is true if in fastbootd
+ */
+ public Map<String, Boolean> getBootloaderAndFastbootdTcpDevices(Map<String, String> serials) {
+ HashMap<String, Boolean> devices = new LinkedHashMap<>();
+ long TIMEOUT = 1500;
+
+ for (Entry<String, String> entry : serials.entrySet()) {
+ CLog.v("Run 'fastboot -s %s getvar is-userspace' command", entry.getValue());
+ CommandResult fastbootResult =
+ mRunUtil.runTimedCmdSilently(
+ TIMEOUT,
+ mFastbootPath,
+ "-s",
+ entry.getValue(),
+ "getvar",
+ "is-userspace");
+ if (fastbootResult.getStatus().equals(CommandStatus.SUCCESS)) {
+ if (fastbootResult.getStderr().contains("yes")) {
+ devices.put(entry.getKey(), true);
+ } else {
+ devices.put(entry.getKey(), false);
+ }
+ }
+ }
+
+ return devices;
+ }
+
+ /**
* Parses the output of "fastboot devices" command. Exposed for unit testing.
*
* @param fastbootOutput the output of fastboot command.
diff --git a/src/com/android/tradefed/device/IDeviceManager.java b/src/com/android/tradefed/device/IDeviceManager.java
index c1c3ff1..d5bf573 100644
--- a/src/com/android/tradefed/device/IDeviceManager.java
+++ b/src/com/android/tradefed/device/IDeviceManager.java
@@ -286,4 +286,13 @@
/** Get the adb version currently in use by the device manager. */
public String getAdbVersion();
+
+ /**
+ * Add a device to fastboot monitor. The fastboot monitor will use 'fastboot_serial' to
+ * communicate with the device.
+ *
+ * @param serial the device's serial number.
+ * @param fastboot_serial the device's fastboot mode serial number.
+ */
+ public void addMonitoringTcpFastbootDevice(String serial, String fastboot_serial);
}
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index fcd8635..8c96162 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -76,6 +76,8 @@
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Clock;
@@ -168,6 +170,9 @@
static final String MAC_ADDRESS_PATTERN = "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}";
static final String MAC_ADDRESS_COMMAND = "su root cat /sys/class/net/wlan0/address";
+ static final String ETHERNET_MAC_ADDRESS_COMMAND = "cat /sys/class/net/eth0/address";
+
+ static final int ETHER_ADDR_LEN = 6;
/** The network monitoring interval in ms. */
private static final int NETWORK_MONITOR_INTERVAL = 10 * 1000;
@@ -225,6 +230,8 @@
private DeviceDescriptor mCachedDeviceDescriptor = null;
private final Object mCacheLock = new Object();
+ private String mFastbootSerialNumber = null;
+
/**
* Interface for a generic device communication attempt.
*/
@@ -337,6 +344,11 @@
mReason = reason;
}
+ public boolean isFastbootOrBootloader() {
+ return mRebootMode == RebootMode.REBOOT_INTO_BOOTLOADER
+ || mRebootMode == RebootMode.REBOOT_INTO_FASTBOOTD;
+ }
+
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException {
getIDevice().reboot(mRebootMode.formatRebootCommand(mReason));
@@ -2050,8 +2062,8 @@
* Builds the OS command for the given fastboot command and args
*/
private String[] buildFastbootCommand(String... commandArgs) {
- return ArrayUtil.buildArray(new String[] {getFastbootPath(), "-s", getSerialNumber()},
- commandArgs);
+ return ArrayUtil.buildArray(
+ new String[] {getFastbootPath(), "-s", getFastbootSerialNumber()}, commandArgs);
}
/**
@@ -2089,6 +2101,15 @@
return false;
}
} catch (AdbCommandRejectedException e) {
+ // Workaround to not recover device if TCP adb is used.
+ if (isAdbTcp()
+ && (action instanceof RebootDeviceAction)
+ && ((RebootDeviceAction) action).isFastbootOrBootloader()) {
+ CLog.d(
+ "Ignore AdbCommandRejectedException when TCP device is rebooted into"
+ + " fastboot.");
+ return true;
+ }
logDeviceActionException(actionDescription, e);
} catch (ShellCommandUnresponsiveException e) {
CLog.w("Device %s stopped responding when attempting %s", getSerialNumber(),
@@ -2099,9 +2120,13 @@
recoverDevice();
}
if (retryAttempts > 0) {
- throw new DeviceUnresponsiveException(String.format("Attempted %s multiple times "
- + "on device %s without communication success. Aborting.", actionDescription,
- getSerialNumber()), getSerialNumber());
+ throw new DeviceUnresponsiveException(
+ String.format(
+ "Attempted %s multiple times "
+ + "on device %s without communication success. Aborting.",
+ actionDescription, getSerialNumber()),
+ getSerialNumber(),
+ DeviceErrorIdentifier.DEVICE_UNRESPONSIVE);
}
return false;
}
@@ -2964,6 +2989,10 @@
throw new UnsupportedOperationException(
String.format("Fastboot is not available and cannot reboot into %s", mode));
}
+
+ // Update fastboot serial number before entering fastboot mode
+ mStateMonitor.setFastbootSerialNumber(getFastbootSerialNumber());
+
// If we go to bootloader, it's probably for flashing so ensure we re-check the provider
mShouldSkipContentProviderSetup = false;
CLog.i(
@@ -3743,6 +3772,78 @@
return null;
}
+ /** {@inheritDoc} */
+ @Override
+ public String getFastbootSerialNumber() {
+ if (mFastbootSerialNumber != null) {
+ return mFastbootSerialNumber;
+ }
+
+ // Only devices which use TCP adb have different fastboot serial number because IPv6
+ // link-local address will be used in fastboot mode.
+ if (!isAdbTcp()) {
+ mFastbootSerialNumber = getSerialNumber();
+ CLog.i(
+ "Device %s's fastboot serial number is %s",
+ getSerialNumber(), mFastbootSerialNumber);
+ return mFastbootSerialNumber;
+ }
+
+ mFastbootSerialNumber = getSerialNumber();
+ byte[] macEui48Bytes;
+
+ try {
+ boolean adbRoot = isAdbRoot();
+ if (!adbRoot) {
+ enableAdbRoot();
+ }
+ macEui48Bytes = getEUI48MacAddressInBytes(ETHERNET_MAC_ADDRESS_COMMAND);
+ if (!adbRoot) {
+ disableAdbRoot();
+ }
+ } catch (DeviceNotAvailableException e) {
+ CLog.e("Device %s isn't available when get fastboot serial number", getSerialNumber());
+ CLog.e(e);
+ return getSerialNumber();
+ }
+
+ String net_interface = getHostOptions().getNetworkInterface();
+ if (net_interface == null || macEui48Bytes == null) {
+ CLog.i(
+ "Device %s's fastboot serial number is %s",
+ getSerialNumber(), mFastbootSerialNumber);
+ return mFastbootSerialNumber;
+ }
+
+ // Create a link-local Inet6Address from the MAC address. The EUI-48 MAC address
+ // is converted to an EUI-64 MAC address per RFC 4291. The resulting EUI-64 is
+ // used to construct a link-local IPv6 address per RFC 4862.
+ byte[] addr = new byte[16];
+ addr[0] = (byte) 0xfe;
+ addr[1] = (byte) 0x80;
+ addr[8] = (byte) (macEui48Bytes[0] ^ (byte) 0x02); // flip the link-local bit
+ addr[9] = macEui48Bytes[1];
+ addr[10] = macEui48Bytes[2];
+ addr[11] = (byte) 0xff;
+ addr[12] = (byte) 0xfe;
+ addr[13] = macEui48Bytes[3];
+ addr[14] = macEui48Bytes[4];
+ addr[15] = macEui48Bytes[5];
+
+ try {
+ String host_addr = Inet6Address.getByAddress(null, addr, 0).getHostAddress();
+ mFastbootSerialNumber = "tcp:" + host_addr.split("%")[0] + "%" + net_interface;
+ } catch (UnknownHostException e) {
+ CLog.w("Failed to get %s's IPv6 link-local address", getSerialNumber());
+ CLog.w(e);
+ }
+
+ CLog.i(
+ "Device %s's fastboot serial number is %s",
+ getSerialNumber(), mFastbootSerialNumber);
+ return mFastbootSerialNumber;
+ }
+
/**
* {@inheritDoc}
*/
@@ -4781,10 +4882,12 @@
}
/**
- * {@inheritDoc}
+ * Query Mac address from the device
+ *
+ * @param command the query command
+ * @return the MAC address of the device, null if it fails to query from the device
*/
- @Override
- public String getMacAddress() {
+ private String getMacAddress(String command) {
if (getIDevice() instanceof StubDevice) {
// Do not query MAC addresses from stub devices.
return null;
@@ -4795,20 +4898,81 @@
}
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
try {
- mIDevice.executeShellCommand(MAC_ADDRESS_COMMAND, receiver);
+ mIDevice.executeShellCommand(command, receiver);
} catch (IOException | TimeoutException | AdbCommandRejectedException |
ShellCommandUnresponsiveException e) {
- CLog.w("Failed to query MAC address for %s", mIDevice.getSerialNumber());
+ CLog.w(
+ "Failed to query MAC address for %s by '%s'",
+ mIDevice.getSerialNumber(), command);
CLog.w(e);
}
String output = receiver.getOutput().trim();
if (isMacAddress(output)) {
return output;
}
- CLog.d("No valid MAC address queried from device %s", mIDevice.getSerialNumber());
+ CLog.d(
+ "No valid MAC address queried from device %s by '%s'",
+ mIDevice.getSerialNumber(), command);
return null;
}
+ /**
+ * Query EUI-48 MAC address from the device
+ *
+ * @param command the query command
+ * @return the EUI-48 MAC address in long, 0 if it fails to query from the device
+ * @throws IllegalArgumentException
+ */
+ long getEUI48MacAddressInLong(String command) {
+ String addr = getMacAddress(command);
+ if (addr == null) {
+ return 0;
+ }
+
+ String[] parts = addr.split(":");
+ if (parts.length != ETHER_ADDR_LEN) {
+ throw new IllegalArgumentException(addr + " was not a valid MAC address");
+ }
+ long longAddr = 0;
+ for (int i = 0; i < parts.length; i++) {
+ int x = Integer.valueOf(parts[i], 16);
+ if (x < 0 || 0xff < x) {
+ throw new IllegalArgumentException(addr + "was not a valid MAC address");
+ }
+ longAddr = x + (longAddr << 8);
+ }
+
+ return longAddr;
+ }
+
+ /**
+ * Query EUI-48 MAC address from the device
+ *
+ * @param command the query command
+ * @return the EUI-48 MAC address in byte[], null if it fails to query from the device
+ * @throws IllegalArgumentException
+ */
+ byte[] getEUI48MacAddressInBytes(String command) {
+ long addr = getEUI48MacAddressInLong(command);
+ if (addr == 0) {
+ return null;
+ }
+
+ byte[] bytes = new byte[ETHER_ADDR_LEN];
+ int index = ETHER_ADDR_LEN;
+ while (index-- > 0) {
+ bytes[index] = (byte) addr;
+ addr = addr >> 8;
+ }
+ return bytes;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getMacAddress() {
+ return getMacAddress(MAC_ADDRESS_COMMAND);
+ }
+
/** {@inheritDoc} */
@Override
public String getSimState() {
diff --git a/src/com/android/tradefed/device/NativeDeviceStateMonitor.java b/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
index 5d55bb7..dcd2da7 100644
--- a/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
+++ b/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
@@ -23,6 +23,7 @@
import com.android.tradefed.device.IDeviceManager.IFastbootListener;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunInterruptedException;
import com.android.tradefed.util.RunUtil;
import java.io.IOException;
@@ -54,6 +55,9 @@
/** The time in ms to wait for a device to available. */
private long mDefaultAvailableTimeout = 6 * 60 * 1000;
+ /** The fastboot mode serial number */
+ private String mFastbootSerialNumber = null;
+
private List<DeviceStateListener> mStateListeners;
private IDeviceManager mMgr;
private final boolean mFastbootEnabled;
@@ -94,6 +98,17 @@
mDefaultAvailableTimeout = timeoutMs;
}
+ /** Set the fastboot mode serial number. */
+ @Override
+ public void setFastbootSerialNumber(String serial) {
+ mFastbootSerialNumber = serial;
+
+ if (mFastbootSerialNumber != null && !mFastbootSerialNumber.equals(getSerialNumber())) {
+ // Add to IDeviceManager to monitor it
+ mMgr.addMonitoringTcpFastbootDevice(getSerialNumber(), mFastbootSerialNumber);
+ }
+ }
+
/**
* {@inheritDoc}
*/
@@ -130,6 +145,15 @@
return getIDevice().getSerialNumber();
}
+ /** {@inheritDoc} */
+ @Override
+ public String getFastbootSerialNumber() {
+ if (mFastbootSerialNumber == null) {
+ mFastbootSerialNumber = getSerialNumber();
+ }
+ return mFastbootSerialNumber;
+ }
+
/**
* {@inheritDoc}
*/
@@ -432,7 +456,7 @@
} catch (InterruptedException e) {
CLog.w("wait for device bootloader state update interrupted");
CLog.w(e);
- throw new RuntimeException(e);
+ throw new RunInterruptedException(e);
} finally {
mMgr.removeFastbootListener(listener);
}
@@ -455,7 +479,7 @@
} catch (InterruptedException e) {
CLog.w("wait for device state interrupted");
CLog.w(e);
- throw new RuntimeException(e);
+ throw new RunInterruptedException(e);
} finally {
removeDeviceStateListener(listener);
}
diff --git a/src/com/android/tradefed/device/WaitDeviceRecovery.java b/src/com/android/tradefed/device/WaitDeviceRecovery.java
index 635f1fb..d71fb06 100644
--- a/src/com/android/tradefed/device/WaitDeviceRecovery.java
+++ b/src/com/android/tradefed/device/WaitDeviceRecovery.java
@@ -137,8 +137,13 @@
"Found device %s in %s but expected online. Rebooting...",
monitor.getSerialNumber(), state));
// TODO: retry if failed
- getRunUtil().runTimedCmd(mFastbootWaitTime, mFastbootPath, "-s",
- monitor.getSerialNumber(), "reboot");
+ getRunUtil()
+ .runTimedCmd(
+ mFastbootWaitTime,
+ mFastbootPath,
+ "-s",
+ monitor.getFastbootSerialNumber(),
+ "reboot");
}
// wait for device online
@@ -231,9 +236,10 @@
}
}
// If no reboot was done, waitForDeviceAvailable has already been checked.
- throw new DeviceUnresponsiveException(String.format(
- "Device %s is online but unresponsive", monitor.getSerialNumber()),
- monitor.getSerialNumber());
+ throw new DeviceUnresponsiveException(
+ String.format("Device %s is online but unresponsive", monitor.getSerialNumber()),
+ monitor.getSerialNumber(),
+ DeviceErrorIdentifier.DEVICE_UNRESPONSIVE);
}
/**
diff --git a/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
new file mode 100644
index 0000000..c4c9697
--- /dev/null
+++ b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2020 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.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.proxy.TradefedDelegator;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.error.HarnessRuntimeException;
+import com.android.tradefed.invoker.TestInvocation.Stage;
+import com.android.tradefed.log.ITestLogger;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.proto.StreamProtoReceiver;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+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.RunInterruptedException;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.SystemUtil;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** {@link InvocationExecution} which delegate the execution to another Tradefed binary. */
+public class DelegatedInvocationExecution extends InvocationExecution {
+
+ /** Timeout to wait for the events received from subprocess to finish being processed. */
+ private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000;
+
+ private File mTmpDelegatedDir = null;
+ private File mGlobalConfig = null;
+ // Output reporting
+ private File mStdoutFile = null;
+ private File mStderrFile = null;
+ private OutputStream mStderr = null;
+ private OutputStream mStdout = null;
+
+ @Override
+ public void reportLogs(ITestDevice device, ITestLogger logger, Stage stage) {
+ // Do nothing
+ }
+
+ @Override
+ public boolean shardConfig(
+ IConfiguration config,
+ TestInformation testInfo,
+ IRescheduler rescheduler,
+ ITestLogger logger) {
+ return false;
+ }
+
+ @Override
+ public void doSetup(TestInformation testInfo, IConfiguration config, ITestLogger listener)
+ throws TargetSetupError, BuildError, DeviceNotAvailableException {
+ // Do nothing
+ }
+
+ @Override
+ public void doTeardown(
+ TestInformation testInfo,
+ IConfiguration config,
+ ITestLogger logger,
+ Throwable exception)
+ throws Throwable {
+ // Do nothing
+ }
+
+ @Override
+ public void runTests(
+ TestInformation info, IConfiguration config, ITestInvocationListener listener)
+ throws Throwable {
+ // Dump the delegated config for debugging
+ File dumpConfig = FileUtil.createTempFile("delegated-config", ".xml");
+ try (PrintWriter pw = new PrintWriter(dumpConfig)) {
+ config.dumpXml(pw);
+ }
+ logAndCleanFile(dumpConfig, LogDataType.XML, listener);
+
+ if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) == null) {
+ throw new ConfigurationException(
+ "Delegate object should not be null in DelegatedInvocation");
+ }
+ TradefedDelegator delegator =
+ (TradefedDelegator)
+ config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT);
+ List<String> commandLine = new ArrayList<>();
+ commandLine.add(SystemUtil.getRunningJavaBinaryPath().getAbsolutePath());
+ mTmpDelegatedDir = FileUtil.createTempDir("delegated-invocation");
+ commandLine.add(String.format("-Djava.io.tmpdir=%s", mTmpDelegatedDir.getAbsolutePath()));
+ commandLine.add("-cp");
+ // Add classpath
+ commandLine.add(delegator.createClasspath());
+ commandLine.add("com.android.tradefed.command.CommandRunner");
+ // Add command line
+ commandLine.addAll(Arrays.asList(delegator.getCommandLine()));
+
+ try (StreamProtoReceiver receiver = createReceiver(listener, info.getContext())) {
+ mStdoutFile = FileUtil.createTempFile("stdout_delegate_", ".log", mTmpDelegatedDir);
+ mStderrFile = FileUtil.createTempFile("stderr_delegate_", ".log", mTmpDelegatedDir);
+ mStderr = new FileOutputStream(mStderrFile);
+ mStdout = new FileOutputStream(mStdoutFile);
+ IRunUtil runUtil = createRunUtil(receiver.getSocketServerPort());
+ CommandResult result = null;
+ RuntimeException runtimeException = null;
+ try {
+ result =
+ runUtil.runTimedCmd(
+ config.getCommandOptions().getInvocationTimeout(),
+ mStdout,
+ mStderr,
+ commandLine.toArray(new String[0]));
+ } catch (RuntimeException e) {
+ runtimeException = e;
+ }
+ if (!receiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
+ throw new RuntimeException(
+ String.format(
+ "Event receiver thread did not complete:\n%s",
+ FileUtil.readStringFromFile(mStderrFile)));
+ }
+ receiver.completeModuleEvents();
+ if (runtimeException != null) {
+ if (runtimeException instanceof RunInterruptedException) {
+ throw runtimeException;
+ }
+ throw new HarnessRuntimeException(
+ runtimeException.getMessage(),
+ runtimeException,
+ InfraErrorIdentifier.UNDETERMINED);
+ }
+ if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
+ throw new HarnessRuntimeException(
+ "Delegated invocation timed out.", InfraErrorIdentifier.UNDETERMINED);
+ }
+ } finally {
+ StreamUtil.close(mStderr);
+ StreamUtil.close(mStdout);
+ logAndCleanFile(mStdoutFile, LogDataType.TEXT, listener);
+ logAndCleanFile(mStderrFile, LogDataType.TEXT, listener);
+ logAndCleanFile(mGlobalConfig, LogDataType.XML, listener);
+ }
+ }
+
+ @Override
+ public void doCleanUp(IInvocationContext context, IConfiguration config, Throwable exception) {
+ super.doCleanUp(context, config, exception);
+ FileUtil.recursiveDelete(mTmpDelegatedDir);
+ FileUtil.deleteFile(mGlobalConfig);
+ }
+
+ private IRunUtil createRunUtil(int port) throws IOException {
+ IRunUtil runUtil = new RunUtil();
+ // Handle the global configs for the subprocess
+ runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
+ runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
+ runUtil.setEnvVariablePriority(EnvPriority.SET);
+ mGlobalConfig = createGlobalConfig();
+ runUtil.setEnvVariable(
+ GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig.getAbsolutePath());
+ runUtil.setEnvVariable("PROTO_REPORTING_PORT", Integer.toString(port));
+ return runUtil;
+ }
+
+ private StreamProtoReceiver createReceiver(
+ ITestInvocationListener listener, IInvocationContext mainContext) throws IOException {
+ StreamProtoReceiver receiver =
+ new StreamProtoReceiver(
+ listener, mainContext, false, false, /* report logs */ false, "");
+ return receiver;
+ }
+
+ private File createGlobalConfig() throws IOException {
+ String[] configList =
+ new String[] {
+ GlobalConfiguration.DEVICE_MANAGER_TYPE_NAME,
+ GlobalConfiguration.KEY_STORE_TYPE_NAME,
+ GlobalConfiguration.HOST_OPTIONS_TYPE_NAME,
+ GlobalConfiguration.SANDBOX_FACTORY_TYPE_NAME,
+ "android-build"
+ };
+ File filteredGlobalConfig =
+ GlobalConfiguration.getInstance().cloneConfigWithFilter(configList);
+ return filteredGlobalConfig;
+ }
+
+ /**
+ * Log the content of given file to listener, then remove the file.
+ *
+ * @param fileToExport the {@link File} pointing to the file to log.
+ * @param type the {@link LogDataType} of the data
+ * @param listener the {@link ITestInvocationListener} where to report the test.
+ */
+ private void logAndCleanFile(
+ File fileToExport, LogDataType type, ITestInvocationListener listener) {
+ if (fileToExport == null) return;
+
+ try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport, true)) {
+ listener.testLog(fileToExport.getName(), type, inputStream);
+ }
+ }
+}
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 0c32e14..25d1cad 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -26,6 +26,8 @@
import com.android.tradefed.config.DynamicRemoteFileResolver;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.proxy.AutomatedReporters;
+import com.android.tradefed.config.proxy.TradefedDelegator;
import com.android.tradefed.device.DeviceManager;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
@@ -33,6 +35,7 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.device.NativeDevice;
+import com.android.tradefed.device.RemoteAndroidDevice;
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.device.TcpDevice;
import com.android.tradefed.device.TestDeviceState;
@@ -119,6 +122,7 @@
public static final String TRADEFED_LOG_NAME = "host_log";
public static final String TRADEFED_END_HOST_LOG = "end_host_log";
+ private static final String TRADEFED_DELEGATED_LOG_NAME = "delegated_parent_log";
/** Suffix used on host_log for the part before sharding occurs. */
static final String BEFORE_SHARDING_SUFFIX = "_before_sharding";
static final String DEVICE_LOG_NAME_PREFIX = "device_logcat_";
@@ -152,6 +156,7 @@
PARENT_SANDBOX,
SANDBOX,
REMOTE_INVOCATION,
+ DELEGATED_INVOCATION
}
private String mStatus = "(not invoked)";
@@ -275,7 +280,11 @@
} catch (RunInterruptedException e) {
exception = e;
CLog.w("Invocation interrupted");
- reportFailure(createFailureFromException(e, FailureStatus.UNSET), listener);
+ // if a stop cause was set, the interruption is most likely due to the invocation being
+ // cancelled
+ if (mStopCause == null) {
+ reportFailure(createFailureFromException(e, FailureStatus.UNSET), listener);
+ }
} catch (AssertionError e) {
exception = e;
CLog.e("Caught AssertionError while running invocation: %s", e.toString());
@@ -488,7 +497,11 @@
}
private void reportHostLog(ITestInvocationListener listener, IConfiguration config) {
- reportHostLog(listener, config, TRADEFED_LOG_NAME);
+ String name = TRADEFED_LOG_NAME;
+ if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+ name = TRADEFED_DELEGATED_LOG_NAME;
+ }
+ reportHostLog(listener, config, name);
}
private void reportHostLog(
@@ -703,6 +716,9 @@
IRescheduler rescheduler,
ITestInvocationListener... extraListeners)
throws DeviceNotAvailableException, Throwable {
+ // Handle the automated reporting
+ applyAutomatedReporters(config);
+
for (ITestInvocationListener listener : extraListeners) {
if (listener instanceof IScheduledInvocationListener) {
mSchedulerListeners.add((IScheduledInvocationListener) listener);
@@ -780,6 +796,9 @@
if (context.getDevices().get(0) instanceof ManagedRemoteDevice) {
mode = RunMode.REMOTE_INVOCATION;
}
+ if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+ mode = RunMode.DELEGATED_INVOCATION;
+ }
IInvocationExecution invocationPath = createInvocationExec(mode);
updateInvocationContext(context, config);
@@ -907,7 +926,8 @@
if (!deviceInit) {
startInvocation(config, context, listener);
}
- if (config.getTests() == null || config.getTests().isEmpty()) {
+ if (!RunMode.DELEGATED_INVOCATION.equals(mode)
+ && (config.getTests() == null || config.getTests().isEmpty())) {
CLog.e("No tests to run");
if (deviceInit) {
// If we did an early setup, do the tear down.
@@ -1003,6 +1023,8 @@
return new SandboxedInvocationExecution();
case REMOTE_INVOCATION:
return new RemoteInvocationExecution();
+ case DELEGATED_INVOCATION:
+ return new DelegatedInvocationExecution();
default:
return new InvocationExecution();
}
@@ -1015,6 +1037,12 @@
PrettyPrintDelimiter.printStageDelimiter(message);
}
+ @VisibleForTesting
+ protected void applyAutomatedReporters(IConfiguration config) {
+ AutomatedReporters autoReport = new AutomatedReporters();
+ autoReport.applyAutomatedReporters(config);
+ }
+
private void logExecuteShellCommand(List<ITestDevice> devices, ITestLogger logger) {
for (ITestDevice device : devices) {
if (!(device instanceof NativeDevice)) {
@@ -1101,7 +1129,8 @@
int countVirtualLost = 0;
for (Entry<ITestDevice, FreeDeviceState> fds : devicesStates.entrySet()) {
// TODO: Rely on the FailureStatus for lost devices instead
- if (fds.getKey().getIDevice() instanceof TcpDevice
+ if ((fds.getKey().getIDevice() instanceof TcpDevice
+ || fds.getKey() instanceof RemoteAndroidDevice)
&& exception instanceof DeviceNotAvailableException) {
countVirtualLost++;
continue;
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index a481cc9..7a4c3ef 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -69,6 +69,8 @@
* invocation scope we should not report it again.
*/
private boolean mReportInvocation = false;
+ /** In some cases we do not need to forward the logs. */
+ private boolean mReportLogs = true;
/** Prefix that will be added to the files logged through the parser. */
private String mFilePrefix;
/** The context from the invocation in progress, not the proto one. */
@@ -115,6 +117,11 @@
mQuietParsing = quiet;
}
+ /** Sets whether or not we should report the logs. */
+ public void setReportLogs(boolean reportLogs) {
+ mReportLogs = reportLogs;
+ }
+
/**
* Main entry function that takes the finalized completed proto and replay its results.
*
@@ -318,6 +325,7 @@
} catch (IOException e) {
CLog.e("Failed to deserialize the invocation exception:");
CLog.e(e);
+ failure.setCause(new RuntimeException(failure.getErrorMessage()));
}
}
}
@@ -500,6 +508,9 @@
if (!(mListener instanceof ILogSaverListener)) {
return;
}
+ if (!mReportLogs) {
+ return;
+ }
ILogSaverListener logger = (ILogSaverListener) mListener;
for (Entry<String, Any> entry : proto.getArtifactsMap().entrySet()) {
try {
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index f7119f3..5e70f63 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -483,6 +483,10 @@
@Override
public final void logAssociation(String dataName, LogFile logFile) {
+ if (mLatestChild == null || mLatestChild.isEmpty()) {
+ CLog.w("Skip logging '%s' logAssociation called out of sequence.", dataName);
+ return;
+ }
TestRecord.Builder current = mLatestChild.peek();
Map<String, Any> fullmap = new HashMap<>();
fullmap.putAll(current.getArtifactsMap());
diff --git a/src/com/android/tradefed/result/proto/StreamProtoReceiver.java b/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
index f674906..ab49396 100644
--- a/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
+++ b/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
@@ -83,7 +83,7 @@
boolean reportInvocation,
boolean quietParsing)
throws IOException {
- this(listener, mainContext, reportInvocation, quietParsing, "subprocess-");
+ this(listener, mainContext, reportInvocation, quietParsing, true, "subprocess-");
}
/**
@@ -102,8 +102,30 @@
boolean quietParsing,
String logNamePrefix)
throws IOException {
+ this(listener, mainContext, reportInvocation, quietParsing, true, logNamePrefix);
+ }
+
+ /**
+ * Ctor.
+ *
+ * @param listener the {@link ITestInvocationListener} where to report the results.
+ * @param reportInvocation Whether or not to report the invocation level events.
+ * @param quietParsing Whether or not to let the parser log debug information.
+ * @param reportLogs Whether or not to report the logs
+ * @param logNamePrefix The prefix for file logged through the parser.
+ * @throws IOException
+ */
+ public StreamProtoReceiver(
+ ITestInvocationListener listener,
+ IInvocationContext mainContext,
+ boolean reportInvocation,
+ boolean quietParsing,
+ boolean reportLogs,
+ String logNamePrefix)
+ throws IOException {
mListener = listener;
mParser = new ProtoResultParser(mListener, mainContext, reportInvocation, logNamePrefix);
+ mParser.setReportLogs(reportLogs);
mParser.setQuiet(quietParsing);
mEventReceiver = new EventReceiverThread();
mEventReceiver.start();
diff --git a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
index 23d605d..a4a564f 100644
--- a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
@@ -38,6 +38,14 @@
private Socket mReportSocket = null;
private boolean mPrintedMessage = false;
+ public void setProtoReportPort(Integer portValue) {
+ mReportPort = portValue;
+ }
+
+ public Integer getProtoReportPort() {
+ return mReportPort;
+ }
+
@Override
public void processStartInvocation(
TestRecord invocationStartRecord, IInvocationContext context) {
diff --git a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
index 772075b..428199d 100644
--- a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
+++ b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
@@ -304,7 +304,7 @@
message = "Run was incomplete. Some tests might not have finished.";
}
serializer.startTag(NS, MODULES_NOT_DONE_REASON);
- serializer.attribute(NS, MESSAGE_ATTR, message);
+ serializer.attribute(NS, MESSAGE_ATTR, sanitizeXmlContent(message));
serializer.endTag(NS, MODULES_NOT_DONE_REASON);
}
serializeTestCases(serializer, module.getTestResults());
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index 110230e..71b13a4 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -124,8 +124,17 @@
long timeout = config.getCommandOptions().getInvocationTimeout();
// Allow interruption, subprocess should handle signals itself
mRunUtil.allowInterrupt(true);
- CommandResult result =
- mRunUtil.runTimedCmd(timeout, mStdout, mStderr, mCmdArgs.toArray(new String[0]));
+ CommandResult result = null;
+ try {
+ result =
+ mRunUtil.runTimedCmd(
+ timeout, mStdout, mStderr, mCmdArgs.toArray(new String[0]));
+ } catch (RuntimeException interrupted) {
+ CLog.e("Sandbox runtimedCmd threw an exception");
+ CLog.e(interrupted);
+ result = new CommandResult(CommandStatus.EXCEPTION);
+ result.setStdout(StreamUtil.getStackTrace(interrupted));
+ }
boolean failedStatus = false;
String stderrText;
diff --git a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
index 3e32e33..aa912a8 100644
--- a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
+++ b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
@@ -134,7 +134,14 @@
charge = runTest(testInfo, listener);
elapsedTimeMs = getCurrentTimeMs() - elapsedTimeMs;
} catch (DeviceNotAvailableException e) {
- FailureDescription failure = FailureDescription.create(e.getMessage()).setCause(e);
+ FailureDescription failure =
+ FailureDescription.create(e.getMessage())
+ .setCause(e)
+ .setErrorIdentifier(e.getErrorId())
+ .setOrigin(e.getOrigin());
+ if (e.getErrorId() != null) {
+ failure.setFailureStatus(e.getErrorId().status());
+ }
listener.testRunFailed(failure);
throw e;
} finally {
diff --git a/src/com/android/tradefed/testtype/UsbResetTest.java b/src/com/android/tradefed/testtype/UsbResetTest.java
index 8e659b9..12c0915 100644
--- a/src/com/android/tradefed/testtype/UsbResetTest.java
+++ b/src/com/android/tradefed/testtype/UsbResetTest.java
@@ -24,6 +24,7 @@
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
@@ -46,7 +47,8 @@
if (usbDevice == null) {
throw new DeviceNotAvailableException(
String.format("Device '%s' not found during USB reset.", serial),
- serial);
+ serial,
+ DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
} else {
CLog.d("Resetting USB port for device '%s'", serial);
usbDevice.reset();
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 0372eb7..9032b00 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -509,7 +509,13 @@
// After the run, if the test failed (even after retry the final result passed) has
// failed, capture a bugreport.
if (retriableTest.getResultListener().hasLastAttemptFailed()) {
- captureBugreport(listener, getId());
+ captureBugreport(
+ listener,
+ getId(),
+ retriableTest
+ .getResultListener()
+ .getCurrentRunResults()
+ .getRunFailureDescription());
}
}
} finally {
@@ -609,7 +615,13 @@
return retriableTest;
}
- private void captureBugreport(ITestLogger listener, String moduleId) {
+ private void captureBugreport(
+ ITestLogger listener, String moduleId, FailureDescription failure) {
+ FailureStatus status = failure.getFailureStatus();
+ if (!FailureStatus.LOST_SYSTEM_UNDER_TEST.equals(status)
+ && !FailureStatus.SYSTEM_UNDER_TEST_CRASHED.equals(status)) {
+ return;
+ }
for (ITestDevice device : mModuleInvocationContext.getDevices()) {
if (device.getIDevice() instanceof StubDevice) {
continue;
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 85f0a43..26a7f4a 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -341,7 +341,9 @@
}
String fullId =
String.format("%s[%s]", baseId, param.getParameterIdentifier());
- if (shouldRunParameterized(baseId, fullId, mForcedParameter)) {
+ String nameWithParam =
+ String.format("%s[%s]", name, param.getParameterIdentifier());
+ if (shouldRunParameterized(baseId, fullId, nameWithParam, mForcedParameter)) {
IConfiguration paramConfig =
mConfigFactory.createConfigurationFromArgs(pathArg);
// Mark the parameter in the metadata
@@ -351,7 +353,7 @@
ConfigurationDescriptor.ACTIVE_PARAMETER_KEY,
param.getParameterIdentifier());
param.addParameterSpecificConfig(paramConfig);
- setUpConfig(name, baseId, fullId, paramConfig, abi);
+ setUpConfig(name, nameWithParam, baseId, fullId, paramConfig, abi);
param.applySetup(paramConfig);
toRun.put(fullId, paramConfig);
}
@@ -367,7 +369,8 @@
// If we find any parameterized combination for mainline modules.
for (String param : mainlineParams) {
String fullId = String.format("%s[%s]", baseId, param);
- if (!shouldRunParameterized(baseId, fullId, null)) {
+ String nameWithParam = String.format("%s[%s]", name, param);
+ if (!shouldRunParameterized(baseId, fullId, nameWithParam, null)) {
continue;
}
// Create mainline handler for each defined mainline parameter.
@@ -385,7 +388,7 @@
.addMetadata(
ITestSuite.ACTIVE_MAINLINE_PARAMETER_KEY,
param);
- setUpConfig(name, baseId, fullId, paramConfig, abi);
+ setUpConfig(name, nameWithParam, baseId, fullId, paramConfig, abi);
handler.applySetup(paramConfig);
toRun.put(fullId, paramConfig);
}
@@ -399,7 +402,9 @@
}
if (shouldRunModule(baseId)) {
// Always add the base regular configuration to the execution.
- setUpConfig(name, baseId, baseId, config, abi);
+ // Do not pass the nameWithParam in because it would cause the module args be
+ // injected into config twice if we pass nameWithParam using name.
+ setUpConfig(name, null, baseId, baseId, config, abi);
toRun.put(baseId, config);
}
}
@@ -476,10 +481,14 @@
* in including its parameterization variant.
*/
private boolean shouldRunParameterized(
- String baseModuleId, String parameterModuleId, IModuleParameter forcedModuleParameter) {
+ String baseModuleId,
+ String parameterModuleId,
+ String nameWithParam,
+ IModuleParameter forcedModuleParameter) {
// Explicitly excluded
List<SuiteTestFilter> excluded = getFilterList(mExcludeFilters, parameterModuleId);
- if (containsModuleExclude(excluded)) {
+ List<SuiteTestFilter> excludedParam = getFilterList(mExcludeFilters, nameWithParam);
+ if (containsModuleExclude(excluded) || containsModuleExclude(excludedParam)) {
return false;
}
@@ -492,7 +501,8 @@
}
// Explicitly included
List<SuiteTestFilter> included = getFilterList(mIncludeFilters, parameterModuleId);
- if (mIncludeAll || !included.isEmpty()) {
+ List<SuiteTestFilter> includedParam = getFilterList(mIncludeFilters, nameWithParam);
+ if (mIncludeAll || !included.isEmpty() || !includedParam.isEmpty()) {
return true;
}
return false;
@@ -748,18 +758,23 @@
* Setup the options for the module configuration.
*
* @param name The base name of the module
+ * @param nameWithParam The id of the parameterized mainline module (module name + parameters)
* @param id The base id name of the module.
* @param fullId The full id of the module (usually abi + module name + parameters)
* @param config The module configuration.
* @param abi The abi of the module.
* @throws ConfigurationException
*/
- private void setUpConfig(String name, String id, String fullId, IConfiguration config, IAbi abi)
- throws ConfigurationException {
+ private void setUpConfig(String name, String nameWithParam, String id, String fullId,
+ IConfiguration config, IAbi abi)
+ throws ConfigurationException {
List<OptionDef> optionsToInject = new ArrayList<>();
if (mModuleOptions.containsKey(name)) {
optionsToInject.addAll(mModuleOptions.get(name));
}
+ if (nameWithParam != null && mModuleOptions.containsKey(nameWithParam)) {
+ optionsToInject.addAll(mModuleOptions.get(nameWithParam));
+ }
if (mModuleOptions.containsKey(id)) {
optionsToInject.addAll(mModuleOptions.get(id));
}
@@ -801,6 +816,7 @@
config.validateOptions();
}
+
/** Whether or not the base configuration should be created for all abis or not. */
private boolean shouldCreateMultiAbiForBase(List<IModuleParameter> params) {
for (IModuleParameter param : params) {
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index 587f24e..da1741f 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -157,6 +157,11 @@
testNames.add(testInfo.getName());
}
setIncludeFilter(testNames);
+ // With include filters being set, the test no longer needs group and path settings.
+ // Clear the settings to avoid conflict when the test is running in a shard.
+ mTestGroup = null;
+ mTestMappingPaths.clear();
+ mUseTestMappingPath = false;
}
// load all the configurations with include-filter injected.
@@ -186,6 +191,21 @@
return testConfigs;
}
+ @VisibleForTesting
+ String getTestGroup() {
+ return mTestGroup;
+ }
+
+ @VisibleForTesting
+ List<String> getTestMappingPaths() {
+ return mTestMappingPaths;
+ }
+
+ @VisibleForTesting
+ boolean getUseTestMappingPath() {
+ return mUseTestMappingPath;
+ }
+
/**
* Create individual tests with test infos for a module.
*
diff --git a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
index 0b1a3ad..9e56626 100644
--- a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
@@ -63,6 +63,7 @@
private static final String EXTRACTOR_SUCCESS = "1";
private static final String EXTRACTOR_FAILURE = "0";
private static final String EXTRACTOR_RUNTIME = "trace_extractor_runtime";
+ private static final String RAW_TRACE_FILE_SIZE = "perfetto_trace_file_size_bytes";
private static final String NSS_CACHE_ERROR = "base/nsscache-inl.h failed to lookup";
public enum METRIC_FILE_FORMAT {
@@ -191,6 +192,15 @@
processSrcFile = decompressFile(metricFile);
}
+ // Update the file size metrics.
+ if (processSrcFile != null) {
+ double perfettoFileSizeInBytes = processSrcFile.length();
+ Metric.Builder metricDurationBuilder = Metric.newBuilder();
+ metricDurationBuilder.getMeasurementsBuilder().setSingleDouble(
+ perfettoFileSizeInBytes);
+ data.addMetric(RAW_TRACE_FILE_SIZE, metricDurationBuilder.setType(DataType.RAW));
+ }
+
// Convert to perfetto metric format.
if (mConvertToMetricFile) {
File convertedMetricFile = convertToMetricProto(processSrcFile);
diff --git a/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
index 8d6031b..2594d7f 100644
--- a/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
@@ -23,6 +23,7 @@
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
/**
* Target preparer that performs "adb root" or "adb unroot" based on option "force-root".
@@ -79,7 +80,8 @@
private void throwOrLog(String message, DeviceDescriptor deviceDescriptor)
throws TargetSetupError {
if (mThrowOnError) {
- throw new TargetSetupError(message, deviceDescriptor);
+ throw new TargetSetupError(
+ message, deviceDescriptor, DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
} else {
CLog.w(message + " " + deviceDescriptor);
}
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index ed238fe..eda9d30 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
@@ -15,6 +15,7 @@
*/
package com.android.tradefed.testtype.binary;
+import com.google.common.annotations.VisibleForTesting;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.device.DeviceNotAvailableException;
@@ -30,6 +31,8 @@
import com.android.tradefed.testtype.IShardableTest;
import com.android.tradefed.testtype.ITestCollector;
import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.util.StreamUtil;
import java.io.File;
@@ -87,6 +90,16 @@
private Set<String> mIncludeFilters = new LinkedHashSet<>();
private Set<String> mExcludeFilters = new LinkedHashSet<>();
+ /**
+ * Get test commands.
+ *
+ * @return the test commands.
+ */
+ @VisibleForTesting
+ Map<String, String> getTestCommands() {
+ return mTestCommands;
+ }
+
/** @return the timeout applied to each binary for their execution. */
protected long getTimeoutPerBinaryMs() {
return mTimeoutPerBinaryMs;
@@ -152,7 +165,12 @@
if (shouldSkipCurrentTest(description)) continue;
if (path == null) {
listener.testRunStarted(testName, 0);
- listener.testRunFailed(String.format(NO_BINARY_ERROR, cmd));
+ FailureDescription failure =
+ FailureDescription.create(
+ String.format(NO_BINARY_ERROR, cmd),
+ FailureStatus.TEST_FAILURE)
+ .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+ listener.testRunFailed(failure);
listener.testRunEnded(0L, new HashMap<String, Metric>());
} else {
listener.testRunStarted(testName, 1);
@@ -247,26 +265,47 @@
/** {@inheritDoc} */
@Override
public final Collection<IRemoteTest> split() {
- if (mBinaryPaths.size() <= 2) {
+ int testCount = mBinaryPaths.size() + mTestCommands.size();
+ if (testCount <= 2) {
return null;
}
Collection<IRemoteTest> tests = new ArrayList<>();
for (String path : mBinaryPaths) {
- tests.add(getTestShard(path));
+ tests.add(getTestShard(path, null, null));
+ }
+ Map<String, String> testCommands = new LinkedHashMap<>(mTestCommands);
+ for (String testName : testCommands.keySet()) {
+ String cmd = testCommands.get(testName);
+ tests.add(getTestShard(null, testName, cmd));
}
return tests;
}
- private IRemoteTest getTestShard(String path) {
+ /**
+ * Get a testShard of ExecutableBaseTest.
+ *
+ * @param binaryPath the binary path for ExecutableHostTest.
+ * @param testName the test name for ExecutableTargetTest.
+ * @param cmd the test command for ExecutableTargetTest.
+ * @return a shard{@link IRemoteTest} of ExecutableBaseTest{@link ExecutableBaseTest}
+ */
+ private IRemoteTest getTestShard(String binaryPath, String testName, String cmd) {
ExecutableBaseTest shard = null;
try {
shard = this.getClass().getDeclaredConstructor().newInstance();
OptionCopier.copyOptionsNoThrow(this, shard);
- // We approximate the runtime of each shard to be equal since we can't know.
- shard.mRuntimeHintMs = mRuntimeHintMs / shard.mBinaryPaths.size();
- // Set one binary per shard
shard.mBinaryPaths.clear();
- shard.mBinaryPaths.add(path);
+ shard.mTestCommands.clear();
+ if (binaryPath != null) {
+ // Set one binary per shard
+ shard.mBinaryPaths.add(binaryPath);
+ } else if (testName != null && cmd != null) {
+ // Set one test command per shard
+ shard.mTestCommands.put(testName, cmd);
+ }
+ // Copy the filters to each shard
+ shard.mExcludeFilters.addAll(mExcludeFilters);
+ shard.mIncludeFilters.addAll(mIncludeFilters);
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
index 3d44499..50492f1 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
@@ -29,6 +29,7 @@
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.util.CommandResult;
@@ -158,8 +159,14 @@
try {
getTestInfo().getDevice().waitForDeviceAvailable();
} catch (DeviceNotAvailableException e) {
- listener.testRunFailed(
- String.format("Device became unavailable after %s.", binaryPath));
+ FailureDescription failure =
+ FailureDescription.create(
+ String.format(
+ "Device became unavailable after %s.", binaryPath),
+ FailureStatus.LOST_SYSTEM_UNDER_TEST)
+ .setErrorIdentifier(DeviceErrorIdentifier.DEVICE_UNAVAILABLE)
+ .setCause(e);
+ listener.testRunFailed(failure);
throw e;
}
}
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
index 85e7ae8..181117c 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableTargetTest.java
@@ -18,8 +18,10 @@
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
@@ -84,7 +86,10 @@
String.format(
"binary returned non-zero. Exit code: %d, stderr: %s, stdout: %s",
result.getExitCode(), result.getStderr(), result.getStdout());
- listener.testFailed(description, error_message);
+ listener.testFailed(
+ description,
+ FailureDescription.create(error_message)
+ .setFailureStatus(FailureStatus.TEST_FAILURE));
}
}
}
diff --git a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
index 91893ab..b871688 100644
--- a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
@@ -26,9 +26,11 @@
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.ByteArrayInputStreamSource;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
@@ -73,6 +75,7 @@
@VisibleForTesting static final String USE_TEST_OUTPUT_FILE_OPTION = "use-test-output-file";
static final String TEST_OUTPUT_FILE_FLAG = "test-output-file";
+ private static final String PYTHON_LOG_STDOUT_FORMAT = "%s-stdout";
private static final String PYTHON_LOG_STDERR_FORMAT = "%s-stderr";
private static final String PYTHON_LOG_TEST_OUTPUT_FORMAT = "%s-test-output";
@@ -316,7 +319,13 @@
+ "%s\nstderr:%s",
result.getStdout(), result.getStderr());
}
-
+ if (result.getStdout() != null) {
+ try (InputStreamSource data =
+ new ByteArrayInputStreamSource(result.getStdout().getBytes())) {
+ listener.testLog(
+ String.format(PYTHON_LOG_STDOUT_FORMAT, runName), LogDataType.TEXT, data);
+ }
+ }
File stderrFile = null;
try {
// Note that we still log stderr when parsing results from a test-written output file
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
index bd4df58..42187c5 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
@@ -24,9 +24,11 @@
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.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
@@ -130,8 +132,9 @@
commandLine.add(file.getAbsolutePath());
CLog.d("Run single Rust File: " + file.getAbsolutePath());
- // Add all the other options
+ // Add all the other options and include/exclude filters.
commandLine.addAll(mTestOptions);
+ addFiltersToArgs(commandLine);
List<String> listCommandLine = new ArrayList<>(commandLine);
listCommandLine.add("--list");
@@ -162,11 +165,15 @@
getRunUtil().runTimedCmd(mTestTimeout, commandLine.toArray(new String[0]));
long testTimeMs = System.currentTimeMillis() - startTimeMs;
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
- listener.testRunFailed("Fail to run: " + file.getAbsolutePath());
- CLog.e(
- "Something went wrong when running the rust binary:\nstdout: "
- + "%s\nstderr:%s",
- result.getStdout(), result.getStderr());
+ String message =
+ String.format(
+ "Something went wrong when running the rust binary:Exit Code: %s"
+ + "\nstdout: %s\nstderr: %s",
+ result.getExitCode(), result.getStdout(), result.getStderr());
+ FailureDescription failure =
+ FailureDescription.create(message, FailureStatus.TEST_FAILURE);
+ listener.testRunFailed(failure);
+ CLog.e(message);
}
File resultFile = null;
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java b/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
index 019e4b8..a35b722 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
@@ -32,9 +32,9 @@
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.NativeCodeCoverageListener;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
import com.android.tradefed.util.NativeCodeCoverageFlusher;
import java.io.File;
@@ -88,8 +88,6 @@
return mTestModule;
}
- // TODO(chh): implement test filter
-
/**
* Gets the path where tests live on the device.
*
@@ -166,6 +164,7 @@
} else {
cmd = fullPath;
}
+ cmd = addFiltersToCommand(cmd);
int testCount = 0;
try {
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java b/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
index a4c479e..aa84c00 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.IShellOutputReceiver;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.ITestFilterReceiver;
@@ -50,7 +51,12 @@
isTimeVal = true)
protected long mTestTimeout = 20 * 1000L; // milliseconds
+ @Option(
+ name = "include-filter",
+ description = "A substr filter of the test names to run; only the first one is used.")
private Set<String> mIncludeFilters = new LinkedHashSet<>();
+
+ @Option(name = "exclude-filter", description = "A substr filter of the test names to skip.")
private Set<String> mExcludeFilters = new LinkedHashSet<>();
// A wrapper that can be redefined in unit tests to create a (mocked) result parser.
@@ -88,10 +94,6 @@
return testCount;
}
- // TODO(b/145607401): make rust test runners accept filters
- // Now the following are just dummy methods,
- // to shut off run-time warning about not implementing ITestFilterReceiver.
-
/** {@inheritDoc} */
@Override
public void addIncludeFilter(String filter) {
@@ -139,4 +141,32 @@
public Set<String> getExcludeFilters() {
return mExcludeFilters;
}
+
+ private void checkMultipleIncludeFilters() {
+ if (mIncludeFilters.size() > 1) {
+ CLog.e("Found multiple include filters; all except the 1st are ignored.");
+ }
+ }
+
+ protected void addFiltersToArgs(List<String> args) {
+ checkMultipleIncludeFilters();
+ for (String s : mIncludeFilters) {
+ args.add(s);
+ }
+ for (String s : mExcludeFilters) {
+ args.add("--skip");
+ args.add(s);
+ }
+ }
+
+ protected String addFiltersToCommand(String cmd) {
+ if (!mIncludeFilters.isEmpty()) {
+ checkMultipleIncludeFilters();
+ cmd += " " + String.join(" ", mIncludeFilters);
+ }
+ if (!mExcludeFilters.isEmpty()) {
+ cmd += " --skip " + String.join(" --skip ", mExcludeFilters);
+ }
+ return cmd;
+ }
}
diff --git a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java b/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
index 4b19a35..c3ef3a5 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
+++ b/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
@@ -36,7 +36,8 @@
FAILED_TO_CONNECT_TO_GCE(30_501, FailureStatus.LOST_SYSTEM_UNDER_TEST),
ERROR_AFTER_FLASHING(30_502, FailureStatus.LOST_SYSTEM_UNDER_TEST),
- DEVICE_UNAVAILABLE(30_750, FailureStatus.LOST_SYSTEM_UNDER_TEST);
+ DEVICE_UNAVAILABLE(30_750, FailureStatus.LOST_SYSTEM_UNDER_TEST),
+ DEVICE_UNRESPONSIVE(30_751, FailureStatus.LOST_SYSTEM_UNDER_TEST);
private final long code;
private final FailureStatus status;
diff --git a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java b/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
index 3001231..8d65913 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
+++ b/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
@@ -26,6 +26,7 @@
// 10_001 - 10_500: General errors
ARTIFACT_NOT_FOUND(10_001, FailureStatus.INFRA_FAILURE),
FAIL_TO_CREATE_FILE(10_002, FailureStatus.INFRA_FAILURE),
+ INVOCATION_CANCELLED(10_003, FailureStatus.CANCELLED),
// 10_501 - 11_000: Build, Artifacts download related errors
ARTIFACT_REMOTE_PATH_NULL(10_501, FailureStatus.INFRA_FAILURE),
diff --git a/tests/res/testdata/test_mapping_with_mainline b/tests/res/testdata/test_mapping_with_mainline
index 8275f16..8e6afc8 100644
--- a/tests/res/testdata/test_mapping_with_mainline
+++ b/tests/res/testdata/test_mapping_with_mainline
@@ -17,7 +17,12 @@
]
},
{
- "name": "test[mod1.apk+mod2.apk]"
+ "name": "test[mod1.apk+mod2.apk]",
+ "options": [
+ {
+ "exclude-annotation": "test-annotation"
+ }
+ ]
}
]
}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 3262fff..832cc950 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -62,6 +62,7 @@
import com.android.tradefed.config.SandboxConfigurationFactoryTest;
import com.android.tradefed.config.gcs.GCSConfigurationFactoryTest;
import com.android.tradefed.config.gcs.GCSConfigurationServerTest;
+import com.android.tradefed.config.proxy.AutomatedReportersTest;
import com.android.tradefed.config.remote.GcsRemoteFileResolverTest;
import com.android.tradefed.config.remote.HttpRemoteFileResolverTest;
import com.android.tradefed.config.remote.LocalFileResolverTest;
@@ -480,6 +481,9 @@
GCSConfigurationServerTest.class,
GCSConfigurationFactoryTest.class,
+ // config.proxy
+ AutomatedReportersTest.class,
+
// config.remote
GcsRemoteFileResolverTest.class,
HttpRemoteFileResolverTest.class,
diff --git a/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java b/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java
index 53167f8..a1f2e59 100644
--- a/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java
+++ b/tests/src/com/android/tradefed/build/BootstrapBuildProviderTest.java
@@ -22,8 +22,10 @@
import com.android.ddmlib.IDevice;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.invoker.ExecutionFiles;
import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.util.FileUtil;
import org.easymock.EasyMock;
import org.junit.Before;
@@ -31,6 +33,8 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import java.io.File;
+
/** Unit tests for {@link BootstrapBuildProvider}. */
@RunWith(JUnit4.class)
public class BootstrapBuildProviderTest {
@@ -71,6 +75,34 @@
}
}
+ @Test
+ public void testGetBuild_add_extra_file() throws Exception {
+ EasyMock.expect(mMockDevice.getBuildId()).andReturn("5");
+ EasyMock.expect(mMockDevice.getIDevice()).andReturn(EasyMock.createMock(IDevice.class));
+ EasyMock.expect(mMockDevice.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
+ EasyMock.expect(mMockDevice.getProperty(EasyMock.anyObject())).andStubReturn("property");
+ EasyMock.expect(mMockDevice.getProductVariant()).andStubReturn("variant");
+ EasyMock.expect(mMockDevice.getBuildFlavor()).andStubReturn("flavor");
+ EasyMock.expect(mMockDevice.getBuildAlias()).andStubReturn("alias");
+ EasyMock.replay(mMockDevice);
+ OptionSetter setter = new OptionSetter(mProvider);
+ File tmpDir = FileUtil.createTempDir("tmp");
+ File file_1 = new File(tmpDir, "sys.img");
+ setter.setOptionValue("extra-file", "file_1", file_1.getAbsolutePath());
+ IBuildInfo res = mProvider.getBuild(mMockDevice);
+ assertNotNull(res);
+ try {
+ assertTrue(res instanceof IDeviceBuildInfo);
+ // Ensure tests dir is never null
+ assertTrue(((IDeviceBuildInfo) res).getTestsDir() != null);
+ assertEquals(((IDeviceBuildInfo) res).getFile("file_1"), file_1);
+ EasyMock.verify(mMockDevice);
+ } finally {
+ mProvider.cleanUp(res);
+ FileUtil.recursiveDelete(tmpDir);
+ }
+ }
+
/**
* Test that when using the provider with a StubDevice information that cannot be queried are
* stubbed.
diff --git a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
index 53e268c..ffa3255 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -16,6 +16,9 @@
package com.android.tradefed.config;
import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IBuildProvider;
+import com.android.tradefed.build.IDeviceBuildProvider;
import com.android.tradefed.build.LocalDeviceBuildProvider;
import com.android.tradefed.config.ConfigurationDef.ConfigObjectDef;
import com.android.tradefed.config.ConfigurationFactory.ConfigId;
@@ -1801,6 +1804,28 @@
}
}
+ /** Test that a YAML config command line parse correctly. */
+ public void testCreateConfigurationFromArgs_yaml() throws Exception {
+ IConfiguration config =
+ mFactory.createConfigurationFromArgs(
+ new String[] {
+ "yaml/test-config.tf_yaml",
+ "--build-id",
+ "5",
+ "--build-flavor",
+ "test",
+ "--branch",
+ "main"
+ });
+ assertNotNull(config);
+ IBuildProvider provider = config.getBuildProvider();
+ assertTrue(provider instanceof IDeviceBuildProvider);
+ IBuildInfo info = ((IDeviceBuildProvider) provider).getBuild(null);
+ assertEquals("5", info.getBuildId());
+ assertEquals("test", info.getBuildFlavor());
+ assertEquals("main", info.getBuildBranch());
+ }
+
private static String getClassName(String name) {
// -6 because of .class
return name.substring(0, name.length() - 6).replace('/', '.');
diff --git a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
index cf20f8f..eded0ac 100644
--- a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
@@ -28,6 +28,7 @@
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.StubBuildProvider;
import com.android.tradefed.config.DynamicRemoteFileResolver.FileResolverLoader;
+import com.android.tradefed.config.DynamicRemoteFileResolver.ResolverLoadingException;
import com.android.tradefed.config.DynamicRemoteFileResolver.ServiceFileResolverLoader;
import com.android.tradefed.config.remote.GcsRemoteFileResolver;
import com.android.tradefed.config.remote.IRemoteFileResolver;
@@ -168,34 +169,6 @@
EasyMock.verify(mMockResolver);
}
- @Test
- public void testResolveWithQuery_overrides() throws Exception {
- RemoteFileOption object = new RemoteFileOption();
- OptionSetter setter = new OptionSetter(object);
-
- File fake = temporaryFolder.newFile();
-
- setter.setOptionValue("remote-file", "gs://fake/path?key=value");
- assertEquals("gs:/fake/path?key=value", object.remoteFile.getPath());
-
- Map<String, String> testMap = new HashMap<>();
- testMap.put("key", "override" /* The args value is overriden*/);
- EasyMock.expect(
- mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path")), EasyMock.eq(testMap)))
- .andReturn(fake);
- EasyMock.replay(mMockResolver);
- Map<String, String> extraArgs = new HashMap<>();
- extraArgs.put("key", "override");
- mResolver.addExtraArgs(extraArgs);
- Set<File> downloadedFile = setter.validateRemoteFilePath(mResolver);
- assertEquals(1, downloadedFile.size());
- File downloaded = downloadedFile.iterator().next();
- // The file has been replaced by the downloaded one.
- assertEquals(downloaded.getAbsolutePath(), object.remoteFile.getAbsolutePath());
- EasyMock.verify(mMockResolver);
- }
-
/** Test to make sure that a dynamic download marked as "optional" does not throw */
@Test
public void testResolveOptional() throws Exception {
@@ -661,6 +634,210 @@
}
@Test
+ public void resolverLoader_throwsIfMissingMandatoryOption() throws Exception {
+ ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = ResolverWithOptions.minimalConfig();
+ config.remove(ResolverWithOptions.MANDATORY_OPTION_FQN);
+
+ try {
+ loader.load(ResolverWithOptions.PROTOCOL, config);
+ fail();
+ } catch (ResolverLoadingException expected) {
+ assertThat(expected)
+ .hasCauseThat()
+ .hasMessageThat()
+ .contains(ResolverWithOptions.MANDATORY_OPTION_NAME);
+ }
+ }
+
+ @Test
+ public void resolverLoader_setsNonMandatoryOption() throws Exception {
+ String optionValue = "value";
+ ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = ResolverWithOptions.minimalConfig();
+ config.put(ResolverWithOptions.NON_MANDATORY_OPTION_FQN, optionValue);
+
+ IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+ assertThat(((ResolverWithOptions) resolver).nonMandatoryOption).isEqualTo(optionValue);
+ }
+
+ @Test
+ public void resolverLoader_throwsForMapOption() throws Exception {
+ ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = ResolverWithOptions.minimalConfig();
+ config.put(ResolverWithOptions.MAP_OPTION_FQN, "key=value");
+
+ try {
+ loader.load(ResolverWithOptions.PROTOCOL, config);
+ fail();
+ } catch (ResolverLoadingException expected) {
+ assertThat(expected)
+ .hasCauseThat()
+ .hasMessageThat()
+ .contains(ResolverWithOptions.MAP_OPTION_FQN);
+ assertThat(expected).hasCauseThat().hasMessageThat().contains("not supported");
+ }
+ }
+
+ @Test
+ public void resolverLoader_resolvesFileOption() throws Exception {
+ File file = temporaryFolder.newFile();
+ ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = ResolverWithOptions.minimalConfig();
+ config.put(ResolverWithOptions.MANDATORY_OPTION_FQN, file.toURI().toString());
+
+ IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+ assertThat(((ResolverWithOptions) resolver).mandatoryOption).isEqualTo(file);
+ }
+
+ @Test
+ public void resolverLoader_doesNotResolveFileOptionWithUnsupportedScheme() throws Exception {
+ ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = ResolverWithOptions.minimalConfig();
+ config.put(ResolverWithOptions.MANDATORY_OPTION_FQN, "missing://tmp");
+
+ IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+ assertThat(((ResolverWithOptions) resolver).mandatoryOption.toString())
+ .contains("missing:");
+ }
+
+ @Test
+ public void resolverLoader_resolvesResolverFileOption() throws Exception {
+ File f = new File("/tmp/a-file");
+ ClassLoader classLoader =
+ classLoaderWithProviders(
+ ResolverWithOptions.class
+ .getName(), // Contains a file option resolved by the next.
+ AnotherResolverWithOptions.class.getName());
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = new HashMap<>();
+ config.putAll(ResolverWithOptions.minimalConfig());
+ config.putAll(AnotherResolverWithOptions.minimalConfig());
+ config.put(
+ ResolverWithOptions.MANDATORY_OPTION_FQN,
+ AnotherResolverWithOptions.PROTOCOL + "://a-file");
+ config.put(AnotherResolverWithOptions.MANDATORY_OPTION_FQN, f.toString());
+
+ IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+ assertThat(((ResolverWithOptions) resolver).mandatoryOption).isEqualTo(f);
+ }
+
+ @Test
+ public void resolverLoader_throwsOnInitializationCycles() throws Exception {
+ ClassLoader classLoader = classLoaderWithProviders(ResolverWithOptions.class.getName());
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = ResolverWithOptions.minimalConfig();
+ config.put(
+ ResolverWithOptions.MANDATORY_OPTION_FQN,
+ ResolverWithOptions.PROTOCOL + "://a-file");
+
+ try {
+ loader.load(ResolverWithOptions.PROTOCOL, config);
+ fail();
+ } catch (ResolverLoadingException expected) {
+ assertThat(expected)
+ .hasCauseThat()
+ .hasCauseThat()
+ .hasMessageThat()
+ .contains("Cycle detected");
+ }
+ }
+
+ @Test
+ public void resolverLoader_onlyInitializesOptionsOfUsedResolvers() throws Exception {
+ File f = new File("/tmp/a-file");
+ ClassLoader classLoader =
+ classLoaderWithProviders(
+ ResolverWithOptions.class.getName(),
+ AnotherResolverWithOptions.class
+ .getName()); // Requires option but never used.
+ FileResolverLoader loader = new ServiceFileResolverLoader(classLoader);
+ Map<String, String> config = new HashMap<>();
+ config.putAll(ResolverWithOptions.minimalConfig());
+ config.put(ResolverWithOptions.MANDATORY_OPTION_FQN, f.toString());
+
+ IRemoteFileResolver resolver = loader.load(ResolverWithOptions.PROTOCOL, config);
+
+ assertThat(((ResolverWithOptions) resolver).mandatoryOption).isEqualTo(f);
+ }
+
+ public static final class ResolverWithOptions implements IRemoteFileResolver {
+ static final String PROTOCOL = "rwo";
+ static final String MANDATORY_OPTION_NAME = "mandatory";
+ static final String MANDATORY_OPTION_FQN =
+ optionFqn(ResolverWithOptions.class, MANDATORY_OPTION_NAME);
+ static final String NON_MANDATORY_OPTION_NAME = "nonMandatory";
+ static final String NON_MANDATORY_OPTION_FQN =
+ optionFqn(ResolverWithOptions.class, NON_MANDATORY_OPTION_NAME);
+ static final String MAP_OPTION_NAME = "map";
+ static final String MAP_OPTION_FQN = optionFqn(ResolverWithOptions.class, MAP_OPTION_NAME);
+
+ static Map<String, String> minimalConfig() {
+ Map<String, String> config = new HashMap<>();
+ config.put(MANDATORY_OPTION_FQN, "anything");
+ return config;
+ }
+
+ @Option(name = MANDATORY_OPTION_NAME, mandatory = true)
+ private File mandatoryOption;
+
+ @Option(name = NON_MANDATORY_OPTION_NAME)
+ private String nonMandatoryOption;
+
+ @Option(name = MAP_OPTION_NAME)
+ private Map<String, String> mapOption;
+
+ @Override
+ public String getSupportedProtocol() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public File resolveRemoteFiles(File consideredFile, Map<String, String> queryArgs) {
+ return mandatoryOption;
+ }
+ }
+
+ public static final class AnotherResolverWithOptions implements IRemoteFileResolver {
+ static final String PROTOCOL = "arwo";
+ static final String MANDATORY_OPTION_NAME = "mandatory";
+ static final String MANDATORY_OPTION_FQN =
+ optionFqn(AnotherResolverWithOptions.class, MANDATORY_OPTION_NAME);
+
+ static Map<String, String> minimalConfig() {
+ Map<String, String> config = new HashMap<>();
+ config.put(MANDATORY_OPTION_FQN, "anything");
+ return config;
+ }
+
+ @Option(name = MANDATORY_OPTION_NAME, mandatory = true)
+ private File mandatoryOption;
+
+ @Override
+ public String getSupportedProtocol() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public File resolveRemoteFiles(File consideredFile, Map<String, String> queryArgs) {
+ return mandatoryOption;
+ }
+ }
+
+ private static String optionFqn(Class<?> cls, String name) {
+ return cls.getName() + ":" + name;
+ }
+
+ @Test
public void testMultiDevices() throws Exception {
IConfiguration configuration = new Configuration("test", "test");
diff --git a/tests/src/com/android/tradefed/config/proxy/AutomatedReportersTest.java b/tests/src/com/android/tradefed/config/proxy/AutomatedReportersTest.java
new file mode 100644
index 0000000..9e3684f
--- /dev/null
+++ b/tests/src/com/android/tradefed/config/proxy/AutomatedReportersTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 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.config.proxy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.result.proto.StreamProtoResultReporter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AutomatedReporters}. */
+@RunWith(JUnit4.class)
+public class AutomatedReportersTest {
+
+ private AutomatedReporters mReporter =
+ new AutomatedReporters() {
+ @Override
+ protected String getEnv(String key) {
+ if (key.equals("PROTO_REPORTING_PORT")) {
+ return "8888";
+ }
+ return null;
+ }
+ };
+
+ @Test
+ public void testReporting() {
+ IConfiguration config = new Configuration("name", "test");
+ assertEquals(1, config.getTestInvocationListeners().size());
+ mReporter.applyAutomatedReporters(config);
+ assertEquals(2, config.getTestInvocationListeners().size());
+ assertTrue(config.getTestInvocationListeners().get(1) instanceof StreamProtoResultReporter);
+ StreamProtoResultReporter protoReporter =
+ (StreamProtoResultReporter) config.getTestInvocationListeners().get(1);
+ assertEquals(Integer.valueOf(8888), protoReporter.getProtoReportPort());
+ }
+}
diff --git a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
index a001f55..6a1c081 100644
--- a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
+++ b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertTrue;
import com.android.tradefed.build.DependenciesResolver;
+import com.android.tradefed.build.StubBuildProvider;
import com.android.tradefed.config.ConfigurationDef;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.result.ITestInvocationListener;
@@ -57,7 +58,7 @@
@Test
public void testParseConfig() throws Exception {
try (InputStream res = readFromRes(YAML_TEST_CONFIG_1)) {
- mParser.parse(mConfigDef, "source", res);
+ mParser.parse(mConfigDef, "source", res, false);
// Create the configuration to test the flow
IConfiguration config = mConfigDef.createConfiguration();
config.validateOptions();
@@ -104,6 +105,46 @@
}
}
+ @Test
+ public void testParseModule() throws Exception {
+ try (InputStream res = readFromRes(YAML_TEST_CONFIG_1)) {
+ mParser.parse(mConfigDef, "source", res, true /* createdAsModule */);
+ // Create the configuration to test the flow
+ IConfiguration config = mConfigDef.createConfiguration();
+ config.validateOptions();
+ // build provider isn't set
+ assertTrue(config.getBuildProvider() instanceof StubBuildProvider);
+ // Test
+ assertEquals(1, config.getTests().size());
+ assertTrue(config.getTests().get(0) instanceof AndroidJUnitTest);
+ assertEquals(
+ "android.package",
+ ((AndroidJUnitTest) config.getTests().get(0)).getPackageName());
+
+ // Dependencies
+ // apk dependencies
+ assertEquals(2, config.getTargetPreparers().size());
+ ITargetPreparer installApk = config.getTargetPreparers().get(0);
+ assertTrue(installApk instanceof SuiteApkInstaller);
+ assertThat(((SuiteApkInstaller) installApk).getTestsFileName())
+ .containsExactly(
+ new File("test.apk"), new File("test2.apk"), new File("test1.apk"));
+ // device file dependencies
+ ITargetPreparer pushFile = config.getTargetPreparers().get(1);
+ assertTrue(pushFile instanceof PushFilePreparer);
+ assertThat(((PushFilePreparer) pushFile).getPushSpecs(null))
+ .containsExactly(
+ "/sdcard/",
+ new File("tobepushed2.txt"),
+ "/sdcard",
+ new File("tobepushed.txt"));
+ // Result reporters aren't set
+ List<ITestInvocationListener> listeners = config.getTestInvocationListeners();
+ // TODO: Renable when matching project is updated
+ // assertTrue(listeners.get(0) instanceof TextResultReporter);
+ }
+ }
+
private InputStream readFromRes(String resourceFile) {
return getClass().getResourceAsStream(resourceFile);
}
diff --git a/tests/src/com/android/tradefed/device/MockDeviceManager.java b/tests/src/com/android/tradefed/device/MockDeviceManager.java
index 86b58f9..997b972 100644
--- a/tests/src/com/android/tradefed/device/MockDeviceManager.java
+++ b/tests/src/com/android/tradefed/device/MockDeviceManager.java
@@ -454,4 +454,9 @@
public String getAdbVersion() {
return null;
}
+
+ @Override
+ public void addMonitoringTcpFastbootDevice(String serial, String fastboot_serial) {
+ // ignore
+ }
}
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index c77f3ab..2a3974d 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -1686,10 +1686,16 @@
public TestDeviceState getDeviceState() {
return TestDeviceState.ONLINE;
}
+
+ @Override
+ public String getFastbootSerialNumber() {
+ return MOCK_DEVICE_SERIAL;
+ }
};
String into = "bootloader";
mMockIDevice.reboot(into);
EasyMock.expectLastCall();
+ mMockStateMonitor.setFastbootSerialNumber(MOCK_DEVICE_SERIAL);
EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
.andReturn(true);
EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -1718,7 +1724,13 @@
}
return new CommandResult();
}
+
+ @Override
+ public String getFastbootSerialNumber() {
+ return MOCK_DEVICE_SERIAL;
+ }
};
+ mMockStateMonitor.setFastbootSerialNumber(MOCK_DEVICE_SERIAL);
EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
.andReturn(true);
EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -1736,10 +1748,16 @@
public TestDeviceState getDeviceState() {
return TestDeviceState.ONLINE;
}
+
+ @Override
+ public String getFastbootSerialNumber() {
+ return MOCK_DEVICE_SERIAL;
+ }
};
String into = "fastboot";
mMockIDevice.reboot(into);
EasyMock.expectLastCall();
+ mMockStateMonitor.setFastbootSerialNumber(MOCK_DEVICE_SERIAL);
EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
.andReturn(true);
EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -1768,7 +1786,13 @@
}
return new CommandResult();
}
+
+ @Override
+ public String getFastbootSerialNumber() {
+ return MOCK_DEVICE_SERIAL;
+ }
};
+ mMockStateMonitor.setFastbootSerialNumber(MOCK_DEVICE_SERIAL);
EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
.andReturn(true);
EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -3235,4 +3259,116 @@
.andReturn(stubResult);
EasyMock.replay(mMockRunUtil);
}
+
+ /** Test {@link NativeDevice#getFastbootSerialNumber()} with USB adb. */
+ @Test
+ public void testGetFastbootSerialNumber_usb_adb() {
+ mTestDevice =
+ new TestableAndroidNativeDevice() {
+ @Override
+ public boolean isAdbTcp() {
+ return false;
+ }
+ };
+ EasyMock.replay(mMockIDevice);
+ assertEquals(MOCK_DEVICE_SERIAL, mTestDevice.getFastbootSerialNumber());
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test {@link NativeDevice#getFastbootSerialNumber()} with non-root TCP adb. */
+ @Test
+ public void testGetFastbootSerialNumber_non_root_tcp_adb() throws Exception {
+ String address = "00:30:1b:ba:81:28";
+ IDevice device =
+ new StubDevice(MOCK_DEVICE_SERIAL) {
+ @Override
+ public void executeShellCommand(String command, IShellOutputReceiver receiver)
+ throws TimeoutException, AdbCommandRejectedException,
+ ShellCommandUnresponsiveException, IOException {
+ receiver.addOutput(address.getBytes(), 0, address.length());
+ }
+ };
+ mTestDevice =
+ new TestableAndroidNativeDevice() {
+ @Override
+ IHostOptions getHostOptions() {
+ return new HostOptions() {
+ @Override
+ public String getNetworkInterface() {
+ return "en0";
+ }
+ };
+ }
+
+ @Override
+ public boolean isAdbTcp() {
+ return true;
+ }
+
+ @Override
+ public boolean isAdbRoot() throws DeviceNotAvailableException {
+ return false;
+ }
+
+ @Override
+ public boolean enableAdbRoot() throws DeviceNotAvailableException {
+ return true;
+ }
+
+ @Override
+ public boolean disableAdbRoot() throws DeviceNotAvailableException {
+ return true;
+ }
+ };
+ mMockIDevice.executeShellCommand(EasyMock.anyObject(), EasyMock.anyObject());
+ EasyMock.expectLastCall().andDelegateTo(device).anyTimes();
+ EasyMock.replay(mMockIDevice);
+ assertEquals(
+ "tcp:fe80:0:0:0:230:1bff:feba:8128%en0", mTestDevice.getFastbootSerialNumber());
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test {@link NativeDevice#getFastbootSerialNumber()} with root TCP adb. */
+ @Test
+ public void testGetFastbootSerialNumber_root_tcp_adb() throws Exception {
+ String address = "00:30:1b:ba:81:28";
+ IDevice device =
+ new StubDevice(MOCK_DEVICE_SERIAL) {
+ @Override
+ public void executeShellCommand(String command, IShellOutputReceiver receiver)
+ throws TimeoutException, AdbCommandRejectedException,
+ ShellCommandUnresponsiveException, IOException {
+ receiver.addOutput(address.getBytes(), 0, address.length());
+ }
+ };
+ mTestDevice =
+ new TestableAndroidNativeDevice() {
+ @Override
+ IHostOptions getHostOptions() {
+ return new HostOptions() {
+ @Override
+ public String getNetworkInterface() {
+ return "en0";
+ }
+ };
+ }
+
+ @Override
+ public boolean isAdbTcp() {
+ return true;
+ }
+
+ @Override
+ public boolean isAdbRoot() throws DeviceNotAvailableException {
+ return true;
+ }
+ };
+
+ mMockIDevice.executeShellCommand(EasyMock.anyObject(), EasyMock.anyObject());
+ EasyMock.expectLastCall().andDelegateTo(device).anyTimes();
+ EasyMock.replay(mMockIDevice);
+ assertEquals(
+ "tcp:fe80:0:0:0:230:1bff:feba:8128%en0", mTestDevice.getFastbootSerialNumber());
+ EasyMock.verify(mMockIDevice);
+ }
}
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 5de8772..e6007fd 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -129,6 +129,11 @@
// Avoid issue with GlobalConfiguration
return new HostOptions();
}
+
+ @Override
+ public boolean isAdbTcp() {
+ return false;
+ }
}
/**
diff --git a/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java b/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
index 3b5cb68..855403a 100644
--- a/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
+++ b/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
@@ -197,6 +197,7 @@
EasyMock.eq("reboot")))
.andReturn(result);
+ EasyMock.expect(mMockMonitor.getFastbootSerialNumber()).andReturn("serial");
EasyMock.expect(mMockMonitor.waitForDeviceOnline(EasyMock.anyLong())).andReturn(null);
replayMocks();
try {
@@ -247,6 +248,7 @@
CommandResult result = new CommandResult();
result.setStatus(CommandStatus.SUCCESS);
// expect reboot
+ EasyMock.expect(mMockMonitor.getFastbootSerialNumber()).andReturn("serial");
EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("reboot"))).
andReturn(result);
diff --git a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
index 24418a6..a9807b0 100644
--- a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
@@ -120,6 +120,9 @@
assertTrue("Trace duration metrics not available but expected.",
currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
.getSingleDouble() >= 0);
+ assertTrue("Trace file size metric is not available.",
+ currentMetrics.get("perfetto_trace_file_size_bytes").getMeasurements()
+ .getSingleDouble() >= 0);
}
@Test
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index 77624d5..b66ba56 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -89,6 +89,7 @@
.andReturn(null);
EasyMock.expect(mMockConfig.getConfigurationObject(ShardHelper.LAST_SHARD_DETECTOR))
.andReturn(null);
+ EasyMock.expect(mMockConfig.getConfigurationObject("DELEGATE")).andStubReturn(null);
mMockRescheduler = EasyMock.createMock(IRescheduler.class);
mMockTestListener = EasyMock.createMock(ITestInvocationListener.class);
mMockLogSaver = EasyMock.createMock(ILogSaver.class);
@@ -113,6 +114,11 @@
}
@Override
+ protected void applyAutomatedReporters(IConfiguration config) {
+ // Empty on purpose
+ }
+
+ @Override
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index 2dd6327..f8c4eb2 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -309,7 +309,7 @@
@Override
protected void setExitCode(ExitCode code, Throwable stack) {
- // empty on purpose
+ // Empty on purpose
}
@Override
@@ -320,7 +320,12 @@
@Override
public void registerExecutionFiles(ExecutionFiles executionFiles) {
- // Empty of purpose
+ // Empty on purpose
+ }
+
+ @Override
+ protected void applyAutomatedReporters(IConfiguration config) {
+ // Empty on purpose
}
@Override
@@ -1659,6 +1664,11 @@
}
@Override
+ protected void applyAutomatedReporters(IConfiguration config) {
+ // Empty on purpose
+ }
+
+ @Override
protected void addInvocationMetric(InvocationMetricKey key, long value) {}
@Override
@@ -1732,6 +1742,11 @@
}
@Override
+ protected void applyAutomatedReporters(IConfiguration config) {
+ // Empty on purpose
+ }
+
+ @Override
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
@@ -1839,6 +1854,11 @@
}
@Override
+ protected void applyAutomatedReporters(IConfiguration config) {
+ // Empty on purpose
+ }
+
+ @Override
InvocationScope getInvocationScope() {
// Avoid re-entry in the current TF invocation scope for unit tests.
return new InvocationScope();
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
index 5dae1dd..ceec73d 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
@@ -36,6 +36,8 @@
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
@@ -97,8 +99,12 @@
mExecutableTest.run(mTestInfo, mMockListener);
verify(mMockListener, Mockito.times(1)).testRunStarted(eq("test"), eq(0));
- verify(mMockListener, Mockito.times(1))
- .testRunFailed(String.format(ExecutableBaseTest.NO_BINARY_ERROR, path));
+ FailureDescription failure =
+ FailureDescription.create(
+ String.format(ExecutableBaseTest.NO_BINARY_ERROR, path),
+ FailureStatus.TEST_FAILURE)
+ .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+ verify(mMockListener, Mockito.times(1)).testRunFailed(failure);
verify(mMockListener, Mockito.times(1))
.testRunEnded(eq(0L), Mockito.<HashMap<String, Metric>>any());
}
@@ -184,20 +190,25 @@
doThrow(new DeviceNotAvailableException("test", "serial"))
.when(mMockDevice)
.waitForDeviceAvailable();
+ DeviceNotAvailableException dnae = null;
try {
mExecutableTest.run(mTestInfo, mMockListener);
fail("Should have thrown an exception.");
} catch (DeviceNotAvailableException expected) {
// Expected
+ dnae = expected;
}
verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
- verify(mMockListener, Mockito.times(1))
- .testRunFailed(
- eq(
+ FailureDescription failure =
+ FailureDescription.create(
String.format(
"Device became unavailable after %s.",
- tmpBinary.getAbsolutePath())));
+ tmpBinary.getAbsolutePath()),
+ FailureStatus.LOST_SYSTEM_UNDER_TEST)
+ .setErrorIdentifier(DeviceErrorIdentifier.DEVICE_UNAVAILABLE)
+ .setCause(dnae);
+ verify(mMockListener, Mockito.times(1)).testRunFailed(eq(failure));
verify(mMockListener, Mockito.times(0)).testFailed(any(), (String) any());
verify(mMockListener, Mockito.times(1))
.testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
@@ -259,12 +270,14 @@
mExecutableTest.run(mTestInfo, mMockListener);
verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(0));
- verify(mMockListener, Mockito.times(1))
- .testRunFailed(
- eq(
+ FailureDescription failure =
+ FailureDescription.create(
String.format(
ExecutableBaseTest.NO_BINARY_ERROR,
- tmpBinary.getName())));
+ tmpBinary.getName()),
+ FailureStatus.TEST_FAILURE)
+ .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+ verify(mMockListener, Mockito.times(1)).testRunFailed(eq(failure));
verify(mMockListener, Mockito.times(1))
.testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
} finally {
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
index 05cf59a..5c628df 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
@@ -15,6 +15,7 @@
*/
package com.android.tradefed.testtype.binary;
+import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import com.android.tradefed.config.ConfigurationException;
@@ -24,8 +25,12 @@
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.metrics.proto.MetricMeasurement;
+import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.CommandResult;
import org.junit.Before;
@@ -35,6 +40,8 @@
import org.mockito.Mockito;
import java.util.HashMap;
+import java.util.Collection;
+import java.util.Map;
/** Unit tests for {@link com.android.tradefed.testtype.binary.ExecutableTargetTest}. */
@RunWith(JUnit4.class)
@@ -45,7 +52,6 @@
private final String testCmd2 = "cmd2";
private final String testName3 = "testName3";
private final String testCmd3 = "cmd3";
- private static final String NO_BINARY_ERROR = "Binary %s does not exist.";
private static final String ERROR_MESSAGE = "binary returned non-zero exit code.";
private ITestInvocationListener mListener = null;
@@ -133,12 +139,20 @@
mExecutableTargetTest.run(mTestInfo, mListener);
// run cmd1 test
Mockito.verify(mListener, Mockito.times(0)).testRunStarted(eq(testName1), eq(1));
- Mockito.verify(mListener, Mockito.times(1))
- .testRunFailed(String.format(NO_BINARY_ERROR, testCmd1));
+ FailureDescription failure1 =
+ FailureDescription.create(
+ String.format(ExecutableBaseTest.NO_BINARY_ERROR, testCmd1),
+ FailureStatus.TEST_FAILURE)
+ .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+ Mockito.verify(mListener, Mockito.times(1)).testRunFailed(failure1);
// run cmd2 test
Mockito.verify(mListener, Mockito.times(0)).testRunStarted(eq(testName2), eq(1));
- Mockito.verify(mListener, Mockito.times(1))
- .testRunFailed(String.format(NO_BINARY_ERROR, testCmd2));
+ FailureDescription failure2 =
+ FailureDescription.create(
+ String.format(ExecutableBaseTest.NO_BINARY_ERROR, testCmd2),
+ FailureStatus.TEST_FAILURE)
+ .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+ Mockito.verify(mListener, Mockito.times(1)).testRunFailed(failure2);
Mockito.verify(mListener, Mockito.times(2))
.testRunEnded(
Mockito.anyLong(),
@@ -358,4 +372,31 @@
Mockito.eq(testDescription3),
Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
}
+
+ /** Test split() for sharding */
+ @Test
+ public void testShard_Split() throws ConfigurationException {
+ mExecutableTargetTest = new ExecutableTargetTest();
+ // Set test commands
+ OptionSetter setter = new OptionSetter(mExecutableTargetTest);
+ setter.setOptionValue("test-command-line", testName1, testCmd1);
+ setter.setOptionValue("test-command-line", testName2, testCmd2);
+ setter.setOptionValue("test-command-line", testName3, testCmd3);
+ // Split the shard.
+ Collection<IRemoteTest> testShards = mExecutableTargetTest.split();
+ // Test the size of the test Shard.
+ assertEquals(3, testShards.size());
+ // Test the command of each shard.
+ for (IRemoteTest test : testShards) {
+ Map<String, String> TestCommands = ((ExecutableTargetTest) test).getTestCommands();
+ String cmd1 = TestCommands.get(testName1);
+ if (cmd1 != null) assertEquals(testCmd1, cmd1);
+ String cmd2 = TestCommands.get(testName2);
+ if (cmd2 != null) assertEquals(testCmd2, cmd2);
+ String cmd3 = TestCommands.get(testName3);
+ if (cmd3 != null) assertEquals(testCmd3, cmd3);
+ // The test command should equals to one of them.
+ assertEquals(true, cmd1 != null || cmd2 != null || cmd3 != null);
+ }
+ }
}
diff --git a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
index c681fc7..de69a05 100644
--- a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
@@ -69,13 +69,13 @@
private static final String DEVICE_SERIAL = "X123SER";
private static final long DEFAULT_TIME_OUT = 30 * 1000L;
private static final String TEST_RESULT_FILE_NAME = "test_summary.yaml";
- private static final String TEMP_DIR = "/tmp";
private MoblyBinaryHostTest mSpyTest;
private ITestDevice mMockDevice;
private IRunUtil mMockRunUtil;
private MoblyYamlResultParser mMockParser;
private InputStream mMockSummaryInputStream;
+ private File mMoblyTestDir;
private File mMoblyBinary; // used by python-binaries option
private File mMoblyBinary2; // used by par-file-name option
private DeviceBuildInfo mMockBuildInfo;
@@ -90,15 +90,15 @@
Mockito.doReturn(mMockRunUtil).when(mSpyTest).getRunUtil();
Mockito.doReturn(DEFAULT_TIME_OUT).when(mSpyTest).getTestTimeout();
Mockito.doReturn("not_adb").when(mSpyTest).getAdbPath();
- mMoblyBinary = FileUtil.createTempFile("mobly_binary", ".par");
- mMoblyBinary2 = FileUtil.createTempFile("mobly_binary_2", ".par");
+ mMoblyTestDir = FileUtil.createTempDir("mobly_tests");
+ mMoblyBinary = FileUtil.createTempFile("mobly_binary", ".par", mMoblyTestDir);
+ mMoblyBinary2 = FileUtil.createTempFile("mobly_binary_2", ".par", mMoblyTestDir);
mSpyTest.setBuild(mMockBuildInfo);
}
@After
public void tearDown() throws Exception {
- FileUtil.deleteFile(mMoblyBinary);
- FileUtil.deleteFile(mMoblyBinary2);
+ FileUtil.recursiveDelete(mMoblyTestDir);
}
@Test
@@ -142,7 +142,7 @@
public void testRun_withParFileNameOption() throws Exception {
OptionSetter setter = new OptionSetter(mSpyTest);
setter.setOptionValue("par-file-name", mMoblyBinary2.getName());
- Mockito.doReturn(new File(TEMP_DIR)).when(mMockBuildInfo).getTestsDir();
+ Mockito.doReturn(mMoblyTestDir).when(mMockBuildInfo).getTestsDir();
File testResult = new File(mSpyTest.getLogDirAbsolutePath(), TEST_RESULT_FILE_NAME);
Mockito.when(mMockRunUtil.runTimedCmd(anyLong(), any()))
.thenAnswer(
@@ -168,7 +168,7 @@
public void testRun_withParFileNameOption_binaryNotFound() throws Exception {
OptionSetter setter = new OptionSetter(mSpyTest);
setter.setOptionValue("par-file-name", mMoblyBinary2.getName());
- Mockito.doReturn(new File(TEMP_DIR)).when(mMockBuildInfo).getTestsDir();
+ Mockito.doReturn(mMoblyTestDir).when(mMockBuildInfo).getTestsDir();
FileUtil.deleteFile(mMoblyBinary2);
try {
@@ -205,6 +205,9 @@
fail("Should have thrown an exception");
} catch (RuntimeException e) {
assertThat(
+ String.format(
+ "An unexpected exception was thrown, full stack trace: %s",
+ Throwables.getStackTraceAsString(e)),
e.getMessage(),
containsString(
"Fail to find test summary file test_summary.yaml under directory"));
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index b0f8d09..1cb5dd1 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -122,6 +122,7 @@
CommandResult res = new CommandResult();
res.setStatus(CommandStatus.SUCCESS);
+ res.setStdout("python binary stdout.");
res.setStderr("TEST_RUN_STARTED {\"testCount\": 5, \"runName\": \"TestSuite\"}");
EasyMock.expect(
mMockRunUtil.runTimedCmd(
@@ -133,6 +134,10 @@
EasyMock.eq(0),
EasyMock.anyLong());
mMockListener.testLog(
+ EasyMock.eq(binary.getName() + "-stdout"),
+ EasyMock.eq(LogDataType.TEXT),
+ EasyMock.anyObject());
+ mMockListener.testLog(
EasyMock.eq(binary.getName() + "-stderr"),
EasyMock.eq(LogDataType.TEXT),
EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
index 91bb1d1..782eac0 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
@@ -20,6 +20,7 @@
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.util.CommandResult;
@@ -64,23 +65,34 @@
mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
}
+ private CommandResult newCommandResult(CommandStatus status, String stderr, String stdout) {
+ CommandResult res = new CommandResult();
+ res.setStatus(status);
+ res.setStderr(stderr);
+ res.setStdout(stdout);
+ return res;
+ }
+
+ private String resultCount(int pass, int fail, int ignore) {
+ return "test result: ok. " + pass + " passed; " + fail + " failed; " + ignore + " ignored;";
+ }
+
+ private CommandResult successResult(String stderr, String stdout) throws Exception {
+ return newCommandResult(CommandStatus.SUCCESS, stderr, stdout);
+ }
+
/** Add mocked call "binary --list" to count the number of tests. */
private void mockCountTests(File binary, int numOfTest) throws Exception {
- CommandResult res = new CommandResult();
- res.setStatus(CommandStatus.SUCCESS);
- res.setStderr("");
- res.setStdout(numOfTest + " tests, 0 benchmarks");
EasyMock.expect(
mMockRunUtil.runTimedCmdSilently(
EasyMock.anyLong(),
EasyMock.eq(binary.getAbsolutePath()),
EasyMock.eq("--list")))
- .andReturn(res);
+ .andReturn(successResult("", numOfTest + " tests, 0 benchmarks"));
}
- /** Add mocked call to count tests and testRunStarted. */
- private void mockTestRunStarted(File binary, int count) throws Exception {
- mockCountTests(binary, count);
+ /** Add mocked testRunStarted call to the listener. */
+ private void mockListenerStarted(File binary, int count) throws Exception {
mMockListener.testRunStarted(
EasyMock.eq(binary.getName()),
EasyMock.eq(count),
@@ -88,23 +100,21 @@
EasyMock.anyLong());
}
- /** Add mocked call to "binary" with result status, stderr, and stdout. */
- private void mockRunTest(File binary, CommandStatus status, String stderr, String stdout)
- throws Exception {
- CommandResult res = new CommandResult();
- res.setStatus(status);
- res.setStderr(stderr);
- res.setStdout(stdout);
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(
- EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
- .andReturn(res);
+ /** Add mocked call to check listener log file. */
+ private void mockListenerLog(File binary) {
mMockListener.testLog(
EasyMock.eq(binary.getName() + "-stderr"),
EasyMock.eq(LogDataType.TEXT),
EasyMock.anyObject());
}
+ private void mockTestRunExpect(File binary, CommandResult res) throws Exception {
+ EasyMock.expect(
+ mMockRunUtil.runTimedCmd(
+ EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
+ .andReturn(res);
+ }
+
/** Add mocked call to testRunEnded. */
private void mockTestRunEnded() {
mMockListener.testRunEnded(
@@ -125,12 +135,11 @@
try {
OptionSetter setter = new OptionSetter(mTest);
setter.setOptionValue("test-file", binary.getAbsolutePath());
- mockTestRunStarted(binary, 9);
- mockRunTest(
- binary,
- CommandStatus.SUCCESS,
- "",
- "test result: ok. 6 passed; 1 failed; 2 ignored;");
+ mockCountTests(binary, 9);
+ mockListenerStarted(binary, 9);
+ mockListenerLog(binary);
+ CommandResult res = successResult("", resultCount(6, 1, 2));
+ mockTestRunExpect(binary, res);
mockTestRunEnded();
callReplayRunVerify();
} finally {
@@ -151,12 +160,11 @@
try {
OptionSetter setter = new OptionSetter(mTest);
setter.setOptionValue("test-file", binary.getAbsolutePath());
- mockTestRunStarted(binary, 9);
- mockRunTest(
- binary,
- CommandStatus.SUCCESS,
- "",
- "test result: ok. 6 passed; 1 failed; 2 ignored;");
+ mockCountTests(binary, 9);
+ mockListenerStarted(binary, 9);
+ mockListenerLog(binary);
+ CommandResult res = successResult("", resultCount(6, 1, 2));
+ mockTestRunExpect(binary, res);
mockTestRunEnded();
callReplayRunVerify();
} finally {
@@ -171,11 +179,13 @@
try {
OptionSetter setter = new OptionSetter(mTest);
setter.setOptionValue("test-file", binary.getAbsolutePath());
- mockTestRunStarted(binary, 0);
- mockRunTest(
- binary, CommandStatus.EXCEPTION, "Count not execute.", "Could not execute.");
+ mockCountTests(binary, 0);
+ mockListenerStarted(binary, 0);
+ mockListenerLog(binary);
+ CommandResult res = newCommandResult(CommandStatus.EXCEPTION, "Err.", "Exception.");
+ mockTestRunExpect(binary, res);
mMockListener.testRunFailed((String) EasyMock.anyObject());
- mMockListener.testRunFailed((String) EasyMock.anyObject());
+ mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
mockTestRunEnded();
callReplayRunVerify();
} finally {
@@ -190,10 +200,7 @@
try {
OptionSetter setter = new OptionSetter(mTest);
setter.setOptionValue("test-file", binary.getAbsolutePath());
- CommandResult listRes = new CommandResult();
- listRes.setStatus(CommandStatus.FAILED);
- listRes.setStderr("");
- listRes.setStdout("");
+ CommandResult listRes = newCommandResult(CommandStatus.FAILED, "", "");
EasyMock.expect(
mMockRunUtil.runTimedCmdSilently(
EasyMock.anyLong(),
@@ -205,12 +212,10 @@
EasyMock.eq(0),
EasyMock.anyInt(),
EasyMock.anyLong());
- mockRunTest(
- binary,
- CommandStatus.FAILED,
- "",
- "test result: ok. 6 passed; 1 failed; 2 ignored;");
- mMockListener.testRunFailed((String) EasyMock.anyObject());
+ mockListenerLog(binary);
+ CommandResult res = newCommandResult(CommandStatus.FAILED, "", resultCount(6, 1, 2));
+ mockTestRunExpect(binary, res);
+ mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
mockTestRunEnded();
callReplayRunVerify();
} finally {
@@ -225,13 +230,99 @@
try {
OptionSetter setter = new OptionSetter(mTest);
setter.setOptionValue("test-file", binary.getAbsolutePath());
- mockTestRunStarted(binary, 9);
- mockRunTest(
- binary,
- CommandStatus.FAILED,
- "",
- "test result: ok. 6 passed; 1 failed; 2 ignored;");
- mMockListener.testRunFailed((String) EasyMock.anyObject());
+ mockCountTests(binary, 9);
+ mockListenerStarted(binary, 9);
+ mockListenerLog(binary);
+ CommandResult res = newCommandResult(CommandStatus.FAILED, "", resultCount(6, 1, 2));
+ mockTestRunExpect(binary, res);
+ mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
+ mockTestRunEnded();
+ callReplayRunVerify();
+ } finally {
+ FileUtil.deleteFile(binary);
+ }
+ }
+
+ /** Test the exclude filtering of test methods. */
+ @Test
+ public void testExcludeFilter() throws Exception {
+ File binary = FileUtil.createTempFile("rust-dir", "");
+ try {
+ OptionSetter setter = new OptionSetter(mTest);
+ setter.setOptionValue("test-file", binary.getAbsolutePath());
+ setter.setOptionValue("exclude-filter", "NotMe");
+ setter.setOptionValue("exclude-filter", "Long");
+ EasyMock.expect(
+ mMockRunUtil.runTimedCmdSilently(
+ EasyMock.anyLong(),
+ EasyMock.eq(binary.getAbsolutePath()),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("NotMe"),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("Long"),
+ EasyMock.eq("--list")))
+ .andReturn(successResult("", "9 tests, 0 benchmarks"));
+ mockListenerStarted(binary, 9);
+ mockListenerLog(binary);
+ CommandResult res = successResult("", resultCount(6, 1, 2));
+ EasyMock.expect(
+ mMockRunUtil.runTimedCmd(
+ EasyMock.anyLong(),
+ EasyMock.eq(binary.getAbsolutePath()),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("NotMe"),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("Long")))
+ .andReturn(res);
+
+ mockTestRunEnded();
+ callReplayRunVerify();
+ } finally {
+ FileUtil.deleteFile(binary);
+ }
+ }
+
+ /** Test both include and exclude filters. */
+ @Test
+ public void testIncludeExcludeFilter() throws Exception {
+ File binary = FileUtil.createTempFile("rust-dir", "");
+ try {
+ OptionSetter setter = new OptionSetter(mTest);
+ setter.setOptionValue("test-file", binary.getAbsolutePath());
+ setter.setOptionValue("exclude-filter", "NotMe");
+ setter.setOptionValue("include-filter", "OnlyMe");
+ setter.setOptionValue("exclude-filter", "Other");
+ setter.setOptionValue("include-filter", "Me2");
+ // We always pass the include-filter before exclude-filter strings.
+ // Multiple include filters are accepted but all except the 1st are ignored.
+ EasyMock.expect(
+ mMockRunUtil.runTimedCmdSilently(
+ EasyMock.anyLong(),
+ EasyMock.eq(binary.getAbsolutePath()),
+ EasyMock.eq("OnlyMe"),
+ EasyMock.eq("Me2"),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("NotMe"),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("Other"),
+ EasyMock.eq("--list")))
+ .andReturn(successResult("", "3 tests, 0 benchmarks"));
+ mockListenerStarted(binary, 3);
+
+ mockListenerLog(binary);
+ CommandResult res = successResult("", resultCount(3, 0, 0));
+ EasyMock.expect(
+ mMockRunUtil.runTimedCmd(
+ EasyMock.anyLong(),
+ EasyMock.eq(binary.getAbsolutePath()),
+ EasyMock.eq("OnlyMe"),
+ EasyMock.eq("Me2"),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("NotMe"),
+ EasyMock.eq("--skip"),
+ EasyMock.eq("Other")))
+ .andReturn(res);
+
mockTestRunEnded();
callReplayRunVerify();
} finally {
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java b/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
index 2672314..a633ed1 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
@@ -328,11 +328,13 @@
EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
EasyMock.expect(
mMockITestDevice.executeShellCommand(
- "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+ "find /data/misc/trace -name '*.gcda' | tar -cvf"
+ + " /data/misc/trace/coverage.tar -T -"))
.andReturn("");
EasyMock.expect(
mMockITestDevice.executeShellCommand(
- "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+ "find /data/misc/trace -name '*.gcda' | tar -cvf"
+ + " /data/misc/trace/coverage.tar -T -"))
.andReturn("");
File tmpFile1 = FileUtil.createTempFile("coverage", ".tar");
EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile1);
@@ -431,11 +433,13 @@
EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
EasyMock.expect(
mMockITestDevice.executeShellCommand(
- "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+ "find /data/misc/trace -name '*.gcda' | tar -cvf"
+ + " /data/misc/trace/coverage.tar -T -"))
.andReturn("");
EasyMock.expect(
mMockITestDevice.executeShellCommand(
- "find /data/misc/trace -name '*.gcda' | tar -cvf /data/misc/trace/coverage.tar -T -"))
+ "find /data/misc/trace -name '*.gcda' | tar -cvf"
+ + " /data/misc/trace/coverage.tar -T -"))
.andReturn("");
File tmpFile1 = FileUtil.createTempFile("coverage", ".tar");
EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile1);
@@ -479,4 +483,53 @@
mockTestRunEnded();
callReplayRunVerify();
}
+
+ /**
+ * Helper function to do the actual filtering test.
+ *
+ * @param filterString The string to search for in the Mock, to verify filtering was called
+ * @throws DeviceNotAvailableException
+ */
+ private void doTestFilter(String filterString) throws DeviceNotAvailableException {
+ final String testPath = RustBinaryTest.DEFAULT_TEST_PATH;
+ final String test1 = "test1";
+ final String testPath1 = String.format("%s/%s", testPath, test1);
+ final String[] files = new String[] {test1};
+
+ // Find files
+ MockFileUtil.setMockDirContents(mMockITestDevice, testPath, test1);
+ EasyMock.expect(mMockITestDevice.doesFileExist(testPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.getChildren(testPath)).andReturn(files);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+ EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+
+ mockCountTests(testPath1 + filterString, "test1\n3 tests, 0 benchmarks\n");
+ mockTestRunStarted("test1", 3);
+ mockShellCommand(test1 + filterString);
+ mockTestRunEnded();
+ callReplayRunVerify();
+ }
+
+ /** Test the exclude-filter option. */
+ @Test
+ public void testExcludeFilter() throws Exception {
+ OptionSetter setter = new OptionSetter(mRustBinaryTest);
+ setter.setOptionValue("exclude-filter", "NotMe");
+ setter.setOptionValue("exclude-filter", "Long");
+ doTestFilter(" --skip NotMe --skip Long");
+ }
+
+ /** Test both include- and exclude-filter options. */
+ @Test
+ public void testIncludeExcludeFilter() throws Exception {
+ OptionSetter setter = new OptionSetter(mRustBinaryTest);
+ setter.setOptionValue("exclude-filter", "NotMe2");
+ setter.setOptionValue("include-filter", "OnlyMe");
+ setter.setOptionValue("exclude-filter", "other");
+ setter.setOptionValue("include-filter", "Me2");
+ // Include filters are passed before exclude filters.
+ // Multiple include filters are accepted, but all except the 1st are ignored.
+ doTestFilter(" OnlyMe Me2 --skip NotMe2 --skip other");
+ }
}
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 592a71d..7d89e93 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -59,6 +59,7 @@
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.MultiFailureDescription;
import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.retry.BaseRetryDecision;
import com.android.tradefed.retry.IRetryDecision;
@@ -661,7 +662,9 @@
fake.setTest(
new StubCollectingTest(
new DeviceUnresponsiveException(
- "unresponsive", "serial")));
+ "unresponsive",
+ "serial",
+ DeviceErrorIdentifier.DEVICE_UNRESPONSIVE)));
testConfig.put(TEST_CONFIG_NAME, fake);
} catch (ConfigurationException e) {
CLog.e(e);
@@ -683,7 +686,8 @@
mMockListener.testRunStarted(
EasyMock.eq(TEST_CONFIG_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
EasyMock.expectLastCall().times(1);
- mMockListener.testRunFailed(FailureDescription.create("unresponsive"));
+ mMockListener.testRunFailed(
+ FailureDescription.create("unresponsive", FailureStatus.LOST_SYSTEM_UNDER_TEST));
EasyMock.expect(
mMockDevice.logBugreport(
EasyMock.eq("module-test-failure-SERIAL-bugreport"),
@@ -811,11 +815,6 @@
EasyMock.expectLastCall().times(1);
Capture<FailureDescription> captured = new Capture<>();
mMockListener.testRunFailed(EasyMock.capture(captured));
- EasyMock.expect(
- mMockDevice.logBugreport(
- EasyMock.eq("module-test-failure-SERIAL-bugreport"),
- EasyMock.anyObject()))
- .andReturn(true);
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(1);
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
index 18e3ce8..f9c62b5 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
@@ -38,7 +38,6 @@
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.invoker.IInvocationContext;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.invoker.TestInvocation;
@@ -57,6 +56,8 @@
import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.retry.BaseRetryDecision;
import com.android.tradefed.retry.IRetryDecision;
import com.android.tradefed.targetprep.BaseTargetPreparer;
@@ -160,10 +161,12 @@
TestDescription test = new TestDescription(mRunName + "class", "test" + i);
listener.testStarted(test);
if (mShouldThrow && i == mNumTest / 2) {
- throw new DeviceNotAvailableException("unavailable", "serial");
+ throw new DeviceNotAvailableException(
+ "unavailable", "serial", DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
}
if (mDeviceUnresponsive) {
- throw new DeviceUnresponsiveException("unresponsive", "serial");
+ throw new DeviceUnresponsiveException(
+ "unresponsive", "serial", DeviceErrorIdentifier.DEVICE_UNRESPONSIVE);
}
if (mThrowError && i == mNumTest / 2) {
throw new AssertionError("assert error");
@@ -1048,12 +1051,6 @@
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
- // Run failed
- EasyMock.expect(mMockDevice.getIDevice()).andReturn(EasyMock.createMock(IDevice.class));
- EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("serial");
- EasyMock.expect(mMockDevice.logBugreport(EasyMock.anyObject(), EasyMock.anyObject()))
- .andReturn(true);
-
replayMocks();
mModule.run(mModuleInfo, mMockListener);
// Only one module
@@ -1388,7 +1385,8 @@
EasyMock.<HashMap<String, Metric>>anyObject());
}
mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
- FailureDescription issues = FailureDescription.create("unresponsive");
+ FailureDescription issues =
+ FailureDescription.create("unresponsive", FailureStatus.LOST_SYSTEM_UNDER_TEST);
mMockListener.testRunFailed(issues);
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -1564,7 +1562,6 @@
mModule.setBuild(mMockBuildInfo);
mModule.setDevice(mMockDevice);
- EasyMock.expect(mMockDevice.getIDevice()).andReturn(new StubDevice("fake"));
EasyMock.expect(mMockPrep.isDisabled()).andReturn(false).times(2);
// no isTearDownDisabled() expected for setup
mMockPrep.setUp(EasyMock.eq(mModuleInfo));
diff --git a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
index 7d77450..4a58724 100644
--- a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
@@ -424,6 +424,37 @@
assertEquals("armeabi-v7a", descriptor.getAbi().getName());
}
+ /**
+ * Test that if the base module is excluded in full, the filters of parameterized modules are
+ * still populated with the proper filters.
+ */
+ @Test
+ public void testFilterParameterized_excludeFilter_parameter() throws Exception {
+ Map<String, List<SuiteTestFilter>> excludeFilters = new LinkedHashMap<>();
+ createInstantModuleConfig("basemodule");
+ SuiteTestFilter fullFilter = SuiteTestFilter.createFrom("armeabi-v7a basemodule[instant]");
+ excludeFilters.put("basemodule[instant]", Arrays.asList(fullFilter));
+
+ mRepo =
+ new SuiteModuleLoader(
+ new LinkedHashMap<String, List<SuiteTestFilter>>(),
+ excludeFilters,
+ new ArrayList<>(),
+ new ArrayList<>());
+ mRepo.setParameterizedModules(true);
+
+ List<String> patterns = new ArrayList<>();
+ patterns.add(".*.config");
+ patterns.add(".*.xml");
+ LinkedHashMap<String, IConfiguration> res =
+ mRepo.loadConfigsFromDirectory(
+ Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+ assertEquals(1, res.size());
+ // Full module was excluded completely
+ IConfiguration instantModule = res.get("armeabi-v7a basemodule[instant]");
+ assertNull(instantModule);
+ }
+
@Test
public void testFilterParameterized_includeFilter_base() throws Exception {
Map<String, List<SuiteTestFilter>> includeFilters = new LinkedHashMap<>();
@@ -478,6 +509,36 @@
assertNotNull(instantModule);
}
+ @Test
+ public void testFilterParameterized_WithModuleArg() throws Exception {
+ List<String> moduleArgs = new ArrayList<>();
+ createInstantModuleConfig("basemodule");
+ moduleArgs.add("basemodule[instant]:exclude-annotation:test-annotation");
+
+ mRepo =
+ new SuiteModuleLoader(
+ new LinkedHashMap<String, List<SuiteTestFilter>>(),
+ new LinkedHashMap<String, List<SuiteTestFilter>>(),
+ new ArrayList<>(),
+ moduleArgs);
+ mRepo.setParameterizedModules(true);
+
+ List<String> patterns = new ArrayList<>();
+ patterns.add(".*.config");
+ patterns.add(".*.xml");
+ LinkedHashMap<String, IConfiguration> res =
+ mRepo.loadConfigsFromDirectory(
+ Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+ assertEquals(2, res.size());
+ IConfiguration instantModule = res.get("armeabi-v7a basemodule[instant]");
+ assertNotNull(instantModule);
+ TestSuiteStub stubTest = (TestSuiteStub) instantModule.getTests().get(0);
+ assertEquals(2, stubTest.getExcludeAnnotations().size());
+ List<String> expected =
+ Arrays.asList("android.platform.test.annotations.AppModeFull", "test-annotation");
+ assertTrue(stubTest.getExcludeAnnotations().containsAll(expected));
+ }
+
/**
* Test that the configuration can be found if specifying specific path.
*/
@@ -759,6 +820,40 @@
/**
* Test that generate the correct IConfiguration objects based on the defined mainline modules
+ * with given module args.
+ */
+ @Test
+ public void testLoadParameterizedMainlineModule_WithModuleArgs() throws Exception {
+ List<String> moduleArgs = new ArrayList<>();
+ moduleArgs.add("basemodule[mod1.apk]:exclude-annotation:test-annotation");
+ createMainlineModuleConfig("basemodule");
+
+ mRepo =
+ new SuiteModuleLoader(
+ new LinkedHashMap<String, List<SuiteTestFilter>>(),
+ new LinkedHashMap<String, List<SuiteTestFilter>>(),
+ new ArrayList<>(),
+ moduleArgs);
+ mRepo.setInvocationContext(mContext);
+ mRepo.setMainlineParameterizedModules(true);
+
+ List<String> patterns = new ArrayList<>();
+ patterns.add(".*.config");
+ patterns.add(".*.xml");
+ LinkedHashMap<String, IConfiguration> res =
+ mRepo.loadConfigsFromDirectory(
+ Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+ assertEquals(3, res.size());
+ IConfiguration module1 = res.get("armeabi-v7a basemodule[mod1.apk]");
+ assertNotNull(module1);
+ TestSuiteStub stubTest = (TestSuiteStub) module1.getTests().get(0);
+ assertEquals(1, stubTest.getExcludeAnnotations().size());
+ assertEquals("test-annotation", stubTest.getExcludeAnnotations().iterator().next());
+ EasyMock.verify(mMockBuildInfo);
+ }
+
+ /**
+ * Test that generate the correct IConfiguration objects based on the defined mainline modules
* with given include-filter and exclude-filter.
*/
@Test
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index 99d8164..07e0807 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -31,7 +31,7 @@
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.testtype.Abi;
-import com.android.tradefed.testtype.GTest;
+import com.android.tradefed.testtype.HostTest;
import com.android.tradefed.testtype.IAbi;
import com.android.tradefed.testtype.IAbiReceiver;
import com.android.tradefed.testtype.IRemoteTest;
@@ -53,6 +53,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
@@ -93,7 +94,7 @@
+ " <option name=\"config-descriptor:metadata\" key=\"mainline-param\" value=\"mod2.apk\" />"
+ " <option name=\"config-descriptor:metadata\" key=\"mainline-param\" value=\"mod1.apk+mod2.apk\" />"
+ " <option name=\"config-descriptor:metadata\" key=\"mainline-param\" value=\"mod1.apk+mod2.apk+mod3.apk\" />"
- + " <test class=\"com.android.tradefed.testtype.GTest\" />\n"
+ + " <test class=\"com.android.tradefed.testtype.HostTest\" />\n"
+ "</configuration>";
@Before
@@ -514,6 +515,58 @@
}
}
+ /**
+ * Test for {@link TestMappingSuiteRunner#loadTests()} for loading tests from test_mappings.zip
+ * and run with shard, and no test is split due to exclude-filter.
+ */
+ @Test
+ public void testLoadTests_shardNoTest() throws Exception {
+ File tempDir = null;
+ try {
+ tempDir = FileUtil.createTempDir("test_mapping");
+
+ File srcDir = FileUtil.createTempDir("src", tempDir);
+ String srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+ InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+ File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+ srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+ resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+ File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+ ZipUtil.createZip(srcDir, zipFile);
+
+ mOptionSetter.setOptionValue("test-mapping-test-group", "postsubmit");
+ mOptionSetter.setOptionValue("test-mapping-path", srcDir.getName());
+ mOptionSetter.setOptionValue("exclude-filter", "suite/stub1");
+
+ IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+ EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+ .andStubReturn(null);
+ EasyMock.expect(mockBuildInfo.getTestsDir())
+ .andStubReturn(new File("non-existing-dir"));
+ EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+ mTestInfo
+ .getContext()
+ .addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mockBuildInfo);
+ EasyMock.replay(mockBuildInfo);
+
+ Collection<IRemoteTest> tests = mRunner.split(2, mTestInfo);
+ assertEquals(null, tests);
+ assertEquals(2, mRunner.getIncludeFilter().size());
+ assertEquals(null, mRunner.getTestGroup());
+ assertEquals(0, mRunner.getTestMappingPaths().size());
+ assertEquals(false, mRunner.getUseTestMappingPath());
+ EasyMock.verify(mockBuildInfo);
+ } finally {
+ // Clean up the static variable due to the usage of option `test-mapping-path`.
+ TestMapping.setTestMappingPaths(new ArrayList<String>());
+ FileUtil.recursiveDelete(tempDir);
+ }
+ }
+
/** Test for {@link TestMappingSuiteRunner#loadTests()} to fail when no test is found. */
@Test(expected = RuntimeException.class)
public void testLoadTests_noTest() throws Exception {
@@ -870,14 +923,16 @@
assertTrue(configMap.containsKey(ABI_1 + " test[mod1.apk]"));
assertTrue(configMap.containsKey(ABI_1 + " test[mod2.apk]"));
assertTrue(configMap.containsKey(ABI_1 + " test[mod1.apk+mod2.apk]"));
- GTest test = (GTest) configMap.get(ABI_1 + " test[mod1.apk]").getTests().get(0);
+ HostTest test = (HostTest) configMap.get(ABI_1 + " test[mod1.apk]").getTests().get(0);
assertTrue(test.getIncludeFilters().contains("test-filter"));
- test = (GTest) configMap.get(ABI_1 + " test[mod2.apk]").getTests().get(0);
+ test = (HostTest) configMap.get(ABI_1 + " test[mod2.apk]").getTests().get(0);
assertTrue(test.getIncludeFilters().contains("test-filter2"));
- test = (GTest) configMap.get(ABI_1 + " test[mod1.apk+mod2.apk]").getTests().get(0);
+ test = (HostTest) configMap.get(ABI_1 + " test[mod1.apk+mod2.apk]").getTests().get(0);
assertTrue(test.getIncludeFilters().isEmpty());
+ assertEquals(1, test.getExcludeAnnotations().size());
+ assertEquals("test-annotation", test.getExcludeAnnotations().iterator().next());
EasyMock.verify(mockBuildInfo);
} finally {
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index c1537ca..e23201f 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -166,6 +166,8 @@
EasyMock.replay(mockBuildInfo);
+ // Ensure the static variable doesn't have any relative path configured.
+ TestMapping.setTestMappingPaths(new ArrayList<String>());
Set<TestInfo> tests = TestMapping.getTests(mockBuildInfo, "presubmit", false, null);
assertEquals(0, tests.size());