• 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 
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