1 /* 2 * Copyright (C) 2016 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.compatibility.common.util; 18 19 import com.google.common.annotations.VisibleForTesting; 20 import com.google.common.base.Joiner; 21 import com.google.common.base.Strings; 22 import com.google.common.hash.BloomFilter; 23 import com.google.common.hash.Funnels; 24 25 import java.io.BufferedInputStream; 26 import java.io.BufferedOutputStream; 27 import java.io.File; 28 import java.io.FileInputStream; 29 import java.io.FileOutputStream; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.ObjectInput; 33 import java.io.ObjectInputStream; 34 import java.io.ObjectOutput; 35 import java.io.ObjectOutputStream; 36 import java.io.OutputStream; 37 import java.io.Serializable; 38 import java.security.DigestException; 39 import java.security.MessageDigest; 40 import java.security.NoSuchAlgorithmException; 41 import java.util.Arrays; 42 import java.util.HashMap; 43 44 /*** 45 * Calculate and store checksum values for files and test results 46 */ 47 public final class ChecksumReporter implements Serializable { 48 49 public static final String NAME = "checksum.data"; 50 public static final String PREV_NAME = "checksum.previous.data"; 51 52 private static final double DEFAULT_FPP = 0.05; 53 private static final String SEPARATOR = "/"; 54 private static final String ID_SEPARATOR = "@"; 55 private static final String NAME_SEPARATOR = "."; 56 57 private static final short CURRENT_VERSION = 1; 58 // Serialized format Id (ie magic number) used to identify serialized data. 59 static final short SERIALIZED_FORMAT_CODE = 650; 60 61 private final BloomFilter<CharSequence> mResultChecksum; 62 private final HashMap<String, byte[]> mFileChecksum; 63 private final short mVersion; 64 65 /*** 66 * Calculate checksum of test results and files in result directory and write to disk 67 * @param dir test results directory 68 * @param result test results 69 * @return true if successful, false if unable to calculate or store the checksum 70 */ 71 public static boolean tryCreateChecksum(File dir, IInvocationResult result) { 72 try { 73 int totalCount = countTestResults(result); 74 ChecksumReporter checksumReporter = 75 new ChecksumReporter(totalCount, DEFAULT_FPP, CURRENT_VERSION); 76 checksumReporter.addInvocation(result); 77 checksumReporter.addDirectory(dir); 78 checksumReporter.saveToFile(dir); 79 } catch (Exception e) { 80 return false; 81 } 82 return true; 83 } 84 85 /*** 86 * Create Checksum Reporter from data saved on disk 87 * @param directory 88 * @return 89 * @throws ChecksumValidationException 90 */ 91 public static ChecksumReporter load(File directory) throws ChecksumValidationException { 92 ChecksumReporter reporter = new ChecksumReporter(directory); 93 if (reporter.getCapacity() > 1.1) { 94 throw new ChecksumValidationException("Capacity exceeded."); 95 } 96 return reporter; 97 } 98 99 /*** 100 * Deserialize checksum from file 101 * @param directory the parent directory containing the checksum file 102 * @throws ChecksumValidationException 103 */ 104 public ChecksumReporter(File directory) throws ChecksumValidationException { 105 File file = new File(directory, ChecksumReporter.NAME); 106 try (FileInputStream fileStream = new FileInputStream(file); 107 InputStream outputStream = new BufferedInputStream(fileStream); 108 ObjectInput objectInput = new ObjectInputStream(outputStream)) { 109 short magicNumber = objectInput.readShort(); 110 switch (magicNumber) { 111 case SERIALIZED_FORMAT_CODE: 112 mVersion = objectInput.readShort(); 113 mResultChecksum = (BloomFilter<CharSequence>) objectInput.readObject(); 114 mFileChecksum = (HashMap<String, byte[]>) objectInput.readObject(); 115 break; 116 default: 117 throw new ChecksumValidationException("Unknown format of serialized data."); 118 } 119 } catch (Exception e) { 120 throw new ChecksumValidationException("Unable to load checksum from file", e); 121 } 122 if (mVersion > CURRENT_VERSION) { 123 throw new ChecksumValidationException( 124 "File contains a newer version of ChecksumReporter"); 125 } 126 } 127 128 /*** 129 * Create new instance of ChecksumReporter 130 * @param testCount the number of test results that will be stored 131 * @param fpp the false positive percentage for result lookup misses 132 */ 133 public ChecksumReporter(int testCount, double fpp, short version) { 134 mResultChecksum = BloomFilter.create(Funnels.unencodedCharsFunnel(), 135 testCount, fpp); 136 mFileChecksum = new HashMap<>(); 137 mVersion = version; 138 } 139 140 /*** 141 * Add each test result from each module and test case 142 */ 143 public void addInvocation(IInvocationResult invocationResult) { 144 for (IModuleResult module : invocationResult.getModules()) { 145 String buildFingerprint = invocationResult.getBuildFingerprint(); 146 addModuleResult(module, buildFingerprint); 147 for (ICaseResult caseResult : module.getResults()) { 148 for (ITestResult testResult : caseResult.getResults()) { 149 addTestResult(testResult, module, buildFingerprint); 150 } 151 } 152 } 153 } 154 155 /*** 156 * Calculate CRC of file and store the result 157 * @param file crc calculated on this file 158 * @param path part of the key to identify the files crc 159 */ 160 public void addFile(File file, String path) { 161 byte[] crc; 162 try { 163 crc = calculateFileChecksum(file); 164 } catch (ChecksumValidationException e) { 165 crc = new byte[0]; 166 } 167 String key = path + SEPARATOR + file.getName(); 168 mFileChecksum.put(key, crc); 169 } 170 171 @VisibleForTesting 172 public boolean containsFile(File file, String path) { 173 String key = path + SEPARATOR + file.getName(); 174 if (mFileChecksum.containsKey(key)) 175 { 176 try { 177 byte[] crc = calculateFileChecksum(file); 178 return Arrays.equals(mFileChecksum.get(key), crc); 179 } catch (ChecksumValidationException e) { 180 return false; 181 } 182 } 183 return false; 184 } 185 186 /*** 187 * Adds all child files recursively through all sub directories 188 * @param directory target that is deeply searched for files 189 */ 190 public void addDirectory(File directory) { 191 addDirectory(directory, directory.getName()); 192 } 193 194 /*** 195 * @param path the relative path to the current directory from the base directory 196 */ 197 private void addDirectory(File directory, String path) { 198 for(String childName : directory.list()) { 199 File child = new File(directory, childName); 200 if (child.isDirectory()) { 201 addDirectory(child, path + SEPARATOR + child.getName()); 202 } else { 203 addFile(child, path); 204 } 205 } 206 } 207 208 /*** 209 * Calculate checksum of test result and store the value 210 * @param testResult the target of the checksum 211 * @param moduleResult the module that contains the test result 212 * @param buildFingerprint the fingerprint the test execution is running against 213 */ 214 public void addTestResult( 215 ITestResult testResult, IModuleResult moduleResult, String buildFingerprint) { 216 217 String signature = generateTestResultSignature(testResult, moduleResult, buildFingerprint); 218 mResultChecksum.put(signature); 219 } 220 221 @VisibleForTesting 222 public boolean containsTestResult( 223 ITestResult testResult, IModuleResult moduleResult, String buildFingerprint) { 224 225 String signature = generateTestResultSignature(testResult, moduleResult, buildFingerprint); 226 return mResultChecksum.mightContain(signature); 227 } 228 229 /*** 230 * Calculate checksm of module result and store value 231 * @param moduleResult the target of the checksum 232 * @param buildFingerprint the fingerprint the test execution is running against 233 */ 234 public void addModuleResult(IModuleResult moduleResult, String buildFingerprint) { 235 mResultChecksum.put( 236 generateModuleResultSignature(moduleResult, buildFingerprint)); 237 mResultChecksum.put( 238 generateModuleSummarySignature(moduleResult, buildFingerprint)); 239 } 240 241 @VisibleForTesting 242 public Boolean containsModuleResult(IModuleResult moduleResult, String buildFingerprint) { 243 return mResultChecksum.mightContain( 244 generateModuleResultSignature(moduleResult, buildFingerprint)); 245 } 246 247 /*** 248 * Write the checksum data to disk. 249 * Overwrites existing file 250 * @param directory 251 * @throws IOException 252 */ 253 public void saveToFile(File directory) throws IOException { 254 File file = new File(directory, NAME); 255 256 try (FileOutputStream fileStream = new FileOutputStream(file, false); 257 OutputStream outputStream = new BufferedOutputStream(fileStream); 258 ObjectOutput objectOutput = new ObjectOutputStream(outputStream)) { 259 objectOutput.writeShort(SERIALIZED_FORMAT_CODE); 260 objectOutput.writeShort(mVersion); 261 objectOutput.writeObject(mResultChecksum); 262 objectOutput.writeObject(mFileChecksum); 263 } 264 } 265 266 @VisibleForTesting 267 double getCapacity() { 268 // If default FPP changes: 269 // increment the CURRENT_VERSION and set the denominator based on this.mVersion 270 return mResultChecksum.expectedFpp() / DEFAULT_FPP; 271 } 272 273 static String generateTestResultSignature(ITestResult testResult, IModuleResult module, 274 String buildFingerprint) { 275 StringBuilder sb = new StringBuilder(); 276 String stacktrace = testResult.getStackTrace(); 277 278 stacktrace = stacktrace == null ? "" : stacktrace.trim(); 279 // Line endings for stacktraces are somewhat unpredictable and there is no need to 280 // actually read the result they are all removed for consistency. 281 stacktrace = stacktrace.replaceAll("\\r?\\n|\\r", ""); 282 sb.append(buildFingerprint).append(SEPARATOR) 283 .append(module.getId()).append(SEPARATOR) 284 .append(testResult.getFullName()).append(SEPARATOR) 285 .append(testResult.getResultStatus().getValue()).append(SEPARATOR) 286 .append(stacktrace).append(SEPARATOR); 287 return sb.toString(); 288 } 289 290 static String generateTestResultSignature( 291 String packageName, String suiteName, String caseName, String testName, String abi, 292 String status, 293 String stacktrace, 294 String buildFingerprint) { 295 296 String testId = buildTestId(suiteName, caseName, testName, abi); 297 StringBuilder sb = new StringBuilder(); 298 299 stacktrace = stacktrace == null ? "" : stacktrace.trim(); 300 // Line endings for stacktraces are somewhat unpredictable and there is no need to 301 // actually read the result they are all removed for consistency. 302 stacktrace = stacktrace.replaceAll("\\r?\\n|\\r", ""); 303 sb.append(buildFingerprint) 304 .append(SEPARATOR) 305 .append(packageName) 306 .append(SEPARATOR) 307 .append(testId) 308 .append(SEPARATOR) 309 .append(status) 310 .append(SEPARATOR) 311 .append(stacktrace) 312 .append(SEPARATOR); 313 return sb.toString(); 314 } 315 316 private static String buildTestId( 317 String suiteName, String caseName, String testName, String abi) { 318 String name = Joiner.on(NAME_SEPARATOR).skipNulls().join( 319 Strings.emptyToNull(suiteName), 320 Strings.emptyToNull(caseName), 321 Strings.emptyToNull(testName)); 322 return Joiner.on(ID_SEPARATOR).skipNulls().join( 323 Strings.emptyToNull(name), 324 Strings.emptyToNull(abi)); 325 } 326 327 328 private static String generateModuleResultSignature(IModuleResult module, 329 String buildFingerprint) { 330 StringBuilder sb = new StringBuilder(); 331 sb.append(buildFingerprint).append(SEPARATOR) 332 .append(module.getId()).append(SEPARATOR) 333 .append(module.isDone()).append(SEPARATOR) 334 .append(module.countResults(TestStatus.FAIL)); 335 return sb.toString(); 336 } 337 338 private static String generateModuleSummarySignature(IModuleResult module, 339 String buildFingerprint) { 340 StringBuilder sb = new StringBuilder(); 341 sb.append(buildFingerprint).append(SEPARATOR) 342 .append(module.getId()).append(SEPARATOR) 343 .append(module.countResults(TestStatus.FAIL)); 344 return sb.toString(); 345 } 346 347 static byte[] calculateFileChecksum(File file) throws ChecksumValidationException { 348 349 try (FileInputStream fis = new FileInputStream(file); 350 InputStream inputStream = new BufferedInputStream(fis)) { 351 MessageDigest hashSum = MessageDigest.getInstance("SHA-256"); 352 int cnt; 353 int bufferSize = 8192; 354 byte [] buffer = new byte[bufferSize]; 355 while ((cnt = inputStream.read(buffer)) != -1) { 356 hashSum.update(buffer, 0, cnt); 357 } 358 359 byte[] partialHash = new byte[32]; 360 hashSum.digest(partialHash, 0, 32); 361 return partialHash; 362 } catch (NoSuchAlgorithmException e) { 363 throw new ChecksumValidationException("Unable to hash file.", e); 364 } catch (IOException e) { 365 throw new ChecksumValidationException("Unable to hash file.", e); 366 } catch (DigestException e) { 367 throw new ChecksumValidationException("Unable to hash file.", e); 368 } 369 } 370 371 372 private static int countTestResults(IInvocationResult invocation) { 373 int count = 0; 374 for (IModuleResult module : invocation.getModules()) { 375 // Two entries per module (result & summary) 376 count += 2; 377 for (ICaseResult caseResult : module.getResults()) { 378 count += caseResult.getResults().size(); 379 } 380 } 381 return count; 382 } 383 384 public static class ChecksumValidationException extends Exception { 385 public ChecksumValidationException(String detailMessage) { 386 super(detailMessage); 387 } 388 389 public ChecksumValidationException(String detailMessage, Throwable throwable) { 390 super(detailMessage, throwable); 391 } 392 } 393 } 394