• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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