| // 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.graph; |
| |
| import static org.hamcrest.CoreMatchers.allOf; |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import com.android.tools.r8.CompilationFailedException; |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.TestParametersCollection; |
| import com.android.tools.r8.TestRunResult; |
| import com.android.tools.r8.ToolHelper.DexVm.Version; |
| import com.android.tools.r8.transformers.ClassTransformer; |
| import com.android.tools.r8.utils.DescriptorUtils; |
| import com.android.tools.r8.utils.StringUtils; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameters; |
| import org.objectweb.asm.MethodVisitor; |
| import org.objectweb.asm.Opcodes; |
| |
| @RunWith(Parameterized.class) |
| public class InvokeSuperTest extends TestBase { |
| |
| @Parameters(name = "{0}") |
| public static TestParametersCollection data() { |
| return getTestParameters().withAllRuntimesAndApiLevels().build(); |
| } |
| |
| final TestParameters parameters; |
| |
| public InvokeSuperTest(TestParameters parameters) { |
| this.parameters = parameters; |
| } |
| |
| static final String EXPECTED = |
| StringUtils.lines( |
| "superMethod in SubLevel2", |
| "superMethod in SubLevel2", |
| "superMethod in SubLevel2", |
| "java.lang.NoSuchMethodError", |
| "subLevel1Method in SubLevel2", |
| "subLevel1Method in SubLevel2", |
| "subLevel2Method in SubLevel2", |
| "From SubLevel1: otherSuperMethod in Super"); |
| |
| static final String UNEXPECTED_DEX_5_AND_6_OUTPUT = |
| StringUtils.lines( |
| "superMethod in Super", |
| "superMethod in SubLevel1", |
| "superMethod in SubLevel2", |
| "java.lang.NoSuchMethodError", |
| "subLevel1Method in SubLevel1", |
| "subLevel1Method in SubLevel2", |
| "subLevel2Method in SubLevel2", |
| "From SubLevel1: otherSuperMethod in Super"); |
| |
| String getExpectedOutput() { |
| if (parameters.isDexRuntime()) { |
| Version version = parameters.getRuntime().asDex().getVm().getVersion(); |
| if (version.isAtLeast(Version.V5_1_1) && version.isOlderThanOrEqual(Version.V6_0_1)) { |
| return UNEXPECTED_DEX_5_AND_6_OUTPUT; |
| } |
| } |
| return EXPECTED; |
| } |
| |
| @Test |
| public void testReference() throws Exception { |
| testForRuntime(parameters) |
| .addProgramClasses( |
| MainClass.class, |
| Consumer.class, |
| Super.class, |
| SubLevel1.class, |
| SubLevel2.class, |
| SubClassOfInvokerClass.class) |
| .addProgramClassFileData(InvokerClassDump.dumpVerifying()) |
| .run(parameters.getRuntime(), MainClass.class) |
| .assertSuccessWithOutput(getExpectedOutput()); |
| } |
| |
| @Test |
| public void testR8() throws Exception { |
| testForR8(parameters.getBackend()) |
| .addProgramClasses( |
| MainClass.class, |
| Consumer.class, |
| Super.class, |
| SubLevel1.class, |
| SubLevel2.class, |
| SubClassOfInvokerClass.class) |
| .addProgramClassFileData(InvokerClassDump.dumpVerifying()) |
| .setMinApi(parameters.getApiLevel()) |
| .addKeepMainRule(MainClass.class) |
| .run(parameters.getRuntime(), MainClass.class) |
| .assertSuccessWithOutput(EXPECTED); |
| } |
| |
| @Test |
| public void testReferenceNonVerifying() throws Exception { |
| testForRuntime(parameters) |
| .addProgramClasses( |
| MainClassFailing.class, |
| Consumer.class, |
| Super.class, |
| SubLevel1.class, |
| SubLevel2.class, |
| SubClassOfInvokerClass.class) |
| .addProgramClassFileData(InvokerClassDump.dumpNonVerifying()) |
| .run(parameters.getRuntime(), MainClassFailing.class) |
| .apply(this::checkNonVerifyingResult); |
| } |
| |
| private void checkNonVerifyingResult(TestRunResult<?> result) { |
| // The input is invalid and any JVM will fail at verification time. |
| if (parameters.isCfRuntime()) { |
| result.assertFailureWithErrorThatMatches(containsString(VerifyError.class.getName())); |
| return; |
| } |
| // D8 cannot verify its inputs and the behavior of the compiled output differs. |
| // The failure is due to lambda desugaring on pre-7 and fails at runtime on 7+. |
| Version version = parameters.getRuntime().asDex().getVm().getVersion(); |
| if (version.isOlderThanOrEqual(Version.V6_0_1)) { |
| result.assertFailureWithErrorThatMatches( |
| allOf(containsString("java.lang.NoClassDefFoundError"), containsString("-$$Lambda$"))); |
| return; |
| } |
| result.assertSuccessWithOutputLines(NoSuchMethodError.class.getName()); |
| } |
| |
| @Test |
| public void testR8NonVerifying() throws Exception { |
| try { |
| testForR8(parameters.getBackend()) |
| .addProgramClasses( |
| MainClassFailing.class, |
| Consumer.class, |
| Super.class, |
| SubLevel1.class, |
| SubLevel2.class, |
| SubClassOfInvokerClass.class) |
| .addProgramClassFileData(InvokerClassDump.dumpNonVerifying()) |
| .setMinApi(parameters.getApiLevel()) |
| .addKeepMainRule(MainClassFailing.class) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| diagnostics.assertErrorMessageThatMatches(containsString("Illegal invoke-super")); |
| }); |
| fail("Expected compilation to fail"); |
| } catch (CompilationFailedException e) { |
| // Expected compilation failure. |
| } |
| } |
| |
| /** Copy of {@ref java.util.function.Consumer} to allow tests to run on early versions of art. */ |
| interface Consumer<T> { |
| |
| void accept(T item); |
| } |
| |
| static class Super { |
| |
| public void superMethod() { |
| System.out.println("superMethod in Super"); |
| } |
| |
| public void otherSuperMethod() { |
| System.out.println("otherSuperMethod in Super"); |
| } |
| } |
| |
| static class SubLevel1 extends Super { |
| |
| @Override |
| public void superMethod() { |
| System.out.println("superMethod in SubLevel1"); |
| } |
| |
| public void subLevel1Method() { |
| System.out.println("subLevel1Method in SubLevel1"); |
| } |
| |
| public void otherSuperMethod() { |
| System.out.println("otherSuperMethod in SubLevel1"); |
| } |
| |
| public void callOtherSuperMethod() { |
| System.out.print("From SubLevel1: "); |
| super.otherSuperMethod(); |
| } |
| } |
| |
| static class SubClassOfInvokerClass extends InvokerClass { |
| |
| public void subLevel2Method() { |
| System.out.println("subLevel2Method in SubClassOfInvokerClass"); |
| } |
| } |
| |
| static class SubLevel2 extends SubLevel1 { |
| |
| @Override |
| public void superMethod() { |
| System.out.println("superMethod in SubLevel2"); |
| } |
| |
| @Override |
| public void subLevel1Method() { |
| System.out.println("subLevel1Method in SubLevel2"); |
| } |
| |
| public void subLevel2Method() { |
| System.out.println("subLevel2Method in SubLevel2"); |
| } |
| |
| public void otherSuperMethod() { |
| System.out.println("otherSuperMethod in SubLevel2"); |
| } |
| |
| public void callOtherSuperMethodIndirect() { |
| callOtherSuperMethod(); |
| } |
| } |
| |
| static class MainClass { |
| |
| private static void tryInvoke(Consumer<InvokerClass> function) { |
| InvokerClass invoker = new InvokerClass(); |
| try { |
| function.accept(invoker); |
| } catch (Throwable e) { |
| System.out.println(e.getClass().getCanonicalName()); |
| } |
| } |
| |
| public static void main(String... args) { |
| tryInvoke(InvokerClass::invokeSuperMethodOnSuper); |
| tryInvoke(InvokerClass::invokeSuperMethodOnSubLevel1); |
| tryInvoke(InvokerClass::invokeSuperMethodOnSubLevel2); |
| tryInvoke(InvokerClass::invokeSubLevel1MethodOnSuper); |
| tryInvoke(InvokerClass::invokeSubLevel1MethodOnSubLevel1); |
| tryInvoke(InvokerClass::invokeSubLevel1MethodOnSubLevel2); |
| tryInvoke(InvokerClass::invokeSubLevel2MethodOnSubLevel2); |
| tryInvoke(InvokerClass::callOtherSuperMethodIndirect); |
| } |
| } |
| |
| static class MainClassFailing { |
| |
| private static void tryInvoke(java.util.function.Consumer<InvokerClass> function) { |
| InvokerClass invoker = new InvokerClass(); |
| try { |
| function.accept(invoker); |
| } catch (Throwable e) { |
| System.out.println(e.getClass().getCanonicalName()); |
| } |
| } |
| |
| public static void main(String... args) { |
| tryInvoke(InvokerClass::invokeSubLevel2MethodOnSubClassOfInvokerClass); |
| } |
| } |
| |
| /** |
| * This class is a stub class needed to compile the dependent Java classes. The actual |
| * implementation that will be used at runtime is generated by {@link InvokerClassDump}. |
| */ |
| static class InvokerClass extends SubLevel2 { |
| |
| public void invokeSuperMethodOnSubLevel2() { |
| stubIsUnreachable(); |
| } |
| |
| public void invokeSuperMethodOnSubLevel1() { |
| stubIsUnreachable(); |
| } |
| |
| public void invokeSuperMethodOnSuper() { |
| stubIsUnreachable(); |
| } |
| |
| public void invokeSubLevel1MethodOnSubLevel2() { |
| stubIsUnreachable(); |
| } |
| |
| public void invokeSubLevel1MethodOnSubLevel1() { |
| stubIsUnreachable(); |
| } |
| |
| public void invokeSubLevel1MethodOnSuper() { |
| stubIsUnreachable(); |
| } |
| |
| public void invokeSubLevel2MethodOnSubLevel2() { |
| stubIsUnreachable(); |
| } |
| |
| public void invokeSubLevel2MethodOnSubClassOfInvokerClass() { |
| stubIsUnreachable(); |
| } |
| |
| private static void stubIsUnreachable() { |
| throw new RuntimeException("Stub should never be called."); |
| } |
| } |
| |
| // This modifies the above {@link InvokerClass} with invoke-special for the corresponding methods. |
| static class InvokerClassDump implements Opcodes { |
| |
| public static byte[] dumpVerifying() throws Exception { |
| return dump(true); |
| } |
| |
| public static byte[] dumpNonVerifying() throws Exception { |
| return dump(false); |
| } |
| |
| static byte[] dump(boolean verifying) throws Exception { |
| return transformer(InvokerClass.class) |
| .addClassTransformer( |
| new ClassTransformer() { |
| @Override |
| public MethodVisitor visitMethod( |
| int access, |
| String name, |
| String descriptor, |
| String signature, |
| String[] exceptions) { |
| // Keep the constructor as is. |
| if (name.equals("<init>")) { |
| return super.visitMethod(access, name, descriptor, signature, exceptions); |
| } |
| // Remove all methods not in the form of invokeXonY. |
| if (!name.startsWith("invoke")) { |
| return null; |
| } |
| // If dumping valid methods drop the invoke on a subclass, otherwise drop all |
| // others. |
| if (verifying == name.equals("invokeSubLevel2MethodOnSubClassOfInvokerClass")) { |
| return null; |
| } |
| // Replace the body of invokeXonY methods by invoke of X on class Y. |
| MethodVisitor mv = |
| super.visitMethod(access, name, descriptor, signature, exceptions); |
| int split = name.indexOf("On"); |
| assertTrue(split > 0); |
| String targetMethodRaw = name.substring("invoke".length(), split); |
| String targetMethod = |
| targetMethodRaw.substring(0, 1).toLowerCase() + targetMethodRaw.substring(1); |
| String targetHolderRaw = name.substring(split + 2); |
| String targetHolderType = |
| InvokeSuperTest.class.getTypeName() + "$" + targetHolderRaw; |
| String targetHolderName = |
| DescriptorUtils.getBinaryNameFromJavaType(targetHolderType); |
| mv.visitCode(); |
| mv.visitVarInsn(ALOAD, 0); |
| mv.visitMethodInsn(INVOKESPECIAL, targetHolderName, targetMethod, "()V", false); |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(1, 1); |
| mv.visitEnd(); |
| return null; |
| } |
| }) |
| .transform(); |
| } |
| } |
| } |