|  | // 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.kotlin; | 
|  |  | 
|  | import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; | 
|  | import static org.hamcrest.CoreMatchers.not; | 
|  | import static org.hamcrest.MatcherAssert.assertThat; | 
|  | import static org.junit.Assert.assertEquals; | 
|  | import static org.junit.Assert.assertFalse; | 
|  | import static org.junit.Assert.assertNotNull; | 
|  | import static org.junit.Assert.assertTrue; | 
|  | import static org.junit.Assert.fail; | 
|  |  | 
|  | import com.android.tools.r8.KotlinTestBase; | 
|  | import com.android.tools.r8.OutputMode; | 
|  | import com.android.tools.r8.R8Command; | 
|  | import com.android.tools.r8.ToolHelper; | 
|  | import com.android.tools.r8.ToolHelper.KotlinTargetVersion; | 
|  | import com.android.tools.r8.graph.Code; | 
|  | import com.android.tools.r8.graph.DexCode; | 
|  | import com.android.tools.r8.jasmin.JasminBuilder; | 
|  | import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder; | 
|  | import com.android.tools.r8.naming.MemberNaming.MethodSignature; | 
|  | import com.android.tools.r8.origin.Origin; | 
|  | import com.android.tools.r8.utils.AndroidApp; | 
|  | import com.android.tools.r8.utils.DescriptorUtils; | 
|  | import com.android.tools.r8.utils.InternalOptions; | 
|  | import com.android.tools.r8.utils.StringUtils; | 
|  | import com.android.tools.r8.utils.codeinspector.ClassSubject; | 
|  | import com.android.tools.r8.utils.codeinspector.CodeInspector; | 
|  | import com.android.tools.r8.utils.codeinspector.FieldSubject; | 
|  | import com.android.tools.r8.utils.codeinspector.MethodSubject; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import java.nio.file.Path; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Arrays; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import java.util.function.Consumer; | 
|  | import org.junit.Assume; | 
|  |  | 
|  | public abstract class AbstractR8KotlinTestBase extends KotlinTestBase { | 
|  |  | 
|  | // This is the name of the Jasmin-generated class which contains the "main" method which will | 
|  | // invoke the tested method. | 
|  | private static final String JASMIN_MAIN_CLASS = "TestMain"; | 
|  |  | 
|  | protected final boolean allowAccessModification; | 
|  |  | 
|  | private final List<Path> classpath = new ArrayList<>(); | 
|  | private final List<Path> extraClasspath = new ArrayList<>(); | 
|  |  | 
|  | // Some tests defined in subclasses, e.g., Metadata tests, don't care about access relaxation. | 
|  | protected AbstractR8KotlinTestBase(KotlinTargetVersion kotlinTargetVersion) { | 
|  | this(kotlinTargetVersion, false); | 
|  | } | 
|  |  | 
|  | protected AbstractR8KotlinTestBase( | 
|  | KotlinTargetVersion kotlinTargetVersion, boolean allowAccessModification) { | 
|  | super(kotlinTargetVersion); | 
|  | this.allowAccessModification = allowAccessModification; | 
|  | } | 
|  |  | 
|  | protected void addExtraClasspath(Path path) { | 
|  | extraClasspath.add(path); | 
|  | } | 
|  |  | 
|  | protected static void checkMethodIsInvokedAtLeastOnce( | 
|  | DexCode dexCode, MethodSignature... methodSignatures) { | 
|  | for (MethodSignature methodSignature : methodSignatures) { | 
|  | checkMethodIsInvokedAtLeastOnce(dexCode, methodSignature); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static void checkMethodIsInvokedAtLeastOnce( | 
|  | DexCode dexCode, MethodSignature methodSignature) { | 
|  | assertTrue("No invoke to '" + methodSignature.toString() + "'", | 
|  | Arrays.stream(dexCode.instructions) | 
|  | .filter((instr) -> instr.getMethod() != null) | 
|  | .anyMatch((instr) -> instr.getMethod().name.toString().equals(methodSignature.name))); | 
|  | } | 
|  |  | 
|  | protected static void checkMethodIsNeverInvoked( | 
|  | DexCode dexCode, MethodSignature... methodSignatures) { | 
|  | for (MethodSignature methodSignature : methodSignatures) { | 
|  | checkMethodIsNeverInvoked(dexCode, methodSignature); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static void checkMethodIsNeverInvoked(DexCode dexCode, MethodSignature methodSignature) { | 
|  | assertTrue("At least one invoke to '" + methodSignature.toString() + "'", | 
|  | Arrays.stream(dexCode.instructions) | 
|  | .filter((instr) -> instr.getMethod() != null) | 
|  | .noneMatch((instr) -> instr.getMethod().name.toString().equals(methodSignature.name))); | 
|  | } | 
|  |  | 
|  | protected static void checkMethodsPresence( | 
|  | ClassSubject classSubject, Map<MethodSignature, Boolean> presenceMap) { | 
|  | presenceMap.forEach(((methodSignature, isPresent) -> { | 
|  | MethodSubject methodSubject = classSubject.method(methodSignature); | 
|  | String methodDesc = methodSignature.toString(); | 
|  | String failureMessage = isPresent | 
|  | ? "Method '" + methodDesc + "' should be present" | 
|  | : "Method '" + methodDesc + "' should not be present"; | 
|  |  | 
|  | assertEquals(failureMessage, isPresent, methodSubject.isPresent()); | 
|  | })); | 
|  | } | 
|  |  | 
|  | protected ClassSubject checkClassIsKept(CodeInspector inspector, String className) { | 
|  | checkClassExistsInInput(className); | 
|  | ClassSubject classSubject = inspector.clazz(className); | 
|  | assertNotNull(classSubject); | 
|  | assertTrue("No class " + className, classSubject.isPresent()); | 
|  | return classSubject; | 
|  | } | 
|  |  | 
|  | protected void checkClassIsRemoved(CodeInspector inspector, String className) { | 
|  | checkClassExistsInInput(className); | 
|  | ClassSubject classSubject = inspector.clazz(className); | 
|  | assertNotNull(classSubject); | 
|  | assertThat(classSubject, not(isPresent())); | 
|  | } | 
|  |  | 
|  | protected FieldSubject checkFieldIsKept( | 
|  | ClassSubject classSubject, String fieldType, String fieldName) { | 
|  | // Field must exist in the input. | 
|  | checkFieldPresenceInInput(classSubject.getOriginalName(), fieldType, fieldName, true); | 
|  | FieldSubject fieldSubject = classSubject.field(fieldType, fieldName); | 
|  | assertTrue("No field " + fieldName + " in " + classSubject.getOriginalName(), | 
|  | fieldSubject.isPresent()); | 
|  | return fieldSubject; | 
|  | } | 
|  |  | 
|  | protected FieldSubject checkFieldIsKept(ClassSubject classSubject, String fieldName) { | 
|  | FieldSubject fieldSubject = classSubject.uniqueFieldWithName(fieldName); | 
|  | assertThat(fieldSubject, isPresent()); | 
|  | return fieldSubject; | 
|  | } | 
|  |  | 
|  | protected void checkFieldIsRemoved( | 
|  | ClassSubject classSubject, String fieldType, String fieldName) { | 
|  | // Field must exist in the input. | 
|  | checkFieldPresenceInInput(classSubject.getOriginalName(), fieldType, fieldName, true); | 
|  | FieldSubject fieldSubject = classSubject.field(fieldType, fieldName); | 
|  | assertNotNull(fieldSubject); | 
|  | assertThat(fieldSubject, not(isPresent())); | 
|  | } | 
|  |  | 
|  | protected void checkFieldIsAbsent(ClassSubject classSubject, String fieldType, String fieldName) { | 
|  | // Field must NOT exist in the input. | 
|  | checkFieldPresenceInInput(classSubject.getOriginalName(), fieldType, fieldName, false); | 
|  | FieldSubject fieldSubject = classSubject.field(fieldType, fieldName); | 
|  | assertNotNull(fieldSubject); | 
|  | assertFalse(fieldSubject.isPresent()); | 
|  | } | 
|  |  | 
|  | protected FieldSubject checkFieldIsAbsent(ClassSubject classSubject, String fieldName) { | 
|  | FieldSubject fieldSubject = classSubject.uniqueFieldWithName(fieldName); | 
|  | assertThat(fieldSubject, not(isPresent())); | 
|  | return fieldSubject; | 
|  | } | 
|  |  | 
|  | protected void checkMethodIsAbsent(ClassSubject classSubject, MethodSignature methodSignature) { | 
|  | checkMethodPresenceInInput(classSubject.getOriginalName(), methodSignature, false); | 
|  | checkMethodPresenceInOutput(classSubject, methodSignature, false); | 
|  | } | 
|  |  | 
|  | protected MethodSubject checkMethodIsKept( | 
|  | ClassSubject classSubject, MethodSignature methodSignature) { | 
|  | checkMethodPresenceInInput(classSubject.getOriginalName(), methodSignature, true); | 
|  | return checkMethodIsKeptOrRemoved(classSubject, methodSignature, true); | 
|  | } | 
|  |  | 
|  | protected MethodSubject checkMethodIsKept(ClassSubject classSubject, String methodName) { | 
|  | MethodSubject methodSubject = classSubject.uniqueMethodWithName(methodName); | 
|  | assertThat(methodSubject, isPresent()); | 
|  | return methodSubject; | 
|  | } | 
|  |  | 
|  | protected void checkMethodIsRemoved(ClassSubject classSubject, MethodSignature methodSignature) { | 
|  | checkMethodPresenceInInput(classSubject.getOriginalName(), methodSignature, true); | 
|  | checkMethodIsKeptOrRemoved(classSubject, methodSignature, false); | 
|  | } | 
|  |  | 
|  | protected void checkMethodIsRemoved(ClassSubject classSubject, String methodName) { | 
|  | MethodSubject methodSubject = classSubject.uniqueMethodWithName(methodName); | 
|  | assertThat(methodSubject, not(isPresent())); | 
|  | } | 
|  |  | 
|  | protected MethodSubject checkMethodIsKeptOrRemoved( | 
|  | ClassSubject classSubject, MethodSignature methodSignature, boolean isPresent) { | 
|  | checkMethodPresenceInInput(classSubject.getOriginalName(), methodSignature, true); | 
|  | return checkMethodPresenceInOutput(classSubject, methodSignature, isPresent); | 
|  | } | 
|  |  | 
|  | private MethodSubject checkMethodPresenceInOutput( | 
|  | ClassSubject classSubject, MethodSignature methodSignature, boolean isPresent) { | 
|  | MethodSubject methodSubject = classSubject.method(methodSignature); | 
|  | assertNotNull(methodSubject); | 
|  |  | 
|  | String methodSig = methodSignature.name + methodSignature.toDescriptor(); | 
|  | if (isPresent) { | 
|  | assertTrue("No method " + methodSig + " in output", methodSubject.isPresent()); | 
|  | } else { | 
|  | assertFalse("Method " + methodSig + " exists in output", methodSubject.isPresent()); | 
|  | } | 
|  | return methodSubject; | 
|  | } | 
|  |  | 
|  | protected static DexCode getDexCode(MethodSubject methodSubject) { | 
|  | Code code = methodSubject.getMethod().getCode(); | 
|  | assertNotNull("No code for method " + methodSubject.getMethod().descriptor(), code); | 
|  | assertTrue(code.isDexCode()); | 
|  | return code.asDexCode(); | 
|  | } | 
|  |  | 
|  | private String buildProguardRules(String mainClass) { | 
|  | ProguardRulesBuilder proguardRules = new ProguardRulesBuilder(); | 
|  | proguardRules.appendWithLineSeparator(keepMainProguardConfiguration(mainClass)); | 
|  | proguardRules.dontObfuscate(); | 
|  | if (allowAccessModification) { | 
|  | proguardRules.allowAccessModification(); | 
|  | } | 
|  | return proguardRules.toString(); | 
|  | } | 
|  |  | 
|  | protected String keepAllMembers(String className) { | 
|  | return StringUtils.lines( | 
|  | "-keep class " + className + " {", | 
|  | "  *;", | 
|  | "}"); | 
|  | } | 
|  |  | 
|  | protected String keepMainMethod(String className) { | 
|  | return StringUtils.lines( | 
|  | "-keepclasseswithmembers class " + className + " {", | 
|  | "  public static void main(...);", | 
|  | "}"); | 
|  | } | 
|  |  | 
|  | protected String keepClassMethod(String className, MethodSignature methodSignature) { | 
|  | return StringUtils.lines( | 
|  | "-keep class " + className + " {", | 
|  | methodSignature.toString() + ";", | 
|  | "}"); | 
|  | } | 
|  |  | 
|  | protected String neverInlineMethod(String className, MethodSignature methodSignature) { | 
|  | return StringUtils.lines( | 
|  | "-neverinline class " + className + " {", | 
|  | methodSignature.toString() + ";", | 
|  | "}"); | 
|  | } | 
|  |  | 
|  | protected void runTest(String folder, String mainClass, | 
|  | AndroidAppInspector inspector) throws Exception { | 
|  | runTest(folder, mainClass, null, null, inspector); | 
|  | } | 
|  |  | 
|  | protected void runTest(String folder, String mainClass, | 
|  | Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception { | 
|  | runTest(folder, mainClass, null, optionsConsumer, inspector); | 
|  | } | 
|  |  | 
|  | protected void runTest(String folder, String mainClass, | 
|  | String extraProguardRules, AndroidAppInspector inspector) throws Exception { | 
|  | runTest(folder, mainClass, extraProguardRules, null, inspector); | 
|  | } | 
|  |  | 
|  | protected void runTest(String folder, String mainClass, String extraProguardRules, | 
|  | Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception { | 
|  | Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles()); | 
|  |  | 
|  | String proguardRules = buildProguardRules(mainClass); | 
|  | if (extraProguardRules != null) { | 
|  | proguardRules += extraProguardRules; | 
|  | } | 
|  |  | 
|  | // Build classpath for compilation (and java execution) | 
|  | classpath.clear(); | 
|  | classpath.add(getKotlinJarFile(folder)); | 
|  | classpath.add(getJavaJarFile(folder)); | 
|  | classpath.addAll(extraClasspath); | 
|  |  | 
|  | // Build with R8 | 
|  | AndroidApp.Builder builder = AndroidApp.builder(); | 
|  | builder.addProgramFiles(classpath); | 
|  | R8Command.Builder commandBuilder = | 
|  | ToolHelper.prepareR8CommandBuilder(builder.build(), emptyConsumer(Backend.DEX)) | 
|  | .addLibraryFiles(runtimeJar(Backend.DEX)) | 
|  | .addProguardConfiguration(ImmutableList.of(proguardRules), Origin.unknown()); | 
|  | ToolHelper.allowTestProguardOptions(commandBuilder); | 
|  | AndroidApp app = ToolHelper.runR8(commandBuilder.build(), optionsConsumer); | 
|  |  | 
|  | // Materialize file for execution. | 
|  | Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar"); | 
|  | app.writeToZip(generatedDexFile, OutputMode.DexIndexed); | 
|  |  | 
|  | // Run with ART. | 
|  | String artOutput = | 
|  | ToolHelper.runArtNoVerificationErrors(generatedDexFile.toString(), mainClass); | 
|  |  | 
|  | // Compare with Java. | 
|  | ToolHelper.ProcessResult javaResult = ToolHelper.runJava(classpath, mainClass); | 
|  | if (javaResult.exitCode != 0) { | 
|  | System.out.println(javaResult.stdout); | 
|  | System.err.println(javaResult.stderr); | 
|  | fail("JVM failed for: " + mainClass); | 
|  | } | 
|  | assertEquals("JVM and ART output differ", javaResult.stdout, artOutput); | 
|  |  | 
|  | if (inspector != null) { | 
|  | inspector.inspectApp(app); | 
|  | } | 
|  | } | 
|  |  | 
|  | protected void checkClassExistsInInput(String className) { | 
|  | if (!AsmUtils.doesClassExist(classpath, className)) { | 
|  | throw new AssertionError("Class " + className + " does not exist in input"); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void checkMethodPresenceInInput( | 
|  | String className, MethodSignature methodSignature, boolean isPresent) { | 
|  | boolean foundMethod = AsmUtils.doesMethodExist(classpath, className, | 
|  | methodSignature.name, methodSignature.toDescriptor()); | 
|  | if (isPresent != foundMethod) { | 
|  | throw new AssertionError( | 
|  | "Method " + methodSignature.name + methodSignature.toDescriptor() | 
|  | + " " + (foundMethod ? "exists" : "does not exist") | 
|  | + " in input class " + className + " but is expected to be " | 
|  | + (isPresent ? "present" : "absent")); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void checkFieldPresenceInInput( | 
|  | String className, String fieldType, String fieldName, boolean isPresent) { | 
|  | boolean foundField = AsmUtils.doesFieldExist(classpath, className, fieldName, fieldType); | 
|  | if (isPresent != foundField) { | 
|  | throw new AssertionError( | 
|  | "Field " + fieldName + " " + (foundField ? "exists" : "does not exist") | 
|  | + " in input class " + className + " but is expected to be " | 
|  | + (isPresent ? "present" : "absent")); | 
|  | } | 
|  | } | 
|  |  | 
|  | @FunctionalInterface | 
|  | public interface AndroidAppInspector { | 
|  |  | 
|  | void inspectApp(AndroidApp androidApp) throws Exception; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Generates a "main" class which invokes the given static method (which has no argument and | 
|  | * return void type). This new class is then added to the test classpath. | 
|  | * | 
|  | * @param methodClass the class of the static method to invoke | 
|  | * @param methodName the name of the static method to invoke | 
|  | * @return the name of the generated class | 
|  | */ | 
|  | protected String addMainToClasspath(String methodClass, String methodName) throws Exception { | 
|  | JasminBuilder builder = new JasminBuilder(); | 
|  | ClassBuilder mainClassBuilder = | 
|  | builder.addClass(DescriptorUtils.getBinaryNameFromJavaType(JASMIN_MAIN_CLASS)); | 
|  | mainClassBuilder.addMainMethod( | 
|  | "invokestatic " + methodClass + "/" + methodName + "()V", | 
|  | "return" | 
|  | ); | 
|  |  | 
|  | Path output = writeToJar(builder); | 
|  | addExtraClasspath(output); | 
|  | return JASMIN_MAIN_CLASS; | 
|  | } | 
|  | } |