1 /* 2 * Copyright (C) 2018 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.compatibility.common.tradefed.result.suite; 17 18 import com.android.compatibility.common.util.ChecksumReporter.ChecksumValidationException; 19 import com.android.tradefed.result.TestDescription; 20 import com.android.tradefed.result.TestResult; 21 import com.android.tradefed.result.TestRunResult; 22 import com.android.tradefed.result.TestStatus; 23 24 import com.google.common.hash.BloomFilter; 25 import com.google.common.hash.Funnels; 26 27 import java.io.BufferedInputStream; 28 import java.io.BufferedOutputStream; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.ObjectOutput; 35 import java.io.ObjectOutputStream; 36 import java.io.OutputStream; 37 import java.security.DigestException; 38 import java.security.MessageDigest; 39 import java.security.NoSuchAlgorithmException; 40 import java.util.Collection; 41 import java.util.HashMap; 42 import java.util.Map.Entry; 43 44 /** 45 * Helper to generate the checksum of the results and files. Use 46 * {@link #tryCreateChecksum(File, Collection, String)} to get the checksum file in the result dir. 47 */ 48 public class CertificationChecksumHelper { 49 50 public static final String NAME = "checksum-suite.data"; 51 52 private static final double DEFAULT_FPP = 0.05; 53 private static final String SEPARATOR = "/"; 54 55 private static final short CURRENT_VERSION = 1; 56 // Serialized format Id (ie magic number) used to identify serialized data. 57 static final short SERIALIZED_FORMAT_CODE = 650; 58 59 private final BloomFilter<CharSequence> mResultChecksum; 60 private final HashMap<String, byte[]> mFileChecksum; 61 private final short mVersion; 62 private final String mBuildFingerprint; 63 64 /** 65 * Create new instance of {@link CertificationChecksumHelper} 66 * 67 * @param totalCount the total number of module and test results that will be stored 68 * @param fpp the false positive percentage for result lookup misses 69 * @param version 70 * @param buildFingerprint 71 */ CertificationChecksumHelper( int totalCount, double fpp, short version, String buildFingerprint)72 public CertificationChecksumHelper( 73 int totalCount, double fpp, short version, String buildFingerprint) { 74 mResultChecksum = BloomFilter.create(Funnels.unencodedCharsFunnel(), totalCount, fpp); 75 mFileChecksum = new HashMap<>(); 76 mVersion = version; 77 mBuildFingerprint = buildFingerprint; 78 } 79 80 /** 81 * Calculate checksum of test results and files in result directory and write to disk 82 * @param dir test results directory 83 * @param results test results 84 * @return true if successful, false if unable to calculate or store the checksum 85 */ tryCreateChecksum(File dir, Collection<TestRunResult> results, String buildFingerprint)86 public static boolean tryCreateChecksum(File dir, Collection<TestRunResult> results, 87 String buildFingerprint) { 88 try { 89 // The total number of module result signatures, module summary signatures and test 90 // result signatures. 91 int totalCount = results.size() * 2 + countTestResults(results); 92 CertificationChecksumHelper checksumReporter = 93 new CertificationChecksumHelper(totalCount, DEFAULT_FPP, CURRENT_VERSION, 94 buildFingerprint); 95 checksumReporter.addResults(results); 96 checksumReporter.addDirectory(dir); 97 checksumReporter.saveToFile(dir); 98 } catch (Exception e) { 99 return false; 100 } 101 return true; 102 } 103 104 /*** 105 * Write the checksum data to disk. 106 * Overwrites existing file 107 * @param directory 108 * @throws IOException 109 */ saveToFile(File directory)110 private void saveToFile(File directory) throws IOException { 111 File file = new File(directory, NAME); 112 113 try (FileOutputStream fileStream = new FileOutputStream(file, false); 114 OutputStream outputStream = new BufferedOutputStream(fileStream); 115 ObjectOutput objectOutput = new ObjectOutputStream(outputStream)) { 116 objectOutput.writeShort(SERIALIZED_FORMAT_CODE); 117 objectOutput.writeShort(mVersion); 118 objectOutput.writeObject(mResultChecksum); 119 objectOutput.writeObject(mFileChecksum); 120 } 121 } 122 countTestResults(Collection<TestRunResult> results)123 private static int countTestResults(Collection<TestRunResult> results) { 124 int count = 0; 125 for (TestRunResult result : results) { 126 count += result.getNumTests(); 127 } 128 return count; 129 } 130 addResults(Collection<TestRunResult> results)131 private void addResults(Collection<TestRunResult> results) { 132 for (TestRunResult moduleResult : results) { 133 // First the module result signature 134 mResultChecksum.put( 135 generateModuleResultSignature(moduleResult, mBuildFingerprint)); 136 // Second the module summary signature 137 mResultChecksum.put( 138 generateModuleSummarySignature(moduleResult, mBuildFingerprint)); 139 140 for (Entry<TestDescription, TestResult> caseResult 141 : moduleResult.getTestResults().entrySet()) { 142 mResultChecksum.put(generateTestResultSignature( 143 caseResult, moduleResult, mBuildFingerprint)); 144 } 145 } 146 } 147 generateModuleResultSignature(TestRunResult module, String buildFingerprint)148 private static String generateModuleResultSignature(TestRunResult module, 149 String buildFingerprint) { 150 StringBuilder sb = new StringBuilder(); 151 sb.append(buildFingerprint).append(SEPARATOR) 152 .append(module.getName()).append(SEPARATOR) 153 .append(module.isRunComplete()).append(SEPARATOR) 154 .append(module.getNumTestsInState(TestStatus.FAILURE)); 155 return sb.toString(); 156 } 157 generateModuleSummarySignature(TestRunResult module, String buildFingerprint)158 private static String generateModuleSummarySignature(TestRunResult module, 159 String buildFingerprint) { 160 StringBuilder sb = new StringBuilder(); 161 sb.append(buildFingerprint).append(SEPARATOR) 162 .append(module.getName()).append(SEPARATOR) 163 .append(module.getNumTestsInState(TestStatus.FAILURE)); 164 return sb.toString(); 165 } 166 generateTestResultSignature( Entry<TestDescription, TestResult> testResult, TestRunResult module, String buildFingerprint)167 private static String generateTestResultSignature( 168 Entry<TestDescription, TestResult> testResult, TestRunResult module, 169 String buildFingerprint) { 170 StringBuilder sb = new StringBuilder(); 171 String stacktrace = testResult.getValue().getStackTrace(); 172 173 stacktrace = stacktrace == null ? "" : stacktrace.trim(); 174 // Line endings for stacktraces are somewhat unpredictable and there is no need to 175 // actually read the result they are all removed for consistency. 176 stacktrace = stacktrace.replaceAll("\\r?\\n|\\r", ""); 177 String testResultStatus = 178 TestStatus.convertToCompatibilityString(testResult.getValue().getResultStatus()); 179 sb.append(buildFingerprint) 180 .append(SEPARATOR) 181 .append(module.getName()) 182 .append(SEPARATOR) 183 .append(testResult.getKey().toString()) 184 .append(SEPARATOR) 185 .append(testResultStatus) 186 .append(SEPARATOR) 187 .append(stacktrace) 188 .append(SEPARATOR); 189 return sb.toString(); 190 } 191 192 /*** 193 * Adds all child files recursively through all sub directories 194 * @param directory target that is deeply searched for files 195 */ addDirectory(File directory)196 public void addDirectory(File directory) { 197 addDirectory(directory, directory.getName()); 198 } 199 200 /*** 201 * @param path the relative path to the current directory from the base directory 202 */ addDirectory(File directory, String path)203 private void addDirectory(File directory, String path) { 204 for(String childName : directory.list()) { 205 File child = new File(directory, childName); 206 if (child.isDirectory()) { 207 addDirectory(child, path + SEPARATOR + child.getName()); 208 } else { 209 addFile(child, path); 210 } 211 } 212 } 213 214 /*** 215 * Calculate CRC of file and store the result 216 * @param file crc calculated on this file 217 * @param path part of the key to identify the files crc 218 */ addFile(File file, String path)219 private void addFile(File file, String path) { 220 byte[] crc; 221 try { 222 crc = calculateFileChecksum(file); 223 } catch (ChecksumValidationException e) { 224 crc = new byte[0]; 225 } 226 String key = path + SEPARATOR + file.getName(); 227 mFileChecksum.put(key, crc); 228 } 229 calculateFileChecksum(File file)230 private static byte[] calculateFileChecksum(File file) throws ChecksumValidationException { 231 232 try (FileInputStream fis = new FileInputStream(file); 233 InputStream inputStream = new BufferedInputStream(fis)) { 234 MessageDigest hashSum = MessageDigest.getInstance("SHA-256"); 235 int cnt; 236 int bufferSize = 8192; 237 byte [] buffer = new byte[bufferSize]; 238 while ((cnt = inputStream.read(buffer)) != -1) { 239 hashSum.update(buffer, 0, cnt); 240 } 241 242 byte[] partialHash = new byte[32]; 243 hashSum.digest(partialHash, 0, 32); 244 return partialHash; 245 } catch (NoSuchAlgorithmException e) { 246 throw new ChecksumValidationException("Unable to hash file.", e); 247 } catch (IOException e) { 248 throw new ChecksumValidationException("Unable to hash file.", e); 249 } catch (DigestException e) { 250 throw new ChecksumValidationException("Unable to hash file.", e); 251 } 252 } 253 } 254