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 static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.CLANG;
20 import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.GCOV;
21 
22 import static com.google.common.base.Verify.verify;
23 
24 import com.android.ddmlib.FileListingService;
25 import com.android.ddmlib.IShellOutputReceiver;
26 import com.android.tradefed.config.Option;
27 import com.android.tradefed.config.OptionClass;
28 import com.android.tradefed.device.CollectingOutputReceiver;
29 import com.android.tradefed.device.DeviceNotAvailableException;
30 import com.android.tradefed.device.ITestDevice;
31 import com.android.tradefed.invoker.TestInformation;
32 import com.android.tradefed.log.LogUtil.CLog;
33 import com.android.tradefed.result.ITestInvocationListener;
34 import com.android.tradefed.testtype.coverage.CoverageOptions;
35 import com.android.tradefed.util.AbiUtils;
36 import com.android.tradefed.util.FileUtil;
37 import com.android.tradefed.util.NativeCodeCoverageFlusher;
38 
39 import com.google.common.annotations.VisibleForTesting;
40 
41 import org.json.JSONException;
42 import org.json.JSONObject;
43 
44 import java.io.File;
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.concurrent.TimeUnit;
49 
50 /** A Test that runs a native test package on given device. */
51 @OptionClass(alias = "gtest")
52 public class GTest extends GTestBase implements IDeviceTest {
53 
54     static final String DEFAULT_NATIVETEST_PATH = "/data/nativetest";
55 
56     private ITestDevice mDevice = null;
57 
58     @Option(name = "native-test-device-path",
59             description="The path on the device where native tests are located.")
60     private String mNativeTestDevicePath = DEFAULT_NATIVETEST_PATH;
61 
62     @Option(
63             name = "reboot-before-test",
64             description = "Reboot the device before the test suite starts.")
65     private boolean mRebootBeforeTest = false;
66 
67     @Option(name = "stop-runtime",
68             description = "Stops the Java application runtime before test execution.")
69     private boolean mStopRuntime = false;
70 
71     /** @deprecated Use the --coverage-flush option in CoverageOptions instead. */
72     @Deprecated
73     @Option(
74         name = "coverage-flush",
75         description = "Forces coverage data to be flushed at the end of the test."
76     )
77     private boolean mCoverageFlush = false;
78 
79     /** @deprecated Use the --coverage-processes option in CoverageOptions instead. */
80     @Deprecated
81     @Option(
82         name = "coverage-processes",
83         description = "Name of processes to collect coverage data from."
84     )
85     private List<String> mCoverageProcesses = new ArrayList<>();
86 
87     /** @deprecated Merged into the --coverage-flush option in CoverageOptions instead. */
88     @Deprecated
89     @Option(
90         name = "coverage-clear-before-test",
91         description = "Clears all coverage counters before test execution."
92     )
93     private boolean mCoverageClearBeforeTest = true;
94 
95     @Option(
96             name = "filter-non-matching-abi-folders",
97             description =
98                     "If an abi specific hierarchy seem to exists, only run the parts that "
99                             + "match abi under test.")
100     private boolean mFilterAbiFolders = true;
101 
102     // Max characters allowed for executing GTest via command line
103     private static final int GTEST_CMD_CHAR_LIMIT = 1000;
104     /**
105      * {@inheritDoc}
106      */
107     @Override
setDevice(ITestDevice device)108     public void setDevice(ITestDevice device) {
109         mDevice = device;
110     }
111 
112     /**
113      * {@inheritDoc}
114      */
115     @Override
getDevice()116     public ITestDevice getDevice() {
117         return mDevice;
118     }
119 
120     @Override
loadFilter(String binaryOnDevice)121     protected String loadFilter(String binaryOnDevice) throws DeviceNotAvailableException {
122         try {
123             String filterKey = getTestFilterKey();
124             CLog.i("Loading filter from file for key: '%s'", filterKey);
125             String filterFile = String.format("%s%s", binaryOnDevice, FILTER_EXTENSION);
126             if (getDevice().doesFileExist(filterFile)) {
127                 String content =
128                         getDevice().executeShellCommand(String.format("cat \"%s\"", filterFile));
129                 if (content != null && !content.isEmpty()) {
130                     JSONObject filter = new JSONObject(content);
131                     JSONObject filterObject = filter.getJSONObject(filterKey);
132                     return filterObject.getString("filter");
133                 }
134                 CLog.e("Error with content of the filter file %s: %s", filterFile, content);
135             } else {
136                 CLog.e("Filter file %s not found", filterFile);
137             }
138         } catch (JSONException e) {
139             CLog.e(e);
140         }
141         return null;
142     }
143 
144     /**
145      * Gets the path where native tests live on the device.
146      *
147      * @return The path on the device where the native tests live.
148      */
getTestPath()149     private String getTestPath() {
150         StringBuilder testPath = new StringBuilder(mNativeTestDevicePath);
151         String testModule = getTestModule();
152         if (testModule != null) {
153             testPath.append(FileListingService.FILE_SEPARATOR);
154             testPath.append(testModule);
155         }
156         return testPath.toString();
157     }
158 
159     /**
160      * Executes all native tests in a folder as well as in all subfolders recursively.
161      *
162      * @param root The root folder to begin searching for native tests
163      * @param testDevice The device to run tests on
164      * @param listener the {@link ITestInvocationListener}
165      * @throws DeviceNotAvailableException
166      */
167     @VisibleForTesting
doRunAllTestsInSubdirectory( String root, ITestDevice testDevice, ITestInvocationListener listener)168     void doRunAllTestsInSubdirectory(
169             String root, ITestDevice testDevice, ITestInvocationListener listener)
170             throws DeviceNotAvailableException {
171         if (testDevice.isDirectory(root)) {
172             if (!shouldRunFolder(root)) {
173                 return;
174             }
175             // recursively run tests in all subdirectories
176             for (String child : testDevice.getChildren(root)) {
177                 doRunAllTestsInSubdirectory(root + "/" + child, testDevice, listener);
178             }
179         } else {
180             // assume every file is a valid gtest binary.
181             IShellOutputReceiver resultParser = createResultParser(getFileName(root), listener);
182             if (shouldSkipFile(root)) {
183                 return;
184             }
185             String flags = getAllGTestFlags(root);
186             CLog.i("Running gtest %s %s on %s", root, flags, testDevice.getSerialNumber());
187             if (isEnableXmlOutput()) {
188                 runTestXml(testDevice, root, flags, listener);
189             } else {
190                 runTest(testDevice, resultParser, root, flags);
191             }
192         }
193     }
194 
195     /**
196      * Decide to filter out a folder subpath based on whether or not we should enforce the current
197      * abi under test.
198      */
shouldRunFolder(String path)199     boolean shouldRunFolder(String path) {
200         if (!mFilterAbiFolders) {
201             return true;
202         }
203         if (getAbi() == null) {
204             return true;
205         }
206         String fileName = getFileName(path);
207         if (!AbiUtils.getArchSupported().contains(fileName)) {
208             return true;
209         }
210         if (fileName.equals(AbiUtils.getArchForAbi(getAbi().getName()))) {
211             return true;
212         }
213         return false;
214     }
215 
getFileName(String fullPath)216     String getFileName(String fullPath) {
217         int pos = fullPath.lastIndexOf('/');
218         if (pos == -1) {
219             return fullPath;
220         }
221         String fileName = fullPath.substring(pos + 1);
222         if (fileName.isEmpty()) {
223             throw new IllegalArgumentException("input should not end with \"/\"");
224         }
225         return fileName;
226     }
227 
228     /**
229      * Helper method to determine if we should skip the execution of a given file.
230      *
231      * @param fullPath the full path of the file in question
232      * @return true if we should skip the said file.
233      */
shouldSkipFile(String fullPath)234     protected boolean shouldSkipFile(String fullPath) throws DeviceNotAvailableException {
235         if (fullPath == null || fullPath.isEmpty()) {
236             return true;
237         }
238         // skip any file that's not executable
239         if (!mDevice.isExecutable(fullPath)) {
240             return true;
241         }
242         List<String> fileExclusionFilterRegex = getFileExclusionFilterRegex();
243         if (fileExclusionFilterRegex == null || fileExclusionFilterRegex.isEmpty()) {
244             return false;
245         }
246         for (String regex : fileExclusionFilterRegex) {
247             if (fullPath.matches(regex)) {
248                 CLog.i("File %s matches exclusion file regex %s, skipping", fullPath, regex);
249                 return true;
250             }
251         }
252         return false;
253     }
254 
255     /**
256      * Helper method to run a gtest command from a temporary script, in the case that the command
257      * is too long to be run directly by adb.
258      * @param testDevice the device on which to run the command
259      * @param cmd the command string to run
260      * @param resultParser the output receiver for reading test results
261      */
executeCommandByScript(final ITestDevice testDevice, final String cmd, final IShellOutputReceiver resultParser)262     protected void executeCommandByScript(final ITestDevice testDevice, final String cmd,
263             final IShellOutputReceiver resultParser) throws DeviceNotAvailableException {
264         String tmpFileDevice = "/data/local/tmp/gtest_script.sh";
265         testDevice.pushString(String.format("#!/bin/bash\n%s", cmd), tmpFileDevice);
266         // force file to be executable
267         testDevice.executeShellCommand(String.format("chmod 755 %s", tmpFileDevice));
268         testDevice.executeShellCommand(
269                 String.format("sh %s", tmpFileDevice),
270                 resultParser,
271                 getMaxTestTimeMs() /* maxTimeToShellOutputResponse */,
272                 TimeUnit.MILLISECONDS,
273                 0 /* retry attempts */);
274         testDevice.deleteFile(tmpFileDevice);
275     }
276 
277     @Override
getGTestCmdLine(String fullPath, String flags)278     protected String getGTestCmdLine(String fullPath, String flags) {
279         StringBuilder sb = new StringBuilder();
280         // When sharding a device GTest, add args to the command line
281         if (getShardCount() > 0) {
282             if (isCollectTestsOnly()) {
283                 CLog.w(
284                         "--collect-tests-only option ignores sharding parameters, and will cause "
285                                 + "each shard to collect all tests.");
286             }
287             sb.append(String.format("GTEST_SHARD_INDEX=%s ", getShardIndex()));
288             sb.append(String.format("GTEST_TOTAL_SHARDS=%s ", getShardCount()));
289         }
290         sb.append(super.getGTestCmdLine(fullPath, flags));
291         return sb.toString();
292     }
293 
294     @Override
createFlagFile(String filter)295     protected String createFlagFile(String filter) throws DeviceNotAvailableException {
296         String flagPath = super.createFlagFile(filter);
297         if (flagPath == null) {
298             // Return null to fall back to base filter
299             return null;
300         }
301         File flagFile = new File(flagPath);
302         String devicePath = "/data/local/tmp/" + flagFile.getName();
303         try {
304             if (!mDevice.pushFile(flagFile, devicePath)) {
305                 // Failed to push flagfile, return null to fall back to base filter
306                 return null;
307             }
308         } finally {
309             FileUtil.deleteFile(flagFile);
310         }
311         return devicePath;
312     }
313 
314     /**
315      * Run the given gtest binary
316      *
317      * @param testDevice the {@link ITestDevice}
318      * @param resultParser the test run output parser
319      * @param fullPath absolute file system path to gtest binary on device
320      * @param flags gtest execution flags
321      * @throws DeviceNotAvailableException
322      */
runTest(final ITestDevice testDevice, final IShellOutputReceiver resultParser, final String fullPath, final String flags)323     private void runTest(final ITestDevice testDevice, final IShellOutputReceiver resultParser,
324             final String fullPath, final String flags) throws DeviceNotAvailableException {
325         // TODO: add individual test timeout support, and rerun support
326         try {
327             for (String cmd : getBeforeTestCmd()) {
328                 testDevice.executeShellCommand(cmd);
329             }
330 
331             if (mRebootBeforeTest && !isCollectTestsOnly()) {
332                 CLog.d("Rebooting device before test starts as requested.");
333                 testDevice.reboot();
334             }
335 
336             String cmd = getGTestCmdLine(fullPath, flags);
337             // ensure that command is not too long for adb
338             if (cmd.length() < GTEST_CMD_CHAR_LIMIT) {
339                 testDevice.executeShellCommand(
340                         cmd,
341                         resultParser,
342                         getMaxTestTimeMs() /* maxTimeToShellOutputResponse */,
343                         TimeUnit.MILLISECONDS,
344                         0 /* retryAttempts */);
345             } else {
346                 // wrap adb shell command in script if command is too long for direct execution
347                 executeCommandByScript(testDevice, cmd, resultParser);
348             }
349         } catch (DeviceNotAvailableException e) {
350             throw e;
351         } catch (RuntimeException e) {
352             throw e;
353         } finally {
354             // TODO: consider moving the flush of parser data on exceptions to TestDevice or
355             // AdbHelper
356             resultParser.flush();
357             for (String cmd : getAfterTestCmd()) {
358                 testDevice.executeShellCommand(cmd);
359             }
360         }
361     }
362 
363     /**
364      * Run the given gtest binary and parse XML results This methods typically requires the filter
365      * for .tff and .xml files, otherwise it will post some unwanted results.
366      *
367      * @param testDevice the {@link ITestDevice}
368      * @param fullPath absolute file system path to gtest binary on device
369      * @param flags gtest execution flags
370      * @param listener the {@link ITestInvocationListener}
371      * @throws DeviceNotAvailableException
372      */
runTestXml( final ITestDevice testDevice, final String fullPath, final String flags, ITestInvocationListener listener)373     private void runTestXml(
374             final ITestDevice testDevice,
375             final String fullPath,
376             final String flags,
377             ITestInvocationListener listener)
378             throws DeviceNotAvailableException {
379         CollectingOutputReceiver outputCollector = new CollectingOutputReceiver();
380         File tmpOutput = null;
381         try {
382             String testRunName = fullPath.substring(fullPath.lastIndexOf("/") + 1);
383             tmpOutput = FileUtil.createTempFile(testRunName, ".xml");
384             String tmpResName = fullPath + "_res.xml";
385             String extraFlag = String.format(GTEST_XML_OUTPUT, tmpResName);
386             String fullFlagCmd =  String.format("%s %s", flags, extraFlag);
387 
388             // Run the tests with modified flags
389             runTest(testDevice, outputCollector, fullPath, fullFlagCmd);
390             // Pull the result file, may not exist if issue with the test.
391             testDevice.pullFile(tmpResName, tmpOutput);
392             // Clean the file on the device
393             testDevice.deleteFile(tmpResName);
394             GTestXmlResultParser parser = createXmlParser(testRunName, listener);
395             // Attempt to parse the file, doesn't matter if the content is invalid.
396             if (tmpOutput.exists()) {
397                 parser.parseResult(tmpOutput, outputCollector);
398             }
399         } catch (DeviceNotAvailableException | RuntimeException e) {
400             throw e;
401         } catch (IOException e) {
402             throw new RuntimeException(e);
403         } finally {
404             outputCollector.flush();
405             for (String cmd : getAfterTestCmd()) {
406                 testDevice.executeShellCommand(cmd);
407             }
408             FileUtil.deleteFile(tmpOutput);
409         }
410     }
411 
412     /** {@inheritDoc} */
413     @Override
run(TestInformation testInfo, ITestInvocationListener listener)414     public void run(TestInformation testInfo, ITestInvocationListener listener)
415             throws DeviceNotAvailableException {
416         // TODO: add support for rerunning tests
417         if (mDevice == null) {
418             throw new IllegalArgumentException("Device has not been set");
419         }
420 
421         String testPath = getTestPath();
422         if (!mDevice.doesFileExist(testPath)) {
423             CLog.w("Could not find native test directory %s in %s!", testPath,
424                     mDevice.getSerialNumber());
425             return;
426         }
427         if (mStopRuntime) {
428             mDevice.executeShellCommand("stop");
429         }
430         // Insert the coverage listener if code coverage collection is enabled.
431         listener = addNativeCoverageListenerIfEnabled(listener);
432         listener = addClangCoverageListenerIfEnabled(listener);
433         listener = getGTestListener(listener);
434         NativeCodeCoverageFlusher flusher =
435                 new NativeCodeCoverageFlusher(mDevice, getCoverageOptions().getCoverageProcesses());
436 
437         Throwable throwable = null;
438         try {
439             if (getCoverageOptions().isCoverageEnabled()) {
440                 // Enable abd root on the device, otherwise the following commands will fail.
441                 verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
442 
443                 flusher.resetCoverage();
444 
445                 // Clang will no longer create directories that are part of the GCOV_PREFIX
446                 // environment variable. Force create the /data/misc/trace/testcoverage dir to
447                 // prevent "No such file or directory" errors when writing test coverage to disk.
448                 mDevice.executeShellCommand("mkdir /data/misc/trace/testcoverage");
449             }
450             doRunAllTestsInSubdirectory(testPath, mDevice, listener);
451         } catch (Throwable t) {
452             throwable = t;
453             throw t;
454         } finally {
455             if (!(throwable instanceof DeviceNotAvailableException)) {
456                 if (mStopRuntime) {
457                     mDevice.executeShellCommand("start");
458                     mDevice.waitForDeviceAvailable();
459                 }
460             }
461         }
462     }
463 
464     /**
465      * Adds a listener to pull native code coverage measurements from the device after the test is
466      * complete if coverage is enabled, otherwise returns the same listener.
467      *
468      * @param listener the current chain of listeners
469      * @return a native coverage listener if coverage is enabled, otherwise the original listener
470      */
addNativeCoverageListenerIfEnabled( ITestInvocationListener listener)471     private ITestInvocationListener addNativeCoverageListenerIfEnabled(
472             ITestInvocationListener listener) {
473         CoverageOptions options = getCoverageOptions();
474 
475         if (options.isCoverageEnabled() && options.getCoverageToolchains().contains(GCOV)) {
476             return new NativeCodeCoverageListener(mDevice, options, listener);
477         }
478         return listener;
479     }
480 
481     /**
482      * Adds a listener to pull Clang code coverage measurements from the device after the test is
483      * complete if coverage is enabled, otherwise returns the same listener.
484      *
485      * @param listener the current chain of listeners
486      * @return a native coverage listener if coverage is enabled, otherwise the original listener
487      */
addClangCoverageListenerIfEnabled( ITestInvocationListener listener)488     private ITestInvocationListener addClangCoverageListenerIfEnabled(
489             ITestInvocationListener listener) {
490         CoverageOptions options = getCoverageOptions();
491 
492         if (options.isCoverageEnabled() && options.getCoverageToolchains().contains(CLANG)) {
493             ClangCodeCoverageListener clangListener =
494                     new ClangCodeCoverageListener(mDevice, listener);
495             clangListener.setConfiguration(getConfiguration());
496             return clangListener;
497         }
498         return listener;
499     }
500 }
501