Generate and include checksum data with CTS report submissions
Test: Used exploratory tests and unit tests in ChecksumReporterTest.java
to verify the behavior.
* All TestResults are added from an InvocationResult
* Able to verify TestResult against previously calculated checksum
* CRCs are created for all files in a directory including subdirectories
* Checksum integrity is maintained while serializing to/from disk
* When possible checksum data is compressed before report submission
Change-Id: I8c563499991939c777892c00f24a339980916f04
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
index 6a9a9ab..fa8d8ad 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
@@ -408,7 +408,6 @@
// NOTE: Everything after this line only applies to the master ResultReporter.
-
synchronized(this) {
// The master ResultReporter tracks the progress of all invocations across
// shard ResultReporters. Writing results should not proceed until all
@@ -460,16 +459,16 @@
long startTime = mResult.getStartTime();
try {
+ // Zip the full test results directory.
+ copyDynamicConfigFiles(mBuildHelper.getDynamicConfigFiles(), mResultDir);
+ copyFormattingFiles(mResultDir);
+
File resultFile = ResultHandler.writeResults(mBuildHelper.getSuiteName(),
mBuildHelper.getSuiteVersion(), mBuildHelper.getSuitePlan(),
mBuildHelper.getSuiteBuild(), mResult, mResultDir, startTime,
elapsedTime + startTime, mReferenceUrl, getLogUrl(),
mBuildHelper.getCommandLineArgs());
info("Test Result: %s", resultFile.getCanonicalPath());
-
- // Zip the full test results directory.
- copyDynamicConfigFiles(mBuildHelper.getDynamicConfigFiles(), mResultDir);
- copyFormattingFiles(mResultDir);
File zippedResults = zipResults(mResultDir);
info("Full Result: %s", zippedResults.getCanonicalPath());
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
index a7a31f6..b8b7858 100644
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
@@ -17,6 +17,7 @@
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelperTest;
import com.android.compatibility.common.tradefed.command.CompatibilityConsoleTest;
+import com.android.compatibility.common.tradefed.result.ChecksumReporterTest;
import com.android.compatibility.common.tradefed.result.ConsoleReporterTest;
import com.android.compatibility.common.tradefed.result.ResultReporterTest;
import com.android.compatibility.common.tradefed.result.SubPlanCreatorTest;
@@ -45,6 +46,7 @@
addTestSuite(CompatibilityConsoleTest.class);
addTestSuite(CompatibilityTestTest.class);
addTestSuite(ConsoleReporterTest.class);
+ addTestSuite(ChecksumReporterTest.class);
addTestSuite(ResultReporterTest.class);
addTestSuite(CompatibilityTestTest.class);
addTestSuite(OptionHelperTest.class);
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ChecksumReporterTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ChecksumReporterTest.java
new file mode 100644
index 0000000..068ce45
--- /dev/null
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ChecksumReporterTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.tradefed.result;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildProvider;
+import com.android.compatibility.common.tradefed.result.ResultReporter;
+import com.android.compatibility.common.util.ChecksumReporter;
+import com.android.compatibility.common.util.ChecksumReporter.ChecksumValidationException;
+import com.android.compatibility.common.util.ICaseResult;
+import com.android.compatibility.common.util.IInvocationResult;
+import com.android.compatibility.common.util.IModuleResult;
+import com.android.compatibility.common.util.ITestResult;
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.TestStatus;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.util.FileUtil;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class ChecksumReporterTest extends TestCase {
+
+ private static final String ROOT_PROPERTY = "TESTS_ROOT";
+ private static final String ROOT_DIR_NAME = "root";
+ private static final String SUITE_NAME = "TESTS";
+ private static final String BUILD_NUMBER = "2";
+ private static final String SUITE_PLAN = "cts";
+ private static final String BASE_DIR_NAME = "android-tests";
+ private static final String TESTCASES = "testcases";
+
+ private ChecksumReporter mReporter;
+ private File mRoot = null;
+ private IBuildInfo mBuildInfo;
+ private ReportLog mReportLog = null;
+ private IInvocationResult mInvocationResult;
+ private IModuleResult mModuleResult;
+ private ITestResult mFailedTest;
+
+ @Override
+ public void setUp() throws Exception {
+ mReporter = new ChecksumReporter(100, .001, (short)1);
+ mRoot = FileUtil.createTempDir(ROOT_DIR_NAME);
+ File baseDir = new File(mRoot, BASE_DIR_NAME);
+ baseDir.mkdirs();
+ File testDir = new File(baseDir, TESTCASES);
+ testDir.mkdirs();
+ System.setProperty(ROOT_PROPERTY, mRoot.getAbsolutePath());
+
+ ResultReporter resultReporter = new ResultReporter();
+ CompatibilityBuildProvider provider = new CompatibilityBuildProvider() {
+ @Override
+ protected String getSuiteInfoName() {
+ return SUITE_NAME;
+ }
+ @Override
+ protected String getSuiteInfoBuildNumber() {
+ return BUILD_NUMBER;
+ }
+ @Override
+ protected String getSuiteInfoVersion() {
+ return BUILD_NUMBER;
+ }
+ };
+ OptionSetter setter = new OptionSetter(provider);
+ setter.setOptionValue("plan", SUITE_PLAN);
+ setter.setOptionValue("dynamic-config-url", "");
+ mBuildInfo = provider.getBuild();
+
+ resultReporter.invocationStarted(mBuildInfo);
+ mInvocationResult = resultReporter.getResult();
+ mModuleResult = mInvocationResult.getOrCreateModule("Module-1");
+ mModuleResult.setDone(true);
+ mModuleResult.setNotExecuted(0);
+ ICaseResult caseResult = mModuleResult.getOrCreateResult("Case-1");
+ ITestResult test1 = caseResult.getOrCreateResult("Test1");
+ test1.passed(mReportLog);
+ mFailedTest = caseResult.getOrCreateResult("Test2");
+ mFailedTest.failed("stack-trace - error happened");
+
+ IModuleResult moduleResult2 = mInvocationResult.getOrCreateModule("Module-2");
+ ICaseResult caseResult2 = moduleResult2.getOrCreateResult("Case-2");
+ mModuleResult.setDone(false);
+ mModuleResult.setNotExecuted(1);
+ ITestResult test3 = caseResult2.getOrCreateResult("Test3");
+ test3.passed(mReportLog);
+
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mReporter = null;
+ }
+
+ public void testStoreAndRetrieveTestResults() {
+ mReporter.addInvocation(mInvocationResult);
+ VerifyInvocationResults(mInvocationResult, mReporter);
+ }
+
+ /***
+ * By definition this test is flaky since the checksum has a false positive probability of .1%
+ */
+ public void testInvalidChecksums() {
+ mReporter.addInvocation(mInvocationResult);
+ IModuleResult module = mInvocationResult.getModules().get(1);
+ module.setDone(!module.isDone());
+ String fingerprint = mInvocationResult.getBuildFingerprint();
+ assertFalse("Checksum should contain module: " + module.getName(),
+ mReporter.containsModuleResult(module, fingerprint));
+
+ mFailedTest.setResultStatus(TestStatus.PASS);
+ assertFalse("Checksum should not contain test: " + mFailedTest.getName(),
+ mReporter.containsTestResult(mFailedTest, mModuleResult, fingerprint));
+ assertFalse("Module checksum should verify number of tests",
+ mReporter.containsModuleResult(mModuleResult, fingerprint));
+ }
+
+ public void testFileSerialization()
+ throws IOException, ClassNotFoundException, ChecksumValidationException {
+ mReporter.addInvocation(mInvocationResult);
+
+ File file1 = new File(mRoot, "file1.txt");
+ try (FileWriter fileWriter = new FileWriter(file1, false)) {
+ fileWriter.append("This is a test file");
+ }
+
+ mReporter.addDirectory(mRoot);
+ mReporter.saveToFile(mRoot);
+
+ ChecksumReporter storedChecksum = ChecksumReporter.load(mRoot);
+ VerifyInvocationResults(mInvocationResult, storedChecksum);
+ assertTrue("Serializing checksum maintains file hash",
+ storedChecksum.containsFile(file1, ""));
+ }
+
+ public void testFileCRCOperations() throws IOException {
+ File subDirectory = new File(mRoot, "child");
+ subDirectory.mkdir();
+ File file1 = new File(mRoot, "file1.txt");
+ try (FileWriter fileWriter = new FileWriter(file1, false)) {
+ fileWriter.append("This is a test file");
+ }
+
+ File file2 = new File(subDirectory, "file2.txt");
+ try (FileWriter fileWriter = new FileWriter(file2, false)) {
+ fileWriter.append("This is another test file with a different crc");
+ }
+
+ mReporter.addDirectory(mRoot);
+
+ assertTrue(mReporter.containsFile(file1, ""));
+ assertTrue(mReporter.containsFile(file2, "/child"));
+ assertFalse("Should not contain non-existent file",
+ mReporter.containsFile(new File(mRoot, "fake.txt"), ""));
+
+ File file3 = new File(mRoot, "file3.txt");
+ try (FileWriter fileWriter = new FileWriter(file3, false)) {
+ fileWriter.append("This is a test file added after crc calculated");
+ }
+ assertFalse("Should not contain file created after crc calculated",
+ mReporter.containsFile(file3, ""));
+
+ }
+
+ private void VerifyInvocationResults(IInvocationResult invocation, ChecksumReporter reporter) {
+ for (IModuleResult module : invocation.getModules()) {
+ String buildFingerprint = invocation.getBuildFingerprint();
+ assertTrue("Checksum should contain module: " + module.getName(),
+ reporter.containsModuleResult(module, buildFingerprint));
+ for (ICaseResult caseResult : module.getResults()) {
+ for (ITestResult result : caseResult.getResults()) {
+ assertTrue("Checksum should contain test: " + result.getName(),
+ reporter.containsTestResult(result, module, buildFingerprint));
+ }
+ }
+ }
+ }
+}
diff --git a/common/host-side/util/src/com/android/compatibility/common/util/ChecksumReporter.java b/common/host-side/util/src/com/android/compatibility/common/util/ChecksumReporter.java
new file mode 100644
index 0000000..faac61f
--- /dev/null
+++ b/common/host-side/util/src/com/android/compatibility/common/util/ChecksumReporter.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import com.android.annotations.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.hash.BloomFilter;
+import com.google.common.hash.Funnels;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInput;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/***
+ * Calculate and store checksum values for files and test results
+ */
+public final class ChecksumReporter implements Serializable {
+
+ public static final String NAME = "checksum.data";
+ public static final String PREV_NAME = "checksum.previous.data";
+
+ private static final double DEFAULT_FPP = 0.05;
+ private static final String SEPARATOR = "/";
+ private static final String ID_SEPARATOR = "@";
+ private static final String NAME_SEPARATOR = ".";
+
+ private static final short CURRENT_VERSION = 1;
+ // Serialized format Id (ie magic number) used to identify serialized data.
+ static final short SERIALIZED_FORMAT_CODE = 650;
+
+ private final BloomFilter<CharSequence> mResultChecksum;
+ private final HashMap<String, byte[]> mFileChecksum;
+ private final short mVersion;
+
+ /***
+ * Calculate checksum of test results and files in result directory and write to disk
+ * @param dir test results directory
+ * @param result test results
+ * @return true if successful, false if unable to calculate or store the checksum
+ */
+ public static boolean tryCreateChecksum(File dir, IInvocationResult result) {
+ try {
+ int totalCount = countTestResults(result);
+ ChecksumReporter checksumReporter =
+ new ChecksumReporter(totalCount, DEFAULT_FPP, CURRENT_VERSION);
+ checksumReporter.addInvocation(result);
+ checksumReporter.addDirectory(dir);
+ checksumReporter.saveToFile(dir);
+ } catch (Exception e) {
+ return false;
+ }
+ return true;
+ }
+
+ /***
+ * Create Checksum Reporter from data saved on disk
+ * @param directory
+ * @return
+ * @throws ChecksumValidationException
+ */
+ public static ChecksumReporter load(File directory) throws ChecksumValidationException {
+ ChecksumReporter reporter = new ChecksumReporter(directory);
+ if (reporter.getCapacity() > 1.1) {
+ throw new ChecksumValidationException("Capacity exceeded.");
+ }
+ return reporter;
+ }
+
+ /***
+ * Deserialize checksum from file
+ * @param directory the parent directory containing the checksum file
+ * @throws ChecksumValidationException
+ */
+ public ChecksumReporter(File directory) throws ChecksumValidationException {
+ File file = new File(directory, ChecksumReporter.NAME);
+ try (FileInputStream fileStream = new FileInputStream(file);
+ InputStream outputStream = new BufferedInputStream(fileStream);
+ ObjectInput objectInput = new ObjectInputStream(outputStream)) {
+ short magicNumber = objectInput.readShort();
+ switch (magicNumber) {
+ case SERIALIZED_FORMAT_CODE:
+ mVersion = objectInput.readShort();
+ mResultChecksum = (BloomFilter<CharSequence>) objectInput.readObject();
+ mFileChecksum = (HashMap<String, byte[]>) objectInput.readObject();
+ break;
+ default:
+ throw new ChecksumValidationException("Unknown format of serialized data.");
+ }
+ } catch (Exception e) {
+ throw new ChecksumValidationException("Unable to load checksum from file", e);
+ }
+ if (mVersion > CURRENT_VERSION) {
+ throw new ChecksumValidationException(
+ "File contains a newer version of ChecksumReporter");
+ }
+ }
+
+ /***
+ * Create new instance of ChecksumReporter
+ * @param testCount the number of test results that will be stored
+ * @param fpp the false positive percentage for result lookup misses
+ */
+ public ChecksumReporter(int testCount, double fpp, short version) {
+ mResultChecksum = BloomFilter.create(Funnels.unencodedCharsFunnel(),
+ testCount, fpp);
+ mFileChecksum = new HashMap<>();
+ mVersion = version;
+ }
+
+ /***
+ * Add each test result from each module and test case
+ */
+ public void addInvocation(IInvocationResult invocationResult) {
+ for (IModuleResult module : invocationResult.getModules()) {
+ String buildFingerprint = invocationResult.getBuildFingerprint();
+ addModuleResult(module, buildFingerprint);
+ for (ICaseResult caseResult : module.getResults()) {
+ for (ITestResult testResult : caseResult.getResults()) {
+ addTestResult(testResult, module, buildFingerprint);
+ }
+ }
+ }
+ }
+
+ /***
+ * Calculate CRC of file and store the result
+ * @param file crc calculated on this file
+ * @param path part of the key to identify the files crc
+ */
+ public void addFile(File file, String path) {
+ byte[] crc;
+ try {
+ crc = calculateFileChecksum(file);
+ } catch (ChecksumValidationException e) {
+ crc = new byte[0];
+ }
+ String key = path + SEPARATOR + file.getName();
+ mFileChecksum.put(key, crc);
+ }
+
+ @VisibleForTesting
+ public boolean containsFile(File file, String path) {
+ String key = path + SEPARATOR + file.getName();
+ if (mFileChecksum.containsKey(key))
+ {
+ try {
+ byte[] crc = calculateFileChecksum(file);
+ return Arrays.equals(mFileChecksum.get(key), crc);
+ } catch (ChecksumValidationException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /***
+ * Adds all child files recursively through all sub directories
+ * @param directory target that is deeply searched for files
+ */
+ public void addDirectory(File directory) {
+ addDirectory(directory, directory.getName());
+ }
+
+ /***
+ * @param path the relative path to the current directory from the base directory
+ */
+ private void addDirectory(File directory, String path) {
+ for(String childName : directory.list()) {
+ File child = new File(directory, childName);
+ if (child.isDirectory()) {
+ addDirectory(child, path + SEPARATOR + child.getName());
+ } else {
+ addFile(child, path);
+ }
+ }
+ }
+
+ /***
+ * Calculate checksum of test result and store the value
+ * @param testResult the target of the checksum
+ * @param moduleResult the module that contains the test result
+ * @param buildFingerprint the fingerprint the test execution is running against
+ */
+ public void addTestResult(
+ ITestResult testResult, IModuleResult moduleResult, String buildFingerprint) {
+
+ String signature = generateTestResultSignature(testResult, moduleResult, buildFingerprint);
+ mResultChecksum.put(signature);
+ }
+
+ @VisibleForTesting
+ public boolean containsTestResult(
+ ITestResult testResult, IModuleResult moduleResult, String buildFingerprint) {
+
+ String signature = generateTestResultSignature(testResult, moduleResult, buildFingerprint);
+ return mResultChecksum.mightContain(signature);
+ }
+
+ /***
+ * Calculate checksm of module result and store value
+ * @param moduleResult the target of the checksum
+ * @param buildFingerprint the fingerprint the test execution is running against
+ */
+ public void addModuleResult(IModuleResult moduleResult, String buildFingerprint) {
+ mResultChecksum.put(
+ generateModuleResultSignature(moduleResult, buildFingerprint));
+ mResultChecksum.put(
+ generateModuleSummarySignature(moduleResult, buildFingerprint));
+ }
+
+ @VisibleForTesting
+ public Boolean containsModuleResult(IModuleResult moduleResult, String buildFingerprint) {
+ return mResultChecksum.mightContain(
+ generateModuleResultSignature(moduleResult, buildFingerprint));
+ }
+
+ /***
+ * Write the checksum data to disk.
+ * Overwrites existing file
+ * @param directory
+ * @throws IOException
+ */
+ public void saveToFile(File directory) throws IOException {
+ File file = new File(directory, NAME);
+
+ try (FileOutputStream fileStream = new FileOutputStream(file, false);
+ OutputStream outputStream = new BufferedOutputStream(fileStream);
+ ObjectOutput objectOutput = new ObjectOutputStream(outputStream)) {
+ objectOutput.writeShort(SERIALIZED_FORMAT_CODE);
+ objectOutput.writeShort(mVersion);
+ objectOutput.writeObject(mResultChecksum);
+ objectOutput.writeObject(mFileChecksum);
+ }
+ }
+
+ @VisibleForTesting
+ double getCapacity() {
+ // If default FPP changes:
+ // increment the CURRENT_VERSION and set the denominator based on this.mVersion
+ return mResultChecksum.expectedFpp() / DEFAULT_FPP;
+ }
+
+ static String generateTestResultSignature(ITestResult testResult, IModuleResult module,
+ String buildFingerprint) {
+ StringBuilder sb = new StringBuilder();
+ String stacktrace = testResult.getStackTrace();
+
+ stacktrace = stacktrace == null ? "" : stacktrace.trim();
+ // Line endings for stacktraces are somewhat unpredictable and there is no need to
+ // actually read the result they are all removed for consistency.
+ stacktrace = stacktrace.replaceAll("\\r?\\n|\\r", "");
+ sb.append(buildFingerprint).append(SEPARATOR)
+ .append(module.getId()).append(SEPARATOR)
+ .append(testResult.getFullName()).append(SEPARATOR)
+ .append(testResult.getResultStatus().getValue()).append(SEPARATOR)
+ .append(stacktrace).append(SEPARATOR);
+ return sb.toString();
+ }
+
+ static String generateTestResultSignature(
+ String packageName, String suiteName, String caseName, String testName, String abi,
+ String status,
+ String stacktrace,
+ String buildFingerprint) {
+
+ String testId = buildTestId(suiteName, caseName, testName, abi);
+ StringBuilder sb = new StringBuilder();
+
+ stacktrace = stacktrace == null ? "" : stacktrace.trim();
+ // Line endings for stacktraces are somewhat unpredictable and there is no need to
+ // actually read the result they are all removed for consistency.
+ stacktrace = stacktrace.replaceAll("\\r?\\n|\\r", "");
+ sb.append(buildFingerprint)
+ .append(SEPARATOR)
+ .append(packageName)
+ .append(SEPARATOR)
+ .append(testId)
+ .append(SEPARATOR)
+ .append(status)
+ .append(SEPARATOR)
+ .append(stacktrace)
+ .append(SEPARATOR);
+ return sb.toString();
+ }
+
+ private static String buildTestId(
+ String suiteName, String caseName, String testName, @Nullable String abi) {
+ String name = Joiner.on(NAME_SEPARATOR).skipNulls().join(
+ Strings.emptyToNull(suiteName),
+ Strings.emptyToNull(caseName),
+ Strings.emptyToNull(testName));
+ return Joiner.on(ID_SEPARATOR).skipNulls().join(
+ Strings.emptyToNull(name),
+ Strings.emptyToNull(abi));
+ }
+
+
+ private static String generateModuleResultSignature(IModuleResult module,
+ String buildFingerprint) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(buildFingerprint).append(SEPARATOR)
+ .append(module.getId()).append(SEPARATOR)
+ .append(module.isDone()).append(SEPARATOR)
+ .append(module.getNotExecuted()).append(SEPARATOR)
+ .append(module.countResults(TestStatus.FAIL));
+ return sb.toString();
+ }
+
+ private static String generateModuleSummarySignature(IModuleResult module,
+ String buildFingerprint) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(buildFingerprint).append(SEPARATOR)
+ .append(module.getId()).append(SEPARATOR)
+ .append(module.countResults(TestStatus.FAIL));
+ return sb.toString();
+ }
+
+ static byte[] calculateFileChecksum(File file) throws ChecksumValidationException {
+
+ try (FileInputStream fis = new FileInputStream(file);
+ InputStream inputStream = new BufferedInputStream(fis)) {
+ MessageDigest hashSum = MessageDigest.getInstance("SHA-256");
+ int cnt;
+ int bufferSize = 8192;
+ byte [] buffer = new byte[bufferSize];
+ while ((cnt = inputStream.read(buffer)) != -1) {
+ hashSum.update(buffer, 0, cnt);
+ }
+
+ byte[] partialHash = new byte[32];
+ hashSum.digest(partialHash, 0, 32);
+ return partialHash;
+ } catch (NoSuchAlgorithmException e) {
+ throw new ChecksumValidationException("Unable to hash file.", e);
+ } catch (IOException e) {
+ throw new ChecksumValidationException("Unable to hash file.", e);
+ } catch (DigestException e) {
+ throw new ChecksumValidationException("Unable to hash file.", e);
+ }
+ }
+
+
+ private static int countTestResults(IInvocationResult invocation) {
+ int count = 0;
+ for (IModuleResult module : invocation.getModules()) {
+ // Two entries per module (result & summary)
+ count += 2;
+ for (ICaseResult caseResult : module.getResults()) {
+ count += caseResult.getResults().size();
+ }
+ }
+ return count;
+ }
+
+ public static class ChecksumValidationException extends Exception {
+ public ChecksumValidationException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public ChecksumValidationException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+ }
+}
diff --git a/common/util/src/com/android/compatibility/common/util/ResultHandler.java b/common/host-side/util/src/com/android/compatibility/common/util/ResultHandler.java
similarity index 79%
rename from common/util/src/com/android/compatibility/common/util/ResultHandler.java
rename to common/host-side/util/src/com/android/compatibility/common/util/ResultHandler.java
index 435f515..a6f990e 100644
--- a/common/util/src/com/android/compatibility/common/util/ResultHandler.java
+++ b/common/host-side/util/src/com/android/compatibility/common/util/ResultHandler.java
@@ -15,6 +15,10 @@
*/
package com.android.compatibility.common.util;
+import com.android.compatibility.common.util.ChecksumReporter.ChecksumValidationException;
+
+import com.google.common.base.Strings;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
@@ -28,6 +32,9 @@
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
@@ -98,12 +105,17 @@
* @param resultsDir
*/
public static List<IInvocationResult> getResults(File resultsDir) {
+ return getResults(resultsDir, false);
+ }
+
+ /**
+ * @param resultsDir
+ * @param useChecksum
+ */
+ public static List<IInvocationResult> getResults(
+ File resultsDir, Boolean useChecksum) {
List<IInvocationResult> results = new ArrayList<>();
- File[] files = resultsDir.listFiles();
- if (files == null || files.length == 0) {
- // No results, just return the empty list
- return results;
- }
+ List<File> files = getResultDirectories(resultsDir);;
for (File resultDir : files) {
if (!resultDir.isDirectory()) {
continue;
@@ -113,7 +125,20 @@
if (!resultFile.exists()) {
continue;
}
+ Boolean invocationUseChecksum = useChecksum;
IInvocationResult invocation = new InvocationResult();
+ invocation.setRetryDirectory(resultDir);
+ ChecksumReporter checksumReporter = null;
+ if (invocationUseChecksum) {
+ try {
+ checksumReporter = ChecksumReporter.load(resultDir);
+ invocation.setRetryChecksumStatus(RetryChecksumStatus.RetryWithChecksum);
+ } catch (ChecksumValidationException e) {
+ // Unable to read checksum form previous execution
+ invocation.setRetryChecksumStatus(RetryChecksumStatus.RetryWithoutChecksum);
+ invocationUseChecksum = false;
+ }
+ }
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(new FileReader(resultFile));
@@ -194,10 +219,22 @@
}
}
parser.require(XmlPullParser.END_TAG, NS, TEST_TAG);
+ Boolean checksumMismatch = invocationUseChecksum
+ && !checksumReporter.containsTestResult(
+ test, module, invocation.getBuildFingerprint());
+ if (checksumMismatch) {
+ test.removeResult();
+ }
}
parser.require(XmlPullParser.END_TAG, NS, CASE_TAG);
}
parser.require(XmlPullParser.END_TAG, NS, MODULE_TAG);
+ Boolean checksumMismatch = invocationUseChecksum
+ && !checksumReporter.containsModuleResult(
+ module, invocation.getBuildFingerprint());
+ if (checksumMismatch) {
+ module.setDone(false);
+ }
}
parser.require(XmlPullParser.END_TAG, NS, RESULT_TAG);
results.add(invocation);
@@ -296,6 +333,10 @@
serializer.startTag(NS, BUILD_TAG);
for (Entry<String, String> entry : result.getInvocationInfo().entrySet()) {
serializer.attribute(NS, entry.getKey(), entry.getValue());
+ if (Strings.isNullOrEmpty(result.getBuildFingerprint()) &&
+ entry.getKey().equals(BUILD_FINGERPRINT)) {
+ result.setBuildFingerprint(entry.getValue());
+ }
}
serializer.endTag(NS, BUILD_TAG);
@@ -370,20 +411,59 @@
serializer.endTag(NS, MODULE_TAG);
}
serializer.endDocument();
+ createChecksum(resultDir, result);
return resultFile;
}
+ private static void createChecksum(File resultDir, IInvocationResult invocationResult) {
+ RetryChecksumStatus retryStatus = invocationResult.getRetryChecksumStatus();
+ switch (retryStatus) {
+ case NotRetry: case RetryWithChecksum:
+ // Do not disrupt the process if there is a problem generating checksum.
+ ChecksumReporter.tryCreateChecksum(resultDir, invocationResult);
+ break;
+ case RetryWithoutChecksum:
+ // If the previous run has an invalid checksum file,
+ // copy it into current results folder for future troubleshooting
+ File retryDirectory = invocationResult.getRetryDirectory();
+ Path retryChecksum = FileSystems.getDefault().getPath(
+ retryDirectory.getAbsolutePath(), ChecksumReporter.NAME);
+ if (!retryChecksum.toFile().exists()) {
+ // if no checksum file, check for a copy from a previous retry
+ retryChecksum = FileSystems.getDefault().getPath(
+ retryDirectory.getAbsolutePath(), ChecksumReporter.PREV_NAME);
+ }
+
+ if (retryChecksum.toFile().exists()) {
+ File checksumCopy = new File(resultDir, ChecksumReporter.PREV_NAME);
+ try (FileOutputStream stream = new FileOutputStream(checksumCopy)) {
+ Files.copy(retryChecksum, stream);
+ } catch (IOException e) {
+ // Do not disrupt the process if there is a problem copying checksum
+ }
+ }
+ }
+ }
+
/**
* Find the IInvocationResult for the given sessionId.
*/
public static IInvocationResult findResult(File resultsDir, Integer sessionId)
throws FileNotFoundException {
+ return findResult(resultsDir, sessionId, true);
+ }
+
+ /**
+ * Find the IInvocationResult for the given sessionId.
+ */
+ private static IInvocationResult findResult(
+ File resultsDir, Integer sessionId, Boolean useChecksum) throws FileNotFoundException {
if (sessionId < 0) {
throw new IllegalArgumentException(
String.format("Invalid session id [%d] ", sessionId));
}
- List<IInvocationResult> results = getResults(resultsDir);
+ List<IInvocationResult> results = getResults(resultsDir, useChecksum);
if (results == null || sessionId >= results.size()) {
throw new RuntimeException(String.format("Could not find session [%d]", sessionId));
}
@@ -391,6 +471,33 @@
}
/**
+ * Get a list of child directories that contain test invocation results
+ * @param resultsDir the root test result directory
+ * @return
+ */
+ public static List<File> getResultDirectories(File resultsDir) {
+ List<File> directoryList = new ArrayList<>();
+ File[] files = resultsDir.listFiles();
+ if (files == null || files.length == 0) {
+ // No results, just return the empty list
+ return directoryList;
+ }
+ for (File resultDir : files) {
+ if (!resultDir.isDirectory()) {
+ continue;
+ }
+ // Only include if it contain results file
+ File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
+ if (!resultFile.exists()) {
+ continue;
+ }
+ directoryList.add(resultDir);
+ }
+ Collections.sort(directoryList, (d1, d2) -> d1.getName().compareTo(d2.getName()));
+ return directoryList;
+ }
+
+ /**
* Return the given time as a {@link String} suitable for displaying.
* <p/>
* Example: Fri Aug 20 15:13:03 PDT 2010
diff --git a/common/util/src/com/android/compatibility/common/util/IInvocationResult.java b/common/util/src/com/android/compatibility/common/util/IInvocationResult.java
index 739dd48..2b75371 100644
--- a/common/util/src/com/android/compatibility/common/util/IInvocationResult.java
+++ b/common/util/src/com/android/compatibility/common/util/IInvocationResult.java
@@ -114,4 +114,24 @@
* Return the number of completed test modules for this invocation.
*/
int getModuleCompleteCount();
+
+ /**
+ * Return status of checksum from previous session
+ */
+ RetryChecksumStatus getRetryChecksumStatus();
+
+ /**
+ * Set status of checksum from previous session
+ */
+ void setRetryChecksumStatus(RetryChecksumStatus retryStatus);
+
+ /**
+ * Return the directory of the previous sessions results
+ */
+ File getRetryDirectory();
+
+ /**
+ * Set the directory of the previous sessions results
+ */
+ void setRetryDirectory(File resultDir);
}
diff --git a/common/util/src/com/android/compatibility/common/util/ITestResult.java b/common/util/src/com/android/compatibility/common/util/ITestResult.java
index c35b997..24ee1aa 100644
--- a/common/util/src/com/android/compatibility/common/util/ITestResult.java
+++ b/common/util/src/com/android/compatibility/common/util/ITestResult.java
@@ -142,4 +142,8 @@
*/
boolean isRetry();
+ /**
+ * Clear the existing result and default to 'failed'
+ */
+ void removeResult();
}
diff --git a/common/util/src/com/android/compatibility/common/util/InvocationResult.java b/common/util/src/com/android/compatibility/common/util/InvocationResult.java
index f74c61d..83f1dac 100644
--- a/common/util/src/com/android/compatibility/common/util/InvocationResult.java
+++ b/common/util/src/com/android/compatibility/common/util/InvocationResult.java
@@ -15,6 +15,7 @@
*/
package com.android.compatibility.common.util;
+import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -37,7 +38,8 @@
private String mTestPlan;
private String mCommandLineArgs;
private int mNotExecuted = 0;
-
+ private RetryChecksumStatus mRetryChecksumStatus = RetryChecksumStatus.NotRetry;
+ private File mRetryDirectory = null;
/**
* {@inheritDoc}
*/
@@ -201,4 +203,36 @@
}
return completeModules;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RetryChecksumStatus getRetryChecksumStatus() {
+ return mRetryChecksumStatus;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRetryChecksumStatus(RetryChecksumStatus retryStatus) {
+ mRetryChecksumStatus = retryStatus;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public File getRetryDirectory() {
+ return mRetryDirectory;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRetryDirectory(File resultDir) {
+ mRetryDirectory = resultDir;
+ }
}
diff --git a/common/util/src/com/android/compatibility/common/util/RetryChecksumStatus.java b/common/util/src/com/android/compatibility/common/util/RetryChecksumStatus.java
new file mode 100644
index 0000000..a86ab37
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/RetryChecksumStatus.java
@@ -0,0 +1,8 @@
+package com.android.compatibility.common.util;
+
+
+public enum RetryChecksumStatus {
+ NotRetry,
+ RetryWithChecksum,
+ RetryWithoutChecksum
+}
diff --git a/common/util/src/com/android/compatibility/common/util/TestResult.java b/common/util/src/com/android/compatibility/common/util/TestResult.java
index ba378d0..36b6e5a 100644
--- a/common/util/src/com/android/compatibility/common/util/TestResult.java
+++ b/common/util/src/com/android/compatibility/common/util/TestResult.java
@@ -242,6 +242,15 @@
* {@inheritDoc}
*/
@Override
+ public void removeResult() {
+ setResultStatus(TestStatus.FAIL);
+ setStackTrace("");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public int compareTo(ITestResult another) {
return getName().compareTo(another.getName());
}