Merge "Snap for 6686656 from d09c30b940da8ebed4b9218f5aff2232f9a90d84 to sdk-release" into sdk-release
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/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/res/config/suite/test_mapping_suite.xml b/res/config/suite/test_mapping_suite.xml
index 8496121..4753255 100644
--- a/res/config/suite/test_mapping_suite.xml
+++ b/res/config/suite/test_mapping_suite.xml
@@ -37,6 +37,8 @@
<!-- Tell all HostTests to exclude certain annotations -->
<option name="test-arg" value="com.android.tradefed.testtype.HostTest:exclude-annotation:android.platform.test.annotations.RestrictedBuildTest" />
<option name="test-arg" value="com.android.compatibility.common.tradefed.testtype.JarHostTest:exclude-annotation:android.platform.test.annotations.RestrictedBuildTest" />
+ <option name="test-arg" value="com.android.tradefed.testtype.HostTest:exclude-annotation:android.platform.test.annotations.AppModeInstant" />
+ <option name="test-arg" value="com.android.compatibility.common.tradefed.testtype.JarHostTest:exclude-annotation:android.platform.test.annotations.AppModeInstant" />
<!-- Force GTest to report binary name in results -->
<option name="test-arg" value="com.android.tradefed.testtype.GTest:prepend-filename:true" />
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
new file mode 100644
index 0000000..66ccd89
--- /dev/null
+++ b/src/com/android/tradefed/build/DependenciesResolver.java
@@ -0,0 +1,126 @@
+/*
+ * 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.build;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.dependency.TestDependencyResolver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.ExecutionFiles;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.CurrentInvocation;
+import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.testtype.IInvocationContextReceiver;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/** A new type of provider that allows to get all the dependencies for a test. */
+public class DependenciesResolver
+ implements IBuildProvider, IDeviceBuildProvider, IInvocationContextReceiver {
+
+ @Option(name = "build-id", description = "build id to supply.")
+ private String mBuildId = "0";
+
+ @Option(name = "branch", description = "build branch name to supply.")
+ private String mBranch = null;
+
+ @Option(name = "build-flavor", description = "build flavor name to supply.")
+ private String mBuildFlavor = null;
+
+ @Option(name = "build-os", description = "build os name to supply.")
+ private String mBuildOs = "linux";
+
+ @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;
+
+ @Override
+ public IBuildInfo getBuild(ITestDevice device)
+ throws BuildRetrievalError, DeviceNotAvailableException {
+ 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 =
+ TestDependencyResolver.resolveDependencyFromContext(
+ dependency, build, mInvocationContext);
+ if (f != null) {
+ // TODO: Have way to make named-dependencies
+ getInvocationFiles().put(f.getName(), f);
+ build.setFile(f.getName(), f, "1");
+ }
+ }
+ // Create a tests dir if there are none
+ if (build.getTestsDir() == null) {
+ try {
+ mTestsDir =
+ FileUtil.createTempDir(
+ "bootstrap-test-dir",
+ CurrentInvocation.getInfo(InvocationInfo.WORK_FOLDER));
+ } catch (IOException e) {
+ throw new BuildRetrievalError(
+ e.getMessage(), e, InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
+ }
+ build.setTestsDir(mTestsDir, "1");
+ }
+ return build;
+ }
+
+ @Override
+ public IBuildInfo getBuild() throws BuildRetrievalError {
+ throw new IllegalArgumentException("Should not be called");
+ }
+
+ @Override
+ public void cleanUp(IBuildInfo info) {
+ info.cleanUp();
+ }
+
+ @Override
+ public void setInvocationContext(IInvocationContext invocationContext) {
+ mInvocationContext = invocationContext;
+ }
+
+ @VisibleForTesting
+ public final Set<File> getDependencies() {
+ return mDependencies;
+ }
+
+ @VisibleForTesting
+ ExecutionFiles getInvocationFiles() {
+ return CurrentInvocation.getInvocationFiles();
+ }
+}
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 839ee9b..af7e11b 100644
--- a/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
+++ b/src/com/android/tradefed/config/yaml/OpenObjectLoader.java
@@ -15,8 +15,10 @@
*/
package com.android.tradefed.config.yaml;
-import com.android.tradefed.build.BootstrapBuildProvider;
+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. */
@@ -24,16 +26,46 @@
@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());
- // TODO: Replace by a provider that can handle both local and remote
- loadConfiguration
- .getConfigDef()
- .addConfigObjectDef(
- Configuration.BUILD_PROVIDER_TYPE_NAME,
- BootstrapBuildProvider.class.getName());
+ // Build
+ int classCount =
+ loadConfiguration
+ .getConfigDef()
+ .addConfigObjectDef(
+ Configuration.BUILD_PROVIDER_TYPE_NAME,
+ DependenciesResolver.class.getName());
+ // Set all the dependencies on the provider
+ for (String depencency : loadConfiguration.getDependencies()) {
+ String optionName =
+ String.format(
+ "%s%c%d%c%s",
+ DependenciesResolver.class.getName(),
+ OptionSetter.NAMESPACE_SEPARATOR,
+ classCount,
+ OptionSetter.NAMESPACE_SEPARATOR,
+ "dependency");
+ loadConfiguration
+ .getConfigDef()
+ .addOptionDef(
+ optionName,
+ null,
+ depencency,
+ loadConfiguration.getConfigDef().getName(),
+ Configuration.BUILD_PROVIDER_TYPE_NAME);
+ }
}
}
diff --git a/src/com/android/tradefed/dependency/TestDependencyResolver.java b/src/com/android/tradefed/dependency/TestDependencyResolver.java
new file mode 100644
index 0000000..5034b09
--- /dev/null
+++ b/src/com/android/tradefed/dependency/TestDependencyResolver.java
@@ -0,0 +1,45 @@
+/*
+ * 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.dependency;
+
+import com.android.tradefed.build.BuildRetrievalError;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.invoker.IInvocationContext;
+
+import java.io.File;
+
+/** Helper to resolve dependencies if needed. */
+public class TestDependencyResolver {
+
+ /**
+ * Resolve a single dependency based on some context.
+ *
+ * @param dependency The dependency to be resolved.
+ * @param build The current build information being created.
+ * @param context The invocation context.
+ * @return The resolved dependency or null if unresolved.
+ * @throws BuildRetrievalError
+ */
+ public static File resolveDependencyFromContext(
+ File dependency, IBuildInfo build, IInvocationContext context)
+ throws BuildRetrievalError {
+ if (dependency.exists()) {
+ return dependency;
+ }
+ // TODO: Implement the core logic
+ return null;
+ }
+}
diff --git a/src/com/android/tradefed/device/NativeDeviceStateMonitor.java b/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
index 5d55bb7..ebe4526 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;
@@ -432,7 +433,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 +456,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..80a8f7d 100644
--- a/src/com/android/tradefed/device/WaitDeviceRecovery.java
+++ b/src/com/android/tradefed/device/WaitDeviceRecovery.java
@@ -231,9 +231,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..2927f19 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;
@@ -119,6 +121,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 +155,7 @@
PARENT_SANDBOX,
SANDBOX,
REMOTE_INVOCATION,
+ DELEGATED_INVOCATION
}
private String mStatus = "(not invoked)";
@@ -275,7 +279,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 +496,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 +715,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 +795,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 +925,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 +1022,8 @@
return new SandboxedInvocationExecution();
case REMOTE_INVOCATION:
return new RemoteInvocationExecution();
+ case DELEGATED_INVOCATION:
+ return new DelegatedInvocationExecution();
default:
return new InvocationExecution();
}
@@ -1015,6 +1036,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)) {
diff --git a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
index 5fe2ae5..6da0c2d 100644
--- a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
+++ b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
@@ -21,6 +21,7 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.util.StreamUtil;
@@ -103,6 +104,9 @@
errorMessage = extractCrashAndAddToMessage(errorMessage, mLastStartTime);
}
error.setErrorMessage(errorMessage);
+ if (isCrash(errorMessage)) {
+ error.setErrorIdentifier(DeviceErrorIdentifier.INSTRUMENATION_CRASH);
+ }
super.testRunFailed(error);
}
@@ -114,14 +118,17 @@
/** Attempt to extract the crash from the logcat if the test was seen as started. */
private String extractCrashAndAddToMessage(String errorMessage, Long startTime) {
- if ((errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE))
- && startTime != null) {
+ if (isCrash(errorMessage) && startTime != null) {
mLogcatItem = extractLogcat(mDevice, startTime);
errorMessage = addJavaCrashToString(mLogcatItem, errorMessage);
}
return errorMessage;
}
+ private boolean isCrash(String errorMessage) {
+ return errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE);
+ }
+
/**
* Extract a formatted object from the logcat snippet.
*
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/FormattedGeneratorReporter.java b/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
index dbd521f..9c8bd22 100644
--- a/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
+++ b/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
@@ -17,7 +17,9 @@
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.error.IHarnessException;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.suite.retry.ResultsPlayer;
@@ -68,11 +70,23 @@
@Override
public void invocationFailed(Throwable cause) {
+ FailureDescription description =
+ FailureDescription.create(cause.getMessage()).setCause(cause);
+ if (cause instanceof IHarnessException) {
+ description.setErrorIdentifier(((IHarnessException) cause).getErrorId());
+ }
+ invocationFailed(description);
+ }
+
+ @Override
+ public void invocationFailed(FailureDescription failure) {
// Some exception indicate a harness level issue, the tests result cannot be trusted at
// that point so we should skip the reporting.
- if (cause instanceof TargetSetupError
- || cause instanceof RuntimeException
- || cause instanceof OutOfMemoryError) {
+ Throwable cause = failure.getCause();
+ if (cause != null
+ && (cause instanceof TargetSetupError
+ || cause instanceof RuntimeException
+ || cause instanceof OutOfMemoryError)) {
mTestHarnessError = cause;
}
super.invocationFailed(cause);
diff --git a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
index 06592b2..772075b 100644
--- a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
+++ b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
@@ -34,6 +34,7 @@
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import com.google.common.base.Strings;
+import com.google.common.xml.XmlEscapers;
import com.google.gson.Gson;
import org.xmlpull.v1.XmlPullParser;
@@ -707,7 +708,6 @@
@VisibleForTesting
static String sanitizeXmlContent(String s) {
- // Replace c++ \0 null since Serializer doesn't handle it.
- return s.replace("\0", "[Null]");
+ return XmlEscapers.xmlContentEscaper().escape(s);
}
}
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/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index a5f6316..2db82ba 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -26,6 +26,7 @@
import com.android.tradefed.invoker.logger.InvocationMetricLogger;
import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.util.BinaryState;
import com.android.tradefed.util.MultiMap;
@@ -918,7 +919,8 @@
String.format(
"Failed to connect to wifi network %s on %s",
mWifiSsid, device.getSerialNumber()),
- device.getDeviceDescriptor());
+ device.getDeviceDescriptor(),
+ InfraErrorIdentifier.WIFI_FAILED_CONNECT);
}
}
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/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index c0f0b31..5f00853 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -303,7 +303,10 @@
// TODO: See if it's possible to report IReportNotExecuted
CLog.e("Run in progress was not completed due to:");
CLog.e(dnae);
- runListener.testRunFailed(createFromException(dnae));
+ // If it already was marked as failure do not remark it.
+ if (!mMainGranularRunListener.hasLastAttemptFailed()) {
+ runListener.testRunFailed(createFromException(dnae));
+ }
// Device Not Available Exception are rethrown.
throw dnae;
} finally {
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index bd33a30..0372eb7 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -58,6 +58,7 @@
import com.android.tradefed.result.TestResult;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.result.error.ErrorIdentifier;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.retry.IRetryDecision;
import com.android.tradefed.retry.RetryStatistics;
@@ -687,14 +688,19 @@
String.format(
"Module %s only ran %d out of %d expected tests.",
getId(), numResults, totalExpectedTests);
- runFailureMessages.add(FailureDescription.create(error));
+ FailureDescription mismatch =
+ FailureDescription.create(error)
+ .setFailureStatus(FailureStatus.TEST_FAILURE)
+ .setErrorIdentifier(InfraErrorIdentifier.EXPECTED_TESTS_MISMATCH);
+ runFailureMessages.add(mismatch);
CLog.e(error);
}
if (tearDownException != null) {
FailureDescription failure =
CurrentInvocation.createFailure(
- StreamUtil.getStackTrace(tearDownException), null);
+ StreamUtil.getStackTrace(tearDownException), null)
+ .setCause(tearDownException);
runFailureMessages.add(failure);
}
// If there is any errors report them all at once
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/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/targetprep/WifiPreparer.java b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
index 1959a27..1bfc635 100644
--- a/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
@@ -23,6 +23,7 @@
import com.android.tradefed.invoker.logger.InvocationMetricLogger;
import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
/**
* A {@link ITargetPreparer} that configures wifi on the device if necessary.
@@ -77,8 +78,12 @@
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.WIFI_AP_NAME, mWifiNetwork);
if (!device.connectToWifiNetworkIfNeeded(mWifiNetwork, mWifiPsk)) {
- throw new TargetSetupError(String.format("Failed to connect to wifi network %s on %s",
- mWifiNetwork, device.getSerialNumber()), device.getDeviceDescriptor());
+ throw new TargetSetupError(
+ String.format(
+ "Failed to connect to wifi network %s on %s",
+ mWifiNetwork, device.getSerialNumber()),
+ device.getDeviceDescriptor(),
+ InfraErrorIdentifier.WIFI_FAILED_CONNECT);
}
if (mMonitorNetwork) {
device.enableNetworkMonitor();
diff --git a/test_framework/com/android/tradefed/testtype/ArtRunTest.java b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
index 1b16316..a982104 100644
--- a/test_framework/com/android/tradefed/testtype/ArtRunTest.java
+++ b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
@@ -22,6 +22,7 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
@@ -31,6 +32,7 @@
import com.android.tradefed.util.FileUtil;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
@@ -141,8 +143,8 @@
// Check the output producted by the test.
if (output != null) {
try {
- File expectedFile =
- testInfo.getDependencyFile("expected.txt", /* targetFirst */ false);
+ File expectedFile = getDependencyFileFromRunTestDir(testInfo, "expected.txt");
+ CLog.i("Found expected output for run-test %s: %s", mRunTestName, expectedFile);
String expected = FileUtil.readStringFromFile(expectedFile);
if (!output.equals(expected)) {
String error = String.format("'%s' instead of '%s'", output, expected);
@@ -174,4 +176,24 @@
protected CollectingOutputReceiver createTestOutputReceiver() {
return new CollectingOutputReceiver();
}
+
+ /** Search for a dependency/artifact file in the run-test's directory. */
+ protected File getDependencyFileFromRunTestDir(TestInformation testInfo, String fileName)
+ throws FileNotFoundException {
+ File testsDir = testInfo.executionFiles().get(FilesKey.TARGET_TESTS_DIRECTORY);
+ if (testsDir == null || !testsDir.exists()) {
+ throw new FileNotFoundException(
+ String.format(
+ "Could not find target tests directory for test %s.", mRunTestName));
+ }
+ File runTestDir = new File(testsDir, mRunTestName);
+ File file = FileUtil.findFile(runTestDir, fileName);
+ if (file == null) {
+ throw new FileNotFoundException(
+ String.format(
+ "Could not find an artifact file associated with %s in directory %s.",
+ fileName, runTestDir));
+ }
+ return file;
+ }
}
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index ed238fe..94e4aa6 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,44 @@
/** {@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);
+ }
} 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 5541719..c3ef3a5 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
+++ b/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
@@ -30,11 +30,14 @@
SHELL_COMMAND_ERROR(30_100, FailureStatus.UNSET),
DEVICE_UNEXPECTED_RESPONSE(30_101, FailureStatus.UNSET),
+ INSTRUMENATION_CRASH(30_200, FailureStatus.UNSET),
+
FAILED_TO_LAUNCH_GCE(30_500, FailureStatus.LOST_SYSTEM_UNDER_TEST),
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 1f13121..3001231 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
+++ b/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
@@ -32,6 +32,12 @@
ARTIFACT_UNSUPPORTED_PATH(10_502, FailureStatus.INFRA_FAILURE),
ARTIFACT_DOWNLOAD_ERROR(10_503, FailureStatus.INFRA_FAILURE),
+ // 11_001 - 11_500: environment issues: For example: lab wifi
+ WIFI_FAILED_CONNECT(11_001, FailureStatus.UNSET), // TODO: switch to dependency_issue
+
+ // 12_000 - 12_100: Test issues detected by infra
+ EXPECTED_TESTS_MISMATCH(12_000, FailureStatus.TEST_FAILURE),
+
UNDETERMINED(20_000, FailureStatus.UNSET);
private final long code;
diff --git a/tests/res/testconfigs/yaml/test-config.tf_yaml b/tests/res/testconfigs/yaml/test-config.tf_yaml
index d547953..eb89137 100644
--- a/tests/res/testconfigs/yaml/test-config.tf_yaml
+++ b/tests/res/testconfigs/yaml/test-config.tf_yaml
@@ -1,5 +1,4 @@
description: "Human friendly description of the test"
-infra_run: false
dependencies:
- apks: ["test.apk", "test2.apk"]
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 2fbdc5f..6a1c081 100644
--- a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
+++ b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
@@ -20,7 +20,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
-import com.android.tradefed.build.BootstrapBuildProvider;
+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,12 +58,23 @@
@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();
// build provider
- assertTrue(config.getBuildProvider() instanceof BootstrapBuildProvider);
+ assertTrue(config.getBuildProvider() instanceof DependenciesResolver);
+ DependenciesResolver resolver = (DependenciesResolver) config.getBuildProvider();
+ assertEquals(7, resolver.getDependencies().size());
+ assertThat(resolver.getDependencies())
+ .containsExactly(
+ new File("test.apk"),
+ new File("test2.apk"),
+ new File("test1.apk"),
+ new File("tobepushed2.txt"),
+ new File("tobepushed.txt"),
+ new File("file1.txt"),
+ new File("file2.txt"));
// Test
assertEquals(1, config.getTests().size());
assertTrue(config.getTests().get(0) instanceof AndroidJUnitTest);
@@ -93,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/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/presubmit/TestMappingsValidation.java b/tests/src/com/android/tradefed/presubmit/TestMappingsValidation.java
index 9f5a056..4a5ef92 100644
--- a/tests/src/com/android/tradefed/presubmit/TestMappingsValidation.java
+++ b/tests/src/com/android/tradefed/presubmit/TestMappingsValidation.java
@@ -195,7 +195,10 @@
continue;
}
for (String param : params) {
- errors.add(validateMainlineModuleConfig(param, Set.of(configName)));
+ String error = validateMainlineModuleConfig(param, Set.of(configName));
+ if (!Strings.isNullOrEmpty(error)){
+ errors.add(error);
+ }
}
} catch (ConfigurationException e) {
errors.add(String.format("\t%s: %s", configName, e.getMessage()));
diff --git a/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java b/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
index 67a623a..647db0b 100644
--- a/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
+++ b/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
@@ -170,7 +170,7 @@
assertXmlContainsValue(
content,
"Result/Module/TestCase/Test/Failure/StackTrace",
- "module1 failed.\nstack\n<null>stack[Null]");
+ XmlSuiteResultFormatter.sanitizeXmlContent("module1 failed.\nstack\nstack\0"));
// Test that we can read back the informations
SuiteResultHolder holder = mFormatter.parseResults(mResultDir, false);
assertEquals(holder.completeModules, mResultHolder.completeModules);
@@ -224,7 +224,7 @@
assertXmlContainsValue(
content,
"Result/Module/TestCase/Test/Failure/StackTrace",
- "module1 failed.\nstack\n<null>stack[Null]");
+ XmlSuiteResultFormatter.sanitizeXmlContent("module1 failed.\nstack\nstack\0"));
// Test that we can read back the informations
SuiteResultHolder holder = mFormatter.parseResults(mResultDir, false);
assertEquals(holder.completeModules, mResultHolder.completeModules);
@@ -477,7 +477,7 @@
assertXmlContainsValue(
content,
"Result/Module/TestCase/Test/Failure/StackTrace",
- "module1 failed.\nstack\n<null>stack[Null]");
+ XmlSuiteResultFormatter.sanitizeXmlContent("module1 failed.\nstack\nstack\0"));
// Test that we can read back the informations
SuiteResultHolder holder = mFormatter.parseResults(mResultDir, true);
assertEquals(holder.completeModules, mResultHolder.completeModules);
@@ -671,7 +671,7 @@
new TestDescription("com.class." + runName, runName + ".failed" + i);
fakeRes.testStarted(description);
// Include a null character \0 that is not XML supported
- fakeRes.testFailed(description, runName + " failed.\nstack\n<null>stack\0");
+ fakeRes.testFailed(description, runName + " failed.\nstack\nstack\0");
HashMap<String, Metric> metrics = new HashMap<String, Metric>();
if (withMetrics) {
metrics.put("metric0" + i, TfMetricProtoUtil.stringToMetric("value0" + i));
diff --git a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
index 0e5b502..23d8b75 100644
--- a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
@@ -25,6 +25,7 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
@@ -48,6 +49,9 @@
@RunWith(JUnit4.class)
public class ArtRunTestTest {
+ // Default run-test name.
+ private static final String RUN_TEST_NAME = "run-test";
+
private ITestInvocationListener mMockInvocationListener;
private IAbi mMockAbi;
private ITestDevice mMockITestDevice;
@@ -56,9 +60,9 @@
private ArtRunTest mArtRunTest;
private OptionSetter mSetter;
private TestInformation mTestInfo;
- // Test dependencies directory on host.
- private File mTmpDepsDir;
- // Expected output file (within the dependencies directory).
+ // Target tests directory.
+ private File mTmpTargetTestsDir;
+ // Expected output file (under the target tests directory).
private File mTmpExpectedFile;
@Before
@@ -78,19 +82,23 @@
mArtRunTest.setDevice(mMockITestDevice);
mSetter = new OptionSetter(mArtRunTest);
- // Expected output file.
- mTmpDepsDir = FileUtil.createTempDir("art-run-test-deps");
- mTmpExpectedFile = new File(mTmpDepsDir, "expected.txt");
+ // Set up target tests directory and expected output file.
+ mTmpTargetTestsDir = FileUtil.createTempDir("target_testcases");
+ File runTestDir = new File(mTmpTargetTestsDir, RUN_TEST_NAME);
+ runTestDir.mkdir();
+ mTmpExpectedFile = new File(runTestDir, "expected.txt");
FileWriter fw = new FileWriter(mTmpExpectedFile);
fw.write("output\n");
fw.close();
- mTestInfo = TestInformation.newBuilder().setDependenciesFolder(mTmpDepsDir).build();
+ // Set the target tests directory in test information object.
+ mTestInfo = TestInformation.newBuilder().build();
+ mTestInfo.executionFiles().put(FilesKey.TARGET_TESTS_DIRECTORY, mTmpTargetTestsDir);
}
@After
public void tearDown() {
- FileUtil.recursiveDelete(mTmpDepsDir);
+ FileUtil.recursiveDelete(mTmpTargetTestsDir);
}
/** Helper mocking writing the output of a test command. */
@@ -143,8 +151,7 @@
@Test
public void testRunSingleTest_unsetClasspathOption()
throws ConfigurationException, DeviceNotAvailableException, IOException {
- final String runTestName = "test";
- mSetter.setOptionValue("run-test-name", runTestName);
+ mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
replayMocks();
try {
@@ -160,8 +167,7 @@
@Test
public void testRunSingleTest()
throws ConfigurationException, DeviceNotAvailableException, IOException {
- final String runTestName = "test";
- mSetter.setOptionValue("run-test-name", runTestName);
+ mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
final String classpath = "/data/local/tmp/test/test.jar";
mSetter.setOptionValue("classpath", classpath);
@@ -171,7 +177,7 @@
String runName = "ArtRunTest_abi";
// Beginning of test.
mMockInvocationListener.testRunStarted(runName, 1);
- TestDescription testId = new TestDescription(runName, runTestName);
+ TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
mMockInvocationListener.testStarted(testId);
String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
// Test execution.
@@ -199,8 +205,7 @@
@Test
public void testRunSingleTest_unexpectedOutput()
throws ConfigurationException, DeviceNotAvailableException, IOException {
- final String runTestName = "test";
- mSetter.setOptionValue("run-test-name", runTestName);
+ mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
final String classpath = "/data/local/tmp/test/test.jar";
mSetter.setOptionValue("classpath", classpath);
@@ -210,7 +215,7 @@
String runName = "ArtRunTest_abi";
// Beginning of test.
mMockInvocationListener.testRunStarted(runName, 1);
- TestDescription testId = new TestDescription(runName, runTestName);
+ TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
mMockInvocationListener.testStarted(testId);
String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
// Test execution.
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..ba18dc0 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)
@@ -133,12 +140,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 +373,31 @@
Mockito.eq(testDescription3),
Mockito.eq(new HashMap<String, MetricMeasurement.Metric>()));
}
+
+ /** Test split() for sharding */
+ @Test
+ public void testShard_Split() throws DeviceNotAvailableException, 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 3b0e0f9..de69a05 100644
--- a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
@@ -43,6 +43,8 @@
import com.android.tradefed.util.IRunUtil;
import com.android.utils.FileUtils;
+import com.google.common.base.Throwables;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -67,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;
@@ -88,17 +90,15 @@
Mockito.doReturn(mMockRunUtil).when(mSpyTest).getRunUtil();
Mockito.doReturn(DEFAULT_TIME_OUT).when(mSpyTest).getTestTimeout();
Mockito.doReturn("not_adb").when(mSpyTest).getAdbPath();
- mMoblyBinary = new File(TEMP_DIR, "mobly_binary.par");
- FileUtils.createFile(mMoblyBinary, "");
- mMoblyBinary2 = new File(TEMP_DIR, "mobly_binary_2.par");
- FileUtils.createFile(mMoblyBinary2, "");
+ 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 {
- FileUtils.deleteIfExists(mMoblyBinary);
- FileUtils.deleteIfExists(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 {
@@ -177,6 +177,9 @@
} catch (RuntimeException e) {
verify(mSpyTest, never()).reportLogs(any(), any());
assertEquals(
+ String.format(
+ "An unexpected exception was thrown, full stack trace: %s",
+ Throwables.getStackTraceAsString(e)),
e.getMessage(),
String.format("Couldn't find a par file %s", mMoblyBinary2.getName()));
}
@@ -202,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/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());