| // 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.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.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"; | |
| @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( | |
| new DexDebugTestConfig(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.builder().addProgramFiles(outs).build(), | |
| 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( | |
| new DexDebugTestConfig(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); | |
| } | |
| } |