| // 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.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.android.tools.r8.KotlinTestParameters; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.List; |
| import org.apache.harmony.jpda.tests.framework.jdwp.Value; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameters; |
| |
| @RunWith(Parameterized.class) |
| public class KotlinInlineTest extends KotlinDebugTestBase { |
| |
| public static final String DEBUGGEE_CLASS = "KotlinInline"; |
| public static final String SOURCE_FILE = "KotlinInline.kt"; |
| |
| private final TestParameters parameters; |
| private final KotlinTestParameters kotlinParameters; |
| |
| @Parameters(name = "{0}, {1}") |
| public static List<Object[]> data() { |
| return buildParameters( |
| getTestParameters().withDexRuntimes().withAllApiLevels().build(), |
| getKotlinTestParameters().withAllCompilersAndTargetVersions().build()); |
| } |
| |
| public KotlinInlineTest(TestParameters parameters, KotlinTestParameters kotlinParameters) { |
| this.parameters = parameters; |
| this.kotlinParameters = kotlinParameters; |
| } |
| |
| protected KotlinDebugD8Config getD8Config() { |
| return KotlinDebugD8Config.build(kotlinParameters, parameters.getApiLevel()); |
| } |
| |
| @Test |
| public void testStepOverInline() throws Throwable { |
| String methodName = "singleInline"; |
| runDebugTest( |
| getD8Config(), |
| DEBUGGEE_CLASS, |
| breakpoint(DEBUGGEE_CLASS, methodName), |
| run(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(41, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepOver(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(42, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| kotlinStepOver(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(43, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| run()); |
| } |
| |
| @Test |
| public void testStepIntoInline() throws Throwable { |
| String methodName = "singleInline"; |
| runDebugTest( |
| getD8Config(), |
| DEBUGGEE_CLASS, |
| breakpoint(DEBUGGEE_CLASS, methodName), |
| run(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(41, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepOver(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(42, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepInto(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| // The actual line number (the one encoded in debug information) is different than the |
| // source file one. |
| // TODO(shertz) extract original line number from JSR-45's SMAP (only supported on |
| // Android O+). |
| assertTrue(42 != s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| run()); |
| } |
| |
| @Test |
| public void testStepOutInline() throws Throwable { |
| String methodName = "singleInline"; |
| runDebugTest( |
| getD8Config(), |
| DEBUGGEE_CLASS, |
| breakpoint(DEBUGGEE_CLASS, methodName), |
| run(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(41, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepOver(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(42, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepInto(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| }), |
| kotlinStepOut(), |
| inspect(s -> { |
| assertEquals(DEBUGGEE_CLASS, s.getClassName()); |
| assertEquals(methodName, s.getMethodName()); |
| assertEquals(SOURCE_FILE, s.getSourceFile()); |
| assertEquals(43, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| run()); |
| } |
| |
| private static String mangleFunctionNameFromInlineScope(String functionName) { |
| return "$i$f$" + functionName; |
| } |
| |
| private static String mangleLambdaNameFromInlineScope(String functionName, int lambdaId) { |
| assert lambdaId > 0; |
| return "$i$a$" + functionName + "$" + lambdaId; |
| } |
| |
| private static String mangleLvNameFromInlineScope(String lvName, int inlineDepth) { |
| assert inlineDepth > 0; |
| StringBuilder builder = new StringBuilder(lvName); |
| for (int i = 0; i < inlineDepth; ++i) { |
| builder.append("$iv"); |
| } |
| return builder.toString(); |
| } |
| |
| @Test |
| public void testKotlinInline() throws Throwable { |
| final String inliningMethodName = "invokeInlinedFunctions"; |
| runDebugTest( |
| getD8Config(), |
| DEBUGGEE_CLASS, |
| breakpoint(DEBUGGEE_CLASS, inliningMethodName), |
| run(), |
| inspect( |
| s -> { |
| assertEquals(inliningMethodName, s.getMethodName()); |
| assertEquals(16, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepInto(), |
| inspect( |
| s -> { |
| // We must have stepped into the code of the inlined method but the actual current |
| // method |
| // did not change. |
| assertEquals(inliningMethodName, s.getMethodName()); |
| // TODO(shertz) get the original line if JSR45 is supported by the targeted ART |
| // runtime. |
| s.checkLocal("this"); |
| }), |
| stepInto(), |
| inspect( |
| s -> { |
| assertEquals(inliningMethodName, s.getMethodName()); |
| assertEquals(17, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepInto(), |
| inspect( |
| s -> { |
| assertEquals(inliningMethodName, s.getMethodName()); |
| assertEquals(18, s.getLineNumber()); |
| s.checkLocal("this"); |
| s.checkLocal("inA", Value.createInt(1)); |
| // This is a "hidden" lv added by Kotlin (which is neither initialized nor used). |
| s.checkLocal(mangleFunctionNameFromInlineScope("inlinedA")); |
| s.checkLocal( |
| mangleLambdaNameFromInlineScope( |
| "-inlinedA-KotlinInline$invokeInlinedFunctions", 1)); |
| }), |
| stepInto(), |
| inspect( |
| s -> { |
| // We must have stepped into the code of the second inlined method but the actual |
| // current |
| // method did not change. |
| assertEquals(inliningMethodName, s.getMethodName()); |
| // TODO(shertz) get the original line if JSR45 is supported by the targeted ART |
| // runtime. |
| s.checkLocal("this"); |
| }), |
| stepInto(), |
| inspect( |
| s -> { |
| assertEquals(inliningMethodName, s.getMethodName()); |
| assertEquals(19, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| stepInto(), |
| inspect( |
| s -> { |
| assertEquals(inliningMethodName, s.getMethodName()); |
| assertEquals(20, s.getLineNumber()); |
| s.checkLocal("this"); |
| s.checkLocal("inB", Value.createInt(2)); |
| // This is a "hidden" lv added by Kotlin (which is neither initialized nor used). |
| s.checkLocal(mangleFunctionNameFromInlineScope("inlinedB")); |
| s.checkLocal( |
| mangleLambdaNameFromInlineScope( |
| "-inlinedB-KotlinInline$invokeInlinedFunctions$1", 1)); |
| }), |
| run()); |
| } |
| |
| @Test |
| public void testNestedInlining() throws Throwable { |
| // Count the number of lines in the source file. This is needed to check that inlined code |
| // refers to non-existing line numbers. |
| Path sourceFilePath = |
| AbstractR8KotlinTestBase.getKotlinFileInResource("debug", SOURCE_FILE.replace(".kt", "")); |
| assert sourceFilePath.toFile().exists(); |
| final int maxLineNumber = Files.readAllLines(sourceFilePath).size(); |
| final String inliningMethodName = "testNestedInlining"; |
| |
| // Local variables that represent the scope (start,end) of function's code that has been |
| // inlined. |
| final String inlinee1_inlineScope = mangleFunctionNameFromInlineScope("inlinee1"); |
| final String inlinee2_inlineScope = mangleFunctionNameFromInlineScope("inlinee2"); |
| |
| // Local variables that represent the scope (start,end) of lambda's code that has been inlined. |
| final String inlinee2_lambda1_inlineScope = mangleLambdaNameFromInlineScope("inlinee2", 1); |
| final String inlinee2_lambda2_inlineScope = "$i$a$-inlinee2-KotlinInline$inlinee1$1$iv"; |
| final String c_mangledLvName = mangleLvNameFromInlineScope("c", 1); |
| final String left_mangledLvName = mangleLvNameFromInlineScope("left", 1); |
| final String right_mangledLvName = mangleLvNameFromInlineScope("right", 1); |
| final String p_mangledLvName = mangleLvNameFromInlineScope("p", 2); |
| |
| runDebugTest( |
| getD8Config(), |
| DEBUGGEE_CLASS, |
| breakpoint(DEBUGGEE_CLASS, inliningMethodName), |
| run(), |
| inspect( |
| s -> { |
| assertEquals(inliningMethodName, s.getMethodName()); |
| assertEquals(52, s.getLineNumber()); |
| s.checkLocal("this"); |
| }), |
| checkLocal("this"), |
| checkNoLocals("l1", "l2"), |
| stepOver(), |
| checkLine(SOURCE_FILE, 53), |
| checkLocals("this", "l1"), |
| checkNoLocal("l2"), |
| stepOver(), |
| checkLine(SOURCE_FILE, 54), |
| checkLocals("this", "l1", "l2"), |
| stepInto(), |
| // We jumped into 1st inlinee but the current method is the same |
| checkMethod(DEBUGGEE_CLASS, inliningMethodName), |
| checkLocal(inlinee1_inlineScope), |
| inspect( |
| state -> { |
| assertEquals(SOURCE_FILE, state.getSourceFile()); |
| assertTrue(state.getLineNumber() > maxLineNumber); |
| }), |
| checkNoLocal(c_mangledLvName), |
| stepInto(), |
| checkLocal(c_mangledLvName), |
| stepInto(), |
| // We jumped into 2nd inlinee which is nested in the 1st inlinee |
| checkLocal(inlinee2_inlineScope), |
| checkLocal(inlinee1_inlineScope), |
| inspect( |
| state -> { |
| assertEquals(SOURCE_FILE, state.getSourceFile()); |
| assertTrue(state.getLineNumber() > maxLineNumber); |
| }), |
| // We must see the local variable "p" with a 2-level inline depth. |
| checkLocal(p_mangledLvName), |
| checkNoLocals(left_mangledLvName, right_mangledLvName), |
| // Enter the if block of inlinee2 |
| stepInto(), |
| checkLocal(p_mangledLvName), |
| checkNoLocals(left_mangledLvName, right_mangledLvName), |
| // Enter the inlined lambda |
| stepInto(), |
| checkLocal(p_mangledLvName), |
| checkLocal(inlinee2_lambda2_inlineScope), |
| checkNoLocals(left_mangledLvName, right_mangledLvName), |
| stepInto(), |
| checkLocal(inlinee2_lambda2_inlineScope), |
| checkLocal(left_mangledLvName), |
| checkNoLocal(right_mangledLvName), |
| stepInto(), |
| checkLocals(left_mangledLvName, right_mangledLvName), |
| // Enter "foo" |
| stepInto(), |
| checkMethod(DEBUGGEE_CLASS, "foo"), |
| checkLine(SOURCE_FILE, 34), |
| stepOut(), |
| // We're back to the inline section, at the end of the lambda |
| inspect( |
| state -> { |
| assertEquals(SOURCE_FILE, state.getSourceFile()); |
| assertTrue(state.getLineNumber() > maxLineNumber); |
| }), |
| checkLocal(inlinee1_inlineScope), |
| checkLocal(inlinee2_inlineScope), |
| checkLocal(inlinee2_lambda2_inlineScope), |
| stepInto(), |
| // We're in inlinee2, after the call to the inlined lambda. |
| checkLocal(inlinee1_inlineScope), |
| checkLocal(inlinee2_inlineScope), |
| checkNoLocal(inlinee2_lambda1_inlineScope), |
| checkNoLocal(inlinee2_lambda2_inlineScope), |
| checkLocal(p_mangledLvName), |
| stepInto(), |
| // We're out of inlinee2 |
| checkMethod(DEBUGGEE_CLASS, inliningMethodName), |
| checkLocal(inlinee1_inlineScope), |
| checkNoLocal(inlinee2_inlineScope), |
| checkNoLocal(inlinee2_lambda1_inlineScope), |
| // Enter the new call to "inlinee2" |
| stepInto(), |
| checkMethod(DEBUGGEE_CLASS, inliningMethodName), |
| checkLocal(inlinee1_inlineScope), |
| checkLocal(inlinee2_inlineScope), |
| checkNoLocal(inlinee2_lambda1_inlineScope), |
| checkNoLocal(inlinee2_lambda2_inlineScope), |
| checkNoLocal(p_mangledLvName), |
| stepInto(), |
| checkMethod(DEBUGGEE_CLASS, inliningMethodName), |
| checkLocal(inlinee1_inlineScope), |
| checkLocal(inlinee2_inlineScope), |
| checkNoLocal(inlinee2_lambda1_inlineScope), |
| checkNoLocal(inlinee2_lambda2_inlineScope), |
| checkNoLocal(p_mangledLvName), |
| // We enter the 2nd lambda |
| stepInto(), |
| checkMethod(DEBUGGEE_CLASS, inliningMethodName), |
| checkLocal(inlinee1_inlineScope), |
| checkLocal(inlinee2_inlineScope), |
| checkNoLocal(inlinee2_lambda1_inlineScope), |
| checkNoLocal(inlinee2_lambda2_inlineScope), |
| // Enter the call to "foo" |
| stepInto(), |
| checkMethod(DEBUGGEE_CLASS, "foo"), |
| checkLine(SOURCE_FILE, 34), |
| run()); |
| } |
| |
| } |