| // Copyright (c) 2018, 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.shaking; |
| |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed; |
| import static org.hamcrest.CoreMatchers.not; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertThat; |
| |
| import com.android.tools.r8.ByteDataView; |
| import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer; |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.ToolHelper.DexVm.Version; |
| import com.android.tools.r8.ToolHelper.ProcessResult; |
| import com.android.tools.r8.ir.optimize.Inliner.Reason; |
| import com.android.tools.r8.jasmin.JasminBuilder; |
| import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder; |
| import com.android.tools.r8.utils.AndroidApp; |
| import com.android.tools.r8.utils.DescriptorUtils; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import java.nio.file.Path; |
| import java.util.Collection; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class NonVirtualOverrideTest extends TestBase { |
| |
| private static Class<?> main = NonVirtualOverrideTestClass.class; |
| private static Class<?> A = NonVirtualOverrideTestClass.A.class; |
| private static Class<?> B = NonVirtualOverrideTestClass.B.class; |
| private static Class<?> C = NonVirtualOverrideTestClass.C.class; |
| |
| private final Backend backend; |
| private final boolean enableClassInlining; |
| private final boolean enableVerticalClassMerging; |
| |
| @Parameterized.Parameters(name = "Backend: {0}, class inlining: {1}, vertical class merging: {2}") |
| public static Collection<Object[]> data() { |
| ImmutableList.Builder<Object[]> builder = ImmutableList.builder(); |
| for (Backend backend : Backend.values()) { |
| for (boolean enableClassInlining : ImmutableList.of(true, false)) { |
| for (boolean enableVerticalClassMerging : ImmutableList.of(true, false)) { |
| builder.add(new Object[] {backend, enableClassInlining, enableVerticalClassMerging}); |
| } |
| } |
| } |
| return builder.build(); |
| } |
| |
| public NonVirtualOverrideTest( |
| Backend backend, boolean enableClassInlining, boolean enableVerticalClassMerging) { |
| this.backend = backend; |
| this.enableClassInlining = enableClassInlining; |
| this.enableVerticalClassMerging = enableVerticalClassMerging; |
| } |
| |
| @Test |
| public void test() throws Exception { |
| // Construct B such that it inherits from A and shadows method A.m() with a private method. |
| JasminBuilder jasminBuilder = new JasminBuilder(); |
| ClassBuilder classBuilder = jasminBuilder.addClass(B.getName(), A.getName()); |
| classBuilder.addDefaultConstructor(); |
| for (String methodName : ImmutableList.of("m1", "m2")) { |
| classBuilder.addPrivateVirtualMethod( |
| methodName, ImmutableList.of(), "V", jasminCodeForPrinting("In B." + methodName + "()")); |
| } |
| for (String methodName : ImmutableList.of("m3", "m4")) { |
| classBuilder.addStaticMethod( |
| methodName, ImmutableList.of(), "V", jasminCodeForPrinting("In B." + methodName + "()")); |
| } |
| |
| AndroidApp input = |
| AndroidApp.builder() |
| .addProgramFiles( |
| ToolHelper.getClassFileForTestClass(main), |
| ToolHelper.getClassFileForTestClass(A), |
| ToolHelper.getClassFileForTestClass(C)) |
| .addClassProgramData(jasminBuilder.buildClasses()) |
| .build(); |
| |
| // Run the program using java. |
| String referenceResult; |
| if (backend == Backend.DEX |
| && ToolHelper.getDexVm().getVersion().isOlderThanOrEqual(Version.V7_0_0) |
| && ToolHelper.getDexVm().getVersion().isAtLeast(Version.V5_1_1)) { |
| referenceResult = |
| String.join( |
| System.lineSeparator(), |
| "In A.m1()", |
| "In A.m2()", |
| "In A.m3()", |
| "In A.m4()", |
| "In C.m1()", |
| "In A.m2()", |
| "In C.m3()", |
| "In A.m4()", |
| "In A.m1()", // With Java: Caught IllegalAccessError when calling B.m1() |
| "In A.m3()", // With Java: Caught IncompatibleClassChangeError when calling B.m3() |
| "In C.m1()", // With Java: Caught IllegalAccessError when calling B.m1() |
| "In C.m3()", // With Java: Caught IncompatibleClassChangeError when calling B.m3() |
| "In C.m1()", |
| "In C.m3()", |
| ""); |
| } else { |
| Path referenceJar = temp.getRoot().toPath().resolve("input.jar"); |
| ArchiveConsumer inputConsumer = new ArchiveConsumer(referenceJar); |
| for (Class<?> clazz : ImmutableList.of(main, A, C)) { |
| inputConsumer.accept( |
| ByteDataView.of(ToolHelper.getClassAsBytes(clazz)), |
| DescriptorUtils.javaTypeToDescriptor(clazz.getName()), |
| null); |
| } |
| inputConsumer.accept( |
| ByteDataView.of(jasminBuilder.buildClasses().get(0)), |
| DescriptorUtils.javaTypeToDescriptor(B.getName()), |
| null); |
| inputConsumer.finished(null); |
| |
| ProcessResult javaResult = ToolHelper.runJava(referenceJar, main.getName()); |
| assertEquals(javaResult.exitCode, 0); |
| referenceResult = javaResult.stdout; |
| } |
| |
| // Run the program on Art after is has been compiled with R8. |
| AndroidApp compiled = |
| compileWithR8( |
| input, |
| keepMainProguardConfiguration(main), |
| options -> { |
| options.enableClassInlining = enableClassInlining; |
| options.enableVerticalClassMerging = enableVerticalClassMerging; |
| options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE); |
| }, |
| backend); |
| assertEquals(referenceResult, runOnVM(compiled, main, backend)); |
| |
| // Check that B is present and that it doesn't contain the unused private method m2. |
| if (!enableClassInlining && !enableVerticalClassMerging) { |
| CodeInspector inspector = new CodeInspector(compiled); |
| ClassSubject classSubject = inspector.clazz(B.getName()); |
| assertThat(classSubject, isRenamed()); |
| assertThat(classSubject.method("void", "m1", ImmutableList.of()), isPresent()); |
| assertThat(classSubject.method("void", "m2", ImmutableList.of()), not(isPresent())); |
| assertThat(classSubject.method("void", "m3", ImmutableList.of()), isPresent()); |
| assertThat(classSubject.method("void", "m4", ImmutableList.of()), not(isPresent())); |
| } |
| } |
| |
| private static String[] jasminCodeForPrinting(String message) { |
| return new String[] { |
| ".limit stack 2", |
| ".limit locals 1", |
| "getstatic java/lang/System/out Ljava/io/PrintStream;", |
| "ldc \"" + message + "\"", |
| "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V", |
| "return" |
| }; |
| } |
| } |
| |
| class NonVirtualOverrideTestClass { |
| |
| public static void main(String[] args) { |
| A a = new B(); |
| a.m1(); |
| a.m2(); |
| a.m3(); |
| a.m4(); |
| |
| a = new C(); |
| a.m1(); |
| a.m2(); |
| a.m3(); |
| a.m4(); |
| |
| B b = new B(); |
| try { |
| b.m1(); |
| } catch (IllegalAccessError exception) { |
| System.out.println("Caught IllegalAccessError when calling B.m1()"); |
| } |
| try { |
| b.m3(); |
| } catch (IncompatibleClassChangeError exception) { |
| System.out.println("Caught IncompatibleClassChangeError when calling B.m3()"); |
| } |
| |
| try { |
| b = new C(); |
| b.m1(); |
| } catch (IllegalAccessError exception) { |
| System.out.println("Caught IllegalAccessError when calling B.m1()"); |
| } |
| try { |
| b = new C(); |
| b.m3(); |
| } catch (IncompatibleClassChangeError exception) { |
| System.out.println("Caught IncompatibleClassChangeError when calling B.m3()"); |
| } |
| |
| C c = new C(); |
| c.m1(); |
| c.m3(); |
| } |
| |
| static class A { |
| |
| public void m1() { |
| System.out.println("In A.m1()"); |
| } |
| |
| public void m2() { |
| System.out.println("In A.m2()"); |
| } |
| |
| public void m3() { |
| System.out.println("In A.m3()"); |
| } |
| |
| public void m4() { |
| System.out.println("In A.m4()"); |
| } |
| } |
| |
| static class B extends A { |
| |
| // Will be made private with Jasmin. This method is targeted and can therefore not be removed. |
| public void m1() { |
| System.out.println("In B.m1()"); |
| } |
| |
| // Will be made private with Jasmin. Ends up being dead code because the method is never called. |
| public void m2() { |
| System.out.println("In B.m2()"); |
| } |
| |
| // Will be made static with Jasmin. This method is targeted and can therefore not be removed. |
| public void m3() { |
| System.out.println("In B.m3()"); |
| } |
| |
| // Will be made static with Jasmin. Ends up being dead code because the method is never called. |
| public void m4() { |
| System.out.println("In B.m4()"); |
| } |
| } |
| |
| static class C extends B { |
| |
| public void m1() { |
| System.out.println("In C.m1()"); |
| } |
| |
| public void m3() { |
| System.out.println("In C.m3()"); |
| } |
| } |
| } |