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 */ tryCreateChecksum(File dir, IInvocationResult result)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 */ load(File directory)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 */ ChecksumReporter(File directory)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 */ ChecksumReporter(int testCount, double fpp, short version)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 */ addInvocation(IInvocationResult invocationResult)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 */ addFile(File file, String path)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 containsFile(File file, String path)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 */ addDirectory(File directory)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 */ addDirectory(File directory, String path)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 */ addTestResult( ITestResult testResult, IModuleResult moduleResult, String buildFingerprint)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 containsTestResult( ITestResult testResult, IModuleResult moduleResult, String buildFingerprint)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 */ addModuleResult(IModuleResult moduleResult, String buildFingerprint)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 containsModuleResult(IModuleResult moduleResult, String buildFingerprint)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 */ saveToFile(File directory)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 getCapacity()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 generateTestResultSignature(ITestResult testResult, IModuleResult module, String buildFingerprint)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 generateTestResultSignature( String packageName, String suiteName, String caseName, String testName, String abi, String status, String stacktrace, String buildFingerprint)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 buildTestId( String suiteName, String caseName, String testName, String abi)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 generateModuleResultSignature(IModuleResult module, String buildFingerprint)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 generateModuleSummarySignature(IModuleResult module, String buildFingerprint)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 calculateFileChecksum(File file)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 countTestResults(IInvocationResult invocation)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 { ChecksumValidationException(String detailMessage)385 public ChecksumValidationException(String detailMessage) { 386 super(detailMessage); 387 } 388 ChecksumValidationException(String detailMessage, Throwable throwable)389 public ChecksumValidationException(String detailMessage, Throwable throwable) { 390 super(detailMessage, throwable); 391 } 392 } 393 } 394