Compatibility tests for source file attribute.

Bug: 202367773
Bug: 201269335
Change-Id: Icba97d678f05540c08b9b695e50e14fb73c5aa80
diff --git a/src/test/java/com/android/tools/r8/L8TestRunResult.java b/src/test/java/com/android/tools/r8/L8TestRunResult.java
index a9a9a78..8f6bab8 100644
--- a/src/test/java/com/android/tools/r8/L8TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/L8TestRunResult.java
@@ -49,6 +49,12 @@
   }
 
   @Override
+  public <E extends Throwable> L8TestRunResult inspectFailure(
+      ThrowingConsumer<CodeInspector, E> consumer) {
+    throw new Unimplemented();
+  }
+
+  @Override
   public L8TestRunResult disassemble() throws IOException, ExecutionException {
     throw new Unimplemented();
   }
diff --git a/src/test/java/com/android/tools/r8/ProguardTestRunResult.java b/src/test/java/com/android/tools/r8/ProguardTestRunResult.java
index 3c2bbfc..c5cccbe 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestRunResult.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestRunResult.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
-import java.util.concurrent.ExecutionException;
 
 public class ProguardTestRunResult extends SingleTestRunResult<ProguardTestRunResult> {
 
@@ -34,14 +33,8 @@
     return super.getStackTrace().retrace(proguardMap);
   }
 
-  public StackTrace getOriginalStackTrace() {
-    return super.getStackTrace();
-  }
-
   @Override
-  public CodeInspector inspector() throws IOException, ExecutionException {
-    // See comment in base class.
-    assertSuccess();
+  protected CodeInspector internalGetCodeInspector() throws IOException {
     assertNotNull(app);
     return new CodeInspector(app, proguardMap);
   }
diff --git a/src/test/java/com/android/tools/r8/R8TestRunResult.java b/src/test/java/com/android/tools/r8/R8TestRunResult.java
index 3ef4901..716971d 100644
--- a/src/test/java/com/android/tools/r8/R8TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestRunResult.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.naming.retrace.StackTrace;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ThrowingBiConsumer;
-import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
 import java.io.IOException;
@@ -52,10 +51,6 @@
     return super.getStackTrace().retraceAllowExperimentalMapping(proguardMap);
   }
 
