blob: fdb7faae54527717512f977d650c2e5d5b1c40fc [file] [log] [blame]
// Copyright (c) 2019, 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.desugar.desugaredlibrary;
import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
import static junit.framework.Assert.assertNull;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.CompilationMode;
import com.android.tools.r8.DexIndexedConsumer;
import com.android.tools.r8.DiagnosticsHandler;
import com.android.tools.r8.L8Command;
import com.android.tools.r8.L8TestCompileResult;
import com.android.tools.r8.OutputMode;
import com.android.tools.r8.StringConsumer;
import com.android.tools.r8.StringResource;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.TestState;
import com.android.tools.r8.ThrowableConsumer;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
import com.android.tools.r8.ir.desugar.DesugaredLibraryConfigurationParser;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.tracereferences.TraceReferences;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidAppConsumers;
import com.android.tools.r8.utils.ConsumerUtils;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import org.junit.BeforeClass;
import org.junit.rules.TemporaryFolder;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
public class DesugaredLibraryTestBase extends TestBase {
private static final boolean FORCE_JDK11_DESUGARED_LIB = false;
@BeforeClass
public static void setUpDesugaredLibrary() {
if (!FORCE_JDK11_DESUGARED_LIB) {
return;
}
System.setProperty("desugar_jdk_json_dir", "src/library_desugar/jdk11");
System.setProperty(
"desugar_jdk_libs", "third_party/openjdk/desugar_jdk_libs_11/desugar_jdk_libs.jar");
System.out.println("Forcing the usage of JDK11 desugared library.");
}
public static boolean isJDK11DesugaredLibrary() {
String property = System.getProperty("desugar_jdk_json_dir", "");
return property.contains("jdk11");
}
// For conversions tests, we need DexRuntimes where classes to convert are present (DexRuntimes
// above N and O depending if Stream or Time APIs are used), but we need to compile the program
// with a minAPI below to force the use of conversions.
protected static TestParametersCollection getConversionParametersUpToExcluding(
AndroidApiLevel apiLevel) {
if (apiLevel == AndroidApiLevel.N) {
return getTestParameters()
.withDexRuntimesStartingFromIncluding(Version.V7_0_0)
.withApiLevelsEndingAtExcluding(AndroidApiLevel.N)
.build();
}
if (apiLevel == AndroidApiLevel.O) {
return getTestParameters()
.withDexRuntimesStartingFromIncluding(Version.V8_1_0)
.withApiLevelsEndingAtExcluding(AndroidApiLevel.O)
.build();
}
throw new Error("Unsupported conversion parameters");
}
protected boolean requiresEmulatedInterfaceCoreLibDesugaring(TestParameters parameters) {
return parameters.getApiLevel().isLessThan(apiLevelWithDefaultInterfaceMethodsSupport());
}
protected boolean requiresAnyCoreLibDesugaring(TestParameters parameters) {
return parameters.getApiLevel().getLevel() < AndroidApiLevel.O.getLevel();
}
public static class L8TestBuilder {
private final AndroidApiLevel apiLevel;
private final TestState state;
private CompilationMode mode = CompilationMode.RELEASE;
private String generatedKeepRules = null;
private List<String> keepRules = new ArrayList<>();
private List<Path> additionalProgramFiles = new ArrayList<>();
private Consumer<InternalOptions> optionsModifier = ConsumerUtils.emptyConsumer();
private Path desugarJDKLibs = ToolHelper.getDesugarJDKLibs();
private Path desugarJDKLibsConfiguration = ToolHelper.DESUGAR_LIB_CONVERSIONS;
private StringResource desugaredLibraryConfiguration =
StringResource.fromFile(ToolHelper.getDesugarLibJsonForTesting());
private List<Path> libraryFiles = new ArrayList<>();
public static L8TestBuilder builder(AndroidApiLevel apiLevel, TemporaryFolder temp) {
return new L8TestBuilder(apiLevel, temp);
}
private L8TestBuilder(AndroidApiLevel apiLevel, TemporaryFolder temp) {
this.apiLevel = apiLevel;
this.state = new TestState(temp);
}
public L8TestBuilder addProgramFiles(Collection<Path> programFiles) {
this.additionalProgramFiles.addAll(programFiles);
return this;
}
public L8TestBuilder addLibraryFiles(Path... libraryFiles) {
Collections.addAll(this.libraryFiles, libraryFiles);
return this;
}
public L8TestBuilder addGeneratedKeepRules(String generatedKeepRules) {
assertNull(this.generatedKeepRules);
this.generatedKeepRules = generatedKeepRules;
return this;
}
public L8TestBuilder addKeepRuleFile(Path keepRuleFile) throws IOException {
this.keepRules.add(FileUtils.readTextFile(keepRuleFile, StandardCharsets.UTF_8));
return this;
}
public L8TestBuilder addKeepRuleFiles(Collection<Path> keepRuleFiles) throws IOException {
for (Path keepRuleFile : keepRuleFiles) {
addKeepRuleFile(keepRuleFile);
}
return this;
}
public L8TestBuilder addOptionsModifier(Consumer<InternalOptions> optionsModifier) {
this.optionsModifier = this.optionsModifier.andThen(optionsModifier);
return this;
}
public L8TestBuilder applyIf(boolean condition, ThrowableConsumer<L8TestBuilder> thenConsumer) {
return applyIf(condition, thenConsumer, ThrowableConsumer.empty());
}
public L8TestBuilder applyIf(
boolean condition,
ThrowableConsumer<L8TestBuilder> thenConsumer,
ThrowableConsumer<L8TestBuilder> elseConsumer) {
if (condition) {
thenConsumer.acceptWithRuntimeException(this);
} else {
elseConsumer.acceptWithRuntimeException(this);
}
return this;
}
public L8TestBuilder setDebug() {
this.mode = CompilationMode.DEBUG;
return this;
}
public L8TestBuilder setDesugarJDKLibs(Path desugarJDKLibs) {
this.desugarJDKLibs = desugarJDKLibs;
return this;
}
public L8TestBuilder setDesugarJDKLibsConfiguration(Path desugarJDKLibsConfiguration) {
this.desugarJDKLibsConfiguration = desugarJDKLibsConfiguration;
return this;
}
public L8TestBuilder setDesugaredLibraryConfiguration(Path path) {
this.desugaredLibraryConfiguration = StringResource.fromFile(path);
return this;
}
private L8TestBuilder setDisableL8AnnotationRemoval(boolean disableL8AnnotationRemoval) {
return addOptionsModifier(
options -> options.testing.disableL8AnnotationRemoval = disableL8AnnotationRemoval);
}
public L8TestCompileResult compile()
throws IOException, CompilationFailedException, ExecutionException {
// We wrap exceptions in a RuntimeException to call this from a lambda.
AndroidAppConsumers sink = new AndroidAppConsumers();
L8Command.Builder l8Builder =
L8Command.builder(state.getDiagnosticsHandler())
.addProgramFiles(getProgramFiles())
.addLibraryFiles(getLibraryFiles())
.setMode(mode)
.addDesugaredLibraryConfiguration(desugaredLibraryConfiguration)
.setMinApiLevel(apiLevel.getLevel())
.setProgramConsumer(sink.wrapProgramConsumer(DexIndexedConsumer.emptyConsumer()));
Path mapping = null;
if (!keepRules.isEmpty() || generatedKeepRules != null) {
mapping = state.getNewTempFile("mapping.txt");
l8Builder
.addProguardConfiguration(
ImmutableList.<String>builder()
.addAll(keepRules)
.addAll(
generatedKeepRules != null
? ImmutableList.of(generatedKeepRules)
: Collections.emptyList())
.build(),
Origin.unknown())
.setProguardMapOutputPath(mapping);
}
ToolHelper.runL8(l8Builder.build(), optionsModifier);
return new L8TestCompileResult(sink.build(), apiLevel, generatedKeepRules, mapping, state)
.inspect(
inspector ->
inspector.forAllClasses(
clazz -> assertTrue(clazz.getFinalName().startsWith("j$."))));
}
private Collection<Path> getProgramFiles() {
return ImmutableList.<Path>builder()
.add(desugarJDKLibs)
.add(desugarJDKLibsConfiguration)
.addAll(additionalProgramFiles)
.build();
}
private Collection<Path> getLibraryFiles() {
return libraryFiles;
}
}
protected L8TestBuilder testForL8(AndroidApiLevel apiLevel) {
return new L8TestBuilder(apiLevel, temp);
}
protected Path buildDesugaredLibrary(AndroidApiLevel apiLevel) {
return buildDesugaredLibrary(apiLevel, null, false);
}
protected Path buildDesugaredLibrary(
AndroidApiLevel apiLevel, Consumer<InternalOptions> optionsModifier) {
return buildDesugaredLibrary(apiLevel, null, false, ImmutableList.of(), optionsModifier);
}
protected Path buildDesugaredLibrary(AndroidApiLevel apiLevel, String keepRules) {
return buildDesugaredLibrary(apiLevel, keepRules, true);
}
protected Path buildDesugaredLibrary(AndroidApiLevel apiLevel, String keepRules, boolean shrink) {
return buildDesugaredLibrary(apiLevel, keepRules, shrink, ImmutableList.of(), options -> {});
}
protected Path buildDesugaredLibrary(
AndroidApiLevel apiLevel,
String keepRules,
boolean shrink,
List<Path> additionalProgramFiles) {
return buildDesugaredLibrary(
apiLevel, keepRules, shrink, additionalProgramFiles, options -> {});
}
protected Path buildDesugaredLibrary(
AndroidApiLevel apiLevel,
String generatedKeepRules,
boolean release,
List<Path> additionalProgramFiles,
Consumer<InternalOptions> optionsModifier) {
try {
return testForL8(apiLevel)
.addProgramFiles(additionalProgramFiles)
.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
.applyIf(
release,
builder -> {
if (generatedKeepRules != null && !generatedKeepRules.trim().isEmpty()) {
builder.addGeneratedKeepRules(generatedKeepRules);
}
},
L8TestBuilder::setDebug)
.addOptionsModifier(optionsModifier)
// If we compile extended library here, it means we use TestNG. TestNG requires
// annotations, hence we disable annotation removal. This implies that extra warnings are
// generated.
.setDisableL8AnnotationRemoval(!additionalProgramFiles.isEmpty())
.compile()
.applyIf(
additionalProgramFiles.isEmpty(),
builder ->
builder.inspectDiagnosticMessages(
diagnostics ->
assertTrue(
diagnostics.getInfos().stream()
.noneMatch(
string ->
string
.getDiagnosticMessage()
.startsWith(
"Invalid parameter counts in MethodParameter"
+ " attributes.")))))
.writeToZip();
} catch (CompilationFailedException | ExecutionException | IOException e) {
throw new RuntimeException(e);
}
}
protected void assertLines2By2Correct(String stdOut) {
String[] lines = stdOut.split("\n");
assert lines.length % 2 == 0;
for (int i = 0; i < lines.length; i += 2) {
assertEquals(
"Different lines: " + lines[i] + " || " + lines[i + 1] + "\n" + stdOut,
lines[i],
lines[i + 1]);
}
}
protected static Path[] getAllFilesWithSuffixInDirectory(Path directory, String suffix)
throws IOException {
return Files.walk(directory)
.filter(path -> path.toString().endsWith(suffix))
.toArray(Path[]::new);
}
protected KeepRuleConsumer createKeepRuleConsumer(TestParameters parameters) {
if (requiresAnyCoreLibDesugaring(parameters)) {
return new PresentKeepRuleConsumer();
}
return new AbsentKeepRuleConsumer();
}
public Path getDesugaredLibraryInCF(
TestParameters parameters, Consumer<InternalOptions> configurationForLibraryCompilation)
throws IOException, CompilationFailedException {
Path desugaredLib = temp.newFolder().toPath().resolve("desugar_jdk_libs.jar");
L8Command.Builder l8Builder =
L8Command.builder()
.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
.addProgramFiles(ToolHelper.getDesugarJDKLibs())
.addProgramFiles(ToolHelper.DESUGAR_LIB_CONVERSIONS)
.setMode(CompilationMode.DEBUG)
.addDesugaredLibraryConfiguration(
StringResource.fromFile(ToolHelper.getDesugarLibJsonForTesting()))
.setMinApiLevel(parameters.getApiLevel().getLevel())
.setOutput(desugaredLib, OutputMode.ClassFile);
ToolHelper.runL8(l8Builder.build(), configurationForLibraryCompilation);
return desugaredLib;
}
protected DesugaredLibraryConfiguration configurationWithSupportAllCallbacksFromLibrary(
InternalOptions options,
boolean libraryCompilation,
TestParameters parameters,
boolean supportAllCallbacksFromLibrary) {
return new DesugaredLibraryConfigurationParser(
options.dexItemFactory(),
options.reporter,
libraryCompilation,
parameters.getApiLevel().getLevel())
.parse(
StringResource.fromFile(ToolHelper.getDesugarLibJsonForTesting()),
builder -> builder.setSupportAllCallbacksFromLibrary(supportAllCallbacksFromLibrary));
}
private Map<AndroidApiLevel, Path> desugaredLibraryClassFileCache = new HashMap<>();
// Build the desugared library in class file format.
public Path buildDesugaredLibraryClassFile(AndroidApiLevel apiLevel) throws Exception {
Path desugaredLib = desugaredLibraryClassFileCache.get(apiLevel);
if (desugaredLib != null) {
return desugaredLib;
}
desugaredLib = temp.newFolder().toPath().resolve("desugar_jdk_libs.jar");
L8Command.Builder l8Builder =
L8Command.builder()
.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
.addProgramFiles(ToolHelper.getDesugarJDKLibs())
.addProgramFiles(ToolHelper.DESUGAR_LIB_CONVERSIONS)
.setMode(CompilationMode.DEBUG)
.addDesugaredLibraryConfiguration(
StringResource.fromFile(ToolHelper.getDesugarLibJsonForTesting()))
.setMinApiLevel(apiLevel.getLevel())
.setOutput(desugaredLib, OutputMode.ClassFile);
ToolHelper.runL8(l8Builder.build());
desugaredLibraryClassFileCache.put(apiLevel, desugaredLib);
return desugaredLib;
}
public String collectKeepRulesWithTraceReferences(
Path desugaredProgramClassFile, Path desugaredLibraryClassFile) throws Exception {
Path generatedKeepRules = temp.newFile().toPath();
TraceReferences.run(
"--keep-rules",
"--lib",
ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
"--target",
desugaredLibraryClassFile.toString(),
"--source",
desugaredProgramClassFile.toString(),
"--output",
generatedKeepRules.toString(),
"--map-diagnostics",
"error",
"info");
return FileUtils.readTextFile(generatedKeepRules, Charsets.UTF_8);
}
protected static ClassFileInfo extractClassFileInfo(byte[] classFileBytes) {
class ClassFileInfoExtractor extends ClassVisitor {
private String classBinaryName;
private List<String> interfaces = new ArrayList<>();
private final List<String> methodNames = new ArrayList<>();
private ClassFileInfoExtractor() {
super(ASM_VERSION);
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
classBinaryName = name;
this.interfaces.addAll(Arrays.asList(interfaces));
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
methodNames.add(name);
return super.visitMethod(access, name, desc, signature, exceptions);
}
ClassFileInfo getClassFileInfo() {
return new ClassFileInfo(classBinaryName, interfaces, methodNames);
}
}
ClassReader reader = new ClassReader(classFileBytes);
ClassFileInfoExtractor extractor = new ClassFileInfoExtractor();
reader.accept(
extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return extractor.getClassFileInfo();
}
public interface KeepRuleConsumer extends StringConsumer {
String get();
}
public static class AbsentKeepRuleConsumer implements KeepRuleConsumer {
public String get() {
return null;
}
@Override
public void accept(String string, DiagnosticsHandler handler) {
throw new Unreachable("No desugaring on high API levels");
}
@Override
public void finished(DiagnosticsHandler handler) {
throw new Unreachable("No desugaring on high API levels");
}
}
public static class PresentKeepRuleConsumer implements KeepRuleConsumer {
StringBuilder stringBuilder = new StringBuilder();
String result = null;
@Override
public void accept(String string, DiagnosticsHandler handler) {
assert stringBuilder != null;
assert result == null;
stringBuilder.append(string);
}
@Override
public void finished(DiagnosticsHandler handler) {
assert stringBuilder != null;
assert result == null;
result = stringBuilder.toString();
stringBuilder = null;
}
public String get() {
// TODO(clement): remove that branch once StringConsumer has finished again.
if (stringBuilder != null) {
finished(null);
}
assert stringBuilder == null;
assert result != null;
return result;
}
}
protected static class ClassFileInfo {
private final String classBinaryName;
private List<String> interfaces;
private final List<String> methodNames;
ClassFileInfo(String classBinaryNamename, List<String> interfaces, List<String> methodNames) {
this.classBinaryName = classBinaryNamename;
this.interfaces = interfaces;
this.methodNames = methodNames;
}
public String getClassBinaryName() {
return classBinaryName;
}
public List<String> getInterfaces() {
return interfaces;
}
public List<String> getMethodNames() {
return methodNames;
}
}
}