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