1 /* 2 * Copyright (C) 2021 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.csuite.core; 18 19 import com.android.csuite.core.DeviceUtils.DeviceTimestamp; 20 import com.android.csuite.core.DeviceUtils.DropboxEntry; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.invoker.TestInformation; 23 import com.android.tradefed.log.LogUtil.CLog; 24 import com.android.tradefed.result.ByteArrayInputStreamSource; 25 import com.android.tradefed.result.FileInputStreamSource; 26 import com.android.tradefed.result.InputStreamSource; 27 import com.android.tradefed.result.LogDataType; 28 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; 29 30 import com.google.common.annotations.VisibleForTesting; 31 32 import java.io.File; 33 import java.io.IOException; 34 import java.nio.file.Files; 35 import java.nio.file.Path; 36 import java.util.Arrays; 37 import java.util.Collections; 38 import java.util.List; 39 import java.util.function.BiFunction; 40 import java.util.stream.Collectors; 41 import java.util.stream.Stream; 42 43 /** A utility class that contains common methods used by tests. */ 44 public class TestUtils { 45 private static final String GMS_PACKAGE_NAME = "com.google.android.gms"; 46 private final TestInformation mTestInformation; 47 private final TestArtifactReceiver mTestArtifactReceiver; 48 private final DeviceUtils mDeviceUtils; 49 private static final int MAX_CRASH_SNIPPET_LINES = 60; 50 getInstance(TestInformation testInformation, TestLogData testLogData)51 public static TestUtils getInstance(TestInformation testInformation, TestLogData testLogData) { 52 return new TestUtils( 53 testInformation, 54 new TestLogDataTestArtifactReceiver(testLogData), 55 DeviceUtils.getInstance(testInformation.getDevice())); 56 } 57 getInstance( TestInformation testInformation, TestArtifactReceiver testArtifactReceiver)58 public static TestUtils getInstance( 59 TestInformation testInformation, TestArtifactReceiver testArtifactReceiver) { 60 return new TestUtils( 61 testInformation, 62 testArtifactReceiver, 63 DeviceUtils.getInstance(testInformation.getDevice())); 64 } 65 66 @VisibleForTesting TestUtils( TestInformation testInformation, TestArtifactReceiver testArtifactReceiver, DeviceUtils deviceUtils)67 TestUtils( 68 TestInformation testInformation, 69 TestArtifactReceiver testArtifactReceiver, 70 DeviceUtils deviceUtils) { 71 mTestInformation = testInformation; 72 mTestArtifactReceiver = testArtifactReceiver; 73 mDeviceUtils = deviceUtils; 74 } 75 76 /** 77 * Take a screenshot on the device and save it to the test result artifacts. 78 * 79 * @param prefix The file name prefix. 80 * @throws DeviceNotAvailableException 81 */ collectScreenshot(String prefix)82 public void collectScreenshot(String prefix) throws DeviceNotAvailableException { 83 try (InputStreamSource screenSource = mTestInformation.getDevice().getScreenshot()) { 84 mTestArtifactReceiver.addTestArtifact( 85 prefix + "_screenshot_" + mTestInformation.getDevice().getSerialNumber(), 86 LogDataType.PNG, 87 screenSource); 88 } 89 } 90 91 /** 92 * Record the device screen while running a task and save the video file to the test result 93 * artifacts. 94 * 95 * @param job A job to run while recording the screen. 96 * @param prefix The file name prefix. 97 * @throws DeviceNotAvailableException 98 */ collectScreenRecord( DeviceUtils.RunnableThrowingDeviceNotAvailable job, String prefix)99 public void collectScreenRecord( 100 DeviceUtils.RunnableThrowingDeviceNotAvailable job, String prefix) 101 throws DeviceNotAvailableException { 102 mDeviceUtils.runWithScreenRecording( 103 job, 104 video -> { 105 if (video != null) { 106 mTestArtifactReceiver.addTestArtifact( 107 prefix 108 + "_screenrecord_" 109 + mTestInformation.getDevice().getSerialNumber(), 110 LogDataType.MP4, 111 video); 112 } else { 113 CLog.e("Failed to get screen recording."); 114 } 115 }); 116 } 117 118 /** 119 * Collect the GMS version name and version code, and save them as test result artifacts. 120 * 121 * @param prefix The file name prefix. 122 * @throws DeviceNotAvailableException 123 */ collectGmsVersion(String prefix)124 public void collectGmsVersion(String prefix) throws DeviceNotAvailableException { 125 String gmsVersionCode = mDeviceUtils.getPackageVersionCode(GMS_PACKAGE_NAME); 126 String gmsVersionName = mDeviceUtils.getPackageVersionName(GMS_PACKAGE_NAME); 127 CLog.i("GMS core versionCode=%s, versionName=%s", gmsVersionCode, gmsVersionName); 128 129 // Note: If the file name format needs to be modified, do it with cautions as some users may 130 // be parsing the output file name to get the version information. 131 mTestArtifactReceiver.addTestArtifact( 132 String.format("%s_[GMS_versionCode=%s]", prefix, gmsVersionCode), 133 LogDataType.TEXT, 134 gmsVersionCode.getBytes()); 135 mTestArtifactReceiver.addTestArtifact( 136 String.format("%s_[GMS_versionName=%s]", prefix, gmsVersionName), 137 LogDataType.TEXT, 138 gmsVersionName.getBytes()); 139 } 140 141 /** 142 * Collect the given package's version name and version code, and save them as test result 143 * artifacts. 144 * 145 * @param packageName The package name. 146 * @throws DeviceNotAvailableException 147 */ collectAppVersion(String packageName)148 public void collectAppVersion(String packageName) throws DeviceNotAvailableException { 149 String versionCode = mDeviceUtils.getPackageVersionCode(packageName); 150 String versionName = mDeviceUtils.getPackageVersionName(packageName); 151 CLog.i("Package %s versionCode=%s, versionName=%s", packageName, versionCode, versionName); 152 153 // Note: If the file name format needs to be modified, do it with cautions as some users may 154 // be parsing the output file name to get the version information. 155 mTestArtifactReceiver.addTestArtifact( 156 String.format("%s_[versionCode=%s]", packageName, versionCode), 157 LogDataType.TEXT, 158 versionCode.getBytes()); 159 mTestArtifactReceiver.addTestArtifact( 160 String.format("%s_[versionName=%s]", packageName, versionName), 161 LogDataType.TEXT, 162 versionName.getBytes()); 163 } 164 165 /** 166 * Looks for crash log of a package in the device's dropbox entries. 167 * 168 * @param packageName The package name of an app. 169 * @param startTimeOnDevice The device timestamp after which the check starts. Dropbox items 170 * before this device timestamp will be ignored. 171 * @param saveToFile whether to save the package's full dropbox crash logs to a test output 172 * file. 173 * @return A string of crash log if crash was found; null otherwise. 174 * @throws IOException unexpected IOException 175 */ getDropboxPackageCrashLog( String packageName, DeviceTimestamp startTimeOnDevice, boolean saveToFile)176 public String getDropboxPackageCrashLog( 177 String packageName, DeviceTimestamp startTimeOnDevice, boolean saveToFile) 178 throws IOException { 179 BiFunction<String, Integer, String> truncate = 180 (text, maxLines) -> { 181 String[] lines = text.split("\\r?\\n"); 182 StringBuilder sb = new StringBuilder(); 183 for (int i = 0; i < maxLines && i < lines.length; i++) { 184 sb.append(lines[i]); 185 sb.append('\n'); 186 } 187 if (lines.length > maxLines) { 188 sb.append("... "); 189 sb.append(lines.length - maxLines); 190 sb.append(" more lines truncated ...\n"); 191 } 192 return sb.toString(); 193 }; 194 195 List<DropboxEntry> entries = 196 mDeviceUtils.getDropboxEntries(DeviceUtils.DROPBOX_APP_CRASH_TAGS).stream() 197 .filter(entry -> (entry.getTime() >= startTimeOnDevice.get())) 198 .filter(entry -> entry.getData().contains(packageName)) 199 .collect(Collectors.toList()); 200 201 if (entries.size() == 0) { 202 return null; 203 } 204 205 String fullText = 206 entries.stream() 207 .map( 208 entry -> 209 String.format( 210 "Dropbox tag: %s\n%s", 211 entry.getTag(), entry.getData())) 212 .collect(Collectors.joining("\n============\n")); 213 String truncatedText = 214 entries.stream() 215 .map( 216 entry -> 217 String.format( 218 "Dropbox tag: %s\n%s", 219 entry.getTag(), 220 truncate.apply( 221 entry.getData(), MAX_CRASH_SNIPPET_LINES))) 222 .collect(Collectors.joining("\n============\n")); 223 224 mTestArtifactReceiver.addTestArtifact( 225 String.format("%s_dropbox_entries", packageName), 226 LogDataType.TEXT, 227 fullText.getBytes()); 228 return truncatedText; 229 } 230 231 /** 232 * Generates a list of APK paths where the base.apk of split apk files are always on the first 233 * index if exists. 234 * 235 * <p>If the apk path is a single apk, then the apk is returned. If the apk path is a directory 236 * containing only one non-split apk file, the apk file is returned. If the apk path is a 237 * directory containing split apk files for one package, then the list of apks are returned and 238 * the base.apk sits on the first index. If the apk path does not contain any apk files, or 239 * multiple apk files without base.apk, then an IOException is thrown. 240 * 241 * @return A list of APK paths. 242 * @throws TestUtilsException If failed to read the apk path or unexpected number of apk files 243 * are found under the path. 244 */ listApks(Path root)245 public static List<Path> listApks(Path root) throws TestUtilsException { 246 // The apk path points to a non-split apk file. 247 if (Files.isRegularFile(root)) { 248 if (!root.toString().endsWith(".apk")) { 249 throw new TestUtilsException( 250 "The file on the given apk path is not an apk file: " + root); 251 } 252 return List.of(root); 253 } 254 255 List<Path> apks; 256 CLog.d("APK path = " + root); 257 try (Stream<Path> fileTree = Files.walk(root)) { 258 apks = 259 fileTree.filter(Files::isRegularFile) 260 .filter(path -> path.getFileName().toString().endsWith(".apk")) 261 .collect(Collectors.toList()); 262 } catch (IOException e) { 263 throw new TestUtilsException("Failed to list apk files.", e); 264 } 265 266 if (apks.isEmpty()) { 267 throw new TestUtilsException("The apk directory does not contain any apk files"); 268 } 269 270 // The apk path contains a single non-split apk or the base.apk of a split-apk. 271 if (apks.size() == 1) { 272 return apks; 273 } 274 275 if (apks.stream().map(path -> path.getParent().toString()).distinct().count() != 1) { 276 throw new TestUtilsException( 277 "Apk files are not all in the same folder: " 278 + Arrays.deepToString(apks.toArray(new Path[apks.size()]))); 279 } 280 281 if (apks.stream().filter(path -> path.getFileName().toString().equals("base.apk")).count() 282 == 0) { 283 throw new TestUtilsException( 284 "Multiple non-split apk files detected: " 285 + Arrays.deepToString(apks.toArray(new Path[apks.size()]))); 286 } 287 288 Collections.sort( 289 apks, 290 (first, second) -> first.getFileName().toString().equals("base.apk") ? -1 : 0); 291 292 return apks; 293 } 294 295 /** Returns the test information. */ getTestInformation()296 public TestInformation getTestInformation() { 297 return mTestInformation; 298 } 299 300 /** Returns the test artifact receiver. */ getTestArtifactReceiver()301 public TestArtifactReceiver getTestArtifactReceiver() { 302 return mTestArtifactReceiver; 303 } 304 305 /** Returns the device utils. */ getDeviceUtils()306 public DeviceUtils getDeviceUtils() { 307 return mDeviceUtils; 308 } 309 310 /** An exception class representing exceptions thrown from the test utils. */ 311 public static final class TestUtilsException extends Exception { 312 /** 313 * Constructs a new {@link TestUtilsException} with a meaningful error message. 314 * 315 * @param message A error message describing the cause of the error. 316 */ TestUtilsException(String message)317 private TestUtilsException(String message) { 318 super(message); 319 } 320 321 /** 322 * Constructs a new {@link TestUtilsException} with a meaningful error message, and a cause. 323 * 324 * @param message A detailed error message. 325 * @param cause A {@link Throwable} capturing the original cause of the TestUtilsException. 326 */ TestUtilsException(String message, Throwable cause)327 private TestUtilsException(String message, Throwable cause) { 328 super(message, cause); 329 } 330 331 /** 332 * Constructs a new {@link TestUtilsException} with a cause. 333 * 334 * @param cause A {@link Throwable} capturing the original cause of the TestUtilsException. 335 */ TestUtilsException(Throwable cause)336 private TestUtilsException(Throwable cause) { 337 super(cause); 338 } 339 } 340 341 public static class TestLogDataTestArtifactReceiver implements TestArtifactReceiver { 342 @SuppressWarnings("hiding") 343 private final TestLogData mTestLogData; 344 TestLogDataTestArtifactReceiver(TestLogData testLogData)345 public TestLogDataTestArtifactReceiver(TestLogData testLogData) { 346 mTestLogData = testLogData; 347 } 348 349 @Override addTestArtifact(String name, LogDataType type, byte[] bytes)350 public void addTestArtifact(String name, LogDataType type, byte[] bytes) { 351 mTestLogData.addTestLog(name, type, new ByteArrayInputStreamSource(bytes)); 352 } 353 354 @Override addTestArtifact(String name, LogDataType type, File file)355 public void addTestArtifact(String name, LogDataType type, File file) { 356 mTestLogData.addTestLog(name, type, new FileInputStreamSource(file)); 357 } 358 359 @Override addTestArtifact(String name, LogDataType type, InputStreamSource source)360 public void addTestArtifact(String name, LogDataType type, InputStreamSource source) { 361 mTestLogData.addTestLog(name, type, source); 362 } 363 } 364 365 public interface TestArtifactReceiver { 366 367 /** 368 * Add a test artifact. 369 * 370 * @param name File name. 371 * @param type Output data type. 372 * @param bytes The output data. 373 */ addTestArtifact(String name, LogDataType type, byte[] bytes)374 void addTestArtifact(String name, LogDataType type, byte[] bytes); 375 376 /** 377 * Add a test artifact. 378 * 379 * @param name File name. 380 * @param type Output data type. 381 * @param inputStreamSource The inputStreamSource. 382 */ addTestArtifact(String name, LogDataType type, InputStreamSource inputStreamSource)383 void addTestArtifact(String name, LogDataType type, InputStreamSource inputStreamSource); 384 385 /** 386 * Add a test artifact. 387 * 388 * @param name File name. 389 * @param type Output data type. 390 * @param file The output file. 391 */ addTestArtifact(String name, LogDataType type, File file)392 void addTestArtifact(String name, LogDataType type, File file); 393 } 394 } 395