Add caching of kotlin compilations in tests

This extends the command cache with output caching.

Only succeeding kotlin compilations are cached.

Change-Id: I03ae1fce9ed82a3af33a407a6d1ca868aa428d49
diff --git a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
index 642e319..63616f3 100644
--- a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -12,13 +12,20 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.ToolHelper.CacheLookupKey;
+import com.android.tools.r8.ToolHelper.CommandResultCache;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.structural.Ordered;
+import com.google.common.hash.Hasher;
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -339,7 +346,45 @@
   }
 
   private ProcessResult compileInternal(Path output) throws IOException {
-    List<String> cmdline = new ArrayList<>();
+    CommandLineAndHasherConsumers commandLineAndHasherConsumers =
+        buildCommandLineAndHasherConsumers(output);
+    CacheLookupKey cacheLookupKey = null;
+    if (CommandResultCache.isEnabled()) {
+      cacheLookupKey =
+          new CacheLookupKey(
+              hasher ->
+                  commandLineAndHasherConsumers.hasherConsumers.forEach(
+                      hasherConsumer -> hasherConsumer.acceptWithRuntimeException(hasher)));
+      Pair<ProcessResult, Path> lookupResult =
+          CommandResultCache.getInstance().lookup(cacheLookupKey);
+      if (lookupResult != null
+          && lookupResult.getFirst().exitCode == 0
+          && lookupResult.getSecond() != null) {
+        Files.copy(lookupResult.getSecond(), output);
+        return lookupResult.getFirst();
+      }
+    }
+    ProcessBuilder builder = new ProcessBuilder(commandLineAndHasherConsumers.cmdline);
+    if (ToolHelper.isNewGradleSetup()) {
+      builder.directory(new File(ToolHelper.getProjectRoot()));
+    }
+    ProcessResult processResult = ToolHelper.runProcess(builder);
+    if (CommandResultCache.isEnabled()) {
+      CommandResultCache.getInstance().putResult(processResult, cacheLookupKey, output);
+    }
+    return processResult;
+  }
+
+  public static class CommandLineAndHasherConsumers {
+    final List<String> cmdline = new ArrayList<>();
+    final List<ThrowingConsumer<Hasher, IOException>> hasherConsumers = new ArrayList<>();
+  }
+
+  private CommandLineAndHasherConsumers buildCommandLineAndHasherConsumers(Path output)
+      throws IOException {
+    CommandLineAndHasherConsumers commandLineAndHasherConsumers =
+        new CommandLineAndHasherConsumers();
+    List<String> cmdline = commandLineAndHasherConsumers.cmdline;
     cmdline.add(jdk.getJavaExecutable().toString());
     if (enableAssertions) {
       cmdline.add("-ea");
@@ -354,8 +399,15 @@
     cmdline.add(jdk.getJavaHome().toString());
     cmdline.add("-jvm-target");
     cmdline.add(targetVersion.getJvmTargetString());
+    // Until now this is just command line files, no inputs, hash existing command
+    String noneFileCommandLineArguments = StringUtils.join("", cmdline);
+    commandLineAndHasherConsumers.hasherConsumers.add(
+        hasher -> hasher.putString(noneFileCommandLineArguments, StandardCharsets.UTF_8));
+
     for (Path source : sources) {
       cmdline.add(source.toString());
+      commandLineAndHasherConsumers.hasherConsumers.add(
+          hasher -> hasher.putBytes(Files.readAllBytes(source)));
     }
     cmdline.add("-d");
     cmdline.add(output.toString());
@@ -365,12 +417,19 @@
           .stream()
           .map(Path::toString)
           .collect(Collectors.joining(isWindows() ? ";" : ":")));
+      for (Path path : classpath) {
+        commandLineAndHasherConsumers.hasherConsumers.add(
+            hasher -> {
+              hasher.putString("--cp", StandardCharsets.UTF_8);
+              hasher.putBytes(Files.readAllBytes(path));
+            });
+      }
     }
     cmdline.addAll(additionalArguments);
