blob: e8910675a8ab12df65d1235f3ad0607bb5c28013 [file] [log] [blame]
// Copyright (c) 2020, 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.relocator;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static junit.framework.TestCase.assertFalse;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.Pair;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
import com.android.tools.r8.utils.codeinspector.FoundFieldSubject;
import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
import com.android.tools.r8.utils.codeinspector.LocalVariableTable;
import com.android.tools.r8.utils.codeinspector.LocalVariableTable.LocalVariableTableEntry;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class RelocatorTest extends TestBase {
private final boolean external;
@Parameters(name = "{0}, external: {1}")
public static List<Object[]> data() {
return buildParameters(getTestParameters().withNoneRuntime().build(), BooleanUtils.values());
}
public RelocatorTest(TestParameters parameters, boolean external) {
this.external = external;
}
@Test
public void testRelocatorIdentity()
throws IOException, CompilationFailedException, ExecutionException {
Path output = temp.newFile("output.jar").toPath();
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, new HashMap<>(), output);
inspectAllClassesRelocated(ToolHelper.R8_WITH_DEPS_JAR, output, "", "");
}
@Test
public void testRelocatorEmptyToSomething() throws Exception {
String originalPrefix = "";
String newPrefix = "foo.bar.baz";
Path output = temp.newFile("output.jar").toPath();
Map<String, String> mapping = new HashMap<>();
mapping.put(originalPrefix, newPrefix);
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
// TODO(b/155618698): Extend relocator with a richer language such that java.lang.Object is not
// relocated.
RuntimeException compilationError =
assertThrows(
RuntimeException.class,
() ->
inspectAllClassesRelocated(
ToolHelper.R8_WITH_DEPS_JAR, output, originalPrefix, newPrefix + "."));
assertThat(compilationError.getMessage(), containsString("must extend class java.lang.Object"));
}
@Test
public void testRelocatorSomethingToEmpty() throws IOException {
String originalPrefix = "com.android.tools.r8";
String newPrefix = "";
Path output = temp.newFile("output.jar").toPath();
Map<String, String> mapping = new HashMap<>();
mapping.put(originalPrefix, newPrefix);
// TODO(b/155047618): Fixup the type after rewriting.
CompilationFailedException compilationFailedException =
assertThrows(
CompilationFailedException.class,
() -> {
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
});
}
@Test
public void testRelocateKeepsDebugInfo()
throws IOException, CompilationFailedException, ExecutionException {
String originalPrefix = "com.android.tools.r8";
String newPrefix = "com.android.tools.r8";
Path output = temp.newFile("output.jar").toPath();
Map<String, String> mapping = new HashMap<>();
mapping.put(originalPrefix, newPrefix);
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
// Assert that all classes are the same, have the same methods and debug info:
CodeInspector originalInspector = new CodeInspector(ToolHelper.R8_WITH_DEPS_JAR);
CodeInspector relocatedInspector = new CodeInspector(output);
for (FoundClassSubject clazz : originalInspector.allClasses()) {
ClassSubject relocatedClass = relocatedInspector.clazz(clazz.getFinalName());
assertThat(relocatedClass, isPresent());
assertEquals(
clazz.getDexProgramClass().sourceFile, relocatedClass.getDexProgramClass().sourceFile);
for (FoundMethodSubject originalMethod : clazz.allMethods()) {
MethodSubject relocatedMethod = relocatedClass.method(originalMethod.asMethodReference());
assertThat(relocatedMethod, isPresent());
assertEquals(originalMethod.hasLineNumberTable(), relocatedMethod.hasLineNumberTable());
if (originalMethod.hasLineNumberTable()) {
// TODO(b/155303677): Figure out why we cannot assert the same lines.
// assertEquals(
// originalMethod.getLineNumberTable().getLines().size(),
// relocatedMethod.getLineNumberTable().getLines().size());
}
assertEquals(
originalMethod.hasLocalVariableTable(), relocatedMethod.hasLocalVariableTable());
if (originalMethod.hasLocalVariableTable()) {
LocalVariableTable originalVariableTable = originalMethod.getLocalVariableTable();
LocalVariableTable relocatedVariableTable = relocatedMethod.getLocalVariableTable();
assertEquals(originalVariableTable.size(), relocatedVariableTable.size());
for (int i = 0; i < originalVariableTable.getEntries().size(); i++) {
LocalVariableTableEntry originalEntry = originalVariableTable.get(i);
LocalVariableTableEntry relocatedEntry = relocatedVariableTable.get(i);
assertEquals(originalEntry.name, relocatedEntry.name);
assertEquals(originalEntry.signature, relocatedEntry.signature);
assertEquals(originalEntry.type.toString(), relocatedEntry.type.toString());
}
}
}
}
}
@Test
public void testRelocateAll() throws IOException, CompilationFailedException, ExecutionException {
String originalPrefix = "com.android.tools.r8";
String newPrefix = "foo.bar.baz";
Map<String, String> mapping = new HashMap<>();
mapping.put("some.package.that.does.not.exist", "foo");
mapping.put(originalPrefix, newPrefix);
Path output = temp.newFile("output.jar").toPath();
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
inspectAllClassesRelocated(ToolHelper.R8_WITH_DEPS_JAR, output, originalPrefix, newPrefix);
}
@Test
public void testOrderingOfPrefixes() throws Exception {
String originalPrefix = "com.android";
String newPrefix = "foo.bar.baz";
Path output = temp.newFile("output.jar").toPath();
Map<String, String> mapping = new LinkedHashMap<>();
mapping.put(originalPrefix, newPrefix);
mapping.put("com.android.tools.r8", "qux");
CompilationFailedException exception =
assertThrows(
CompilationFailedException.class,
() -> runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output));
assertThat(
exception.getCause().getMessage(),
containsString("can be relocated by multiple mappings."));
}
@Test
public void testNoReEntry() throws IOException, CompilationFailedException, ExecutionException {
// TODO(b/154909222): Check if this is the behavior we would like.
String originalPrefix = "com.android";
String newPrefix = "foo.bar.baz";
Map<String, String> mapping = new LinkedHashMap<>();
mapping.put(originalPrefix, newPrefix);
mapping.put(newPrefix, "qux");
Path output = temp.newFile("output.jar").toPath();
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
inspectAllClassesRelocated(ToolHelper.R8_WITH_DEPS_JAR, output, originalPrefix, newPrefix);
// Assert that no mappings of com.android.tools.r8 -> qux exists.
CodeInspector inspector = new CodeInspector(output);
assertFalse(
inspector.allClasses().stream().anyMatch(clazz -> clazz.getFinalName().startsWith("qux")));
}
@Test
public void testMultiplePackages()
throws IOException, ExecutionException, CompilationFailedException {
Set<String> seenPackages = new HashSet<>();
List<Pair<String, String>> packageMappings = new ArrayList<>();
Map<String, String> mapping = new LinkedHashMap<>();
CodeInspector inspector = new CodeInspector(ToolHelper.R8_WITH_DEPS_JAR);
int packageNameCounter = 0;
// Generate a mapping for each package name directly below com.android.tools.r8.
for (FoundClassSubject clazz : inspector.allClasses()) {
String packageName = clazz.getDexProgramClass().getType().getPackageName();
String prefix = "com.android.tools.r8.";
if (!packageName.startsWith(prefix)) {
continue;
}
int nextPackageNameIndex = packageName.indexOf('.', prefix.length());
if (nextPackageNameIndex > prefix.length()) {
String mappedPackageName =
prefix + packageName.substring(prefix.length(), nextPackageNameIndex);
if (seenPackages.add(mappedPackageName)) {
String relocatedPackageName = "number" + packageNameCounter++;
packageMappings.add(new Pair<>(mappedPackageName, relocatedPackageName));
mapping.put(mappedPackageName, relocatedPackageName);
}
}
}
Path output = temp.newFile("output.jar").toPath();
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
for (Pair<String, String> packageMapping : packageMappings) {
inspectAllClassesRelocated(
ToolHelper.R8_WITH_DEPS_JAR,
output,
packageMapping.getFirst(),
packageMapping.getSecond());
}
}
@Test
public void testPartialPrefix()
throws CompilationFailedException, IOException, ExecutionException {
String originalPrefix = "com.android.tools.r";
String newPrefix = "i_cannot_w";
Map<String, String> mapping = new LinkedHashMap<>();
mapping.put(originalPrefix, newPrefix);
Path output = temp.newFile("output.jar").toPath();
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
inspectAllClassesRelocated(ToolHelper.R8_WITH_DEPS_JAR, output, originalPrefix, originalPrefix);
}
@Test
public void testBootstrap() throws IOException, CompilationFailedException, ExecutionException {
String originalPrefix = "com.android.tools.r8";
String newPrefix = "relocated_r8";
Map<String, String> mapping = new LinkedHashMap<>();
mapping.put(originalPrefix, newPrefix);
Path output = temp.newFile("output.jar").toPath();
runRelocator(ToolHelper.R8_WITH_DEPS_JAR, mapping, output);
// Check that all classes has been remapped.
inspectAllClassesRelocated(ToolHelper.R8_WITH_DEPS_JAR, output, originalPrefix, newPrefix);
inspectAllSignaturesNotContainingString(output, originalPrefix);
// We should be able to call the relocated relocator.
Path bootstrapOutput = temp.newFile("bootstrap.jar").toPath();
ProcessResult processResult =
ToolHelper.runJava(
output,
newPrefix + ".SwissArmyKnife",
"relocator",
"--input",
output.toString(),
"--output",
bootstrapOutput.toString(),
"--map",
newPrefix + "->" + originalPrefix);
System.out.println(processResult.stderr);
assertEquals(0, processResult.exitCode);
inspectAllClassesRelocated(output, bootstrapOutput, newPrefix, originalPrefix);
inspectAllSignaturesNotContainingString(bootstrapOutput, newPrefix);
// Assert that this is infact an identity transformation.
inspectAllClassesRelocated(ToolHelper.R8_WITH_DEPS_JAR, bootstrapOutput, "", "");
}
@Test
public void testNest() throws IOException, CompilationFailedException, ExecutionException {
String originalPrefix = "com.android.tools.r8";
String newPrefix = "com.android.tools.r8";
Path output = temp.newFile("output.jar").toPath();
Map<String, String> mapping = new HashMap<>();
mapping.put(originalPrefix, newPrefix);
runRelocator(ToolHelper.R8_WITH_DEPS_11_JAR, mapping, output);
// Assert that all classes are the same, have the same methods and nest info.
CodeInspector originalInspector = new CodeInspector(ToolHelper.R8_WITH_DEPS_11_JAR);
CodeInspector relocatedInspector = new CodeInspector(output);
for (FoundClassSubject originalSubject : originalInspector.allClasses()) {
ClassSubject relocatedSubject = relocatedInspector.clazz(originalSubject.getFinalName());
assertThat(relocatedSubject, isPresent());
DexClass originalClass = originalSubject.getDexProgramClass();
DexClass relocatedClass = relocatedSubject.getDexProgramClass();
assertEquals(originalClass.isNestHost(), relocatedClass.isNestHost());
assertEquals(originalClass.isNestMember(), relocatedClass.isNestMember());
if (originalClass.isInANest()) {
assertEquals(
originalClass.getNestHost().descriptor, relocatedClass.getNestHost().descriptor);
}
}
}
private void runRelocator(Path input, Map<String, String> mapping, Path output)
throws CompilationFailedException {
if (external) {
List<String> args = new ArrayList<>();
args.add("--input");
args.add(input.toString());
args.add("--output");
args.add(output.toString());
mapping.forEach(
(key, value) -> {
args.add("--map");
args.add(key + "->" + value);
});
RelocatorCommandLine.run(args.toArray(new String[0]));
} else {
RelocatorCommand.Builder builder =
RelocatorCommand.builder().addProgramFiles(input).setOutputPath(output);
mapping.forEach(
(key, value) ->
builder.addPackageMapping(
Reference.packageFromString(key), Reference.packageFromString(value)));
Relocator.run(builder.build());
}
}
private void inspectAllClassesRelocated(
Path original, Path relocated, String originalPrefix, String newPrefix)
throws IOException, ExecutionException {
CodeInspector originalInspector = new CodeInspector(original);
CodeInspector relocatedInspector = new CodeInspector(relocated);
for (FoundClassSubject clazz : originalInspector.allClasses()) {
if (originalPrefix.isEmpty()
|| clazz
.getFinalName()
.startsWith(originalPrefix + DescriptorUtils.JAVA_PACKAGE_SEPARATOR)) {
String relocatedName = newPrefix + clazz.getFinalName().substring(originalPrefix.length());
ClassSubject relocatedClass = relocatedInspector.clazz(relocatedName);
assertThat(relocatedClass, isPresent());
}
}
}
private void inspectAllSignaturesNotContainingString(Path relocated, String originalPrefix)
throws IOException, ExecutionException {
CodeInspector relocatedInspector = new CodeInspector(relocated);
for (FoundClassSubject clazz : relocatedInspector.allClasses()) {
assertThat(clazz.getFinalSignatureAttribute(), not(containsString(originalPrefix)));
for (FoundMethodSubject method : clazz.allMethods()) {
assertThat(method.getJvmMethodSignatureAsString(), not(containsString(originalPrefix)));
}
for (FoundFieldSubject field : clazz.allFields()) {
assertThat(field.getJvmFieldSignatureAsString(), not(containsString(originalPrefix)));
}
}
}
}