Home | History | Annotate | Download | only in util
      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      */
     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      */
     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      */
    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      */
    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      */
    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      */
    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
    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      */
    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      */
    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      */
    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
    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      */
    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
    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      */
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 {
    385         public ChecksumValidationException(String detailMessage) {
    386             super(detailMessage);
    387         }
    388 
    389         public ChecksumValidationException(String detailMessage, Throwable throwable) {
    390             super(detailMessage, throwable);
    391         }
    392     }
    393 }
    394