Smali test to verify line-change assumption in an Art debugging session.
R=sgjesse, shertz
Change-Id: Ic5dd2a1ee9286888318bd680d36591d4f817d155
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 65fe4aa..1cd5c71 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -152,10 +152,14 @@
ToolHelper.getMinApiLevelForDexVm(ToolHelper.getDexVm()) >= Constants.ANDROID_N_API;
}
- protected final boolean isRunningJava() {
+ protected static boolean isRunningJava() {
return RUNTIME_KIND == RuntimeKind.JAVA;
}
+ protected static boolean isRunningArt() {
+ return RUNTIME_KIND == RuntimeKind.ART;
+ }
+
protected final void runDebugTest(String debuggeeClass, JUnit3Wrapper.Command... commands)
throws Throwable {
runDebugTest(Collections.emptyList(), debuggeeClass, Arrays.asList(commands));
diff --git a/src/test/java/com/android/tools/r8/debug/SmaliDebugTest.java b/src/test/java/com/android/tools/r8/debug/SmaliDebugTest.java
new file mode 100644
index 0000000..9c5a636
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/SmaliDebugTest.java
@@ -0,0 +1,211 @@
+// 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 com.android.tools.r8.debug;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.code.IfEqz;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.debuginfo.DebugInfoInspector;
+import com.android.tools.r8.graph.DexDebugEntry;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.smali.SmaliTestBase.SmaliBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class SmaliDebugTest extends DebugTestBase {
+
+ static final String FILE = "SmaliDebugTestDebuggee.smali";
+ static final String CLASS = "SmaliDebugTestDebuggee";
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ Assume.assumeTrue(DebugTestBase.isRunningArt());
+ }
+
+ @Rule
+ public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+ /**
+ * Simple test to check setup works for the Java source, ala:
+ *
+ * static int method(int x) {
+ * if (x != 0) {
+ * return x
+ * } else {
+ * return 42;
+ * }
+ * }
+ */
+ @Test
+ public void testSimpleIf() throws Throwable {
+ String methodName = "simpleIf";
+ runDebugTest(buildSimpleIf(methodName), CLASS,
+ breakpoint(CLASS, methodName),
+ run(),
+ // first call x == 0
+ checkLine(FILE, 1),
+ checkLocal("x", Value.createInt(0)),
+ stepOver(),
+ checkLine(FILE, 4),
+ run(),
+ // second call x == 1
+ checkLine(FILE, 1),
+ checkLocal("x", Value.createInt(1)),
+ stepOver(),
+ checkLine(FILE, 2),
+ run());
+ }
+
+ private List<Path> buildSimpleIf(String methodName)
+ throws Throwable {
+ SmaliBuilder builder = new SmaliBuilder();
+ builder.addClass(CLASS);
+ builder.setSourceFile(FILE);
+ builder.addStaticMethod("int", methodName, Collections.singletonList("int"), 0,
+ ".param p0, \"x\" # I",
+ ".line 1",
+ " if-eqz p0, :onzero",
+ ".line 2",
+ " return p0",
+ ":onzero",
+ ".line 4",
+ " const p0, 42",
+ " return p0");
+
+ builder.addMainMethod(2,
+ " const v0, 0",
+ " invoke-static { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+ " move-result v1",
+ " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+ " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+ " const v0, 1",
+ " invoke-static { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+ " move-result v1",
+ " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+ " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+ " return-void"
+ );
+ return buildAndRun(builder);
+ }
+
+ /**
+ * Test that a jump to a position that happens after a "default" debug event still triggers a
+ * break point due to the line change.
+ */
+ @Test
+ public void testJumpAfterLineChange() throws Throwable {
+ String methodName = "jumpAfterLineChange";
+ List<Path> outs = buildJumpAfterLineChange(methodName);
+
+ // Verify that the PC associated with the line entry 4 is prior to the target of the condition.
+ DebugInfoInspector info = new DebugInfoInspector(AndroidApp.fromProgramFiles(outs), CLASS,
+ new MethodSignature(methodName, "int", new String[]{ "int" }));
+ IfEqz cond = null;
+ for (Instruction instruction : info.getMethod().getCode().asDexCode().instructions) {
+ if (instruction.getOpcode() == IfEqz.OPCODE) {
+ cond = (IfEqz) instruction;
+ break;
+ }
+ }
+ assertNotNull(cond);
+ int target = cond.getTargets()[0] + cond.getOffset();
+ int linePC = -1;
+ for (DexDebugEntry entry : info.getEntries()) {
+ if (entry.line == 4) {
+ linePC = entry.address;
+ break;
+ }
+ }
+ assertTrue(linePC > -1);
+ assertTrue(target > linePC);
+
+ // Run debugger to verify that we step to line 4 and the values of v0 and v1 are unchanged.
+ runDebugTest(outs, CLASS,
+ breakpoint(CLASS, methodName),
+ run(),
+ // first call x == 0
+ checkLine(FILE, 1),
+ checkLocal("x", Value.createInt(0)),
+ checkNoLocal("v0"),
+ checkNoLocal("v1"),
+ stepOver(),
+ checkLine(FILE, 4),
+ checkLocal("v0", Value.createInt(0)),
+ checkLocal("v1", Value.createInt(7)),
+ stepOver(),
+ checkLine(FILE, 5),
+ checkLocal("v0", Value.createInt(42)),
+ checkLocal("v1", Value.createInt(7)),
+ run(),
+ // second call x == 1
+ checkLine(FILE, 1),
+ checkLocal("x", Value.createInt(1)),
+ checkNoLocal("v0"),
+ checkNoLocal("v1"),
+ stepOver(),
+ checkLine(FILE, 2),
+ checkNoLocal("v0"),
+ checkNoLocal("v1"),
+ run());
+ }
+
+ private List<Path> buildJumpAfterLineChange(String methodName)
+ throws Throwable {
+ SmaliBuilder builder = new SmaliBuilder();
+ builder.addClass(CLASS);
+ builder.setSourceFile(FILE);
+ builder.addStaticMethod("int", methodName, Collections.singletonList("int"), 2,
+ ".param p0, \"x\" # I",
+ ".line 1",
+ " const v0, 0",
+ " const v1, 7",
+ " if-eqz p0, :onzero",
+ ".line 2",
+ " return p0",
+ ".line 4",
+ ".local v0, \"v0\":I",
+ ".local v1, \"v1\":I",
+ " const v0, 1234", // odd and unreachable code
+ " const v1, 5678", // ...
+ ":onzero",
+ " const v0, 42",
+ ".line 5",
+ " return v0");
+
+ builder.addMainMethod(2,
+ " const v0, 0",
+ " invoke-static { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+ " move-result v1",
+ " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+ " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+ " const v0, 1",
+ " invoke-static { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+ " move-result v1",
+ " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+ " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+ " return-void"
+ );
+ return buildAndRun(builder);
+ }
+
+ private List<Path> buildAndRun(SmaliBuilder builder) throws Throwable {
+ byte[] bytes = builder.compile();
+ Path out = temp.getRoot().toPath().resolve("classes.dex");
+ Files.write(out, bytes);
+ ToolHelper.runArtNoVerificationErrors(out.toString(), CLASS);
+ return Collections.singletonList(out);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
index 029b1ad..5d15735 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
@@ -196,4 +196,12 @@
String.join(",", remaining.stream().map(Object::toString).collect(Collectors.toList())),
expected, expected + remaining.size());
}
+
+ public DexEncodedMethod getMethod() {
+ return method;
+ }
+
+ public List<DexDebugEntry> getEntries() {
+ return entries;
+ }
}
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java b/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
index 470f16e..b3a4060 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
@@ -13,8 +13,6 @@
import com.android.tools.r8.utils.DexInspector;
import com.android.tools.r8.utils.DexInspector.ClassSubject;
import com.android.tools.r8.utils.InternalOptions;
-import java.io.IOException;
-import org.antlr.runtime.RecognitionException;
import org.junit.Test;
public class SmaliBuildTest extends SmaliTestBase {
@@ -47,7 +45,7 @@
}
@Test
- public void buildWithLibrary() throws IOException, RecognitionException {
+ public void buildWithLibrary() throws Throwable {
// Build simple "Hello, world!" application.
SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
builder.addMainMethod(
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
index 66d0304..ea0d767 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
@@ -7,6 +7,7 @@
import static org.junit.Assert.assertEquals;
import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.errors.DexOverflowException;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
@@ -32,7 +33,7 @@
new Timing("SmaliTest"))
.read();
assertEquals(smali, application.smali(new InternalOptions()));
- } catch (IOException | RecognitionException | ExecutionException e) {
+ } catch (IOException | RecognitionException | ExecutionException | DexOverflowException e) {
throw new RuntimeException(e);
}
}
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index cecc10e..2e3088a 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -80,6 +80,7 @@
String name;
String superName;
List<String> implementedInterfaces;
+ String sourceFile = null;
List<String> source = new ArrayList<>();
Builder(String name, String superName, List<String> implementedInterfaces) {
@@ -124,6 +125,9 @@
appendSuper(builder);
appendImplementedInterfaces(builder);
builder.append("\n");
+ if (sourceFile != null) {
+ builder.append(".source \"").append(sourceFile).append("\"\n");
+ }
writeSource(builder);
return builder.toString();
}
@@ -199,6 +203,10 @@
classes.put(name, new InterfaceBuilder(name, superName));
}
+ public void setSourceFile(String file) {
+ classes.get(currentClassName).sourceFile = file;
+ }
+
public void addDefaultConstructor() {
String superDescriptor =
DescriptorUtils.javaTypeToDescriptor(classes.get(currentClassName).superName);
@@ -350,7 +358,8 @@
return result;
}
- public byte[] compile() throws IOException, RecognitionException {
+ public byte[] compile()
+ throws IOException, RecognitionException, DexOverflowException, ExecutionException {
return Smali.compile(build());
}
@@ -425,7 +434,7 @@
protected DexApplication buildApplication(SmaliBuilder builder, InternalOptions options) {
try {
return buildApplication(AndroidApp.fromDexProgramData(builder.compile()), options);
- } catch (IOException | RecognitionException e) {
+ } catch (IOException | RecognitionException | ExecutionException | DexOverflowException e) {
throw new RuntimeException(e);
}
}
@@ -438,7 +447,7 @@
.addLibraryFiles(FilteredClassPath.unfiltered(ToolHelper.getDefaultAndroidJar()))
.build();
return buildApplication(input, options);
- } catch (IOException | RecognitionException e) {
+ } catch (IOException | RecognitionException | ExecutionException | DexOverflowException e) {
throw new RuntimeException(e);
}
}
diff --git a/src/test/java/com/android/tools/r8/utils/Smali.java b/src/test/java/com/android/tools/r8/utils/Smali.java
index fbc5e16..93edf4d 100644
--- a/src/test/java/com/android/tools/r8/utils/Smali.java
+++ b/src/test/java/com/android/tools/r8/utils/Smali.java
@@ -3,12 +3,20 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.dex.ApplicationWriter;
+import com.android.tools.r8.errors.DexOverflowException;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.naming.NamingLens;
import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.TokenSource;
@@ -25,26 +33,28 @@
// Adapted from org.jf.smali.SmaliTestUtils.
public class Smali {
- public static byte[] compile(String smaliText) throws RecognitionException, IOException {
+ public static byte[] compile(String smaliText)
+ throws RecognitionException, IOException, DexOverflowException, ExecutionException {
return compile(smaliText, 15);
}
- public static byte[] compile(String... smaliText) throws RecognitionException, IOException {
+ public static byte[] compile(String... smaliText)
+ throws RecognitionException, IOException, DexOverflowException, ExecutionException {
return compile(Arrays.asList(smaliText), 15);
}
public static byte[] compile(String smaliText, int apiLevel)
- throws IOException, RecognitionException {
+ throws IOException, RecognitionException, DexOverflowException, ExecutionException {
return compile(ImmutableList.of(smaliText), apiLevel);
}
public static byte[] compile(List<String> smaliTexts)
- throws RecognitionException, IOException {
+ throws RecognitionException, IOException, DexOverflowException, ExecutionException {
return compile(smaliTexts, 15);
}
public static byte[] compile(List<String> smaliTexts, int apiLevel)
- throws RecognitionException, IOException {
+ throws RecognitionException, IOException, ExecutionException, DexOverflowException {
DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(apiLevel));
for (String smaliText : smaliTexts) {
@@ -85,8 +95,21 @@
dexBuilder.writeTo(dataStore);
- // TODO(sgjesse): This returns the full backingstore from MemoryDataStore, which by default
- // is 1024k bytes. Our dex file reader does not complain though.
- return dataStore.getData();
+ // This returns the full backingstore from MemoryDataStore, which by default is 1024k bytes.
+ // We process it via our reader and writer to trim it to the exact size and update its checksum.
+ byte[] data = dataStore.getData();
+ AndroidApp app = AndroidApp.fromDexProgramData(data);
+ InternalOptions options = new InternalOptions();
+ ExecutorService executor = ThreadUtils.getExecutorService(1);
+ try {
+ DexApplication dexApp = new ApplicationReader(
+ app, options, new Timing("smali")).read(executor);
+ ApplicationWriter writer = new ApplicationWriter(
+ dexApp, null, options, null, null, NamingLens.getIdentityLens(), null);
+ AndroidApp trimmed = writer.write(null, executor);
+ return ByteStreams.toByteArray(trimmed.getDexProgramResources().get(0).getStream());
+ } finally {
+ executor.shutdown();
+ }
}
}