Merge "Remove useless conversion between string and type"
diff --git a/src/main/java/com/android/tools/r8/errors/Unreachable.java b/src/main/java/com/android/tools/r8/errors/Unreachable.java
index 4b17eba..0eb71d2 100644
--- a/src/main/java/com/android/tools/r8/errors/Unreachable.java
+++ b/src/main/java/com/android/tools/r8/errors/Unreachable.java
@@ -14,4 +14,8 @@
   public Unreachable(String s) {
     super(s);
   }
+
+  public Unreachable(Throwable cause) {
+    super(cause);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index d6e1a2d..ff0e4df 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.desugar;
 
 import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexCallSite;
@@ -21,9 +22,11 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeSuper;
 import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.Sets;
@@ -62,6 +65,7 @@
   // Public for testing.
   public static final String COMPANION_CLASS_NAME_SUFFIX = "-CC";
   private static final String DEFAULT_METHOD_PREFIX = "$default$";
+  private static final String PRIVATE_METHOD_PREFIX = "$private$";
 
   private final IRConverter converter;
   private final InternalOptions options;
@@ -156,7 +160,7 @@
             // exception but we can not report it as error since it can also be the intended
             // behavior.
             warnMissingType(encodedMethod.method, method.holder);
-          } else if (clazz != null && (clazz.isInterface() && !clazz.isLibraryClass())) {
+          } else if (clazz.isInterface() && !clazz.isLibraryClass()) {
             // NOTE: we intentionally don't desugar super calls into interface methods
             // coming from android.jar since it is only possible in case v24+ version
             // of android.jar is provided.
@@ -166,6 +170,35 @@
                 new InvokeStatic(defaultAsMethodOfCompanionClass(method),
                     invokeSuper.outValue(), invokeSuper.arguments()));
           }
+          continue;
+        }
+
+        if (instruction.isInvokeDirect()) {
+          InvokeDirect invokeDirect = instruction.asInvokeDirect();
+          DexMethod method = invokeDirect.getInvokedMethod();
+          if (factory.isConstructor(method)) {
+            continue;
+          }
+
+          // This is a private instance method call. Note that the referenced method
+          // is expected to be in the current class since it is private, but desugaring
+          // may move some methods or their code into other classes.
+          DexClass clazz = findDefinitionFor(method.holder);
+          if (clazz == null) {
+            // Report missing class since we don't know if it is an interface.
+            warnMissingType(encodedMethod.method, method.holder);
+
+          } else if (clazz.isInterface()) {
+            if (clazz.isLibraryClass()) {
+              throw new CompilationError("Unexpected call to a private method " +
+                  "defined in library class " + clazz.toSourceString(),
+                  getMethodOrigin(encodedMethod.method));
+
+            }
+            instructions.replaceCurrentInstruction(
+                new InvokeStatic(privateAsMethodOfCompanionClass(method),
+                    invokeDirect.outValue(), invokeDirect.arguments()));
+          }
         }
       }
     }
@@ -204,6 +237,20 @@
     return factory.createType(ccTypeDescriptor);
   }
 
+  // Checks if `type` is a companion class.
+  private boolean isCompanionClassType(DexType type) {
+    return type.descriptor.toString().endsWith(COMPANION_CLASS_NAME_SUFFIX + ";");
+  }
+
+  // Gets the interface class for a companion class `type`.
+  private DexType getInterfaceClassType(DexType type) {
+    assert isCompanionClassType(type);
+    String descriptor = type.descriptor.toString();
+    String interfaceTypeDescriptor = descriptor.substring(0,
+        descriptor.length() - 1 - COMPANION_CLASS_NAME_SUFFIX.length()) + ";";
+    return factory.createType(interfaceTypeDescriptor);
+  }
+
   private boolean isInMainDexList(DexType iface) {
     return converter.appInfo.isInMainDexList(iface);
   }
@@ -214,8 +261,7 @@
     return factory.createMethod(getCompanionClassType(method.holder), method.proto, method.name);
   }
 
