1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tradefed.testtype;
18 
19 import com.android.ddmlib.FileListingService;
20 import com.android.ddmlib.IShellOutputReceiver;
21 import com.android.tradefed.config.Option;
22 import com.android.tradefed.config.OptionClass;
23 import com.android.tradefed.device.CollectingOutputReceiver;
24 import com.android.tradefed.device.DeviceNotAvailableException;
25 import com.android.tradefed.device.ITestDevice;
26 import com.android.tradefed.invoker.TestInformation;
27 import com.android.tradefed.log.LogUtil.CLog;
28 import com.android.tradefed.result.ITestInvocationListener;
29 import com.android.tradefed.testtype.coverage.CoverageOptions;
30 import com.android.tradefed.util.AbiUtils;
31 import com.android.tradefed.util.FileUtil;
32 
33 import com.google.common.annotations.VisibleForTesting;
34 
35 import org.json.JSONException;
36 import org.json.JSONObject;
37 
38 import java.io.File;
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.LinkedHashSet;
42 import java.util.List;
43 import java.util.Set;
44 import java.util.concurrent.TimeUnit;
45 
46 /** A Test that runs a native test package on given device. */
47 @OptionClass(alias = "gtest")
48 public class GTest extends GTestBase implements IDeviceTest {
49 
50     static final String DEFAULT_NATIVETEST_PATH = "/data/nativetest";
51 
52     private ITestDevice mDevice = null;
53 
54     @Option(name = "native-test-device-path",
55             description="The path on the device where native tests are located.")
56     private String mNativeTestDevicePath = DEFAULT_NATIVETEST_PATH;
57 
58     @Option(
59             name = "reboot-before-test",
60             description = "Reboot the device before the test suite starts.")
61     private boolean mRebootBeforeTest = false;
62 
63     @Option(name = "stop-runtime",
64             description = "Stops the Java application runtime before test execution.")
65     private boolean mStopRuntime = false;
66 
67     /** @deprecated Use the --coverage-flush option in CoverageOptions instead. */
68     @Deprecated
69     @Option(
70         name = "coverage-flush",
71         description = "Forces coverage data to be flushed at the end of the test."
72     )
73     private boolean mCoverageFlush = false;
74 
75     /** @deprecated Use the --coverage-processes option in CoverageOptions instead. */
76     @Deprecated
77     @Option(
78         name = "coverage-processes",
79         description = "Name of processes to collect coverage data from."
80     )
81     private List<String> mCoverageProcesses = new ArrayList<>();
82 
83     /** @deprecated Merged into the --coverage-flush option in CoverageOptions instead. */
84     @Deprecated
85     @Option(
86         name = "coverage-clear-before-test",
87         description = "Clears all coverage counters before test execution."
88     )
89     private boolean mCoverageClearBeforeTest = true;
90 
91     @Option(
92             name = "filter-non-matching-abi-folders",
93             description =
94                     "If an abi specific hierarchy seem to exists, only run the parts that "
95                             + "match abi under test.")
96     private boolean mFilterAbiFolders = true;
97 
98     @Option(
99             name = "use-updated-shard-retry",
100             description = "Whether to use the updated logic for retry with sharding.")
101     private boolean mUseUpdatedShardRetry = true;
102 
103     @Option(
104             name = "force-no-test-error",
105             description = "Whether to throw an error if no test binary is found to execute.")
106     private boolean mForceNoTestError = false;
107 
108     /** Whether any incomplete test is found in the current run. */
109     private boolean mIncompleteTestFound = false;
110 
111     /** List of tests that failed in the current test run when test run was complete. */
112     private Set<String> mCurFailedTests = new LinkedHashSet<>();
113 
114     // Max characters allowed for executing GTest via command line
115     private static final int GTEST_CMD_CHAR_LIMIT = 1000;
116     /**
117      * {@inheritDoc}
118      */
119     @Override
setDevice(ITestDevice device)120     public void setDevice(ITestDevice device) {
121         mDevice = device;
122     }
123 
124     /**
125      * {@inheritDoc}
126      */
127     @Override
getDevice()128     public ITestDevice getDevice() {
129         return mDevice;
130     }
131 
132     @Override
loadFilter(String binaryOnDevice)133     protected String loadFilter(String binaryOnDevice) throws DeviceNotAvailableException {
134         try {
135             String filterKey = getTestFilterKey();
136             CLog.i("Loading filter from file for key: '%s'", filterKey);
137             String filterFile = String.format("%s%s", binaryOnDevice, FILTER_EXTENSION);
138             if (getDevice().doesFileExist(filterFile)) {
139                 String content =
140                         getDevice().executeShellCommand(String.format("cat \"%s\"", filterFile));
141                 if (content != null && !content.isEmpty()) {
142                     JSONObject filter = new JSONObject(content);
143                     JSONObject filterObject = filter.getJSONObject(filterKey);
144                     return filterObject.getString("filter");
145                 }
146                 CLog.e("Error with content of the filter file %s: %s", filterFile, content);
147             } else {
148                 CLog.e("Filter file %s not found", filterFile);
149             }
150         } catch (JSONException e) {
151             CLog.e(e);
152         }
153         return null;
154     }
155 
156     /**
157      * Gets the path where native tests live on the device.
158      *
159      * @return The path on the device where the native tests live.
160      */
getTestPath()161     private String getTestPath() {
162         StringBuilder testPath = new StringBuilder(mNativeTestDevicePath);
163         String testModule = getTestModule();
164         if (testModule != null) {
165             testPath.append(FileListingService.FILE_SEPARATOR);
166             testPath.append(testModule);
167         }
168         return testPath.toString();
169     }
170 
setNativeTestDevicePath(String path)171     public void setNativeTestDevicePath(String path) {
172         mNativeTestDevicePath = path;
173     }
174 
175     /**
176      * Executes all native tests in a folder as well as in all subfolders recursively.
177      *
178      * @param root The root folder to begin searching for native tests
179      * @param testDevice The device to run tests on
180      * @param listener the {@link ITestInvocationListener}
181      * @throws DeviceNotAvailableException
182      */
183     @VisibleForTesting
doRunAllTestsInSubdirectory( String root, ITestDevice testDevice, ITestInvocationListener listener)184     void doRunAllTestsInSubdirectory(
185             String root, ITestDevice testDevice, ITestInvocationListener listener)
186             throws DeviceNotAvailableException {
187         Set<String> excludeDirectories = new LinkedHashSet<>();
188         // Decide to filter out a folder sub-path based on whether we should enforce the
189         // current abi under test.
190         if (mFilterAbiFolders && getAbi() != null) {
191             String currentArch = AbiUtils.getArchForAbi(getAbi().getName());
192             // exclude all abi specific folders, that is not the current abi, from the search
193             for (String supportedArch : AbiUtils.getArchSupported()) {
194                 if (!supportedArch.equals(currentArch)) {
195                     excludeDirectories.add(supportedArch);
196                 }
197             }
198         }
199         String[] executableFiles = getExecutableFiles(testDevice, root, excludeDirectories);
200         boolean gtestExecutableFound = false;
201         for (String filePath : executableFiles) {
202             if (shouldRunFile(filePath)) {
203                 gtestExecutableFound = true;
204                 IShellOutputReceiver resultParser =
205                         createResultParser(getFileName(filePath), listener);
206                 String flags = getAllGTestFlags(filePath);
207                 CLog.i("Running gtest %s %s on %s", filePath, flags, testDevice.getSerialNumber());
208                 if (isEnableXmlOutput()) {
209                     runTestXml(testDevice, filePath, flags, listener);
210                 } else {
211                     runTest(testDevice, resultParser, filePath, flags);
212                 }
213             }
214         }
215         if (!gtestExecutableFound) {
216             CLog.d("Failed to find any native test in directory %s.", root);
217             if (mForceNoTestError) {
218                 throw new RuntimeException(
219                         String.format("Failed to find any native test in directory %s.", root));
220             }
221         }
222     }
223 
getFileName(String fullPath)224     String getFileName(String fullPath) {
225         int pos = fullPath.lastIndexOf('/');
226         if (pos == -1) {
227             return fullPath;
228         }
229         String fileName = fullPath.substring(pos + 1);
230         if (fileName.isEmpty()) {
231             throw new IllegalArgumentException("input should not end with \"/\"");
232         }
233         return fileName;
234     }
235 
236     /**
237      * Helper method to determine if we should execute a given file.
238      *
239      * @param fullPath the full path of the file in question
240      * @return true if we should execute the said file.
241      */
shouldRunFile(String fullPath)242     protected boolean shouldRunFile(String fullPath) {
243         if (fullPath == null || fullPath.isEmpty()) {
244             return false;
245         }
246 
247         // look for files that start with the module name if it is set
248         String moduleName = getTestModule();
249         String fileName = getFileName(fullPath);
250         if (moduleName != null && !fileName.startsWith(moduleName)) {
251             return false;
252         }
253 
254         // filter out files excluded by the exclusion regex, for example .so files
255         List<String> fileExclusionFilterRegex = getFileExclusionFilterRegex();
256         for (String regex : fileExclusionFilterRegex) {
257             if (fullPath.matches(regex)) {
258                 CLog.i("File %s matches exclusion file regex %s, skipping", fullPath, regex);
259                 return false;
260             }
261         }
262 
263         return true;
264     }
265 
266     /**
267      * Helper method to run a gtest command from a temporary script, in the case that the command
268      * is too long to be run directly by adb.
269      * @param testDevice the device on which to run the command
270      * @param cmd the command string to run
271      * @param resultParser the output receiver for reading test results
272      */
executeCommandByScript(final ITestDevice testDevice, final String cmd, final IShellOutputReceiver resultParser)273     protected void executeCommandByScript(final ITestDevice testDevice, final String cmd,
274             final IShellOutputReceiver resultParser) throws DeviceNotAvailableException {
275         String tmpFileDevice = "/data/local/tmp/gtest_script.sh";
276         testDevice.pushString(String.format("#!/bin/bash\n%s", cmd), tmpFileDevice);
277         // force file to be executable
278         testDevice.executeShellCommand(String.format("chmod 755 %s", tmpFileDevice));
279         testDevice.executeShellCommand(
280                 String.format("sh %s", tmpFileDevice),
281                 resultParser,
282                 getMaxTestTimeMs() /* maxTimeToShellOutputResponse */,
283                 TimeUnit.MILLISECONDS,
284                 0 /* retry attempts */);
285         testDevice.deleteFile(tmpFileDevice);
286     }
287 
288     @Override
getGTestCmdLine(String fullPath, String flags)289     protected String getGTestCmdLine(String fullPath, String flags) {
290         StringBuilder sb = new StringBuilder();
291         // When sharding a device GTest, add args to the command line
292         if (getShardCount() > 0) {
293             if (isCollectTestsOnly()) {
294                 CLog.w(
295                         "--collect-tests-only option ignores sharding parameters, and will cause "
296                                 + "each shard to collect all tests.");
297             }
298             sb.append(String.format("GTEST_SHARD_INDEX=%s ", getShardIndex()));
299             sb.append(String.format("GTEST_TOTAL_SHARDS=%s ", getShardCount()));
300         }
301         if (isCoverageEnabled()) {
302             sb.append("LLVM_PROFILE_FILE=/data/local/tmp/clang-%m.profraw ");
303         }
304         sb.append(super.getGTestCmdLine(fullPath, flags));
305         return sb.toString();
306     }
307 
308     @Override
createFlagFile(String filter)309     protected String createFlagFile(String filter) throws DeviceNotAvailableException {
310         String flagPath = super.createFlagFile(filter);
311         if (flagPath == null) {
312             // Return null to fall back to base filter
313             return null;
314         }
315         File flagFile = new File(flagPath);
316         String devicePath = "/data/local/tmp/" + flagFile.getName();
317         try {
318             if (!mDevice.pushFile(flagFile, devicePath)) {
319                 // Failed to push flagfile, return null to fall back to base filter
320                 return null;
321             }
322         } finally {
323             FileUtil.deleteFile(flagFile);
324         }
325         return devicePath;
326     }
327 
328     /**
329      * Run the given gtest binary
330      *
331      * @param testDevice the {@link ITestDevice}
332      * @param resultParser the test run output parser
333      * @param fullPath absolute file system path to gtest binary on device
334      * @param flags gtest execution flags
335      * @throws DeviceNotAvailableException
336      */
runTest(final ITestDevice testDevice, final IShellOutputReceiver resultParser, final String fullPath, final String flags)337     private void runTest(final ITestDevice testDevice, final IShellOutputReceiver resultParser,
338             final String fullPath, final String flags) throws DeviceNotAvailableException {
339         // TODO: add individual test timeout support, and rerun support
340         try {
341             for (String cmd : getBeforeTestCmd()) {
342                 testDevice.executeShellCommand(cmd);
343             }
344 
345             if (mRebootBeforeTest && !isCollectTestsOnly()) {
346                 CLog.d("Rebooting device before test starts as requested.");
347                 testDevice.reboot();
348             }
349 
350             String cmd = getGTestCmdLine(fullPath, flags);
351             // ensure that command is not too long for adb
352             if (cmd.length() < GTEST_CMD_CHAR_LIMIT) {
353                 testDevice.executeShellCommand(
354                         cmd,
355                         resultParser,
356                         getMaxTestTimeMs() /* maxTimeToShellOutputResponse */,
357                         TimeUnit.MILLISECONDS,
358                         0 /* retryAttempts */);
359             } else {
360                 // wrap adb shell command in script if command is too long for direct execution
361                 executeCommandByScript(testDevice, cmd, resultParser);
362             }
363         } catch (DeviceNotAvailableException e) {
364             throw e;
365         } catch (RuntimeException e) {
366             throw e;
367         } finally {
368             // TODO: consider moving the flush of parser data on exceptions to TestDevice or
369             // AdbHelper
370             resultParser.flush();
371             if (resultParser instanceof GTestResultParser) {
372                 if (((GTestResultParser) resultParser).isTestRunIncomplete()) {
373                     mIncompleteTestFound = true;
374                 } else {
375                     // if test run is complete, collect the failed tests so that they can be retried
376                     mCurFailedTests.addAll(((GTestResultParser) resultParser).getFailedTests());
377                 }
378             }
379             for (String cmd : getAfterTestCmd()) {
380                 testDevice.executeShellCommand(cmd);
381             }
382         }
383     }
384 
385     /**
386      * Run the given gtest binary and parse XML results This methods typically requires the filter
387      * for .tff and .xml files, otherwise it will post some unwanted results.
388      *
389      * @param testDevice the {@link ITestDevice}
390      * @param fullPath absolute file system path to gtest binary on device
391      * @param flags gtest execution flags
392      * @param listener the {@link ITestInvocationListener}
393      * @throws DeviceNotAvailableException
394      */
runTestXml( final ITestDevice testDevice, final String fullPath, final String flags, ITestInvocationListener listener)395     private void runTestXml(
396             final ITestDevice testDevice,
397             final String fullPath,
398             final String flags,
399             ITestInvocationListener listener)
400             throws DeviceNotAvailableException {
401         CollectingOutputReceiver outputCollector = new CollectingOutputReceiver();
402         File tmpOutput = null;
403         try {
404             String testRunName = fullPath.substring(fullPath.lastIndexOf("/") + 1);
405             tmpOutput = FileUtil.createTempFile(testRunName, ".xml");
406             String tmpResName = fullPath + "_res.xml";
407             String extraFlag = String.format(GTEST_XML_OUTPUT, tmpResName);
408             String fullFlagCmd =  String.format("%s %s", flags, extraFlag);
409 
410             // Run the tests with modified flags
411             runTest(testDevice, outputCollector, fullPath, fullFlagCmd);
412             // Pull the result file, may not exist if issue with the test.
413             testDevice.pullFile(tmpResName, tmpOutput);
414             // Clean the file on the device
415             testDevice.deleteFile(tmpResName);
416             GTestXmlResultParser parser = createXmlParser(testRunName, listener);
417             // Attempt to parse the file, doesn't matter if the content is invalid.
418             if (tmpOutput.exists()) {
419                 parser.parseResult(tmpOutput, outputCollector);
420                 if (parser.isTestRunIncomplete()) {
421                     mIncompleteTestFound = true;
422                 } else {
423                     // if test run is complete, collect the failed tests so that they can be retried
424                     mCurFailedTests.addAll(parser.getFailedTests());
425                 }
426             }
427         } catch (DeviceNotAvailableException | RuntimeException e) {
428             throw e;
429         } catch (IOException e) {
430             throw new RuntimeException(e);
431         } finally {
432             outputCollector.flush();
433             for (String cmd : getAfterTestCmd()) {
434                 testDevice.executeShellCommand(cmd);
435             }
436             FileUtil.deleteFile(tmpOutput);
437         }
438     }
439 
440     /** {@inheritDoc} */
441     @Override
run(TestInformation testInfo, ITestInvocationListener listener)442     public void run(TestInformation testInfo, ITestInvocationListener listener)
443             throws DeviceNotAvailableException {
444         // TODO: add support for rerunning tests
445         if (mDevice == null) {
446             throw new IllegalArgumentException("Device has not been set");
447         }
448 
449         String testPath = getTestPath();
450         if (!mDevice.doesFileExist(testPath)) {
451             CLog.w("Could not find native test directory %s in %s!", testPath,
452                     mDevice.getSerialNumber());
453             return;
454         }
455         // Reset flags that are used to track results of current test run.
456         mIncompleteTestFound = false;
457         mCurFailedTests = new LinkedHashSet<>();
458 
459         if (mStopRuntime) {
460             mDevice.executeShellCommand("stop");
461         }
462         listener = getGTestListener(listener);
463 
464         Throwable throwable = null;
465         try {
466             doRunAllTestsInSubdirectory(testPath, mDevice, listener);
467         } catch (Throwable t) {
468             throwable = t;
469             // if we encounter any errors, count it as test Incomplete so that retry attempts
470             // during sharding uses a full retry.
471             mIncompleteTestFound = true;
472             throw t;
473         } finally {
474             if (mUseUpdatedShardRetry) {
475                 // notify of test execution will enable the new sharding retry behavior since Gtest
476                 // will be aware of retries.
477                 notifyTestExecution(mIncompleteTestFound, mCurFailedTests);
478             }
479             if (!(throwable instanceof DeviceNotAvailableException)) {
480                 if (mStopRuntime) {
481                     mDevice.executeShellCommand("start");
482                     mDevice.waitForDeviceAvailable();
483                 }
484             }
485         }
486     }
487 
isRebootBeforeTestEnabled()488     public boolean isRebootBeforeTestEnabled() {
489         return mRebootBeforeTest;
490     }
491 
492     /**
493      * Searches directories recursively to find all executable files.
494      *
495      * @param device {@link ITestDevice} where the search will occur.
496      * @param deviceFilePath is the path on the device where to do the search.
497      * @param excludeDirectories Set of directory names that must be excluded from the search.
498      * @return Array of string containing all the executable file paths on the device.
499      * @throws DeviceNotAvailableException
500      */
getExecutableFiles( ITestDevice device, String deviceFilePath, Set<String> excludeDirectories)501     private String[] getExecutableFiles(
502             ITestDevice device, String deviceFilePath, Set<String> excludeDirectories)
503             throws DeviceNotAvailableException {
504         String cmd = String.format("find -L %s -type f -perm -u+r,u+x", deviceFilePath);
505 
506         if (excludeDirectories != null && !excludeDirectories.isEmpty()) {
507             for (String directoryName : excludeDirectories) {
508                 cmd += String.format(" -not -path \"*/%s/*\"", directoryName);
509             }
510         }
511 
512         String output = device.executeShellCommand(cmd);
513         if (output.trim().isEmpty()) {
514             return new String[0];
515         }
516         return output.split("\r?\n");
517     }
518 
519     /** Checks if native coverage is enabled. */
isCoverageEnabled()520     private boolean isCoverageEnabled() {
521         CoverageOptions options = getConfiguration().getCoverageOptions();
522         return options.isCoverageEnabled()
523                 && (options.getCoverageToolchains().contains(CoverageOptions.Toolchain.GCOV)
524                         || options.getCoverageToolchains()
525                                 .contains(CoverageOptions.Toolchain.CLANG));
526     }
527 }
528