-    ProcessBuilder builder = new ProcessBuilder(cmdline);
-    if (ToolHelper.isNewGradleSetup()) {
-      builder.directory(new File(ToolHelper.getProjectRoot()));
-    }
-    return ToolHelper.runProcess(builder);
+    commandLineAndHasherConsumers.hasherConsumers.add(
+        hasher -> additionalArguments.forEach(s -> hasher.putString(s, StandardCharsets.UTF_8)));
+    return commandLineAndHasherConsumers;
   }
+
+
 }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 8f31142..270396d 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -39,6 +39,7 @@
 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.Pair;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
@@ -591,7 +592,7 @@
 
     private DexVm version;
     private boolean withArtFrameworks;
-    private ArtResultCacheLookupKey artResultCacheLookupKey;
+    private CacheLookupKey artResultCacheLookupKey;
     private boolean noCaching = false;
 
     public ArtCommandBuilder() {
@@ -646,7 +647,7 @@
     }
 
     private boolean useCache() {
-      return !noCaching && CommandResultCache.getInstance() != null;
+      return !noCaching && CommandResultCache.isEnabled();
     }
 
     public void cacheResult(ProcessResult result) {
@@ -654,18 +655,20 @@
       // put invalid entries into the cache.
       if (useCache() && result.exitCode == 0) {
         assert artResultCacheLookupKey != null;
-        CommandResultCache.getInstance().putResult(result, artResultCacheLookupKey);
+        CommandResultCache.getInstance().putResult(result, artResultCacheLookupKey, null);
       }
     }
 
-    public ProcessResult getCachedResults() {
+    public ProcessResult getCachedResults() throws IOException {
       if (!useCache()) {
         return null;
       }
       assert artResultCacheLookupKey == null;
       // Reuse the key when storing results if this is not already cached.
-      artResultCacheLookupKey = new ArtResultCacheLookupKey(this::hashParts);
-      return CommandResultCache.getInstance().lookup(artResultCacheLookupKey);
+      artResultCacheLookupKey = new CacheLookupKey(this::hashParts);
+      Pair<ProcessResult, Path> lookup =
+          CommandResultCache.getInstance().lookup(artResultCacheLookupKey);
+      return lookup == null ? null : lookup.getFirst();
     }
 
     private void hashParts(Hasher hasher) {
@@ -696,11 +699,11 @@
     }
   }
 
