| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tradefed.testtype; |
| |
| import com.android.ddmlib.FileListingService; |
| import com.android.ddmlib.IShellOutputReceiver; |
| import com.android.tradefed.config.Option; |
| import com.android.tradefed.config.OptionClass; |
| import com.android.tradefed.device.CollectingOutputReceiver; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.invoker.TestInformation; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.result.ITestInvocationListener; |
| import com.android.tradefed.testtype.coverage.CoverageOptions; |
| import com.android.tradefed.util.AbiUtils; |
| import com.android.tradefed.util.FileUtil; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| |
| /** A Test that runs a native test package on given device. */ |
| @OptionClass(alias = "gtest") |
| public class GTest extends GTestBase implements IDeviceTest { |
| |
| static final String DEFAULT_NATIVETEST_PATH = "/data/nativetest"; |
| |
| private ITestDevice mDevice = null; |
| |
| @Option(name = "native-test-device-path", |
| description="The path on the device where native tests are located.") |
| private String mNativeTestDevicePath = DEFAULT_NATIVETEST_PATH; |
| |
| @Option( |
| name = "reboot-before-test", |
| description = "Reboot the device before the test suite starts.") |
| private boolean mRebootBeforeTest = false; |
| |
| @Option(name = "stop-runtime", |
| description = "Stops the Java application runtime before test execution.") |
| private boolean mStopRuntime = false; |
| |
| /** @deprecated Use the --coverage-flush option in CoverageOptions instead. */ |
| @Deprecated |
| @Option( |
| name = "coverage-flush", |
| description = "Forces coverage data to be flushed at the end of the test." |
| ) |
| private boolean mCoverageFlush = false; |
| |
| /** @deprecated Use the --coverage-processes option in CoverageOptions instead. */ |
| @Deprecated |
| @Option( |
| name = "coverage-processes", |
| description = "Name of processes to collect coverage data from." |
| ) |
| private List<String> mCoverageProcesses = new ArrayList<>(); |
| |
| /** @deprecated Merged into the --coverage-flush option in CoverageOptions instead. */ |
| @Deprecated |
| @Option( |
| name = "coverage-clear-before-test", |
| description = "Clears all coverage counters before test execution." |
| ) |
| private boolean mCoverageClearBeforeTest = true; |
| |
| @Option( |
| name = "filter-non-matching-abi-folders", |
| description = |
| "If an abi specific hierarchy seem to exists, only run the parts that " |
| + "match abi under test.") |
| private boolean mFilterAbiFolders = true; |
| |
| @Option( |
| name = "use-updated-shard-retry", |
| description = "Whether to use the updated logic for retry with sharding.") |
| private boolean mUseUpdatedShardRetry = true; |
| |
| /** Whether any incomplete test is found in the current run. */ |
| private boolean mIncompleteTestFound = false; |
| |
| /** List of tests that failed in the current test run when test run was complete. */ |
| private Set<String> mCurFailedTests = new LinkedHashSet<>(); |
| |
| // Max characters allowed for executing GTest via command line |
| private static final int GTEST_CMD_CHAR_LIMIT = 1000; |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void setDevice(ITestDevice device) { |
| mDevice = device; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public ITestDevice getDevice() { |
| return mDevice; |
| } |
| |
| @Override |
| protected String loadFilter(String binaryOnDevice) throws DeviceNotAvailableException { |
| try { |
| String filterKey = getTestFilterKey(); |
| CLog.i("Loading filter from file for key: '%s'", filterKey); |
| String filterFile = String.format("%s%s", binaryOnDevice, FILTER_EXTENSION); |
| if (getDevice().doesFileExist(filterFile)) { |
| String content = |
| getDevice().executeShellCommand(String.format("cat \"%s\"", filterFile)); |
| if (content != null && !content.isEmpty()) { |
| JSONObject filter = new JSONObject(content); |
| JSONObject filterObject = filter.getJSONObject(filterKey); |
| return filterObject.getString("filter"); |
| } |
| CLog.e("Error with content of the filter file %s: %s", filterFile, content); |
| } else { |
| CLog.e("Filter file %s not found", filterFile); |
| } |
| } catch (JSONException e) { |
| CLog.e(e); |
| } |
| return null; |
| } |
| |
| /** |
| * 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(mNativeTestDevicePath); |
| String testModule = getTestModule(); |
| if (testModule != null) { |
| testPath.append(FileListingService.FILE_SEPARATOR); |
| testPath.append(testModule); |
| } |
| return testPath.toString(); |
| } |
| |
| public void setNativeTestDevicePath(String path) { |
| mNativeTestDevicePath = path; |
| } |
| |
| /** |
| * Executes all native tests in a folder as well as in all subfolders recursively. |
| * |
| * @param root The root folder to begin searching for native tests |
| * @param testDevice The device to run tests on |
| * @param listener the {@link ITestInvocationListener} |
| * @throws DeviceNotAvailableException |
| */ |
| @VisibleForTesting |
| void doRunAllTestsInSubdirectory( |
| String root, ITestDevice testDevice, ITestInvocationListener listener) |
| throws DeviceNotAvailableException { |
| Set<String> excludeDirectories = new LinkedHashSet<>(); |
| // Decide to filter out a folder sub-path based on whether we should enforce the |
| // current abi under test. |
| if (mFilterAbiFolders && getAbi() != null) { |
| String currentArch = AbiUtils.getArchForAbi(getAbi().getName()); |
| // exclude all abi specific folders, that is not the current abi, from the search |
| for (String supportedArch : AbiUtils.getArchSupported()) { |
| if (!supportedArch.equals(currentArch)) { |
| excludeDirectories.add(supportedArch); |
| } |
| } |
| } |
| String[] executableFiles = getExecutableFiles(testDevice, root, excludeDirectories); |
| for (String filePath : executableFiles) { |
| if (shouldRunFile(filePath)) { |
| IShellOutputReceiver resultParser = |
| createResultParser(getFileName(filePath), listener); |
| String flags = getAllGTestFlags(filePath); |
| CLog.i("Running gtest %s %s on %s", filePath, flags, testDevice.getSerialNumber()); |
| if (isEnableXmlOutput()) { |
| runTestXml(testDevice, filePath, flags, listener); |
| } else { |
| runTest(testDevice, resultParser, filePath, flags); |
| } |
| } |
| } |
| } |
| |
| String getFileName(String fullPath) { |
| int pos = fullPath.lastIndexOf('/'); |
| if (pos == -1) { |
| return fullPath; |
| } |
| String fileName = fullPath.substring(pos + 1); |
| if (fileName.isEmpty()) { |
| throw new IllegalArgumentException("input should not end with \"/\""); |
| } |
| return fileName; |
| } |
| |
| /** |
| * Helper method to determine if we should execute a given file. |
| * |
| * @param fullPath the full path of the file in question |
| * @return true if we should execute the said file. |
| */ |
| protected boolean shouldRunFile(String fullPath) { |
| if (fullPath == null || fullPath.isEmpty()) { |
| return false; |
| } |
| |
| // look for files that start with the module name if it is set |
| String moduleName = getTestModule(); |
| String fileName = getFileName(fullPath); |
| if (moduleName != null && !fileName.startsWith(moduleName)) { |
| return false; |
| } |
| |
| // filter out files excluded by the exclusion regex, for example .so files |
| List<String> fileExclusionFilterRegex = getFileExclusionFilterRegex(); |
| for (String regex : fileExclusionFilterRegex) { |
| if (fullPath.matches(regex)) { |
| CLog.i("File %s matches exclusion file regex %s, skipping", fullPath, regex); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Helper method to run a gtest command from a temporary script, in the case that the command |
| * is too long to be run directly by adb. |
| * @param testDevice the device on which to run the command |
| * @param cmd the command string to run |
| * @param resultParser the output receiver for reading test results |
| */ |
| protected void executeCommandByScript(final ITestDevice testDevice, final String cmd, |
| final IShellOutputReceiver resultParser) throws DeviceNotAvailableException { |
| String tmpFileDevice = "/data/local/tmp/gtest_script.sh"; |
| testDevice.pushString(String.format("#!/bin/bash\n%s", cmd), tmpFileDevice); |
| // force file to be executable |
| testDevice.executeShellCommand(String.format("chmod 755 %s", tmpFileDevice)); |
| testDevice.executeShellCommand( |
| String.format("sh %s", tmpFileDevice), |
| resultParser, |
| getMaxTestTimeMs() /* maxTimeToShellOutputResponse */, |
| TimeUnit.MILLISECONDS, |
| 0 /* retry attempts */); |
| testDevice.deleteFile(tmpFileDevice); |
| } |
| |
| @Override |
| protected String getGTestCmdLine(String fullPath, String flags) { |
| StringBuilder sb = new StringBuilder(); |
| // When sharding a device GTest, add args to the command line |
| if (getShardCount() > 0) { |
| if (isCollectTestsOnly()) { |
| CLog.w( |
| "--collect-tests-only option ignores sharding parameters, and will cause " |
| + "each shard to collect all tests."); |
| } |
| sb.append(String.format("GTEST_SHARD_INDEX=%s ", getShardIndex())); |
| sb.append(String.format("GTEST_TOTAL_SHARDS=%s ", getShardCount())); |
| } |
| if (isCoverageEnabled()) { |
| sb.append("LLVM_PROFILE_FILE=/data/local/tmp/clang-%m.profraw "); |
| } |
| sb.append(super.getGTestCmdLine(fullPath, flags)); |
| return sb.toString(); |
| } |
| |
| @Override |
| protected String createFlagFile(String filter) throws DeviceNotAvailableException { |
| String flagPath = super.createFlagFile(filter); |
| if (flagPath == null) { |
| // Return null to fall back to base filter |
| return null; |
| } |
| File flagFile = new File(flagPath); |
| String devicePath = "/data/local/tmp/" + flagFile.getName(); |
| try { |
| if (!mDevice.pushFile(flagFile, devicePath)) { |
| // Failed to push flagfile, return null to fall back to base filter |
| return null; |
| } |
| } finally { |
| FileUtil.deleteFile(flagFile); |
| } |
| return devicePath; |
| } |
| |
| /** |
| * Run the given gtest binary |
| * |
| * @param testDevice the {@link ITestDevice} |
| * @param resultParser the test run output parser |
| * @param fullPath absolute file system path to gtest binary on device |
| * @param flags gtest execution flags |
| * @throws DeviceNotAvailableException |
| */ |
| private void runTest(final ITestDevice testDevice, final IShellOutputReceiver resultParser, |
| final String fullPath, final String flags) throws DeviceNotAvailableException { |
| // TODO: add individual test timeout support, and rerun support |
| try { |
| for (String cmd : getBeforeTestCmd()) { |
| testDevice.executeShellCommand(cmd); |
| } |
| |
| if (mRebootBeforeTest && !isCollectTestsOnly()) { |
| CLog.d("Rebooting device before test starts as requested."); |
| testDevice.reboot(); |
| } |
| |
| String cmd = getGTestCmdLine(fullPath, flags); |
| // ensure that command is not too long for adb |
| if (cmd.length() < GTEST_CMD_CHAR_LIMIT) { |
| testDevice.executeShellCommand( |
| cmd, |
| resultParser, |
| getMaxTestTimeMs() /* maxTimeToShellOutputResponse */, |
| TimeUnit.MILLISECONDS, |
| 0 /* retryAttempts */); |
| } else { |
| // wrap adb shell command in script if command is too long for direct execution |
| executeCommandByScript(testDevice, cmd, resultParser); |
| } |
| } catch (DeviceNotAvailableException e) { |
| throw e; |
| } catch (RuntimeException e) { |
| throw e; |
| } finally { |
| // TODO: consider moving the flush of parser data on exceptions to TestDevice or |
| // AdbHelper |
| resultParser.flush(); |
| if (resultParser instanceof GTestResultParser) { |
| if (((GTestResultParser) resultParser).isTestRunIncomplete()) { |
| mIncompleteTestFound = true; |
| } else { |
| // if test run is complete, collect the failed tests so that they can be retried |
| mCurFailedTests.addAll(((GTestResultParser) resultParser).getFailedTests()); |
| } |
| } |
| for (String cmd : getAfterTestCmd()) { |
| testDevice.executeShellCommand(cmd); |
| } |
| } |
| } |
| |
| /** |
| * Run the given gtest binary and parse XML results 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 gtest binary on device |
| * @param flags gtest execution flags |
| * @param listener the {@link ITestInvocationListener} |
| * @throws DeviceNotAvailableException |
| */ |
| private void runTestXml( |
| final ITestDevice testDevice, |
| final String fullPath, |
| final String flags, |
| ITestInvocationListener listener) |
| throws DeviceNotAvailableException { |
| CollectingOutputReceiver outputCollector = new CollectingOutputReceiver(); |
| File tmpOutput = null; |
| try { |
| String testRunName = fullPath.substring(fullPath.lastIndexOf("/") + 1); |
| tmpOutput = FileUtil.createTempFile(testRunName, ".xml"); |
| String tmpResName = fullPath + "_res.xml"; |
| String extraFlag = String.format(GTEST_XML_OUTPUT, tmpResName); |
| String fullFlagCmd = String.format("%s %s", flags, extraFlag); |
| |
| // Run the tests with modified flags |
| runTest(testDevice, outputCollector, fullPath, fullFlagCmd); |
| // Pull the result file, may not exist if issue with the test. |
| testDevice.pullFile(tmpResName, tmpOutput); |
| // Clean the file on the device |
| testDevice.deleteFile(tmpResName); |
| GTestXmlResultParser parser = createXmlParser(testRunName, listener); |
| // Attempt to parse the file, doesn't matter if the content is invalid. |
| if (tmpOutput.exists()) { |
| parser.parseResult(tmpOutput, outputCollector); |
| if (parser.isTestRunIncomplete()) { |
| mIncompleteTestFound = true; |
| } else { |
| // if test run is complete, collect the failed tests so that they can be retried |
| mCurFailedTests.addAll(parser.getFailedTests()); |
| } |
| } |
| } catch (DeviceNotAvailableException | RuntimeException e) { |
| throw e; |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } finally { |
| outputCollector.flush(); |
| for (String cmd : getAfterTestCmd()) { |
| testDevice.executeShellCommand(cmd); |
| } |
| FileUtil.deleteFile(tmpOutput); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void run(TestInformation testInfo, ITestInvocationListener listener) |
| throws DeviceNotAvailableException { |
| // TODO: add support for rerunning tests |
| if (mDevice == null) { |
| throw new IllegalArgumentException("Device has not been set"); |
| } |
| |
| String testPath = getTestPath(); |
| if (!mDevice.doesFileExist(testPath)) { |
| CLog.w("Could not find native test directory %s in %s!", testPath, |
| mDevice.getSerialNumber()); |
| return; |
| } |
| // Reset flags that are used to track results of current test run. |
| mIncompleteTestFound = false; |
| mCurFailedTests = new LinkedHashSet<>(); |
| |
| if (mStopRuntime) { |
| mDevice.executeShellCommand("stop"); |
| } |
| listener = getGTestListener(listener); |
| |
| Throwable throwable = null; |
| try { |
| doRunAllTestsInSubdirectory(testPath, mDevice, listener); |
| } catch (Throwable t) { |
| throwable = t; |
| // if we encounter any errors, count it as test Incomplete so that retry attempts |
| // during sharding uses a full retry. |
| mIncompleteTestFound = true; |
| throw t; |
| } finally { |
| if (mUseUpdatedShardRetry) { |
| // notify of test execution will enable the new sharding retry behavior since Gtest |
| // will be aware of retries. |
| notifyTestExecution(mIncompleteTestFound, mCurFailedTests); |
| } |
| if (!(throwable instanceof DeviceNotAvailableException)) { |
| if (mStopRuntime) { |
| mDevice.executeShellCommand("start"); |
| mDevice.waitForDeviceAvailable(); |
| } |
| } |
| } |
| } |
| |
| public boolean isRebootBeforeTestEnabled() { |
| return mRebootBeforeTest; |
| } |
| |
| /** |
| * Searches directories recursively to find all executable files. |
| * |
| * @param device {@link ITestDevice} where the search will occur. |
| * @param deviceFilePath is the path on the device where to do the search. |
| * @param excludeDirectories Set of directory names that must be excluded from the search. |
| * @return Array of string containing all the executable file paths on the device. |
| * @throws DeviceNotAvailableException |
| */ |
| private String[] getExecutableFiles( |
| ITestDevice device, String deviceFilePath, Set<String> excludeDirectories) |
| throws DeviceNotAvailableException { |
| String cmd = String.format("find -L %s -type f -perm -a=x", deviceFilePath); |
| |
| if (excludeDirectories != null && !excludeDirectories.isEmpty()) { |
| for (String directoryName : excludeDirectories) { |
| cmd += String.format(" -not -path \"*/%s/*\"", directoryName); |
| } |
| } |
| |
| String output = device.executeShellCommand(cmd); |
| if (output.trim().isEmpty()) { |
| return new String[0]; |
| } |
| return output.split("\r?\n"); |
| } |
| |
| /** Checks if native coverage is enabled. */ |
| private boolean isCoverageEnabled() { |
| CoverageOptions options = getConfiguration().getCoverageOptions(); |
| return options.isCoverageEnabled() |
| && (options.getCoverageToolchains().contains(CoverageOptions.Toolchain.GCOV) |
| || options.getCoverageToolchains() |
| .contains(CoverageOptions.Toolchain.CLANG)); |
| } |
| } |