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 import com.android.tradefed.util.ZipUtil; 30 31 import com.google.common.annotations.VisibleForTesting; 32 33 import java.io.File; 34 import java.io.IOException; 35 import java.nio.file.Files; 36 import java.nio.file.Path; 37 import java.util.Arrays; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.function.BiFunction; 41 import java.util.regex.Matcher; 42 import java.util.regex.Pattern; 43 import java.util.stream.Collectors; 44 import java.util.stream.Stream; 45 46 /** A utility class that contains common methods used by tests. */ 47 public class TestUtils { 48 private static final String GMS_PACKAGE_NAME = "com.google.android.gms"; 49 private final TestInformation mTestInformation; 50 private final TestArtifactReceiver mTestArtifactReceiver; 51 private final DeviceUtils mDeviceUtils; 52 private static final int MAX_CRASH_SNIPPET_LINES = 60; 53 // Pattern for finding a package name following one of the tags such as "Process:" or 54 // "Package:". 55 private static final Pattern DROPBOX_PACKAGE_NAME_PATTERN = 56 Pattern.compile( 57 "(Process|Cmdline|Package|Cmd line):(" 58 + " *)([a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); 59 60 public enum TakeEffectWhen { 61 NEVER, 62 ON_FAIL, 63 ON_PASS, 64 ALWAYS, 65 } 66 getInstance(TestInformation testInformation, TestLogData testLogData)67 public static TestUtils getInstance(TestInformation testInformation, TestLogData testLogData) { 68 return new TestUtils( 69 testInformation, 70 new TestLogDataTestArtifactReceiver(testLogData), 71 DeviceUtils.getInstance(testInformation.getDevice())); 72 } 73 getInstance( TestInformation testInformation, TestArtifactReceiver testArtifactReceiver)74 public static TestUtils getInstance( 75 TestInformation testInformation, TestArtifactReceiver testArtifactReceiver) { 76 return new TestUtils( 77 testInformation, 78 testArtifactReceiver, 79 DeviceUtils.getInstance(testInformation.getDevice())); 80 } 81 82 @VisibleForTesting TestUtils( TestInformation testInformation, TestArtifactReceiver testArtifactReceiver, DeviceUtils deviceUtils)83 TestUtils( 84 TestInformation testInformation, 85 TestArtifactReceiver testArtifactReceiver, 86 DeviceUtils deviceUtils) { 87 mTestInformation = testInformation; 88 mTestArtifactReceiver = testArtifactReceiver; 89 mDeviceUtils = deviceUtils; 90 } 91 92 /** 93 * Take a screenshot on the device and save it to the test result artifacts. 94 * 95 * @param prefix The file name prefix. 96 * @throws DeviceNotAvailableException 97 */ collectScreenshot(String prefix)98 public void collectScreenshot(String prefix) throws DeviceNotAvailableException { 99 try (InputStreamSource screenSource = mTestInformation.getDevice().getScreenshot()) { 100 mTestArtifactReceiver.addTestArtifact( 101 prefix + "_screenshot_" + mTestInformation.getDevice().getSerialNumber(), 102 LogDataType.PNG, 103 screenSource); 104 } 105 } 106 107 /** 108 * Record the device screen while running a task and save the video file to the test result 109 * artifacts. 110 * 111 * @param job A job to run while recording the screen. 112 * @param prefix The file name prefix. 113 * @throws DeviceNotAvailableException 114 */ collectScreenRecord( DeviceUtils.RunnableThrowingDeviceNotAvailable job, String prefix)115 public void collectScreenRecord( 116 DeviceUtils.RunnableThrowingDeviceNotAvailable job, String prefix) 117 throws DeviceNotAvailableException { 118 mDeviceUtils.runWithScreenRecording( 119 job, 120 video -> { 121 if (video != null) { 122 mTestArtifactReceiver.addTestArtifact( 123 prefix 124 + "_screenrecord_" 125 + mTestInformation.getDevice().getSerialNumber(), 126 LogDataType.MP4, 127 video); 128 } else { 129 CLog.e("Failed to get screen recording."); 130 } 131 }); 132 } 133 134 /** 135 * Saves test APK files when conditions on the test result is met. 136 * 137 * @param when Conditions to save the apks based on the test result. 138 * @param testPassed The test result. 139 * @param prefix Output file name prefix 140 * @param apks A list of files that can be files, directories, or a mix of both. 141 * @return true if apk files are saved as artifacts. False otherwise. 142 */ saveApks( TakeEffectWhen when, boolean testPassed, String prefix, List<File> apks)143 public boolean saveApks( 144 TakeEffectWhen when, boolean testPassed, String prefix, List<File> apks) { 145 if (apks.isEmpty() || when == TakeEffectWhen.NEVER) { 146 return false; 147 } 148 149 if ((when == TakeEffectWhen.ON_FAIL && testPassed) 150 || (when == TakeEffectWhen.ON_PASS && !testPassed)) { 151 return false; 152 } 153 154 try { 155 File outputZip = ZipUtil.createZip(apks); 156 getTestArtifactReceiver().addTestArtifact(prefix + "-apks", LogDataType.ZIP, outputZip); 157 return true; 158 } catch (IOException e) { 159 CLog.e("Failed to zip the apks: " + e); 160 } 161 162 return false; 163 } 164 165 /** 166 * Collect the GMS version name and version code, and save them as test result artifacts. 167 * 168 * @param prefix The file name prefix. 169 * @throws DeviceNotAvailableException 170 */ collectGmsVersion(String prefix)171 public void collectGmsVersion(String prefix) throws DeviceNotAvailableException { 172 String gmsVersionCode = mDeviceUtils.getPackageVersionCode(GMS_PACKAGE_NAME); 173 String gmsVersionName = mDeviceUtils.getPackageVersionName(GMS_PACKAGE_NAME); 174 CLog.i("GMS core versionCode=%s, versionName=%s", gmsVersionCode, gmsVersionName); 175 176 // Note: If the file name format needs to be modified, do it with cautions as some users may 177 // be parsing the output file name to get the version information. 178 mTestArtifactReceiver.addTestArtifact( 179 String.format("%s_[GMS_versionCode=%s]", prefix, gmsVersionCode), 180 LogDataType.TEXT, 181 gmsVersionCode.getBytes()); 182 mTestArtifactReceiver.addTestArtifact( 183 String.format("%s_[GMS_versionName=%s]", prefix, gmsVersionName), 184 LogDataType.TEXT, 185 gmsVersionName.getBytes()); 186 } 187 188 /** 189 * Collect the given package's version name and version code, and save them as test result 190 * artifacts. 191 * 192 * @param packageName The package name. 193 * @throws DeviceNotAvailableException 194 */ collectAppVersion(String packageName)195 public void collectAppVersion(String packageName) throws DeviceNotAvailableException { 196 String versionCode = mDeviceUtils.getPackageVersionCode(packageName); 197 String versionName = mDeviceUtils.getPackageVersionName(packageName); 198 CLog.i("Package %s versionCode=%s, versionName=%s", packageName, versionCode, versionName); 199 200 // Note: If the file name format needs to be modified, do it with cautions as some users may 201 // be parsing the output file name to get the version information. 202 mTestArtifactReceiver.addTestArtifact( 203 String.format("%s_[versionCode=%s]", packageName, versionCode), 204 LogDataType.TEXT, 205 versionCode.getBytes()); 206 mTestArtifactReceiver.addTestArtifact( 207 String.format("%s_[versionName=%s]", packageName, versionName), 208 LogDataType.TEXT, 209 versionName.getBytes()); 210 } 211 212 /** 213 * Looks for crash log of a package in the device's dropbox entries. 214 * 215 * @param packageName The package name of an app. 216 * @param startTimeOnDevice The device timestamp after which the check starts. Dropbox items 217 * before this device timestamp will be ignored. 218 * @param saveToFile whether to save the package's full dropbox crash logs to a test output 219 * file. 220 * @return A string of crash log if crash was found; null otherwise. 221 * @throws IOException unexpected IOException 222 */ getDropboxPackageCrashLog( String packageName, DeviceTimestamp startTimeOnDevice, boolean saveToFile)223 public String getDropboxPackageCrashLog( 224 String packageName, DeviceTimestamp startTimeOnDevice, boolean saveToFile) 225 throws IOException { 226 BiFunction<String, Integer, String> truncate = 227 (text, maxLines) -> { 228 String[] lines = text.split("\\r?\\n"); 229 StringBuilder sb = new StringBuilder(); 230 for (int i = 0; i < maxLines && i < lines.length; i++) { 231 sb.append(lines[i]); 232 sb.append('\n'); 233 } 234 if (lines.length > maxLines) { 235 sb.append("... "); 236 sb.append(lines.length - maxLines); 237 sb.append(" more lines truncated ...\n"); 238 } 239 return sb.toString(); 240 }; 241 242 List<DropboxEntry> entries = 243 mDeviceUtils.getDropboxEntries(DeviceUtils.DROPBOX_APP_CRASH_TAGS).stream() 244 .filter(entry -> (entry.getTime() >= startTimeOnDevice.get())) 245 .filter( 246 entry -> 247 isDropboxEntryFromPackageProcess( 248 entry.getData(), packageName)) 249 .collect(Collectors.toList()); 250 251 if (entries.size() == 0) { 252 return null; 253 } 254 255 String fullText = 256 entries.stream() 257 .map( 258 entry -> 259 String.format( 260 "Dropbox tag: %s\n%s", 261 entry.getTag(), entry.getData())) 262 .collect(Collectors.joining("\n============\n")); 263 String truncatedText = 264 entries.stream() 265 .map( 266 entry -> 267 String.format( 268 "Dropbox tag: %s\n%s", 269 entry.getTag(), 270 truncate.apply( 271 entry.getData(), MAX_CRASH_SNIPPET_LINES))) 272 .collect(Collectors.joining("\n============\n")); 273 274 mTestArtifactReceiver.addTestArtifact( 275 String.format("%s_dropbox_entries", packageName), 276 LogDataType.TEXT, 277 fullText.getBytes()); 278 return truncatedText; 279 } 280 281 @VisibleForTesting isDropboxEntryFromPackageProcess(String entryData, String packageName)282 boolean isDropboxEntryFromPackageProcess(String entryData, String packageName) { 283 Matcher m = DROPBOX_PACKAGE_NAME_PATTERN.matcher(entryData); 284 285 boolean matched = false; 286 while (m.find()) { 287 matched = true; 288 if (m.group(3).equals(packageName)) { 289 return true; 290 } 291 } 292 293 if (matched) { 294 return false; 295 } 296 297 // If the process name is not identified, fall back to checking if the package name is 298 // present in the entry. This is because the process name detection logic above does not 299 // guarantee to identify the process name. 300 return Pattern.compile( 301 String.format( 302 // Pattern for checking whether a given package name exists. 303 "(.*(?:[^a-zA-Z0-9_\\.]+)|^)%s((?:[^a-zA-Z0-9_\\.]+).*|$)", 304 packageName.replaceAll("\\.", "\\\\."))) 305 .matcher(entryData) 306 .find(); 307 } 308 309 /** 310 * Generates a list of APK paths where the base.apk of split apk files are always on the first 311 * index if exists. 312 * 313 * <p>If the input path points to a single apk file, then the same path is returned. If the 314 * input path is a directory containing only one non-split apk file, the apk file path is 315 * returned. If the apk path is a directory containing split apk files for one package, then the 316 * list of apks are returned and the base.apk sits on the first index. If the path contains obb 317 * files, then they will be included at the end of the returned path list. If the apk path does 318 * not contain any apk files, or multiple apk files without base.apk, then an IOException is 319 * thrown. 320 * 321 * @return A list of APK paths with OBB files if available. 322 * @throws TestUtilsException If failed to read the apk path or unexpected number of apk files 323 * are found under the path. 324 */ listApks(Path root)325 public static List<Path> listApks(Path root) throws TestUtilsException { 326 // The apk path points to a non-split apk file. 327 if (Files.isRegularFile(root)) { 328 if (!root.toString().endsWith(".apk")) { 329 throw new TestUtilsException( 330 "The file on the given apk path is not an apk file: " + root); 331 } 332 return List.of(root); 333 } 334 335 List<Path> apksAndObbs; 336 CLog.d("APK path = " + root); 337 try (Stream<Path> fileTree = Files.walk(root)) { 338 apksAndObbs = 339 fileTree.filter(Files::isRegularFile) 340 .filter( 341 path -> 342 path.getFileName() 343 .toString() 344 .toLowerCase() 345 .endsWith(".apk") 346 || path.getFileName() 347 .toString() 348 .toLowerCase() 349 .endsWith(".obb")) 350 .collect(Collectors.toList()); 351 } catch (IOException e) { 352 throw new TestUtilsException("Failed to list apk files.", e); 353 } 354 355 List<Path> apkFiles = 356 apksAndObbs.stream() 357 .filter(path -> path.getFileName().toString().endsWith(".apk")) 358 .collect(Collectors.toList()); 359 360 if (apkFiles.isEmpty()) { 361 throw new TestUtilsException( 362 "Empty APK directory. Cannot find any APK files under " + root); 363 } 364 365 if (apkFiles.stream().map(path -> path.getParent().toString()).distinct().count() != 1) { 366 throw new TestUtilsException( 367 "Apk files are not all in the same folder: " 368 + Arrays.deepToString( 369 apksAndObbs.toArray(new Path[apksAndObbs.size()]))); 370 } 371 372 if (apkFiles.size() > 1 373 && apkFiles.stream() 374 .filter(path -> path.getFileName().toString().equals("base.apk")) 375 .count() 376 == 0) { 377 throw new TestUtilsException( 378 "Base apk is not found: " 379 + Arrays.deepToString( 380 apksAndObbs.toArray(new Path[apksAndObbs.size()]))); 381 } 382 383 if (apksAndObbs.stream() 384 .filter( 385 path -> 386 path.getFileName().toString().endsWith(".obb") 387 && path.getFileName().toString().startsWith("main")) 388 .count() 389 > 1) { 390 throw new TestUtilsException( 391 "Multiple main obb files are found: " 392 + Arrays.deepToString( 393 apksAndObbs.toArray(new Path[apksAndObbs.size()]))); 394 } 395 396 Collections.sort( 397 apksAndObbs, 398 (first, second) -> { 399 if (first.getFileName().toString().equals("base.apk")) { 400 return -1; 401 } else if (first.getFileName().toString().toLowerCase().endsWith(".obb")) { 402 return 1; 403 } else { 404 return first.getFileName().compareTo(second.getFileName()); 405 } 406 }); 407 408 return apksAndObbs; 409 } 410 411 /** Returns the test information. */ getTestInformation()412 public TestInformation getTestInformation() { 413 return mTestInformation; 414 } 415 416 /** Returns the test artifact receiver. */ getTestArtifactReceiver()417 public TestArtifactReceiver getTestArtifactReceiver() { 418 return mTestArtifactReceiver; 419 } 420 421 /** Returns the device utils. */ getDeviceUtils()422 public DeviceUtils getDeviceUtils() { 423 return mDeviceUtils; 424 } 425 426 /** An exception class representing exceptions thrown from the test utils. */ 427 public static final class TestUtilsException extends Exception { 428 /** 429 * Constructs a new {@link TestUtilsException} with a meaningful error message. 430 * 431 * @param message A error message describing the cause of the error. 432 */ TestUtilsException(String message)433 private TestUtilsException(String message) { 434 super(message); 435 } 436 437 /** 438 * Constructs a new {@link TestUtilsException} with a meaningful error message, and a cause. 439 * 440 * @param message A detailed error message. 441 * @param cause A {@link Throwable} capturing the original cause of the TestUtilsException. 442 */ TestUtilsException(String message, Throwable cause)443 private TestUtilsException(String message, Throwable cause) { 444 super(message, cause); 445 } 446 447 /** 448 * Constructs a new {@link TestUtilsException} with a cause. 449 * 450 * @param cause A {@link Throwable} capturing the original cause of the TestUtilsException. 451 */ TestUtilsException(Throwable cause)452 private TestUtilsException(Throwable cause) { 453 super(cause); 454 } 455 } 456 457 public static class TestLogDataTestArtifactReceiver implements TestArtifactReceiver { 458 @SuppressWarnings("hiding") 459 private final TestLogData mTestLogData; 460 TestLogDataTestArtifactReceiver(TestLogData testLogData)461 public TestLogDataTestArtifactReceiver(TestLogData testLogData) { 462 mTestLogData = testLogData; 463 } 464 465 @Override addTestArtifact(String name, LogDataType type, byte[] bytes)466 public void addTestArtifact(String name, LogDataType type, byte[] bytes) { 467 mTestLogData.addTestLog(name, type, new ByteArrayInputStreamSource(bytes)); 468 } 469 470 @Override addTestArtifact(String name, LogDataType type, File file)471 public void addTestArtifact(String name, LogDataType type, File file) { 472 mTestLogData.addTestLog(name, type, new FileInputStreamSource(file)); 473 } 474 475 @Override addTestArtifact(String name, LogDataType type, InputStreamSource source)476 public void addTestArtifact(String name, LogDataType type, InputStreamSource source) { 477 mTestLogData.addTestLog(name, type, source); 478 } 479 } 480 481 public interface TestArtifactReceiver { 482 483 /** 484 * Add a test artifact. 485 * 486 * @param name File name. 487 * @param type Output data type. 488 * @param bytes The output data. 489 */ addTestArtifact(String name, LogDataType type, byte[] bytes)490 void addTestArtifact(String name, LogDataType type, byte[] bytes); 491 492 /** 493 * Add a test artifact. 494 * 495 * @param name File name. 496 * @param type Output data type. 497 * @param inputStreamSource The inputStreamSource. 498 */ addTestArtifact(String name, LogDataType type, InputStreamSource inputStreamSource)499 void addTestArtifact(String name, LogDataType type, InputStreamSource inputStreamSource); 500 501 /** 502 * Add a test artifact. 503 * 504 * @param name File name. 505 * @param type Output data type. 506 * @param file The output file. 507 */ addTestArtifact(String name, LogDataType type, File file)508 void addTestArtifact(String name, LogDataType type, File file); 509 } 510 } 511