Merge "Use golden files for runtime tests on Windows or Mac"
diff --git a/build.gradle b/build.gradle
index f379bbf..8ea2b3f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1239,6 +1239,18 @@
 }
 
 test {
+    if (project.hasProperty('generate_golden_files_to')) {
+        systemProperty 'generate_golden_files_to', project.property('generate_golden_files_to')
+        assert project.hasProperty('HEAD_sha1')
+        systemProperty 'test_git_HEAD_sha1', project.property('HEAD_sha1')
+    }
+
+    if (project.hasProperty('use_golden_files_in')) {
+        systemProperty 'use_golden_files_in', project.property('use_golden_files_in')
+        assert project.hasProperty('HEAD_sha1')
+        systemProperty 'test_git_HEAD_sha1', project.property('HEAD_sha1')
+    }
+
     dependsOn getJarsFromSupportLibs
     testLogging.exceptionFormat = 'full'
     if (project.hasProperty('print_test_stdout')) {
diff --git a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
index ede7e53..e01373c 100644
--- a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
@@ -15,6 +15,7 @@
 import org.hamcrest.core.IsInstanceOf;
 import org.hamcrest.core.StringContains;
 import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.internal.matchers.ThrowableMessageMatcher;
 
@@ -156,16 +157,20 @@
     // TODO check compilation warnings are correctly reported
 
     // Missing interface B is causing the wrong code to be executed.
-    if (ToolHelper.artSupported()) {
-      thrown.expect(AssertionError.class);
-      execute(
-          "testMissingInterfaceDesugared2AndroidK",
-          "desugaringwithmissingclasstest2.Main",
-          new Path[] {
-              lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
-          },
-          new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+    if (!ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
+      return;
     }
+    if (ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
+      thrown.expect(AssertionError.class);
+    }
+    execute(
+        "testMissingInterfaceDesugared2AndroidK",
+        "desugaringwithmissingclasstest2.Main",
+        new Path[] {
+            lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
+        },
+        new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+
   }
 
   @Test
@@ -256,17 +261,20 @@
     Path testDex = test.build();
     // TODO check compilation warnings are correctly reported
 
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
     // Missing interface B is causing the wrong method to be executed.
-    if (ToolHelper.artSupported()) {
+    if (ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
       thrown.expect(AssertionError.class);
-      execute(
-          "testCallToMissingSuperInterfaceDesugaredAndroidK",
-          "desugaringwithmissingclasstest3.Main",
-          new Path[] {
-              lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
-          },
-          new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
     }
+    execute(
+        "testCallToMissingSuperInterfaceDesugaredAndroidK",
+        "desugaringwithmissingclasstest3.Main",
+        new Path[] {
+            lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
+        },
+        new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 44eb344..2032866 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Kind;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
@@ -22,6 +23,7 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -1362,6 +1364,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   public R8RunArtTestsTest(String name, DexTool toolchain) {
     this.name = name;
     this.toolchain = toolchain;
@@ -1769,7 +1774,7 @@
         specification.disableInlining, specification.disableClassInlining,
         specification.hasMissingClasses);
 
-    if (!ToolHelper.artSupported()) {
+    if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
       return;
     }
 
@@ -1790,9 +1795,11 @@
 
     boolean compileOnly = System.getProperty("jctf_compile_only", "0").equals("1");
     if (compileOnly || specification.skipArt) {
-      // verify dex code instead of running it
-      Path oatFile = temp.getRoot().toPath().resolve("all.oat");
-      ToolHelper.runDex2Oat(processedFile.toPath(), oatFile);
+      if (ToolHelper.isDex2OatSupported()) {
+        // verify dex code instead of running it
+        Path oatFile = temp.getRoot().toPath().resolve("all.oat");
+        ToolHelper.runDex2Oat(processedFile.toPath(), oatFile);
+      }
       return;
     }
 
@@ -1964,7 +1971,7 @@
           specification.hasMissingClasses);
     }
 
-    if (!specification.skipArt && ToolHelper.artSupported()) {
+    if (!specification.skipArt && (ToolHelper.artSupported() || ToolHelper.dealsWithGoldenFiles())) {
       File originalFile;
       File processedFile;
 
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index 62dd4af..fe5ae72 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import java.io.IOException;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
@@ -21,6 +22,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -69,6 +71,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   private final Input input;
   private final CompilerUnderTest compiler;
   private final CompilationMode mode;
@@ -183,9 +188,8 @@
 
   @Test
   public void outputIsIdentical() throws IOException, InterruptedException, ExecutionException {
-    if (!ToolHelper.artSupported()) {
-      return;
-    }
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
 
     DexVm vm = ToolHelper.getDexVm();
 
diff --git a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
index fc373d2..0f3cadc 100644
--- a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
@@ -91,6 +92,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Parameters(name = "{0}")
   public static Collection<String[]> data() {
     return Arrays.asList(new String[][]{
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
index bb8c686..83df845 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
@@ -73,7 +74,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -118,6 +119,8 @@
 
   @Rule public ExpectedException thrown = ExpectedException.none();
 
+  @Rule public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Test
   public void staticInterfaceMethods() throws Throwable {
     test("staticinterfacemethods", "interfacemethods", "StaticInterfaceMethods")
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 9fdbf0a..2c6a72a 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -47,6 +48,7 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestWatcher;
 
 public abstract class RunExamplesAndroidOTest
       <B extends BaseCommand.Builder<? extends BaseCommand, B>> {
@@ -148,7 +150,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -270,6 +272,9 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   boolean failsOn(Map<ToolHelper.DexVm.Version, List<String>> failsOn, String name) {
     Version vmVersion = ToolHelper.getDexVm().getVersion();
     return failsOn.containsKey(vmVersion)
@@ -553,14 +558,14 @@
       throws IOException {
 
     boolean expectedToFail = expectedToFail(testName);
-    if (expectedToFail) {
+    if (expectedToFail && !ToolHelper.compareAgaintsGoldenFiles()) {
       thrown.expect(Throwable.class);
     }
     String output = ToolHelper.runArtNoVerificationErrors(
         Arrays.stream(dexes).map(path -> path.toString()).collect(Collectors.toList()),
         qualifiedMainClass,
         null);
-    if (!expectedToFail && !skipRunningOnJvm(testName)) {
+    if (!expectedToFail && !skipRunningOnJvm(testName) && !ToolHelper.compareAgaintsGoldenFiles()) {
       ToolHelper.ProcessResult javaResult =
           ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
       assertEquals("JVM run failed", javaResult.exitCode, 0);
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index bc898bd..182fe80 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -143,7 +144,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -193,6 +194,9 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   boolean failsOn(Map<DexVm.Version, List<String>> failsOn, String name) {
     DexVm.Version vmVersion = ToolHelper.getDexVm().getVersion();
     return failsOn.containsKey(vmVersion)
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index 281b0da..284ed68 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -143,7 +144,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -224,6 +225,9 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   boolean failsOn(Map<DexVm.Version, List<String>> failsOn, String name) {
     DexVm.Version vmVersion = ToolHelper.getDexVm().getVersion();
     return failsOn.containsKey(vmVersion)
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 331af0a..add86c2 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.PreloadedClassFileProvider;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -59,6 +60,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   /**
    * Check if tests should also run Proguard when applicable.
    */
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 0a4416a..000076e 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -23,18 +23,23 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.CharStreams;
+import com.google.gson.Gson;
 import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -702,6 +707,10 @@
     return true;
   }
 
+  public static boolean isDex2OatSupported() {
+    return !isWindows();
+  }
+
   public static Path getClassPathForTests() {
     return Paths.get(BUILD_DIR, "classes", "test");
   }
@@ -1021,17 +1030,197 @@
     return runArtRaw(files, mainClass, extras, null);
   }
 
+  // Index used to name directory aimed at storing dex files and process result
+  // for one invokation of runArtRaw() in order to avoid conflicts in case of
+  // multiple calls within the same test.
+  private static int testOutputPathIndex = 0;
+
   public static ProcessResult runArtRaw(List<String> files, String mainClass,
       Consumer<ArtCommandBuilder> extras, DexVm version)
       throws IOException {
-    ArtCommandBuilder builder =
-        version != null ? new ArtCommandBuilder(version) : new ArtCommandBuilder();
-    files.forEach(builder::appendClasspath);
-    builder.setMainClass(mainClass);
-    if (extras != null) {
-      extras.accept(builder);
+
+      ArtCommandBuilder builder =
+          version != null ? new ArtCommandBuilder(version) : new ArtCommandBuilder();
+      files.forEach(builder::appendClasspath);
+      builder.setMainClass(mainClass);
+      if (extras != null) {
+        extras.accept(builder);
+      }
+
+      ProcessResult processResult = null;
+
+      // Whenever we start a new test method we reset the index count.
+      String reset_output_index = System.getProperty("reset_output_index");
+      if (reset_output_index != null) {
+        System.clearProperty("reset_output_index");
+        testOutputPathIndex = 0;
+      } else {
+        assert testOutputPathIndex >= 0;
+        testOutputPathIndex++;
+      }
+
+      String goldenFilesDirInProp = System.getProperty("use_golden_files_in");
+      if (goldenFilesDirInProp != null) {
+        File goldenFileDir = new File(goldenFilesDirInProp);
+        assert goldenFileDir.isDirectory();
+        processResult = compareAgainstGoldenFiles(
+            files.stream().map(f -> new File(f)).collect(Collectors.toList()), goldenFileDir);
+        if (processResult.exitCode == 0) {
+          processResult = readProcessResult(goldenFileDir);
+        }
+      } else {
+        processResult = runArtProcessRaw(builder);
+      }
+
+      String goldenFilesDirToProp = System.getProperty("generate_golden_files_to");
+      if (goldenFilesDirToProp != null) {
+        File goldenFileDir = new File(goldenFilesDirToProp);
+        assert goldenFileDir.isDirectory();
+        storeAsGoldenFiles(files.stream().map(f -> new File(f)).collect(Collectors.toList()),
+            goldenFileDir);
+        storeProcessResult(processResult, goldenFileDir);
+      }
+
+      return processResult;
+  }
+
+  private static Path findNonConflictingDestinationFilePath(Path testOutputPath) {
+    int index = 0;
+    Path destFilePath;
+    do {
+      destFilePath = Paths.get(testOutputPath.toString(),
+          "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION);
+      index++;
+    } while (destFilePath.toFile().exists());
+
+    return destFilePath;
+  }
+
+  private static Path getTestOutputPath(File destDir) throws IOException {
+    assert destDir.exists();
+    assert destDir.isDirectory();
+
+    String testClassName = System.getProperty("test_class_name");
+    String testName = System.getProperty("test_name");
+    String headSha1 = System.getProperty("test_git_HEAD_sha1");
+
+    assert testClassName != null;
+    assert testName != null;
+    assert headSha1 != null;
+
+    return Files.createDirectories(
+         Paths.get(destDir.getAbsolutePath(), headSha1, testClassName, testName + "-" + String
+             .format("%03d", testOutputPathIndex)));
+  }
+
+  private static List<File> unzipDexFilesArchive(File zipFile) throws IOException {
+    File tmpDir = Files.createTempDirectory("r8-test-").toFile();
+    tmpDir.deleteOnExit();
+    return ZipUtils.unzip(zipFile.getAbsolutePath(), tmpDir);
+  }
+
+  private static void storeAsGoldenFiles(List<File> files, File destDir) throws IOException {
+    Path testOutputPath = getTestOutputPath(destDir);
+
+    for (File f : files) {
+      Path filePath = f.toPath();
+      // TODO(jmhenaff): Check it's been produced by D8/R8?
+      List<File> testFiles = Collections.singletonList(f);
+      if (FileUtils.isArchive(filePath)) {
+        testFiles = unzipDexFilesArchive(f);
+      }
+      for (File testFile : testFiles) {
+        Path testFilePath = testFile.toPath();
+        if (FileUtils.isDexFile(testFilePath)) {
+          Path destFile = findNonConflictingDestinationFilePath(testOutputPath);
+          FileUtils.writeToFile(destFile, null, Files.readAllBytes(testFilePath));
+        }
+      }
     }
-    return runArtProcessRaw(builder);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void storeProcessResult(ProcessResult processResult, File dest)
+      throws IOException {
+    Gson gson = new Gson();
+    Path testOutputPath = getTestOutputPath(dest);
+    try (FileWriter fw = new FileWriter(new File(testOutputPath.toFile(), "processResult.json"))) {
+      gson.toJson(processResult, ProcessResult.class, fw);
+    }
+  }
+
+  private static ProcessResult readProcessResult(File dest) throws IOException {
+    File processResultFile = new File(getTestOutputPath(dest).toFile(), "processResult.json");
+    Gson gson = new Gson();
+    try (FileReader fr = new FileReader(processResultFile)) {
+      return gson.fromJson(fr, ProcessResult.class);
+    }
+  }
+
+  private static ProcessResult compareAgainstGoldenFiles(List<File> files, File destDir)
+      throws IOException {
+    Path testOutputPath = getTestOutputPath(destDir);
+
+    int index = 0;
+    String stdErr = "";
+    boolean passed = true;
+    for (File f : files) {
+      Path filePath = f.toPath();
+
+      List<File> testFiles = Collections.singletonList(f);
+      if (FileUtils.isArchive(filePath)) {
+        testFiles = unzipDexFilesArchive(f);
+      }
+
+      for (File testFile : testFiles) {
+        Path testFilePath = testFile.toPath();
+        // TODO(jmhenaff): Check it's been produced by D8/R8?
+        if (FileUtils.isDexFile(testFilePath)) {
+          File goldenFile = Paths.get(testOutputPath.toString(),
+              "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile();
+          if (!goldenFile.exists()) {
+            String fileDesc = "'" + testFile.getAbsolutePath() + "'";
+            if (FileUtils.isZipFile(filePath)) {
+              fileDesc += " (extracted from '" + f.getAbsolutePath() + "')";
+            }
+            stdErr += "Cannot find golden file '" + goldenFile.getAbsolutePath()
+                + "' to compare against test file " + fileDesc + "\n";
+            passed = false;
+          } else if (!com.google.common.io.Files.equal(testFile, goldenFile)) {
+            String fileDesc = "'" + testFile.getAbsolutePath() + "'";
+            if (FileUtils.isZipFile(filePath)) {
+              fileDesc += " (extracted from '" + f.getAbsolutePath() + "')";
+            }
+            stdErr +=
+                "File " + fileDesc + " differs from golden file '" + goldenFile.getAbsolutePath()
+                    + "'\n";
+            passed = false;
+          }
+          index++;
+        }
+      }
+    }
+    // Ensure we processed as many files as there are golden files
+    File goldenFile = Paths.get(testOutputPath.toString(),
+        "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile();
+    if (goldenFile.exists()) {
+      stdErr += "Less dex files have been produced: there is at least one more golden file ('"
+          + goldenFile.getAbsolutePath() + "'\n";
+      passed = false;
+    }
+    return new ProcessResult(passed ? 0 : -1, "", stdErr);
+  }
+
+  public static boolean dealsWithGoldenFiles() {
+    return compareAgaintsGoldenFiles() || generateGoldenFiles();
+  }
+
+  public static boolean compareAgaintsGoldenFiles() {
+    return System.getProperty("use_golden_files_in") != null;
+  }
+
+  public static boolean generateGoldenFiles() {
+    return System.getProperty("generate_golden_files_to") != null;
   }
 
   public static ProcessResult runArtNoVerificationErrorsRaw(String file, String mainClass)
@@ -1086,7 +1275,7 @@
   }
 
   private static ProcessResult runArtProcessRaw(ArtCommandBuilder builder) throws IOException {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(artSupported() || dealsWithGoldenFiles());
     ProcessResult result;
     if (builder.isForDevice()) {
       try {
@@ -1161,7 +1350,7 @@
   }
 
   public static void runDex2Oat(Path file, Path outFile, DexVm vm) throws IOException {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(ToolHelper.isDex2OatSupported());
     // TODO(jmhenaff): find a way to run this on windows (push dex and run on device/emulator?)
     Assume.assumeTrue(!ToolHelper.isWindows());
     assert Files.exists(file);
diff --git a/src/test/java/com/android/tools/r8/VmTestRunner.java b/src/test/java/com/android/tools/r8/VmTestRunner.java
index 978cbe7..a3e76a4 100644
--- a/src/test/java/com/android/tools/r8/VmTestRunner.java
+++ b/src/test/java/com/android/tools/r8/VmTestRunner.java
@@ -68,7 +68,7 @@
   @Override
   protected boolean isIgnored(FrameworkMethod child) {
     // Do not run VM tests if running VMs is not even supported.
-    if (!ToolHelper.artSupported()) {
+    if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
       return true;
     }
     if (super.isIgnored(child)) {
diff --git a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
index a24e95a..7d88649 100644
--- a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import java.nio.file.Path;
 import org.junit.Rule;
 import org.junit.Test;
@@ -25,6 +26,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Test
   public void test() throws Exception {
     ProcessResult runInput =
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
index e43ffd7..6069b4c 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -31,6 +31,7 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
+
 @RunWith(Parameterized.class)
 public class MethodHandleTestRunner extends TestBase {
   static final Class<?> CLASS = MethodHandleTest.class;
@@ -143,8 +144,8 @@
     if (runInput.exitCode != runDex.exitCode) {
       System.out.println(runDex.stderr);
     }
-    assertEquals(runInput.stdout, runDex.stdout);
     assertEquals(runInput.exitCode, runDex.exitCode);
+    assertEquals(runInput.stdout, runDex.stdout);
   }
 
   private void build(ProgramConsumer programConsumer) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 25356ae..d41ee84 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongList;
@@ -113,6 +114,9 @@
   @Rule
   public TestName testName = new TestName();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   protected static final boolean supportsDefaultMethod(DebugTestConfig config) {
     return config.isCfRuntime()
         || ToolHelper.getMinApiLevelForDexVm().getLevel() >= AndroidApiLevel.N.getLevel();
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
index d0ad1df..6f85b18 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -31,6 +32,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   static AndroidApp compileWithD8(Class... classes) throws IOException, CompilationFailedException {
     D8Command.Builder builder = D8Command.builder();
     for (Class clazz : classes) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
index 6760bdd..cb6df88 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
@@ -35,7 +35,7 @@
   }
 
   private void buildAndTest(String folder, String mainClass) throws Exception {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
 
     Path proguardRules = Paths.get(ToolHelper.EXAMPLES_DIR, folder, "keep-rules.txt");
     Path jarFile =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
index 57d694d..86c15a2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
@@ -87,4 +87,4 @@
     assertEquals(0, artOutput.exitCode);
     assertEquals(javaOutput.stdout.trim(), artOutput.stdout.trim());
   }
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java b/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
index bdedf43..3028525 100644
--- a/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
+++ b/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.io.File;
@@ -30,6 +31,7 @@
 import org.junit.Assume;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
@@ -267,6 +269,9 @@
   @ClassRule
   public static TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @BeforeClass
   public static void compileLibraries() throws Exception {
     // Selects appropriate jar according to min api level for the selected runtime.
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index c175e04..f99934e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -223,7 +223,7 @@
 
   protected void runTest(String folder, String mainClass, String extraProguardRules,
       Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
 
     String proguardRules = buildProguardRules(mainClass);
     if (extraProguardRules != null) {
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
index e2dfb6d..46a439d 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -26,6 +27,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -60,6 +62,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   public MemberRebindingTest(TestConfiguration configuration) {
     this.kind = configuration.kind;
     originalDex = configuration.getDexPath();
@@ -319,9 +324,8 @@
 
   @Test
   public void memberRebindingTest() throws IOException, InterruptedException, ExecutionException {
-    if (!ToolHelper.artSupported()) {
-      return;
-    }
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
     String out = temp.getRoot().getCanonicalPath();
     Path processed = Paths.get(out, "classes.dex");
 
diff --git a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
index 2df0612..66161e1 100644
--- a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
+++ b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -23,6 +24,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Test
   public void dataResourceTest() throws IOException, CompilationFailedException {
     String packageName = "dataresource";
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index f341969..c9488b4 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.DexInspector.FoundFieldSubject;
 import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -76,6 +77,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   protected TreeShakingTest(
       String name, String mainClass, Frontend frontend, Backend backend, MinifyMode minify) {
     this.name = name;
@@ -204,7 +208,7 @@
       }
       return;
     }
-    if (!ToolHelper.artSupported()) {
+    if (!ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
       return;
     }
     Consumer<ArtCommandBuilder> extraArtArgs = builder -> {
diff --git a/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java b/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java
new file mode 100644
index 0000000..5295c0e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * This {@link TestWatcher} passes test description information
+ * (namely test class and method names) to the test framework
+ * via system properties.
+ */
+public class TestDescriptionWatcher extends TestWatcher {
+
+  @Override
+  protected void starting(Description description) {
+    System.setProperty("test_class_name", description.getClassName());
+    System.setProperty("test_name", description.getMethodName());
+    System.setProperty("reset_output_index", "true");
+  }
+
+  @Override
+  protected void finished(Description description) {
+    System.clearProperty("test_class_name");
+    System.clearProperty("test_name");
+  }
+
+}
diff --git a/tools/download_from_x20.py b/tools/download_from_x20.py
index 01ba4e1..5f64684 100755
--- a/tools/download_from_x20.py
+++ b/tools/download_from_x20.py
@@ -19,17 +19,10 @@
 def parse_options():
   return optparse.OptionParser().parse_args()
 
-def extract_dir(filename):
-  return filename[0:len(filename) - len('.tar.gz')]
-
-def unpack_archive(filename):
-  dest_dir = extract_dir(filename)
-  if os.path.exists(dest_dir):
-    print 'Deleting existing dir %s' % dest_dir
-    shutil.rmtree(dest_dir)
-  dirname = os.path.dirname(os.path.abspath(filename))
-  with tarfile.open(filename, 'r:gz') as tar:
-    tar.extractall(path=dirname)
+def download(src, dest):
+  print 'Downloading %s to %s' % (src, dest)
+  shutil.copyfile(src, dest)
+  utils.unpack_archive(dest)
 
 def Main():
   (options, args) = parse_options()
@@ -41,20 +34,18 @@
     sha1 = input_sha.readline()
   if os.path.exists(dest) and utils.get_sha1(dest) == sha1:
     print 'sha1 matches, not downloading'
-    dest_dir = extract_dir(dest)
+    dest_dir = utils.extract_dir(dest)
     if os.path.exists(dest_dir):
       print 'destination directory exists, no extraction'
     else:
-      unpack_archive(dest)
+      utils.unpack_archive(dest)
     return
   src = os.path.join(GMSCORE_DEPS, sha1)
   if not os.path.exists(src):
     print 'File (%s) does not exist on x20' % src
     print 'Maybe pass -Pno_internal to your gradle invocation'
     return 42
-  print 'Downloading %s to %s' % (src, dest)
-  shutil.copyfile(src, dest)
-  unpack_archive(dest)
+  download(src, dest)
 
 if __name__ == '__main__':
   sys.exit(Main())
diff --git a/tools/test.py b/tools/test.py
index ed99c1d..b92c8dc 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -15,6 +15,7 @@
 import utils
 import uuid
 import notify
+import upload_to_x20
 
 
 ALL_ART_VMS = ["default", "7.0.0", "6.0.1", "5.1.1", "4.4.4", "4.0.4"]
@@ -78,6 +79,13 @@
           ' Note that the directory will not be cleared before the test.')
   result.add_option('--java_home',
       help='Use a custom java version to run tests.')
+  result.add_option('--generate_golden_files_to',
+      help='Store dex files produced by tests in the specified directory.'
+           ' It is aimed to be read on platforms with no host runtime available'
+           ' for comparison.')
+  result.add_option('--use_golden_files_in',
+      help='Download golden files hierarchy for this commit in the specified'
+           ' location and use them instead of executing on host runtime.')
 
   return result.parse_args()
 
@@ -93,7 +101,7 @@
 def Main():
   (options, args) = ParseOptions()
 
-  gradle_args = []
+  gradle_args = ['--stacktrace']
   # Set all necessary Gradle properties and options first.
   if options.verbose:
     gradle_args.append('-Pprint_test_stdout')
@@ -134,7 +142,16 @@
       os.makedirs(options.test_dir)
   if options.java_home:
     gradle_args.append('-Dorg.gradle.java.home=' + options.java_home)
-
+  if options.generate_golden_files_to:
+    gradle_args.append('-Pgenerate_golden_files_to=' + options.generate_golden_files_to)
+    if not os.path.exists(options.generate_golden_files_to):
+      os.makedirs(options.generate_golden_files_to)
+    gradle_args.append('-PHEAD_sha1=' + utils.get_HEAD_sha1())
+  if options.use_golden_files_in:
+    gradle_args.append('-Puse_golden_files_in=' + options.use_golden_files_in)
+    if not os.path.exists(options.use_golden_files_in):
+      os.makedirs(options.use_golden_files_in)
+    gradle_args.append('-PHEAD_sha1=' + utils.get_HEAD_sha1())
   # Add Gradle tasks
   gradle_args.append('cleanTest')
   gradle_args.append('test')
@@ -146,12 +163,29 @@
     # Create Jacoco report after tests.
     gradle_args.append('jacocoTestReport')
 
+  if options.use_golden_files_in:
+    sha1 = '%s' % utils.get_HEAD_sha1()
+    with utils.ChangedWorkingDirectory(options.use_golden_files_in):
+      utils.download_file_from_cloud_storage(
+                                    'gs://r8-test-results/golden-files/%s.tar.gz' % sha1,
+                                    '%s.tar.gz' % sha1)
+      utils.unpack_archive('%s.tar.gz' % sha1)
+
+
   # Now run tests on selected runtime(s).
   vms_to_test = [options.dex_vm] if options.dex_vm != "all" else ALL_ART_VMS
   for art_vm in vms_to_test:
     vm_kind_to_test = "_" + options.dex_vm_kind if art_vm != "default" else ""
     return_code = gradle.RunGradle(gradle_args + ['-Pdex_vm=%s' % (art_vm + vm_kind_to_test)],
                                    throw_on_failure=False)
+
+    if options.generate_golden_files_to:
+      sha1 = '%s' % utils.get_HEAD_sha1()
+      with utils.ChangedWorkingDirectory(options.generate_golden_files_to):
+        archive = utils.create_archive(sha1)
+        utils.upload_file_to_cloud_storage(archive,
+                                           'gs://r8-test-results/golden-files/' + archive)
+
     if return_code != 0:
       if options.archive_failures and os.name != 'nt':
         archive_failures()
diff --git a/tools/upload_to_x20.py b/tools/upload_to_x20.py
index 1efacf5..2d08362 100755
--- a/tools/upload_to_x20.py
+++ b/tools/upload_to_x20.py
@@ -20,11 +20,10 @@
 def parse_options():
   return optparse.OptionParser().parse_args()
 
-def create_archive(name):
-  tarname = '%s.tar.gz' % name
-  with tarfile.open(tarname, 'w:gz') as tar:
-    tar.add(name)
-  return tarname
+def uploadFile(filename, dest):
+  print 'Uploading to %s' % dest
+  shutil.copyfile(filename, dest)
+  subprocess.check_call(['chmod', '664', dest])
 
 def Main():
   (options, args) = parse_options()
@@ -34,12 +33,10 @@
   if not name in os.listdir('.'):
     print 'You must be standing directly below the directory you are uploading'
     return 1
-  filename = create_archive(name)
+  filename = utils.create_archive(name)
   sha1 = utils.get_sha1(filename)
   dest = os.path.join(GMSCORE_DEPS, sha1)
-  print 'Uploading to %s' % dest
-  shutil.copyfile(filename, dest)
-  subprocess.check_call(['chmod', '664', dest])
+  uploadFile(filename, dest)
   sha1_file = '%s.sha1' % filename
   with open(sha1_file, 'w') as output:
     output.write(sha1)
diff --git a/tools/utils.py b/tools/utils.py
index 3aab572..859da97 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -10,6 +10,7 @@
 import shutil
 import subprocess
 import sys
+import tarfile
 import tempfile
 
 TOOLS_DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..')))
@@ -71,6 +72,12 @@
       sha1.update(chunk)
   return sha1.hexdigest()
 
+def get_HEAD_sha1():
+  cmd = ['git', 'rev-parse', 'HEAD']
+  PrintCmd(cmd)
+  with ChangedWorkingDirectory(REPO_ROOT):
+    return subprocess.check_output(cmd).strip()
+
 def makedirs_if_needed(path):
   try:
     os.makedirs(path)
@@ -92,6 +99,29 @@
   PrintCmd(cmd)
   subprocess.check_call(cmd)
 
+def download_file_from_cloud_storage(source, destination):
+  cmd = ['gsutil.py', 'cp', source, destination]
+  PrintCmd(cmd)
+  subprocess.check_call(cmd)
+
+def create_archive(name):
+  tarname = '%s.tar.gz' % name
+  with tarfile.open(tarname, 'w:gz') as tar:
+    tar.add(name)
+  return tarname
+
+def extract_dir(filename):
+  return filename[0:len(filename) - len('.tar.gz')]
+
+def unpack_archive(filename):
+  dest_dir = extract_dir(filename)
+  if os.path.exists(dest_dir):
+    print 'Deleting existing dir %s' % dest_dir
+    shutil.rmtree(dest_dir)
+  dirname = os.path.dirname(os.path.abspath(filename))
+  with tarfile.open(filename, 'r:gz') as tar:
+    tar.extractall(path=dirname)
+
 # Note that gcs is eventually consistent with regards to list operations.
 # This is not a problem in our case, but don't ever use this method
 # for synchronization.