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