1 /*
2  * Copyright (C) 2023 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.tests.chromium.host;
18 
19 import static com.android.tests.chromium.host.InstrumentationFlags.COMMAND_LINE_FLAGS_KEY;
20 import static com.android.tests.chromium.host.InstrumentationFlags.DUMP_COVERAGE_KEY;
21 import static com.android.tests.chromium.host.InstrumentationFlags.EXTRA_SHARD_NANO_TIMEOUT_KEY;
22 import static com.android.tests.chromium.host.InstrumentationFlags.LIBRARY_TO_LOAD_ACTIVITY_KEY;
23 import static com.android.tests.chromium.host.InstrumentationFlags.NATIVE_TEST_ACTIVITY_KEY;
24 import static com.android.tests.chromium.host.InstrumentationFlags.NATIVE_UNIT_TEST_ACTIVITY_KEY;
25 import static com.android.tests.chromium.host.InstrumentationFlags.RUN_IN_SUBTHREAD_KEY;
26 import static com.android.tests.chromium.host.InstrumentationFlags.STDOUT_FILE_KEY;
27 import static com.android.tests.chromium.host.InstrumentationFlags.TEST_RUNNER;
28 
29 import android.annotation.NonNull;
30 
31 import com.android.ddmlib.MultiLineReceiver;
32 import com.android.tradefed.config.Option;
33 import com.android.tradefed.device.CollectingOutputReceiver;
34 import com.android.tradefed.device.DeviceNotAvailableException;
35 import com.android.tradefed.device.ITestDevice;
36 import com.android.tradefed.invoker.TestInformation;
37 import com.android.tradefed.log.LogUtil;
38 import com.android.tradefed.result.ITestInvocationListener;
39 import com.android.tradefed.result.LogDataType;
40 import com.android.tradefed.result.FileInputStreamSource;
41 import com.android.tradefed.testtype.GTestListTestParser;
42 import com.android.tradefed.testtype.GTestResultParser;
43 import com.android.tradefed.testtype.IDeviceTest;
44 import com.android.tradefed.testtype.IRemoteTest;
45 import com.android.tradefed.testtype.ITestCollector;
46 import com.android.tradefed.testtype.ITestFilterReceiver;
47 import com.android.tradefed.util.FileUtil;
48 
49 import com.google.common.base.Joiner;
50 import com.google.common.base.Strings;
51 
52 import java.io.File;
53 import java.io.IOException;
54 import java.nio.file.Files;
55 import java.time.Duration;
56 import java.util.LinkedHashSet;
57 import java.util.Set;
58 import java.util.concurrent.TimeUnit;
59 
60 /**
61  * A host-side test-runner capable of running Chromium unit-tests.
62  */
63 public class ChromiumHostDrivenTest implements IRemoteTest, IDeviceTest, ITestCollector,
64         ITestFilterReceiver {
65 
66     private static final String CLEAR_CLANG_COVERAGE_FILES =
67             "find /data/misc/trace -name '*.profraw' -delete";
68     private static final Duration TESTS_TIMEOUT = Duration.ofMinutes(30);
69     private static final String GTEST_FLAG_PRINT_TIME = "--gtest_print_time";
70     private static final String GTEST_FLAG_FILTER = "--gtest_filter";
71     private static final String GTEST_FLAG_LIST_TESTS = "--gtest_list_tests";
72     private static final String GTEST_FLAG_FILE = "--gtest_flagfile";
73     @Option(
74         name = "include-filter",
75         description = "The set of annotations a test must have to be run.")
76     private Set<String> includeFilters = new LinkedHashSet<>();
77     @Option(
78         name = "exclude-filter",
79         description =
80             "The set of annotations to exclude tests from running. A test must have "
81                 + "none of the annotations in this list to run.")
82     private Set<String> excludeFilters = new LinkedHashSet<>();
83     private boolean collectTestsOnly = false;
84     private ITestDevice device = null;
85 
86     @Option(
87             name = "dump-native-coverage",
88             description = "Force APK under test to dump native test coverage upon exit"
89     )
90     private boolean isCoverageEnabled = false;
91     @Option(
92             name = "library-to-load",
93             description = "Name of the .so file under test"
94     )
95     private String libraryToLoad = "";
96 
97 
98     /**
99      * Creates a temporary file on the host machine then push it to the device in a temporary
100      * location It is necessary to create a temp file for output for each instrumentation run and
101      * not module invocation. This is preferred over using
102      * {@link com.android.tradefed.targetprep.RunCommandTargetPreparer}
103      * because RunCommandTargetPreparer is only run once before the test invocation which leads to
104      * incorrect parsing as the retries will all use the same file for test result outputs.
105      */
106     @NonNull
createTempResultFileOnDevice()107     private String createTempResultFileOnDevice() throws DeviceNotAvailableException {
108         File resultFile = null;
109         String deviceFileDestination;
110         try {
111             resultFile = FileUtil.createTempFile("gtest_results", ".txt");
112             deviceFileDestination = String.format("/data/local/tmp/%s", resultFile.getName());
113             getDevice().pushFile(resultFile, deviceFileDestination);
114             FileUtil.deleteFile(resultFile);
115         } catch (IOException e) {
116             throw new FailedChromiumGTestException(
117                     "Failed to create temp file for result on the device.", e);
118         } finally {
119             FileUtil.deleteFile(resultFile);
120         }
121         return deviceFileDestination;
122     }
123 
124     /**
125      * This creates the gtest filter string which indicates which test should be run.
126      * Sometimes the gtest filter is long (> 500 character) which results in creating
127      * a temporary flag file and have gtest result the filter from the flag file.
128      *
129      * @return A gtest argument for flag file or --gtest_filter directly.
130      */
131     @NonNull
getGTestFilters()132     private String getGTestFilters() throws DeviceNotAvailableException {
133         StringBuilder filter = new StringBuilder();
134         if (!includeFilters.isEmpty() || !excludeFilters.isEmpty()) {
135             filter.append(GTEST_FLAG_FILTER);
136             filter.append('=');
137             Joiner joiner = Joiner.on(":").skipNulls();
138             if (!includeFilters.isEmpty()) {
139                 joiner.appendTo(filter, includeFilters);
140             }
141             if (!excludeFilters.isEmpty()) {
142                 filter.append("-");
143                 joiner.appendTo(filter, excludeFilters);
144             }
145         }
146         String filterFlag = filter.toString();
147         // Handle long args
148         if (filterFlag.length() > 500) {
149             String tmpFlag = createFlagFileOnDevice(filterFlag);
150             return String.format("%s=%s", GTEST_FLAG_FILE, tmpFlag);
151         }
152         return filterFlag;
153     }
154 
155     /**
156      * Helper method for getGTestFilters which creates a temporary flag file and push it to device.
157      *
158      * If it fails to create a file then it will directly use the filter in the adb command.
159      *
160      * @param filter the string to append to the flag file.
161      * @return path to the flag file on device or null if it could not be created.
162      */
163     @NonNull
createFlagFileOnDevice(@onNull String filter)164     private String createFlagFileOnDevice(@NonNull String filter)
165             throws DeviceNotAvailableException {
166         File tmpFlagFile = null;
167         String devicePath;
168         try {
169             tmpFlagFile = FileUtil.createTempFile("flagfile", ".txt");
170             FileUtil.writeToFile(filter, tmpFlagFile);
171             devicePath = String.format("/data/local/tmp/%s", tmpFlagFile.getName());
172             getDevice().pushFile(tmpFlagFile, devicePath);
173         } catch (IOException e) {
174             throw new FailedChromiumGTestException(
175                     "Failed to create temp file for gtest filter flag on the device.", e);
176         } finally {
177             FileUtil.deleteFile(tmpFlagFile);
178         }
179         return devicePath;
180     }
181 
182     @NonNull
getAllGTestFlags()183     private String getAllGTestFlags() throws DeviceNotAvailableException {
184         String flags = String.format("%s %s", GTEST_FLAG_PRINT_TIME, getGTestFilters());
185         if (isCollectTestsOnly()) {
186             flags = String.format("%s %s", flags, GTEST_FLAG_LIST_TESTS);
187         }
188         return flags;
189     }
190 
191     /**
192      * The flags all exist in Chromium's instrumentation APK
193      * {@link org.chromium.build.gtest_apk.NativeTestInstrumentationTestRunner} and
194      * {@link org.chromium.native_test.NativeTest}.
195      *
196      * The following is a brief explanation for each flag
197      * <ul>
198      * <li> NATIVE_TEST_ACTIVITY_KEY: Indicates the name of the activity which should be
199      * started by the instrumentation APK. This activity is responsible for executing gtests.
200      * <li> RUN_IN_SUBTHREAD_KEY: Whether to run the tests in the main-thread or a sub-thread.
201      * <li> EXTRA_SHARD_NANO_TIMEOUT_KEY: Shard timeout (Equal to the test timeout and not
202      * important as we only use a single shard).
203      * <li> LIBRARY_TO_LOAD_ACTIVITY_KEY: Name of the native library which has the code under
204      * test. System.LoadLibrary will be invoked on the value of this flag
205      * <li> STDOUT_FILE_KEY: Path to the file where stdout/stderr will be redirected to.</li>
206      * <li> COMMAND_LINE_FLAGS_KEY: Command line flags delegated to the gtest executor. This is
207      * mostly used for gtest flags
208      * <li> DUMP_COVERAGE_KEY: Flag used to indicate that the apk should not exit before dumping
209      * native coverage.
210      * </ul>
211      *
212      * @param resultFilePath path to a temporary file on the device which the gtest result will be
213      *                       directed to
214      * @return an instrumentation command that can be executed using adb shell am instrument.
215      */
216     @NonNull
createRunAllTestsCommand(@onNull String resultFilePath)217     private String createRunAllTestsCommand(@NonNull String resultFilePath)
218             throws DeviceNotAvailableException {
219         InstrumentationCommandBuilder builder = new InstrumentationCommandBuilder(TEST_RUNNER)
220                 .addArgument(NATIVE_TEST_ACTIVITY_KEY, NATIVE_UNIT_TEST_ACTIVITY_KEY)
221                 .addArgument(RUN_IN_SUBTHREAD_KEY, "1")
222                 .addArgument(EXTRA_SHARD_NANO_TIMEOUT_KEY, String.valueOf(TESTS_TIMEOUT.toNanos()))
223                 .addArgument(LIBRARY_TO_LOAD_ACTIVITY_KEY, libraryToLoad)
224                 .addArgument(STDOUT_FILE_KEY, resultFilePath)
225                 .addArgument(COMMAND_LINE_FLAGS_KEY,
226                         String.format("'%s'", getAllGTestFlags()));
227         if (isCoverageEnabled) {
228             builder.addArgument(DUMP_COVERAGE_KEY, "true");
229         }
230         return builder.build();
231     }
232 
233     /**
234      * Those logs can be found in host_log_%s.txt which is bundled with test execution.
235      *
236      * @param cmd Command used to instrumentation, this has all the flags which can help debugging
237      *            unusual behaviour.
238      */
printHostLogs(@onNull String cmd)239     private void printHostLogs(@NonNull String cmd) {
240         LogUtil.CLog.i(String.format("[Cronet] Library to be loaded: %s\n", libraryToLoad));
241         LogUtil.CLog.i(String.format("[Cronet] Command used to run gtests: adb shell %s\n", cmd));
242         LogUtil.CLog.i(String.format("[Cronet] Native-Coverage = %b", isCoverageEnabled));
243     }
244 
245     /**
246      * This is automatically invoked by the {@link com.android.tradefed.testtype.HostTest}.
247      *
248      * @param testInfo The {@link TestInformation} object containing useful information to run
249      *                 tests.
250      * @param listener the {@link ITestInvocationListener} of test results
251      */
252     @Override
run(TestInformation testInfo, ITestInvocationListener listener)253     public void run(TestInformation testInfo, ITestInvocationListener listener)
254             throws DeviceNotAvailableException {
255         if (Strings.isNullOrEmpty(libraryToLoad)) {
256             throw new IllegalStateException("No library provided to be loaded.");
257         }
258         String resultFilePath = createTempResultFileOnDevice();
259         String cmd = createRunAllTestsCommand(resultFilePath);
260         printHostLogs(cmd);
261         getDevice().executeShellCommand(CLEAR_CLANG_COVERAGE_FILES);
262         ITestInvocationListener listenerWithTime = new TestListenerWithTime(
263                 System.currentTimeMillis(), listener);
264         getDevice().executeShellCommand(cmd, new CollectingOutputReceiver(),
265                 /* maxTimeBeforeTimeOut */ TESTS_TIMEOUT.toMinutes(),
266                 /* timeUnit */ TimeUnit.MINUTES,
267                 /* retryAttempts */ 1);
268         parseAndReport(resultFilePath, listenerWithTime);
269     }
270 
parseAndReport(@onNull String resultFilePath, @NonNull ITestInvocationListener listener)271     private void parseAndReport(@NonNull String resultFilePath,
272             @NonNull ITestInvocationListener listener) throws DeviceNotAvailableException {
273         File resultFile = device.pullFile(resultFilePath);
274         if (resultFile == null) {
275             throw new FailedChromiumGTestException(
276                     "Failed to retrieve gtest results file from device.");
277         }
278         try (FileInputStreamSource data = new FileInputStreamSource(resultFile)) {
279             listener.testLog(
280                 String.format("gtest_output_%s", resultFile.getName()), LogDataType.TEXT, data);
281         }
282         // Loading all the lines is fine since this is done on the host-machine.
283         String[] lines;
284         try {
285             lines = Files.readAllLines(resultFile.toPath()).toArray(String[]::new);
286         } catch (IOException e) {
287             throw new FailedChromiumGTestException(
288                     "Failed to read gtest results file on host machine.", e);
289         }
290         MultiLineReceiver parser;
291         // the parser automatically reports the test result back to the infra through the listener.
292         if (isCollectTestsOnly()) {
293             parser = new GTestListTestParser(libraryToLoad, listener);
294         } else {
295             parser = new GTestResultParser(libraryToLoad, listener);
296         }
297         parser.processNewLines(lines);
298         parser.done();
299     }
300 
301     // ------- Everything below is called by HostTest and should not be invoked manually -----
isCollectTestsOnly()302     public boolean isCollectTestsOnly() {
303         return collectTestsOnly;
304     }
305 
306     @Override
setCollectTestsOnly(boolean shouldCollectTest)307     public void setCollectTestsOnly(boolean shouldCollectTest) {
308         collectTestsOnly = shouldCollectTest;
309     }
310 
cleanFilter(String filter)311     public String cleanFilter(String filter) {
312         return filter.replace('#', '.');
313     }
314 
315     @Override
addIncludeFilter(String filter)316     public void addIncludeFilter(String filter) {
317         includeFilters.add(cleanFilter(filter));
318     }
319 
320     @Override
addAllIncludeFilters(Set<String> filters)321     public void addAllIncludeFilters(Set<String> filters) {
322         for (String filter : filters) {
323             includeFilters.add(cleanFilter(filter));
324         }
325     }
326 
327     @Override
addExcludeFilter(String filter)328     public void addExcludeFilter(String filter) {
329         excludeFilters.add(cleanFilter(filter));
330     }
331 
332     @Override
addAllExcludeFilters(Set<String> filters)333     public void addAllExcludeFilters(Set<String> filters) {
334         for (String filter : filters) {
335             excludeFilters.add(cleanFilter(filter));
336         }
337     }
338 
339     @Override
clearIncludeFilters()340     public void clearIncludeFilters() {
341         includeFilters.clear();
342     }
343 
344     @Override
getIncludeFilters()345     public Set<String> getIncludeFilters() {
346         return includeFilters;
347     }
348 
349     @Override
getExcludeFilters()350     public Set<String> getExcludeFilters() {
351         return excludeFilters;
352     }
353 
354     @Override
clearExcludeFilters()355     public void clearExcludeFilters() {
356         excludeFilters.clear();
357     }
358 
359     @Override
getDevice()360     public ITestDevice getDevice() {
361         return device;
362     }
363 
364     @Override
setDevice(ITestDevice device)365     public void setDevice(ITestDevice device) {
366         this.device = device;
367     }
368 }