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 }