blob: 644fc9ca0620a601ab51dc21349ba13447847fbf [file] [log] [blame]
// 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.containsString;
import static org.junit.Assert.assertTrue;
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.isNewerThanOrEqual(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(r -> checkNonVerifyingResult(r, false));
}
private void checkNonVerifyingResult(TestRunResult<?> result, boolean isR8) {
// The input is invalid and any JVM will fail at verification time.
if (parameters.isCfRuntime()) {
result.assertFailureWithErrorThatThrows(VerifyError.class);
return;
}
// Dex results vary wildly...
Version version = parameters.getRuntime().asDex().getVm().getVersion();
if (!isR8 && version.isOlderThanOrEqual(Version.V4_4_4)) {
result.assertFailureWithErrorThatThrows(VerifyError.class);
} else if (version == Version.V5_1_1 || version == Version.V6_0_1) {
result.assertFailure();
} else {
result.assertSuccessWithOutputThatMatches(containsString(NoSuchMethodError.class.getName()));
}
}
@Test
public void testR8NonVerifying() throws Exception {
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)
.addOptionsModification(o -> o.testing.allowTypeErrors = true)
.run(parameters.getRuntime(), MainClassFailing.class)
.apply(r -> checkNonVerifyingResult(r, true));
}
/** 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(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();
}
}
}