-  // Represent a default interface method as a method of companion class.
-  final DexMethod defaultAsMethodOfCompanionClass(DexMethod method) {
+  private DexMethod instanceAsMethodOfCompanionClass(DexMethod method, String prefix) {
     // Add an implicit argument to represent the receiver.
     DexType[] params = method.proto.parameters.values;
     DexType[] newParams = new DexType[params.length + 1];
@@ -225,7 +271,18 @@
     // Add prefix to avoid name conflicts.
     return factory.createMethod(getCompanionClassType(method.holder),
         factory.createProto(method.proto.returnType, newParams),
-        factory.createString(DEFAULT_METHOD_PREFIX + method.name.toString()));
+        factory.createString(prefix + method.name.toString()));
+  }
+
+  // Represent a default interface method as a method of companion class.
+  final DexMethod defaultAsMethodOfCompanionClass(DexMethod method) {
+    return instanceAsMethodOfCompanionClass(method, DEFAULT_METHOD_PREFIX);
+  }
+
+  // Represent a private instance interface method as a method of companion class.
+  final DexMethod privateAsMethodOfCompanionClass(DexMethod method) {
+    // Add an implicit argument to represent the receiver.
+    return instanceAsMethodOfCompanionClass(method, PRIVATE_METHOD_PREFIX);
   }
 
   /**
@@ -337,8 +394,16 @@
         .append("it is required for default or static interface methods desugaring of `")
         .append(referencedFrom.toSourceString())
         .append("`");
-    DexClass referencedFromClass = converter.appInfo.definitionFor(referencedFrom.getHolder());
     options.reporter.warning(
-        new StringDiagnostic(builder.toString(), referencedFromClass.getOrigin()));
+        new StringDiagnostic(builder.toString(), getMethodOrigin(referencedFrom)));
+  }
+
+  private Origin getMethodOrigin(DexMethod method) {
+    DexType holder = method.getHolder();
+    if (isCompanionClassType(holder)) {
+      holder = getInterfaceClassType(holder);
+    }
+    DexClass clazz = converter.appInfo.definitionFor(holder);
+    return clazz == null ? Origin.unknown() : clazz.getOrigin();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
index 3d162c7..8c17d91 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
@@ -55,7 +55,7 @@
         Code code = virtual.getCode();
         if (code == null) {
           throw new CompilationError("Code is missing for default "
-              + "interface method: " + virtual.method.toSourceString());
+              + "interface method: " + virtual.method.toSourceString(), iface.origin);
         }
 
         MethodAccessFlags newFlags = virtual.accessFlags.copy();
@@ -88,24 +88,53 @@
     }
     remainingMethods.clear();
 
-    // Process static methods, move them into companion class as well.
+    // Process static and private methods, move them into companion class as well,
+    // make private instance methods static.
     for (DexEncodedMethod direct : iface.directMethods()) {
-      if (direct.accessFlags.isPrivate()) {
-        // We only expect to see private methods which are lambda$ methods,
-        // and they are supposed to be relaxed to package private static methods
-        // by this time by lambda rewriter.
-        throw new Unimplemented("Private method are not yet supported.");
+      MethodAccessFlags originalFlags = direct.accessFlags;
+      MethodAccessFlags newFlags = originalFlags.copy();
+      if (originalFlags.isPrivate()) {
+        newFlags.unsetPrivate();
+        newFlags.setPublic();
       }
 
       if (isStaticMethod(direct)) {
+        assert originalFlags.isPrivate() || originalFlags.isPublic()
+            : "Static interface method " + direct.toSourceString() + " is expected to "
+            + "either be public or private in " + iface.origin;
         companionMethods.add(new DexEncodedMethod(
-            rewriter.staticAsMethodOfCompanionClass(direct.method), direct.accessFlags,
+            rewriter.staticAsMethodOfCompanionClass(direct.method), newFlags,
             direct.annotations, direct.parameterAnnotations, direct.getCode()));
+
       } else {
-        // Since there are no interface constructors at this point,
-        // this should only be class constructor.
-        assert rewriter.factory.isClassConstructor(direct.method);
-        remainingMethods.add(direct);
+        if (originalFlags.isPrivate()) {
+          assert !rewriter.factory.isClassConstructor(direct.method)
+              : "Unexpected private constructor " + direct.toSourceString()
+              + " in " + iface.origin;
+          newFlags.setStatic();
+
+          DexMethod companionMethod = rewriter.privateAsMethodOfCompanionClass(direct.method);
+
+          Code code = direct.getCode();
+          if (code == null) {
+            throw new CompilationError("Code is missing for private instance "
+                + "interface method: " + direct.method.toSourceString(), iface.origin);
+          }
+          DexCode dexCode = code.asDexCode();
+          // TODO(ager): Should we give the new first parameter an actual name? Maybe 'this'?
+          dexCode.setDebugInfo(dexCode.debugInfoWithAdditionalFirstParameter(null));
+          assert (dexCode.getDebugInfo() == null)
+              || (companionMethod.getArity() == dexCode.getDebugInfo().parameters.length);
+
+          companionMethods.add(new DexEncodedMethod(companionMethod,
+              newFlags, direct.annotations, direct.parameterAnnotations, code));
+
+        } else {
+          // Since there are no interface constructors at this point,
+          // this should only be class constructor.
+          assert rewriter.factory.isClassConstructor(direct.method);
+          remainingMethods.add(direct);
+        }
       }
     }
     if (remainingMethods.size() < iface.directMethods().length) {
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNaming.java b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
index 6fb166f..711672c 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.utils.DescriptorUtils.javaTypeToDescriptor;
 
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -112,7 +113,8 @@
         write(writer);
         return writer.toString();
       } catch (IOException e) {
-        return e.toString();
+        // StringWriter is not throwing IOException
+        throw new Unreachable(e);
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java b/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java
index 5d34ca4..3ae9d06 100644
--- a/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java
+++ b/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java
@@ -14,11 +14,6 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.google.common.collect.Sets;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -27,6 +22,7 @@
 
 public class MinifiedNameMapPrinter {
 
+  private static final String NEW_LINE = "\n";
   private final DexApplication application;
   private final NamingLens namingLens;
   private final Set<DexType> seenTypes = Sets.newIdentityHashSet();
@@ -42,13 +38,13 @@
     return copy;
   }
 
-  private void writeClass(DexProgramClass clazz, PrintStream out) {
+  private void writeClass(DexProgramClass clazz, StringBuilder out) {
     seenTypes.add(clazz.type);
     DexString descriptor = namingLens.lookupDescriptor(clazz.type);
-    out.print(DescriptorUtils.descriptorToJavaType(clazz.type.descriptor.toSourceString()));
-    out.print(" -> ");
-    out.print(DescriptorUtils.descriptorToJavaType(descriptor.toSourceString()));
-    out.println(":");
+    out.append(DescriptorUtils.descriptorToJavaType(clazz.type.descriptor.toSourceString()));
+    out.append(" -> ");
+    out.append(DescriptorUtils.descriptorToJavaType(descriptor.toSourceString()));
+    out.append(":").append(NEW_LINE);
     writeFields(sortedCopy(
         clazz.instanceFields(), Comparator.comparing(DexEncodedField::toSourceString)), out);
     writeFields(sortedCopy(
@@ -59,39 +55,39 @@
         clazz.virtualMethods(), Comparator.comparing(DexEncodedMethod::toSourceString)), out);
   }
 
-  private void writeType(DexType type, PrintStream out) {
+  private void writeType(DexType type, StringBuilder out) {
     if (type.isClassType() && seenTypes.add(type)) {
       DexString descriptor = namingLens.lookupDescriptor(type);
-      out.print(DescriptorUtils.descriptorToJavaType(type.descriptor.toSourceString()));
-      out.print(" -> ");
-      out.print(DescriptorUtils.descriptorToJavaType(descriptor.toSourceString()));
-      out.println(":");
+      out.append(DescriptorUtils.descriptorToJavaType(type.descriptor.toSourceString()));
+      out.append(" -> ");
+      out.append(DescriptorUtils.descriptorToJavaType(descriptor.toSourceString()));
+      out.append(":").append(NEW_LINE);
     }
   }
 
-  private void writeFields(DexEncodedField[] fields, PrintStream out) {
+  private void writeFields(DexEncodedField[] fields, StringBuilder out) {
     for (DexEncodedField encodedField : fields) {
       DexField field = encodedField.field;
       DexString renamed = namingLens.lookupName(field);
       if (renamed != field.name) {
-        out.print("    ");
-        out.print(field.type.toSourceString());
-        out.print(" ");
-        out.print(field.name.toSourceString());
-        out.print(" -> ");
-        out.println(renamed.toSourceString());
+        out.append("    ");
+        out.append(field.type.toSourceString());
+        out.append(" ");
+        out.append(field.name.toSourceString());
+        out.append(" -> ");
+        out.append(renamed.toSourceString()).append(NEW_LINE);
       }
     }
   }
 
-  private void writeMethod(MethodSignature signature, String renamed, PrintStream out) {
-    out.print("    ");
-    out.print(signature);
-    out.print(" -> ");
-    out.println(renamed);
+  private void writeMethod(MethodSignature signature, String renamed, StringBuilder out) {
+    out.append("    ");
+    out.append(signature.toString());
+    out.append(" -> ");
+    out.append(renamed).append(NEW_LINE);
   }
 
-  private void writeMethods(DexEncodedMethod[] methods, PrintStream out) {
+  private void writeMethods(DexEncodedMethod[] methods, StringBuilder out) {
     for (DexEncodedMethod encodedMethod : methods) {
       DexMethod method = encodedMethod.method;
       DexString renamed = namingLens.lookupName(method);
@@ -103,7 +99,7 @@
     }
   }
 
-  public void write(PrintStream out) {
+  public void write(StringBuilder out) {
     // First write out all classes that have been renamed.
     List<DexProgramClass> classes = new ArrayList<>(application.classes());
     classes.sort(Comparator.comparing(DexProgramClass::toSourceString));
@@ -111,11 +107,4 @@
     // Now write out all types only mentioned in descriptors that have been renamed.
     namingLens.forAllRenamedTypes(type -> writeType(type, out));
   }
-
-  public void write(Path destination) throws IOException {
-    PrintStream out = new PrintStream(Files.newOutputStream(destination), true,
-        StandardCharsets.UTF_8.name());
-    write(out);
-  }
-
 }
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
index f394e76..3863c14 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
@@ -6,7 +6,6 @@
 import com.android.tools.r8.graph.DexApplication;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.io.Writer;
 
@@ -47,11 +46,9 @@
     assert namingLens != null && application != null;
     // TODO(herhut): Should writing of the proguard-map file be split like this?
     if (!namingLens.isIdentityLens()) {
-      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-      PrintStream stream = new PrintStream(bytes);
-      new MinifiedNameMapPrinter(application, namingLens).write(stream);
-      stream.flush();
-      return bytes.toString();
+      StringBuilder map = new StringBuilder();
+      new MinifiedNameMapPrinter(application, namingLens).write(map);
+      return map.toString();
     }
     if (application.getProguardMap() != null) {
       ByteArrayOutputStream bytes = new ByteArrayOutputStream();
diff --git a/src/test/examplesJava9/privateinterfacemethods/C.class b/src/test/examplesJava9/privateinterfacemethods/C.class
new file mode 100644
index 0000000..430b6c1
--- /dev/null
+++ b/src/test/examplesJava9/privateinterfacemethods/C.class
Binary files differ
diff --git a/src/test/examplesJava9/privateinterfacemethods/I$1.class b/src/test/examplesJava9/privateinterfacemethods/I$1.class
new file mode 100644
index 0000000..29644e6
--- /dev/null
+++ b/src/test/examplesJava9/privateinterfacemethods/I$1.class
Binary files differ
diff --git a/src/test/examplesJava9/privateinterfacemethods/I$2.class b/src/test/examplesJava9/privateinterfacemethods/I$2.class
new file mode 100644
index 0000000..243b0af
--- /dev/null
+++ b/src/test/examplesJava9/privateinterfacemethods/I$2.class
Binary files differ
diff --git a/src/test/examplesJava9/privateinterfacemethods/I.class b/src/test/examplesJava9/privateinterfacemethods/I.class
new file mode 100644
index 0000000..893ed9e
--- /dev/null
+++ b/src/test/examplesJava9/privateinterfacemethods/I.class
Binary files differ
diff --git a/src/test/examplesJava9/privateinterfacemethods/IB.class b/src/test/examplesJava9/privateinterfacemethods/IB.class
new file mode 100644
index 0000000..e670d08
--- /dev/null
+++ b/src/test/examplesJava9/privateinterfacemethods/IB.class
Binary files differ
diff --git a/src/test/examplesJava9/privateinterfacemethods/PrivateInterfaceMethods.class b/src/test/examplesJava9/privateinterfacemethods/PrivateInterfaceMethods.class
new file mode 100644
index 0000000..dd3615b
--- /dev/null
+++ b/src/test/examplesJava9/privateinterfacemethods/PrivateInterfaceMethods.class
Binary files differ
diff --git a/src/test/examplesJava9/privateinterfacemethods/PrivateInterfaceMethods.java b/src/test/examplesJava9/privateinterfacemethods/PrivateInterfaceMethods.java
new file mode 100644
index 0000000..a133a7b
--- /dev/null
+++ b/src/test/examplesJava9/privateinterfacemethods/PrivateInterfaceMethods.java
@@ -0,0 +1,59 @@
+// Copyright (c) 2017, 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 privateinterfacemethods;
+
+public class PrivateInterfaceMethods {
+
+  public static void main(String[] args) {
+    System.out.println("1: " + I.DEFAULT.dFoo());
+    System.out.println("2: " + I.DEFAULT.lFoo());
+    System.out.println("3: " + I.xFoo());
+    System.out.println("4: " + new C().dFoo());
+  }
+}
+
+class C implements I {
+
+  public String dFoo() {
+    return "c>" + I.super.dFoo();
+  }
+}
+
+interface IB {
+
+  String dFoo();
+}
+
+interface I {
+
+  I DEFAULT = new I() {{
+    System.out.println("0: " + sFoo(false, this));
+  }};
+
+  static String xFoo() {
+    return "x>" + sFoo(true, null);
+  }
+
+  private static String sFoo(boolean simple, I it) {
+    return simple ? "s"
+        : ("s>" + it.iFoo(true) + ">" + new I() {
+          public String dFoo() {
+            return "a";
+          }
+        }.dFoo());
+  }
+
+  private String iFoo(boolean skip) {
+    return skip ? "i" : ("i>" + sFoo(false, this));
+  }
+
+  default String dFoo() {
+    return "d>" + iFoo(false);
+  }
+
+  default String lFoo() {
+    IB ib = () -> "l>" + iFoo(false);
+    return ib.dFoo();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index afa0f67..9d41d4e 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -173,14 +173,17 @@
   private static Map<DexVm.Version, List<String>> failsOn =
       ImmutableMap.of(
           DexVm.Version.V4_4_4, ImmutableList.of(
+              "native-private-interface-methods",
               // Dex version not supported
               "varhandle"
           ),
           DexVm.Version.V5_1_1, ImmutableList.of(
+              "native-private-interface-methods",
               // Dex version not supported
               "varhandle"
           ),
           DexVm.Version.V6_0_1, ImmutableList.of(
+              "native-private-interface-methods",
               // Dex version not supported
               "varhandle"
           ),
@@ -194,6 +197,21 @@
           )
       );
 
+  // Defines methods failing on JVM, specifies the output to be used for comparison.
+  private static Map<String, String> expectedJvmResult =
+      ImmutableMap.of(
+          "native-private-interface-methods", "0: s>i>a\n"
+              + "1: d>i>s>i>a\n"
+              + "2: l>i>s>i>a\n"
+              + "3: x>s\n"
+              + "4: c>d>i>s>i>a\n",
+          "desugared-private-interface-methods", "0: s>i>a\n"
+              + "1: d>i>s>i>a\n"
+              + "2: l>i>s>i>a\n"
+              + "3: x>s\n"
+              + "4: c>d>i>s>i>a\n"
+      );
+
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
@@ -215,6 +233,21 @@
   }
 
   @Test
+  public void nativePrivateInterfaceMethods() throws Throwable {
+    test("native-private-interface-methods", "privateinterfacemethods", "PrivateInterfaceMethods")
+        .withMinApiLevel(AndroidApiLevel.N.getLevel())
+        .run();
+  }
+
+  @Test
+  public void desugaredPrivateInterfaceMethods() throws Throwable {
+    test("desugared-private-interface-methods", "privateinterfacemethods",
+        "PrivateInterfaceMethods")
+        .withMinApiLevel(AndroidApiLevel.M.getLevel())
+        .run();
+  }
+
+  @Test
   public void varHAndle() throws Throwable {
     test("varhandle", "varhandle", "VarHandleTests")
         .withMinApiLevel(AndroidApiLevel.P.getLevel())
@@ -244,16 +277,23 @@
         Arrays.stream(dexes).map(path -> path.toString()).collect(Collectors.toList()),
         qualifiedMainClass,
         null);
-    if (!expectedToFail) {
+    String jvmResult = null;
+    if (expectedJvmResult.containsKey(testName)) {
+      jvmResult = expectedJvmResult.get(testName);
+    } else if (!expectedToFail) {
       ToolHelper.ProcessResult javaResult =
           ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
       assertEquals("JVM run failed", javaResult.exitCode, 0);
+      jvmResult = javaResult.stdout;
+    }
+
+    if (jvmResult != null) {
       assertTrue(
           "JVM output does not match art output.\n\tjvm: "
-              + javaResult.stdout
+              + jvmResult
               + "\n\tart: "
               + output.replace("\r", ""),
-          output.equals(javaResult.stdout.replace("\r", "")));
+          output.equals(jvmResult.replace("\r", "")));
     }
   }