| /* |
| * Copyright (C) 2019 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.icu.tradefed.testtype; |
| |
| import com.android.ddmlib.FileListingService; |
| import com.android.ddmlib.testrunner.ITestRunListener; |
| import com.android.tradefed.config.Option; |
| import com.android.tradefed.config.OptionClass; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.result.ITestInvocationListener; |
| import com.android.tradefed.testtype.IAbi; |
| import com.android.tradefed.testtype.IAbiReceiver; |
| import com.android.tradefed.testtype.IDeviceTest; |
| import com.android.tradefed.testtype.IRemoteTest; |
| import com.android.tradefed.testtype.IRuntimeHintProvider; |
| import com.android.tradefed.testtype.ITestFilterReceiver; |
| import com.android.tradefed.util.CommandResult; |
| import com.android.tradefed.util.CommandStatus; |
| import com.android.tradefed.util.FileUtil; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.LinkedList; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| import java.util.regex.Pattern; |
| |
| /** A Test that runs a native test package on given device. */ |
| @OptionClass(alias = "icu4c") |
| public class ICU4CTest |
| implements IDeviceTest, |
| ITestFilterReceiver, |
| IRemoteTest, |
| IAbiReceiver, |
| IRuntimeHintProvider { |
| |
| static final String DEFAULT_NATIVETEST_PATH = "/data/local/tmp"; |
| |
| private ITestDevice mDevice = null; |
| private IAbi mAbi = null; |
| |
| @Option(name = "module-name", description = "The name of the native test module to run.") |
| private String mTestModule = null; |
| |
| @Option(name = "command-filter-prefix", |
| description = "The prefix required for each test filter when running the shell command") |
| private String mCommandFilterPrefix = ""; |
| |
| @Option( |
| name = "native-test-timeout", |
| description = |
| "The max time in ms for the test to run. " |
| + "Test run will be aborted if any test takes longer." |
| ) |
| private int mMaxTestTimeMs = 1 * 60 * 1000; |
| |
| @Option(name = "run-test-as", description = "User to execute test binary as.") |
| private String mRunTestAs = null; |
| |
| @Option( |
| name = "runtime-hint", |
| description = "The hint about the test's runtime.", |
| isTimeVal = true |
| ) |
| private long mRuntimeHint = 60000; // 1 minute |
| |
| @Option( |
| name = "no-fail-data-errors", |
| description = "Treat data load failures as warnings, not errors." |
| ) |
| private boolean mNoFailDataErrors = false; |
| |
| @Option( |
| name = "include-filter", |
| description = "The ICU-specific positive filter of the test names to run." |
| ) |
| private Set<String> mIncludeFilters = new LinkedHashSet<>(); |
| |
| |
| @Option( |
| name = "exclude-filter", |
| description = "The ICU-specific negative filter of the test names to run." |
| ) |
| private Set<String> mExcludeFilters = new LinkedHashSet<>(); |
| |
| private static final String TEST_FLAG_NO_FAIL_DATA_ERRORS = "-w"; |
| private static final String TEST_FLAG_XML_OUTPUT = "-x"; |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void setDevice(ITestDevice device) { |
| mDevice = device; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public ITestDevice getDevice() { |
| return mDevice; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void setAbi(IAbi abi) { |
| mAbi = abi; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public IAbi getAbi() { |
| return mAbi; |
| } |
| |
| /** |
| * Set the Android native test module to run. |
| * |
| * @param moduleName The name of the native test module to run |
| */ |
| public void setModuleName(String moduleName) { |
| mTestModule = moduleName; |
| } |
| |
| /** |
| * Get the Android native test module to run. |
| * |
| * @return the name of the native test module to run, or null if not set |
| */ |
| public String getModuleName() { |
| return mTestModule; |
| } |
| |
| /** Set the max time in ms for the test to run. */ |
| @VisibleForTesting |
| void setMaxTestTimeMs(int timeout) { |
| mMaxTestTimeMs = timeout; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public long getRuntimeHint() { |
| return mRuntimeHint; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void addIncludeFilter(String filter) { |
| mIncludeFilters.add(filter); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void addAllIncludeFilters(Set<String> filters) { |
| mIncludeFilters.addAll(filters); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void addExcludeFilter(String filter) { |
| mExcludeFilters.add(filter); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void addAllExcludeFilters(Set<String> filters) { |
| mExcludeFilters.addAll(filters); |
| } |
| |
| @Override |
| public Set<String> getIncludeFilters() { |
| return mIncludeFilters; |
| } |
| |
| @Override |
| public Set<String> getExcludeFilters() { |
| return mExcludeFilters; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void clearExcludeFilters() { |
| mExcludeFilters.clear(); |
| } |
| |
| @Override |
| public void clearIncludeFilters() { |
| mIncludeFilters.clear(); |
| } |
| |
| public void setCommandFilterPrefix(String s) { |
| if (s == null) { |
| throw new NullPointerException("CommandFilterPrefix can't be null"); |
| } |
| mCommandFilterPrefix = s; |
| } |
| |
| public String getCommandFilterPrefix() { |
| return mCommandFilterPrefix; |
| } |
| |
| /** |
| * Gets the path where native tests live on the device. |
| * |
| * @return The path on the device where the native tests live. |
| */ |
| private String getTestPath() { |
| StringBuilder testPath = new StringBuilder(DEFAULT_NATIVETEST_PATH); |
| |
| testPath.append(FileListingService.FILE_SEPARATOR); |
| testPath.append(mTestModule); |
| |
| return testPath.toString(); |
| } |
| |
| protected boolean isDeviceFileExecutable(String fullPath) throws DeviceNotAvailableException { |
| CommandResult commandResult = mDevice.executeShellV2Command(String.format("[ -x %s ]", |
| fullPath)); |
| return commandResult.getExitCode() == 0; |
| } |
| |
| /** |
| * Run the given test binary. |
| * |
| * @param testDevice the {@link ITestDevice} |
| * @param fullPath absolute file system path to test binary on device |
| * @param xmlOutputPath absolute file system path to the XML result file |
| * @throws DeviceNotAvailableException |
| */ |
| private CommandResult doRunTest( |
| final ITestDevice testDevice, final String fullPath, final String xmlOutputPath) |
| throws DeviceNotAvailableException { |
| String cmd = getTestCmdLine(fullPath, xmlOutputPath); |
| CLog.i("Running ICU4C test %s on %s", cmd, testDevice.getSerialNumber()); |
| CommandResult commandResult = |
| testDevice.executeShellV2Command( |
| cmd, |
| mMaxTestTimeMs /* maxTimeToShellOutputResponse */, |
| TimeUnit.MILLISECONDS, |
| 0 /* retryAttempts */); |
| CLog.d( |
| "%s executed with an exit code of %d and status code %s", |
| fullPath, commandResult.getExitCode(), commandResult.getStatus().name()); |
| |
| if (commandResult.getExitCode() != 0) { |
| CLog.e("Command stdout:\n " + commandResult.getStdout()); |
| CLog.e("Command stderr:\n " + commandResult.getStderr()); |
| throw new IllegalStateException( |
| String.format( |
| "%s non-zero exit code %d", fullPath, commandResult.getExitCode())); |
| } |
| |
| if (commandResult.getStatus() != CommandStatus.SUCCESS) { |
| CLog.e("Command stdout:\n " + commandResult.getStdout()); |
| CLog.e("Command stderr:\n " + commandResult.getStderr()); |
| throw new IllegalStateException( |
| String.format( |
| "%s exits with status %s", fullPath, commandResult.getStatus().name())); |
| } |
| |
| return commandResult; |
| } |
| |
| /** |
| * Run the given test binary and parse XML results |
| * |
| * <p>This methods typically requires the filter for .tff and .xml files, otherwise it will post |
| * some unwanted results. |
| * |
| * @param testDevice the {@link ITestDevice} |
| * @param fullPath absolute file system path to test binary on device |
| * @param listener the {@link ITestRunListener} |
| * @throws DeviceNotAvailableException |
| */ |
| private void runTest( |
| final ITestDevice testDevice, final String fullPath, ITestInvocationListener listener) |
| throws DeviceNotAvailableException { |
| CLog.i("Running runTest path: %s", fullPath); |
| String xmlFullPath = fullPath + "_res.xml"; |
| |
| try { |
| CommandResult commandResult = doRunTest(testDevice, fullPath, xmlFullPath); |
| |
| // Pull the result file, may not exist if issue with the test. |
| String testRunName = fullPath.substring(fullPath.lastIndexOf("/") + 1); |
| File tmpOutput = FileUtil.createTempFile(testRunName, ".xml"); |
| testDevice.pullFile(xmlFullPath, tmpOutput); |
| |
| ICU4CXmlResultParser parser = new ICU4CXmlResultParser(mTestModule, |
| testRunName, listener); |
| |
| parser.parseResult(tmpOutput, commandResult); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } finally { |
| // Clean the file on the device |
| testDevice.executeShellCommand("rm " + xmlFullPath); |
| } |
| } |
| |
| /** |
| * Helper method to build the test command to run. |
| * |
| * @param fullPath absolute file system path to test binary on device |
| * @param xmlPath absolute file system path to the XML reult file on device |
| * @return the shell command line to run for the test |
| */ |
| protected String getTestCmdLine(String fullPath, String xmlPath) { |
| List<String> args = new LinkedList<String>(); |
| |
| // su to requested user |
| if (mRunTestAs != null) { |
| args.add(String.format("su %s", mRunTestAs)); |
| } |
| |
| args.add(fullPath); |
| |
| if (mNoFailDataErrors) { |
| args.add(TEST_FLAG_NO_FAIL_DATA_ERRORS); |
| } |
| |
| args.add(TEST_FLAG_XML_OUTPUT); |
| args.add(xmlPath); |
| |
| String cmd = String.join(" ", args); |
| |
| List<String> includeFilters = preprocessIncludeFilters(); |
| if (!includeFilters.isEmpty()) { |
| cmd += " " + String.join(" ", includeFilters); |
| } |
| |
| return cmd; |
| } |
| |
| private List<String> preprocessIncludeFilters() { |
| Set<String> includeFilters = mIncludeFilters; |
| List<String> results = new ArrayList<>(); |
| for (String filter : includeFilters) { |
| if (!filter.startsWith(mTestModule)) { |
| CLog.i("Ignore positive filter which does not contain module prefix \"%s\":%s", |
| mTestModule, filter); |
| continue; |
| } |
| String modifiedFilter = filter.substring(mTestModule.length()); |
| if (filter.length() == 0) { |
| // Ignore because it intends to run all tests when the filter is the module name. |
| continue; |
| } |
| // Android / tradefed uses '.' as package separator, but ICU4C tests use '/'. |
| modifiedFilter = modifiedFilter.replace('.', '/'); |
| |
| if (modifiedFilter.charAt(0) != '/' || modifiedFilter.length() == 1) { |
| CLog.i("Ignore invalid filter:%s", filter); |
| continue; |
| } |
| modifiedFilter = mCommandFilterPrefix + modifiedFilter.substring(1); |
| results.add(modifiedFilter); |
| } |
| return results; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { |
| if (mDevice == null) { |
| throw new IllegalArgumentException("Device has not been set"); |
| } |
| |
| String testPath = getTestPath(); |
| if (!mDevice.doesFileExist(testPath)) { |
| throw new IllegalStateException( |
| String.format( |
| "Could not find native test binary %s in %s!", |
| testPath, mDevice.getSerialNumber())); |
| } |
| if (!isDeviceFileExecutable(testPath)) { |
| throw new IllegalStateException( |
| String.format( |
| "%s exists but is not executable in %s.", |
| testPath, mDevice.getSerialNumber())); |
| } |
| if (!mExcludeFilters.isEmpty()) { |
| // Log a message instead of throwing IllegalStateException. http://b/213284403 |
| CLog.w("ICU4C test suites do not support exclude filters: %s", |
| Arrays.toString(mExcludeFilters.toArray())); |
| } |
| runTest(mDevice, testPath, listener); |
| } |
| } |