|  | // Copyright (c) 2016, 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 com.android.tools.r8.R8FullTestBuilder; | 
|  | import com.android.tools.r8.R8TestCompileResult; | 
|  | import com.android.tools.r8.TestBase; | 
|  | import com.android.tools.r8.TestCompilerBuilder.DiagnosticsConsumer; | 
|  | import com.android.tools.r8.TestParameters; | 
|  | import com.android.tools.r8.TestRuntime.CfRuntime; | 
|  | import com.android.tools.r8.ThrowableConsumer; | 
|  | import com.android.tools.r8.ToolHelper; | 
|  | import com.android.tools.r8.ToolHelper.ArtCommandBuilder; | 
|  | import com.android.tools.r8.ToolHelper.DexVm; | 
|  | import com.android.tools.r8.ToolHelper.ProcessResult; | 
|  | import com.android.tools.r8.naming.MemberNaming.FieldSignature; | 
|  | import com.android.tools.r8.naming.MemberNaming.MethodSignature; | 
|  | import com.android.tools.r8.utils.InternalOptions; | 
|  | import com.android.tools.r8.utils.ListUtils; | 
|  | import com.android.tools.r8.utils.ThrowingConsumer; | 
|  | import com.android.tools.r8.utils.codeinspector.ClassSubject; | 
|  | import com.android.tools.r8.utils.codeinspector.CodeInspector; | 
|  | import com.android.tools.r8.utils.codeinspector.FoundFieldSubject; | 
|  | import com.android.tools.r8.utils.codeinspector.FoundMethodSubject; | 
|  | import java.nio.file.Path; | 
|  | import java.nio.file.Paths; | 
|  | import java.util.Arrays; | 
|  | import java.util.Collections; | 
|  | import java.util.List; | 
|  | import java.util.function.BiConsumer; | 
|  | import java.util.function.Consumer; | 
|  | import org.junit.Assert; | 
|  | import org.junit.runners.Parameterized.Parameters; | 
|  |  | 
|  | /** | 
|  | * Base class of individual tree shaking tests in com.android.tools.r8.shaking.examples. | 
|  | * <p> | 
|  | * To add a new test, add Java files and keep rules to a new subdirectory of src/test/examples and | 
|  | * add a new subclass of TreeShakingTest. To run with multiple minification modes and | 
|  | * frontend/backend combinations, copy the Parameterized setup from one of the existing subclasses, | 
|  | * e.g. {@link com.android.tools.r8.shaking.examples.TreeShaking1Test}. Then create a test method | 
|  | * that calls {@link TreeShakingTest::runTest}, passing in the path to your keep rule file and | 
|  | * lambdas to determine if the right bits of the application are kept or discarded. | 
|  | */ | 
|  | public abstract class TreeShakingTest extends TestBase { | 
|  |  | 
|  | @Parameters(name = "{0} minify:{1}") | 
|  | public static List<Object[]> defaultTreeShakingParameters() { | 
|  | return data(MinifyMode.values()); | 
|  | } | 
|  |  | 
|  | public static List<Object[]> data(MinifyMode[] minifyModes) { | 
|  | return buildParameters(getTestParameters().withAllRuntimesAndApiLevels().build(), minifyModes); | 
|  | } | 
|  |  | 
|  | protected abstract String getName(); | 
|  |  | 
|  | protected abstract String getMainClass(); | 
|  |  | 
|  | protected final TestParameters parameters; | 
|  | private final MinifyMode minify; | 
|  |  | 
|  | public TestParameters getParameters() { | 
|  | return parameters; | 
|  | } | 
|  |  | 
|  | public MinifyMode getMinify() { | 
|  | return minify; | 
|  | } | 
|  |  | 
|  | public TreeShakingTest(TestParameters parameters, MinifyMode minify) { | 
|  | this.parameters = parameters; | 
|  | this.minify = minify; | 
|  | } | 
|  |  | 
|  | protected static void checkSameStructure(CodeInspector ref, CodeInspector inspector) { | 
|  | ref.forAllClasses( | 
|  | refClazz -> | 
|  | checkSameStructure( | 
|  | refClazz, inspector.clazz(refClazz.getDexProgramClass().toSourceString()))); | 
|  | } | 
|  |  | 
|  | private static void checkSameStructure(ClassSubject refClazz, ClassSubject clazz) { | 
|  | Assert.assertTrue(clazz.isPresent()); | 
|  | refClazz.forAllFields(refField -> checkSameStructure(refField, clazz)); | 
|  | refClazz.forAllMethods(refMethod -> checkSameStructure(refMethod, clazz)); | 
|  | } | 
|  |  | 
|  | private static void checkSameStructure(FoundMethodSubject refMethod, ClassSubject clazz) { | 
|  | MethodSignature signature = refMethod.getOriginalSignature(); | 
|  | // Don't check for existence of class initializers, as the code optimization can remove them. | 
|  | if (!refMethod.isClassInitializer()) { | 
|  | Assert.assertTrue( | 
|  | "Missing Method: " | 
|  | + clazz.getDexProgramClass().toSourceString() | 
|  | + "." | 
|  | + signature.toString(), | 
|  | clazz.method(signature).isPresent()); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static void checkSameStructure(FoundFieldSubject refField, ClassSubject clazz) { | 
|  | FieldSignature signature = refField.getOriginalSignature(); | 
|  | Assert.assertTrue( | 
|  | "Missing field: " + signature.type + " " + clazz.getOriginalDescriptor() | 
|  | + "." + signature.name, | 
|  | clazz.field(signature.type, signature.name).isPresent()); | 
|  | } | 
|  |  | 
|  | protected void runTest( | 
|  | ThrowingConsumer<CodeInspector, Exception> inspection, | 
|  | BiConsumer<String, String> outputComparator, | 
|  | BiConsumer<CodeInspector, CodeInspector> dexComparator, | 
|  | List<String> keepRulesFiles) | 
|  | throws Exception { | 
|  | runTest(inspection, outputComparator, dexComparator, keepRulesFiles, null, null, null); | 
|  | } | 
|  |  | 
|  | protected void runTest( | 
|  | ThrowingConsumer<CodeInspector, Exception> inspection, | 
|  | BiConsumer<String, String> outputComparator, | 
|  | BiConsumer<CodeInspector, CodeInspector> dexComparator, | 
|  | List<String> keepRulesFiles, | 
|  | Consumer<InternalOptions> optionsConsumer) | 
|  | throws Exception { | 
|  | runTest( | 
|  | inspection, outputComparator, dexComparator, keepRulesFiles, optionsConsumer, null, null); | 
|  | } | 
|  |  | 
|  | protected void runTest( | 
|  | ThrowingConsumer<CodeInspector, Exception> inspection, | 
|  | BiConsumer<String, String> outputComparator, | 
|  | BiConsumer<CodeInspector, CodeInspector> dexComparator, | 
|  | List<String> keepRulesFiles, | 
|  | Consumer<InternalOptions> optionsConsumer, | 
|  | ThrowableConsumer<R8FullTestBuilder> testBuilderConsumer) | 
|  | throws Exception { | 
|  | runTest( | 
|  | inspection, | 
|  | outputComparator, | 
|  | dexComparator, | 
|  | keepRulesFiles, | 
|  | optionsConsumer, | 
|  | testBuilderConsumer, | 
|  | null); | 
|  | } | 
|  |  | 
|  | protected void runTest( | 
|  | ThrowingConsumer<CodeInspector, Exception> inspection, | 
|  | BiConsumer<String, String> outputComparator, | 
|  | BiConsumer<CodeInspector, CodeInspector> dexComparator, | 
|  | List<String> keepRulesFiles, | 
|  | Consumer<InternalOptions> optionsConsumer, | 
|  | ThrowableConsumer<R8FullTestBuilder> testBuilderConsumer, | 
|  | DiagnosticsConsumer diagnosticsConsumer) | 
|  | throws Exception { | 
|  |  | 
|  | String programFile = ToolHelper.THIRD_PARTY_DIR + getName() + ".jar"; | 
|  |  | 
|  | R8FullTestBuilder testBuilder = | 
|  | testForR8(parameters.getBackend()) | 
|  | // Go through app builder to add dex files. | 
|  | .apply( | 
|  | b -> | 
|  | ToolHelper.getAppBuilder(b.getBuilder()) | 
|  | .addProgramFiles(Paths.get(programFile))) | 
|  | .enableProguardTestOptions() | 
|  | .addDontObfuscateUnless(minify.isMinify()) | 
|  | .setMinApi(parameters) | 
|  | .addKeepRuleFiles( | 
|  | ListUtils.map( | 
|  | keepRulesFiles, | 
|  | keepRulesFile -> Paths.get(ToolHelper.getProjectRoot(), keepRulesFile))) | 
|  | .addLibraryFiles(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR + "shakinglib.jar")) | 
|  | .addDefaultRuntimeLibrary(parameters) | 
|  | .addOptionsModification( | 
|  | options -> { | 
|  | options.inlinerOptions().enableInlining = programFile.contains("inlining"); | 
|  | if (optionsConsumer != null) { | 
|  | optionsConsumer.accept(options); | 
|  | } | 
|  | }) | 
|  | .allowStdoutMessages() | 
|  | .applyIf(testBuilderConsumer != null, testBuilderConsumer); | 
|  | R8TestCompileResult compileResult = | 
|  | diagnosticsConsumer == null | 
|  | ? testBuilder.compile() | 
|  | : testBuilder.compileWithExpectedDiagnostics(diagnosticsConsumer); | 
|  | Path outJar = compileResult.writeToZip(); | 
|  | if (parameters.isCfRuntime()) { | 
|  | Path shakinglib = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "shakinglib.jar"); | 
|  | CfRuntime cfRuntime = parameters.getRuntime().asCf(); | 
|  | ProcessResult resultInput = | 
|  | ToolHelper.runJava( | 
|  | cfRuntime, Arrays.asList(Paths.get(programFile), shakinglib), getMainClass()); | 
|  | Assert.assertEquals(0, resultInput.exitCode); | 
|  | ProcessResult resultOutput = | 
|  | ToolHelper.runJava(cfRuntime, Arrays.asList(outJar, shakinglib), getMainClass()); | 
|  | if (outputComparator != null) { | 
|  | outputComparator.accept(resultInput.stdout, resultOutput.stdout); | 
|  | } else { | 
|  | Assert.assertEquals(resultInput.toString(), resultOutput.toString()); | 
|  | } | 
|  | if (inspection != null) { | 
|  | compileResult.inspect(inspection); | 
|  | } | 
|  | return; | 
|  | } | 
|  | if (!ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) { | 
|  | return; | 
|  | } | 
|  | Path shakingLib = | 
|  | testForD8(Backend.DEX) | 
|  | .addProgramFiles(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR + "shakinglib.jar")) | 
|  | .setMinApi(parameters) | 
|  | .compile() | 
|  | .writeToZip(); | 
|  | Consumer<ArtCommandBuilder> extraArtArgs = | 
|  | builder -> { | 
|  | builder.appendClasspath(shakingLib.toString()); | 
|  | }; | 
|  | String d8Output = | 
|  | testForD8(Backend.DEX) | 
|  | .setMinApi(parameters) | 
|  | .addProgramFiles(Paths.get(programFile)) | 
|  | .compile() | 
|  | .writeToZip() | 
|  | .toString(); | 
|  | DexVm dexVm = parameters.getRuntime().asDex().getVm(); | 
|  | if (outputComparator != null) { | 
|  | String output1 = | 
|  | ToolHelper.runArtNoVerificationErrors( | 
|  | Collections.singletonList(d8Output), getMainClass(), extraArtArgs, dexVm); | 
|  | String output2 = | 
|  | ToolHelper.runArtNoVerificationErrors( | 
|  | Collections.singletonList(outJar.toString()), getMainClass(), extraArtArgs, dexVm); | 
|  | outputComparator.accept(output1, output2); | 
|  | } else { | 
|  | ToolHelper.checkArtOutputIdentical( | 
|  | Collections.singletonList(d8Output), | 
|  | Collections.singletonList(outJar.toString()), | 
|  | getMainClass(), | 
|  | extraArtArgs, | 
|  | null); | 
|  | } | 
|  | if (dexComparator != null) { | 
|  | CodeInspector ref = new CodeInspector(Paths.get(d8Output)); | 
|  | dexComparator.accept(ref, compileResult.inspector()); | 
|  | } | 
|  | if (inspection != null) { | 
|  | compileResult.inspect(inspection); | 
|  | } | 
|  | } | 
|  | } |