-  private static class ArtResultCacheLookupKey {
+  public static class CacheLookupKey {
     private final Consumer<Hasher> hasherConsumer;
     private String hash;
 
-    public ArtResultCacheLookupKey(Consumer<Hasher> hasherConsumer) {
+    public CacheLookupKey(Consumer<Hasher> hasherConsumer) {
       this.hasherConsumer = hasherConsumer;
     }
 
@@ -714,7 +717,7 @@
     }
   }
 
-  private static class CommandResultCache {
+  public static class CommandResultCache {
     private static CommandResultCache INSTANCE =
         System.getProperty("command_cache_dir") != null
             ? new CommandResultCache(Paths.get(System.getProperty("command_cache_dir")))
@@ -730,16 +733,24 @@
       return INSTANCE;
     }
 
-    private Path getStdoutFile(ArtResultCacheLookupKey artResultCacheLookupKey) {
-      return path.resolve(artResultCacheLookupKey.getHash() + ".stdout");
+    public static boolean isEnabled() {
+      return getInstance() != null;
     }
 
-    private Path getStderrFile(ArtResultCacheLookupKey artResultCacheLookupKey) {
-      return path.resolve(artResultCacheLookupKey.getHash() + ".stderr");
+    private Path getStdoutFile(CacheLookupKey cacheLookupKey) {
+      return path.resolve(cacheLookupKey.getHash() + ".stdout");
     }
 
-    private Path getExitCodeFile(ArtResultCacheLookupKey artResultCacheLookupKey) {
-      return path.resolve(artResultCacheLookupKey.getHash());
+    private Path getStderrFile(CacheLookupKey cacheLookupKey) {
+      return path.resolve(cacheLookupKey.getHash() + ".stderr");
+    }
+
+    private Path getOutputFile(CacheLookupKey cacheLookupKey) {
+      return path.resolve(cacheLookupKey.getHash() + ".output");
+    }
+
+    private Path getExitCodeFile(CacheLookupKey cacheLookupKey) {
+      return path.resolve(cacheLookupKey.getHash());
     }
 
     private Path getTempFile(Path path) {
@@ -758,38 +769,47 @@
       return "";
     }
 
-    public ProcessResult lookup(ArtResultCacheLookupKey artResultCacheLookupKey) {
+    public Pair<ProcessResult, Path> lookup(CacheLookupKey cacheLookupKey) {
       // TODO Add concurrency handling!
-      Path exitCodeFile = getExitCodeFile(artResultCacheLookupKey);
+      Path exitCodeFile = getExitCodeFile(cacheLookupKey);
       if (exitCodeFile.toFile().exists()) {
         int exitCode = Integer.parseInt(getStringContent(exitCodeFile));
         // Because of the temp files and order of writing we should never get here with an
         // inconsistent state. It is possible, although unlikely, that the stdout/stderr
         // (and even exitcode if art is non deterministic) are from different, process ids etc,
         // but this should have no impact.
-        return new ProcessResult(
-            exitCode,
-            getStringContent(getStdoutFile(artResultCacheLookupKey)),
-            getStringContent(getStderrFile(artResultCacheLookupKey)));
+
+        Path outputFile = getOutputFile(cacheLookupKey);
+        return new Pair(
+            new ProcessResult(
+                exitCode,
+                getStringContent(getStdoutFile(cacheLookupKey)),
+                getStringContent(getStderrFile(cacheLookupKey))),
+            outputFile.toFile().exists() ? outputFile : null);
       }
       return null;
     }
 
-    public void putResult(ProcessResult result, ArtResultCacheLookupKey artResultCacheLookupKey) {
+    public void putResult(ProcessResult result, CacheLookupKey cacheLookupKey, Path output) {
       try {
         String exitCode = "" + result.exitCode;
         // We avoid race conditions of writing vs reading by first writing all 3 files to temp
         // files, then moving these to the result files, moving last the exitcode file (which is
         // what we use as cache present check)
-        Path exitCodeFile = getExitCodeFile(artResultCacheLookupKey);
+        Path exitCodeFile = getExitCodeFile(cacheLookupKey);
         Path exitCodeTempFile = getTempFile(exitCodeFile);
-        Path stdoutFile = getStdoutFile(artResultCacheLookupKey);
+        Path stdoutFile = getStdoutFile(cacheLookupKey);
         Path stdoutTempFile = getTempFile(stdoutFile);
-        Path stderrFile = getStderrFile(artResultCacheLookupKey);
+        Path stderrFile = getStderrFile(cacheLookupKey);
         Path stderrTempFile = getTempFile(stderrFile);
+        Path outputfile = getOutputFile(cacheLookupKey);
+        Path outputTempFile = getTempFile(outputfile);
         Files.write(exitCodeTempFile, exitCode.getBytes(StandardCharsets.UTF_8));
         Files.write(stdoutTempFile, result.stdout.getBytes(StandardCharsets.UTF_8));
         Files.write(stderrTempFile, result.stderr.getBytes(StandardCharsets.UTF_8));
+        if (output != null) {
+          Files.copy(output, outputTempFile);
+        }
         // Order is important, move exitcode file last!
         Files.move(
             stdoutTempFile,
@@ -801,6 +821,13 @@
             stderrFile,
             StandardCopyOption.ATOMIC_MOVE,
             StandardCopyOption.REPLACE_EXISTING);
+        if (output != null) {
+          Files.move(
+              outputTempFile,
+              outputfile,
+              StandardCopyOption.ATOMIC_MOVE,
+              StandardCopyOption.REPLACE_EXISTING);
+        }
         Files.move(
             exitCodeTempFile,
             exitCodeFile,