-  public StackTrace getOriginalStackTrace() {
-    return super.getStackTrace();
-  }
-
   @Override
   protected CodeInspector internalGetCodeInspector() throws IOException {
     assertNotNull(app);
@@ -63,12 +58,6 @@
   }
 
   public <E extends Throwable> R8TestRunResult inspectOriginalStackTrace(
-      ThrowingConsumer<StackTrace, E> consumer) throws E {
-    consumer.accept(getOriginalStackTrace());
-    return self();
-  }
-
-  public <E extends Throwable> R8TestRunResult inspectOriginalStackTrace(
       ThrowingBiConsumer<StackTrace, CodeInspector, E> consumer) throws E, IOException {
     consumer.accept(getOriginalStackTrace(), internalGetCodeInspector());
     return self();
diff --git a/src/test/java/com/android/tools/r8/SingleTestRunResult.java b/src/test/java/com/android/tools/r8/SingleTestRunResult.java
index 17546ee..726acbd 100644
--- a/src/test/java/com/android/tools/r8/SingleTestRunResult.java
+++ b/src/test/java/com/android/tools/r8/SingleTestRunResult.java
@@ -60,6 +60,10 @@
   }
 
   public StackTrace getStackTrace() {
+    return getOriginalStackTrace();
+  }
+
+  public StackTrace getOriginalStackTrace() {
     if (runtime.isDex()) {
       return StackTrace.extractFromArt(getStdErr(), runtime.asDex().getVm());
     } else {
@@ -129,6 +133,12 @@
     return self();
   }
 
+  public <E extends Throwable> RR inspectOriginalStackTrace(
+      ThrowingConsumer<StackTrace, E> consumer) throws E {
+    consumer.accept(getOriginalStackTrace());
+    return self();
+  }
+
   public RR disassemble(PrintStream ps) throws IOException, ExecutionException {
     ToolHelper.disassemble(app, ps);
     return self();
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index 90d4f75..bd5ad83 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -38,6 +38,9 @@
   public abstract <E extends Throwable> RR inspect(ThrowingConsumer<CodeInspector, E> consumer)
       throws IOException, ExecutionException, E;
 
+  public abstract <E extends Throwable> RR inspectFailure(
+      ThrowingConsumer<CodeInspector, E> consumer) throws IOException, E;
+
   public abstract RR disassemble() throws IOException, ExecutionException;
 
   public <E extends Throwable> RR apply(ThrowingConsumer<RR, E> fn) throws E {
diff --git a/src/test/java/com/android/tools/r8/TestRunResultCollection.java b/src/test/java/com/android/tools/r8/TestRunResultCollection.java
index e166be4..c5421ac 100644
--- a/src/test/java/com/android/tools/r8/TestRunResultCollection.java
+++ b/src/test/java/com/android/tools/r8/TestRunResultCollection.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -57,6 +58,12 @@
     return inspectIf(Predicates.alwaysTrue(), consumer);
   }
 
+  @Override
+  public <E extends Throwable> RR inspectFailure(ThrowingConsumer<CodeInspector, E> consumer)
+      throws IOException, E {
+    throw new Unimplemented();
+  }
+
   public RR applyIf(Predicate<C> filter, Consumer<TestRunResult<?>> thenConsumer) {
     return applyIf(filter, thenConsumer, r -> {});
   }
diff --git a/src/test/java/com/android/tools/r8/naming/sourcefile/SourceFileAttributeCompatTest.java b/src/test/java/com/android/tools/r8/naming/sourcefile/SourceFileAttributeCompatTest.java
new file mode 100644
index 0000000..ac17f3c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/sourcefile/SourceFileAttributeCompatTest.java
@@ -0,0 +1,197 @@
+// Copyright (c) 2021, 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.naming.sourcefile;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ProguardVersion;
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SourceFileAttributeCompatTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withSystemRuntime().build();
+  }
+
+  public SourceFileAttributeCompatTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private String getOriginalSourceFile() {
+    return new Exception().getStackTrace()[0].getFileName();
+  }
+
+  private void commonSetUp(TestShrinkerBuilder<?, ?, ?, ?, ?> builder) {
+    builder
+        .addProgramClasses(TestClass.class, SemiKept.class, NonKept.class)
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules("-keep,allowshrinking class " + SemiKept.class.getName() + " { *; }");
+  }
+
+  private void checkSourceFileIsRemoved(SingleTestRunResult<?> result) throws Exception {
+    // TODO(b/202368282): We should likely emit a "default" source file attribute rather than strip.
+    checkSourceFile(result, null, null, null);
+  }
+
+  private void checkSourceFileIsOriginal(SingleTestRunResult<?> result) throws Exception {
+    String originalSourceFile = getOriginalSourceFile();
+    checkSourceFile(result, originalSourceFile, originalSourceFile, originalSourceFile);
+  }
+
+  private void checkSourceFile(
+      SingleTestRunResult<?> result, String keptValue, String semiKeptValue, String nonKeptValue)
+      throws Exception {
+    result.assertFailure();
+    result.inspectOriginalStackTrace(
+        stackTrace -> {
+          StackTraceLine nonKeptLine = stackTrace.get(0);
+          StackTraceLine semiKeptLine = stackTrace.get(1);
+          StackTraceLine keptLine = stackTrace.get(4);
+          assertEquals(getExpectedSourceFile(nonKeptValue), nonKeptLine.fileName);
+          assertEquals(getExpectedSourceFile(semiKeptValue), semiKeptLine.fileName);
+          assertEquals(getExpectedSourceFile(keptValue), keptLine.fileName);
+        });
+    result.inspectFailure(
+        inspector -> {
+          ClassSubject testClass = inspector.clazz(TestClass.class);
+          ClassSubject semiKept = inspector.clazz(SemiKept.class);
+          ClassSubject nonKept = inspector.clazz(NonKept.class);
+          assertEquals(keptValue, getSourceFileString(testClass));
+          assertEquals(semiKeptValue, getSourceFileString(semiKept));
+          assertEquals(nonKeptValue, getSourceFileString(nonKept));
+        });
+  }
+
+  private String getSourceFileString(ClassSubject subject) {
+    DexString sourceFile = subject.getDexProgramClass().getSourceFile();
+    return sourceFile == null ? null : sourceFile.toString();
+  }
+
+  private String getExpectedSourceFile(String expectedSourceFileValue) {
+    return expectedSourceFileValue == null ? "Unknown Source" : expectedSourceFileValue;
+  }
+
+  private <RR extends SingleTestRunResult<RR>> void testJustKeepMain(
+      TestShrinkerBuilder<?, ?, ?, RR, ?> builder, boolean fullMode) throws Exception {
+    // If the source file attribute is not kept then all compilers will strip it throughout.
+    commonSetUp(builder);
+    builder.run(parameters.getRuntime(), TestClass.class).apply(this::checkSourceFileIsRemoved);
+  }
+
+  private <RR extends SingleTestRunResult<RR>> void testDontObfuscate(
+      TestShrinkerBuilder<?, ?, ?, RR, ?> builder, boolean fullMode) throws Exception {
+    // If minification is off then compat compilers retain it, full mode will remove it.
+    commonSetUp(builder);
+    builder
+        .addKeepRules("-dontobfuscate")
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(fullMode, this::checkSourceFileIsRemoved, this::checkSourceFileIsOriginal);
+  }
+
+  private <RR extends SingleTestRunResult<RR>> void testDontOptimize(
+      TestShrinkerBuilder<?, ?, ?, RR, ?> builder, boolean fullMode) throws Exception {
+    // No effect from -dontoptimize
+    commonSetUp(builder);
+    builder
+        .addKeepRules("-dontoptimize")
+        .run(parameters.getRuntime(), TestClass.class)
+        .apply(this::checkSourceFileIsRemoved);
+  }
+
+  private <RR extends SingleTestRunResult<RR>> void testDontShrink(
+      TestShrinkerBuilder<?, ?, ?, RR, ?> builder, boolean fullMode) throws Exception {
+    // No effect from -dontshrink
+    commonSetUp(builder);
+    builder
+        .addKeepRules("-dontshrink")
+        .run(parameters.getRuntime(), TestClass.class)
+        .apply(this::checkSourceFileIsRemoved);
+  }
+
+  private <RR extends SingleTestRunResult<RR>> void testKeepSourceFileAttribute(
+      TestShrinkerBuilder<?, ?, ?, RR, ?> builder, boolean fullMode) throws Exception {
+    // If the source file attribute is kept, then PG and compat R8 will preserve it in original
+    // form for every input class. R8 will only preserve it for (soft) pinned classes. Others will
+    // be replaced by 'SourceFile'. The use of 'SourceFile' is to ensure VMs still print lines.
+    // TODO(b/202367773): R8 (non-compat) should rather replace it for all classes like line opt.
+    String originalSourceFile = getOriginalSourceFile();
+    String residualSourceFile = fullMode ? "SourceFile" : originalSourceFile;
+    commonSetUp(builder);
+    builder
+        .addKeepAttributeSourceFile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .apply(
+            result ->
+                checkSourceFile(
+                    result, originalSourceFile, originalSourceFile, residualSourceFile));
+  }
+
+  private <RR extends SingleTestRunResult<RR>> void runAllTests(
+      Supplier<TestShrinkerBuilder<?, ?, ?, RR, ?>> builder, boolean fullMode) throws Exception {
+    testJustKeepMain(builder.get(), fullMode);
+    testDontObfuscate(builder.get(), fullMode);
+    testDontOptimize(builder.get(), fullMode);
+    testDontShrink(builder.get(), fullMode);
+    testKeepSourceFileAttribute(builder.get(), fullMode);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    runAllTests(() -> testForR8(parameters.getBackend()), true);
+  }
+
+  @Test
+  public void testCompatR8() throws Exception {
+    runAllTests(() -> testForR8Compat(parameters.getBackend()), false);
+  }
+
+  @Test
+  public void testPG() throws Exception {
+    runAllTests(() -> testForProguard(ProguardVersion.V7_0_0).addDontWarn(getClass()), false);
+  }
+
+  static class NonKept {
+    @Override
+    public String toString() {
+      throw new RuntimeException("BOOM!");
+    }
+  }
+
+  static class SemiKept {
+    final Object o;
+
+    public SemiKept(Object o) {
+      this.o = o;
+    }
+
+    @Override
+    public String toString() {
+      return o.toString();
+    }
+  }
+
+  static class TestClass {
+    public static void main(String[] args) {
+      System.out.println(
+          System.nanoTime() > 0
+              ? new SemiKept(System.nanoTime() > 0 ? new NonKept() : null)
+              : null);
+    }
+  }
+}