1 /* 2 * Copyright (C) 2011 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 package com.android.tradefed.result; 17 18 import com.android.tradefed.device.DeviceNotAvailableException; 19 import com.android.tradefed.device.ITestDevice; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.util.FileUtil; 22 import com.android.tradefed.util.StreamUtil; 23 24 import java.io.File; 25 import java.util.Arrays; 26 import java.util.HashMap; 27 import java.util.HashSet; 28 import java.util.LinkedHashMap; 29 import java.util.LinkedList; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Set; 33 34 /** 35 * A utility class that checks the device for files and sends them to 36 * {@link ITestInvocationListener#testLog(String, LogDataType, InputStreamSource)} if found. 37 */ 38 public class DeviceFileReporter { 39 private final Map<String, LogDataType> mFilePatterns = new LinkedHashMap<>(); 40 private final ITestInvocationListener mListener; 41 private final ITestDevice mDevice; 42 43 /** Whether to ignore files that have already been captured by a prior Pattern */ 44 private boolean mSkipRepeatFiles = true; 45 /** The files which have already been reported */ 46 private Set<String> mReportedFiles = new HashSet<>(); 47 48 /** Whether to attempt to infer data types for patterns with {@code UNKNOWN} data type */ 49 private boolean mInferDataTypes = true; 50 51 private LogDataType mDefaultFileType = LogDataType.UNKNOWN; 52 53 private static final Map<String, LogDataType> DATA_TYPE_REVERSE_MAP = new HashMap<>(); 54 55 static { 56 // Make it easy to map backward from file extension to LogDataType 57 for (LogDataType type : LogDataType.values()) { 58 // Extracted extension will contain a leading dot 59 final String ext = "." + type.getFileExt(); 60 if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) { 61 continue; 62 } 63 64 DATA_TYPE_REVERSE_MAP.put(ext, type); 65 } 66 } 67 /** 68 * Initialize a new DeviceFileReporter with the provided {@link ITestDevice} 69 */ 70 public DeviceFileReporter(ITestDevice device, ITestInvocationListener listener) { 71 // Do a null check here, since otherwise that error would be asynchronous 72 if (device == null || listener == null) { 73 throw new NullPointerException(); 74 } 75 mDevice = device; 76 mListener = listener; 77 } 78 79 /** 80 * Add patterns with the log data type set to the default. 81 * 82 * @param patterns a varargs array of {@link String} filename glob patterns. Should be absolute. 83 * @see #setDefaultLogDataType 84 */ 85 public void addPatterns(String... patterns) { 86 addPatterns(Arrays.asList(patterns)); 87 } 88 89 /** 90 * Add patterns with the log data type set to the default. 91 * 92 * @param patterns a {@link List} of {@link String} filename glob patterns. Should be absolute. 93 * @see #setDefaultLogDataType 94 */ 95 public void addPatterns(List<String> patterns) { 96 for (String pat : patterns) { 97 mFilePatterns.put(pat, mDefaultFileType); 98 } 99 } 100 101 /** 102 * Add patterns with the respective log data types 103 * 104 * @param patterns a {@link Map} of {@link String} filename glob patterns to their respective 105 * {@link LogDataType}s. The globs should be absolute. 106 * @see #setDefaultLogDataType 107 */ 108 public void addPatterns(Map<String, LogDataType> patterns) { 109 mFilePatterns.putAll(patterns); 110 } 111 112 /** 113 * Set the default log data type set for patterns that don't have an associated type. 114 * 115 * @param type the {@link LogDataType} 116 * @see #addPatterns(List) 117 */ 118 public void setDefaultLogDataType(LogDataType type) { 119 if (type == null) { 120 throw new NullPointerException(); 121 } 122 mDefaultFileType = type; 123 } 124 125 /** 126 * Whether or not to skip files which have already been reported. This is only relevant when 127 * multiple patterns are being used, and two or more of those patterns match the same file. 128 * <p /> 129 * Note that this <emph>must only</emph> be called prior to calling {@link #run()}. Doing 130 * otherwise will cause undefined behavior. 131 */ 132 public void setSkipRepeatFiles(boolean skip) { 133 mSkipRepeatFiles = skip; 134 } 135 136 /** 137 * Whether to <emph>attempt to</emph> infer the data types of {@code UNKNOWN} files by checking 138 * the file extensions against a list. 139 * <p /> 140 * Note that, when enabled, these inferences will only be made for patterns with file type 141 * {@code UNKNOWN} (which includes patterns added without a specific type, and without the) 142 * default type having been set manually). If the inference fails, the data type will remain 143 * as {@code UNKNOWN}. 144 */ 145 public void setInferUnknownDataTypes(boolean infer) { 146 mInferDataTypes = infer; 147 } 148 149 /** 150 * Actually search the filesystem for the specified patterns and send them to 151 * {@link ITestInvocationListener#testLog} if found 152 */ 153 public List<String> run() throws DeviceNotAvailableException { 154 List<String> filenames = new LinkedList<>(); 155 CLog.d(String.format("Analyzing %d patterns.", mFilePatterns.size())); 156 for (Map.Entry<String, LogDataType> pat : mFilePatterns.entrySet()) { 157 final String searchCmd = String.format("ls %s", pat.getKey()); 158 final String fileList = mDevice.executeShellCommand(searchCmd); 159 160 for (String filename : fileList.split("\r?\n")) { 161 filename = filename.trim(); 162 if (filename.isEmpty() || filename.endsWith(": No such file or directory")) { 163 continue; 164 } 165 if (mSkipRepeatFiles && mReportedFiles.contains(filename)) { 166 CLog.v("Skipping already-reported file %s", filename); 167 continue; 168 } 169 170 File file = null; 171 InputStreamSource iss = null; 172 try { 173 CLog.d("Trying to pull file '%s' from device %s", filename, 174 mDevice.getSerialNumber()); 175 file = mDevice.pullFile(filename); 176 iss = createIssForFile(file); 177 final LogDataType type = getDataType(filename, pat.getValue()); 178 CLog.d("Local file %s has size %d and type %s", file, file.length(), 179 type.getFileExt()); 180 mListener.testLog(filename, type, iss); 181 filenames.add(filename); 182 mReportedFiles.add(filename); 183 } finally { 184 StreamUtil.cancel(iss); 185 iss = null; 186 FileUtil.deleteFile(file); 187 } 188 } 189 } 190 return filenames; 191 } 192 193 /** 194 * Returns the data type to use for a given file. Will attempt to infer the data type from the 195 * file's extension IFF inferences are enabled, and the current data type is {@code UNKNOWN}. 196 */ 197 LogDataType getDataType(String filename, LogDataType defaultType) { 198 if (!mInferDataTypes) return defaultType; 199 if (!LogDataType.UNKNOWN.equals(defaultType)) return defaultType; 200 201 CLog.d("Running type inference for file %s with default type %s", filename, defaultType); 202 String ext = FileUtil.getExtension(filename); 203 CLog.v("Found raw extension \"%s\"", ext); 204 205 // Normalize the extension 206 if (ext == null) return defaultType; 207 ext = ext.toLowerCase(); 208 209 if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) { 210 final LogDataType newType = DATA_TYPE_REVERSE_MAP.get(ext); 211 CLog.d("Inferred data type %s", newType); 212 return newType; 213 } else { 214 CLog.v("Failed to find a reverse map for extension \"%s\"", ext); 215 return defaultType; 216 } 217 } 218 219 /** 220 * Create an {@link InputStreamSource} for a file 221 * 222 * <p>Exposed for unit testing 223 */ 224 InputStreamSource createIssForFile(File file) { 225 return new FileInputStreamSource(file); 226 } 227 } 228