1 /*
2  * Copyright (C) 2022 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.catbox.result;
18 
19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
20 import com.android.compatibility.common.tradefed.util.CollectorUtil;
21 
22 import com.android.ddmlib.Log.LogLevel;
23 
24 import com.android.tradefed.build.IBuildInfo;
25 import com.android.tradefed.config.Option;
26 import com.android.tradefed.config.OptionClass;
27 import com.android.tradefed.device.DeviceNotAvailableException;
28 import com.android.tradefed.device.ITestDevice;
29 
30 import com.android.tradefed.log.LogUtil.CLog;
31 
32 import com.android.tradefed.targetprep.BuildError;
33 import com.android.tradefed.targetprep.ITargetPreparer;
34 import com.android.tradefed.targetprep.TargetSetupError;
35 
36 import com.android.tradefed.util.CommandResult;
37 import com.android.tradefed.util.CommandStatus;
38 
39 import com.google.common.base.Strings;
40 
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.io.OutputStream;
45 
46 import java.util.HashMap;
47 import java.util.Map;
48 
49 /**
50  * ResultReportCollector is an {@link ITargetPreparer} that pulls the test result reports from the
51  * device and adds it to the Results.
52  */
53 @OptionClass(alias = "result-report-collector")
54 public class ResultReportCollector implements ITargetPreparer {
55     @Option(
56             name = "pull-file-content-uri",
57             description = "Copy the src files from device to destination using content uri")
58     private Map<String, String> mFileContentUriMap = new HashMap<String, String>();
59 
60     @Option(
61             name = "pull-file-path",
62             description = "Copy the src files from device to destination using file path")
63     private Map<String, String> mFilePathMap = new HashMap<String, String>();
64 
65     @Option(
66             name = "pull-dir-path",
67             description = "Copy the src directory from device to destination using directory path")
68     private Map<String, String> mDirPathMap = new HashMap<String, String>();
69 
70     private static final String NO_RESULTS_STRING = "No result found.";
71     private static final String ERROR_MESSAGE_TAG = "[ERROR]";
72 
73     @Override
setUp(ITestDevice device, IBuildInfo buildInfo)74     public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
75             BuildError, DeviceNotAvailableException {
76         // Nothing To Do
77     }
78 
79     @Override
tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)80     public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
81             throws DeviceNotAvailableException {
82         // Pull files from device using Content URI
83         pullFilesUsingContentUri(device, buildInfo, mFileContentUriMap);
84 
85         // Pull files from device using File Paths
86         pullFilesUsingFilePaths(device, buildInfo, mFilePathMap);
87 
88         // Pull directories from device using Directory Paths
89         pullDirUsingFilePaths(device, buildInfo, mDirPathMap);
90     }
91 
92     /** Pull files from the device using Content URI */
pullFilesUsingContentUri(ITestDevice device, IBuildInfo buildInfo, Map<String, String> contentUriMap)93     private void pullFilesUsingContentUri(ITestDevice device, IBuildInfo buildInfo,
94             Map<String, String> contentUriMap) throws DeviceNotAvailableException {
95         if (contentUriMap.isEmpty()) {
96             // No Content URI provided
97             return;
98         }
99         CLog.logAndDisplay(
100                 LogLevel.INFO,
101                 "Started pulling file using Content URI.");
102 
103         // Iterate over given Content URIs
104         for (Map.Entry<String, String> entry: contentUriMap.entrySet()) {
105             // Pull file using Content URI
106             pullFileUsingContentUri(device, buildInfo, entry.getKey(), entry.getValue());
107         }
108 
109         CLog.logAndDisplay(
110                 LogLevel.INFO,
111                 "Completed pulling file using Content URI.");
112     }
113 
pullFileUsingContentUri(ITestDevice device, IBuildInfo buildInfo, String fileContentUri, String destFilePath)114     private void pullFileUsingContentUri(ITestDevice device, IBuildInfo buildInfo,
115             String fileContentUri, String destFilePath) throws DeviceNotAvailableException {
116         // Validate Source and Destination File Path
117         if (Strings.isNullOrEmpty(fileContentUri) || Strings.isNullOrEmpty(destFilePath) ||
118                 !fileContentUri.startsWith("content://")) {
119             CLog.logAndDisplay(
120                     LogLevel.ERROR,
121                     String.format("Either Src or Dest Path is invalid. Source: %s, Destination: %s",
122                             fileContentUri, destFilePath));
123             return;
124         }
125 
126         CLog.logAndDisplay(
127                 LogLevel.INFO,
128                 String.format("Started pulling file. Source File: %s, Destination File: %s",
129                         fileContentUri, destFilePath));
130 
131         // Check if file exist on the device
132         if (!doesFileExist(device, fileContentUri)) {
133             CLog.logAndDisplay(
134                     LogLevel.ERROR,
135                     String.format("File %s does not exist on the device.",
136                             fileContentUri));
137             return;
138         }
139 
140         // Get Destination File using filepath
141         File destinationFile = getDestinationFile(buildInfo, destFilePath);
142 
143         if (destinationFile == null) {
144             CLog.logAndDisplay(
145                     LogLevel.ERROR,
146                     String.format("Unable to get destination file path for %s.",
147                             destFilePath));
148             return;
149         }
150 
151         // Create the result directory if it does not exist
152         // isDestFile=true : Since destination is a file
153         if (!createResultDir(buildInfo, destinationFile, true /* isDestFile */)) {
154             CLog.logAndDisplay(
155                     LogLevel.ERROR,
156                     String.format("Unable to create results directory %s.",
157                             destFilePath));
158             return;
159         }
160 
161         // Create Pull Command
162         String pullCommand =
163                 String.format("content read --user %d --uri %s",
164                         device.getCurrentUser(),
165                         fileContentUri);
166 
167         // Open the output stream to the local file.
168         // try-with-resource should close the stream once the try block completes
169         // so we don't need a finally block to close the stream
170         try (OutputStream localFileStream = new FileOutputStream(destinationFile)) {
171             CommandResult pullResult = device.executeShellV2Command(pullCommand,
172                     localFileStream);
173             if (!isSuccessful(pullResult)) {
174                 String stderr = pullResult.getStderr();
175                 CLog.logAndDisplay(
176                         LogLevel.ERROR,
177                         String.format(
178                                 "Failed to pull a file at '%s' to %s. Error: '%s'",
179                                 fileContentUri, destFilePath, stderr));
180             }
181         } catch(IOException ex) {
182             CLog.logAndDisplay(
183                     LogLevel.ERROR,
184                     String.format("Failed to open OutputStream to the local file %s. Error: %s",
185                             destFilePath, ex.getMessage()));
186             return;
187         }
188 
189         CLog.logAndDisplay(
190                 LogLevel.INFO,
191                 String.format("Completed pulling file. Source File: %s, Destination File: %s",
192                         fileContentUri, destFilePath));
193     }
194 
195     /** Verify if file exists  */
doesFileExist(ITestDevice device, String contentUri)196     private boolean doesFileExist(ITestDevice device, String contentUri)
197             throws DeviceNotAvailableException {
198         String queryContentCommand =
199                 String.format(
200                         "content query --user %d --uri %s", device.getCurrentUser(), contentUri);
201 
202         String listCommandResult = device.executeShellCommand(queryContentCommand);
203 
204         return (!NO_RESULTS_STRING.equals(listCommandResult.trim()));
205     }
206 
207     /** Verify Command Result for success  */
isSuccessful(CommandResult result)208     private boolean isSuccessful(CommandResult result) {
209         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
210             return false;
211         }
212         String stdout = result.getStdout();
213         if (stdout.contains(ERROR_MESSAGE_TAG)) {
214             return false;
215         }
216         return Strings.isNullOrEmpty(result.getStderr());
217     }
218 
219     /** Get Destination File using file path */
getDestinationFile(IBuildInfo buildInfo, String destPath)220     private File getDestinationFile(IBuildInfo buildInfo, String destPath) {
221         File destinationFile = null;
222         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
223         try {
224             // get results directory
225             destinationFile = buildHelper.getResultDir();
226             // get destination file using path
227             destinationFile = new File(destinationFile, destPath);
228         } catch (IOException ex) {
229             CLog.logAndDisplay(
230                     LogLevel.ERROR,
231                     String.format("Unable to get destination file %s: Error: %s.",
232                             destPath, ex.getMessage()));
233             return null;
234         }
235         return destinationFile;
236     }
237 
238     /** Create directory for results */
createResultDir(IBuildInfo buildInfo, File destination, boolean isDestFile)239     private boolean createResultDir(IBuildInfo buildInfo, File destination, boolean isDestFile) {
240         File resultDir = destination;
241         if (isDestFile) {
242             // if filepath, get the parent for creating the directory
243             resultDir = destination.getParentFile();
244         }
245         if (!resultDir.exists() && !resultDir.mkdirs()) {
246             CLog.logAndDisplay(
247                     LogLevel.ERROR,
248                     String.format("Unable to create %s directory", resultDir.getAbsolutePath()));
249             return false;
250         }
251         if (!resultDir.isDirectory()) {
252             CLog.logAndDisplay(
253                     LogLevel.ERROR,
254                     String.format("%s is not a directory", resultDir.getAbsolutePath()));
255             return false;
256         }
257         return true;
258     }
259 
260     /** Pull files from the device using File Path */
pullFilesUsingFilePaths(ITestDevice device, IBuildInfo buildInfo, Map<String, String> filePathMap)261     private void pullFilesUsingFilePaths(ITestDevice device, IBuildInfo buildInfo,
262             Map<String, String> filePathMap) throws DeviceNotAvailableException {
263         if (filePathMap.isEmpty()) {
264             // No File Paths provided
265             return;
266         }
267 
268         CLog.logAndDisplay(
269                 LogLevel.INFO,
270                 "Started pulling file using file path.");
271 
272         // Iterate over given Paths
273         for (Map.Entry<String, String> entry: filePathMap.entrySet()) {
274             // Pull file using File Path
275             pullFileUsingFilePath(device, buildInfo, entry.getKey(), entry.getValue());
276         }
277 
278         CLog.logAndDisplay(
279                 LogLevel.INFO,
280                 "Completed pulling file using file path.");
281     }
282 
283     /** Pull file using File Path */
pullFileUsingFilePath(ITestDevice device, IBuildInfo buildInfo, String filePath, String destPath)284     private void pullFileUsingFilePath(ITestDevice device, IBuildInfo buildInfo, String filePath,
285             String destPath) throws DeviceNotAvailableException {
286         // Validate Source and Destination File Path
287         if (Strings.isNullOrEmpty(filePath) || Strings.isNullOrEmpty(destPath)) {
288             CLog.logAndDisplay(
289                     LogLevel.ERROR,
290                     String.format("Either Src or Dest Path is invalid. Source: %s, Destination: %s",
291                             filePath, destPath));
292             return;
293         }
294 
295         CLog.logAndDisplay(
296                 LogLevel.INFO,
297                 String.format("Started pulling file. Source File: %s, Destination File: %s",
298                         filePath, destPath));
299 
300         // Check if file exist on the device
301         if (!device.doesFileExist(filePath)) {
302             CLog.logAndDisplay(
303                     LogLevel.ERROR,
304                     String.format("File %s does not exist on the device.",
305                             filePath));
306             return;
307         }
308 
309         // Get Destination File using filepath
310         File destinationFile = getDestinationFile(buildInfo, destPath);
311 
312         if (destinationFile == null) {
313             CLog.logAndDisplay(
314                     LogLevel.ERROR,
315                     String.format("Unable to get destination file path for %s.",
316                             destPath));
317             return;
318         }
319 
320         // Create the result directory if it does not exist
321         // isDestFile=true : Since destination is a file
322         if (!createResultDir(buildInfo, destinationFile, true /* isDestFile */)) {
323             CLog.logAndDisplay(
324                     LogLevel.ERROR,
325                     String.format("Unable to create results directory %s.",
326                             destPath));
327             return;
328         }
329 
330         // Pull File
331         boolean isSuccess = device.pullFile(filePath, destinationFile);
332 
333         if (!isSuccess) {
334             CLog.logAndDisplay(
335                     LogLevel.ERROR,
336                     String.format("Failed to pull the file. Source File: %s, Destination File: %s",
337                             filePath, destPath));
338             return;
339         }
340 
341         CLog.logAndDisplay(
342                 LogLevel.INFO,
343                 String.format("Completed pulling file. Source File: %s, Destination File: %s",
344                         filePath, destPath));
345     }
346 
347     /** Pull directories from the device using File Path */
pullDirUsingFilePaths(ITestDevice device, IBuildInfo buildInfo, Map<String, String> dirPathMap)348     private void pullDirUsingFilePaths(ITestDevice device, IBuildInfo buildInfo,
349             Map<String, String> dirPathMap) throws DeviceNotAvailableException {
350         if (dirPathMap.isEmpty())  {
351             // No Dir Paths provided
352             return;
353         }
354 
355         CLog.logAndDisplay(
356                 LogLevel.INFO,
357                 "Started pulling directory using file path.");
358 
359         // Iterate over given Paths
360         for (Map.Entry<String, String> entry: dirPathMap.entrySet()) {
361             // Pull directory using Dir Path
362             pullDirectoryUsingDirPath(device, buildInfo, entry.getKey(), entry.getValue());
363         }
364 
365         CLog.logAndDisplay(
366                 LogLevel.INFO,
367                 "Completed pulling directory using file path.");
368     }
369 
370     /** Pull Dir using Dir Path */
pullDirectoryUsingDirPath(ITestDevice device, IBuildInfo buildInfo, String dirPath, String destPath)371     private void pullDirectoryUsingDirPath(ITestDevice device, IBuildInfo buildInfo, String dirPath,
372             String destPath) throws DeviceNotAvailableException {
373         // Validate Source and Destination File Path
374         if (Strings.isNullOrEmpty(dirPath) || Strings.isNullOrEmpty(destPath)) {
375             CLog.logAndDisplay(
376                     LogLevel.ERROR,
377                     String.format("Either Src or Dest Path is invalid. Source: %s, Destination: %s",
378                             dirPath, destPath));
379             return;
380         }
381 
382         CLog.logAndDisplay(
383                 LogLevel.INFO,
384                 String.format("Started pulling directory. Source File: %s, Destination File: %s",
385                         dirPath, destPath));
386 
387         // Check if directory exist on the device
388         if (!device.doesFileExist(dirPath)) {
389             CLog.logAndDisplay(
390                     LogLevel.ERROR,
391                     String.format("File %s does not exist on the device.",
392                             dirPath));
393             return;
394         }
395 
396         // Get Destination Dir using filepath
397         File destinationDir = getDestinationFile(buildInfo, destPath);
398 
399         if (destinationDir == null) {
400             CLog.logAndDisplay(
401                     LogLevel.ERROR,
402                     String.format("Unable to get destination file path for %s.",
403                             destPath));
404             return;
405         }
406 
407         // Create the result directory if it does not exist
408         // isDestFile=false : Since destination is a directory
409         if (!createResultDir(buildInfo, destinationDir, false /* isDestFile */)) {
410             CLog.logAndDisplay(
411                     LogLevel.ERROR,
412                     String.format("Unable to create results directory %s.",
413                             destPath));
414             return;
415         }
416 
417         // Pull Dir
418         boolean isSuccess = device.pullDir(dirPath, destinationDir);
419 
420         if (!isSuccess) {
421             CLog.logAndDisplay(
422                     LogLevel.ERROR,
423                     String.format("Failed to pull directory. Source Dir: %s, Destination File: %s",
424                             dirPath, destPath));
425             return;
426         }
427 
428         CLog.logAndDisplay(
429                 LogLevel.INFO,
430                 String.format("Completed pulling directory. Source File: %s, Destination File: %s",
431                         dirPath, destPath));
432     }
433 }
434