| // 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.TestDeps; | |
| import com.android.tools.r8.TestParameters; | |
| import com.android.tools.r8.TestParametersBuilder; | |
| 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.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 com.android.tools.r8.utils.internal.ListUtils; | |
| import com.android.tools.r8.utils.internal.ThrowingConsumer; | |
| 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 { | |
| public static TestParametersBuilder getTestParameters() { | |
| return TestBase.getTestParameters().withoutCollapsedDexRuntimes(); | |
| } | |
| @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 Path getProgramFile(); | |
| 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 { | |
| Path programFile = getProgramFile(); | |
| R8FullTestBuilder testBuilder = | |
| testForR8(parameters.getBackend()) | |
| // Go through app builder to add dex files. | |
| .apply(b -> ToolHelper.getAppBuilder(b.getBuilder()).addProgramFiles(programFile)) | |
| .enableProguardTestOptions() | |
| .addDontObfuscateUnless(minify.isMinify()) | |
| .setMinApi(parameters) | |
| .addKeepRuleFiles( | |
| ListUtils.map( | |
| keepRulesFiles, | |
| keepRulesFile -> Paths.get(ToolHelper.getProjectRoot(), keepRulesFile))) | |
| .addLibraryFiles(TestDeps.getExamplesPath("shakinglib.jar")) | |
| .addDefaultRuntimeLibrary(parameters) | |
| .addOptionsModification( | |
| options -> { | |
| options.inlinerOptions().enableInlining = | |
| programFile.toString().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 = TestDeps.getExamplesPath("shakinglib.jar"); | |
| CfRuntime cfRuntime = parameters.getRuntime().asCf(); | |
| ProcessResult resultInput = | |
| ToolHelper.runJava(cfRuntime, Arrays.asList(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(TestDeps.getExamplesPath("shakinglib.jar")) | |
| .setMinApi(parameters) | |
| .compile() | |
| .writeToZip(); | |
| Consumer<ArtCommandBuilder> extraArtArgs = | |
| builder -> { | |
| builder.appendClasspath(shakingLib.toString()); | |
| }; | |
| String d8Output = | |
| testForD8(Backend.DEX) | |
| .setMinApi(parameters) | |
| .addProgramFiles(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, | |
| dexVm); | |
| } | |
| if (dexComparator != null) { | |
| CodeInspector ref = new CodeInspector(Paths.get(d8Output)); | |
| dexComparator.accept(ref, compileResult.inspector()); | |
| } | |
| if (inspection != null) { | |
| compileResult.inspect(inspection); | |
| } | |
| } | |
| } |