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