blob: b165f4b66dd4125053fc52983af7c9eddf355a9a [file] [log] [blame]
// 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.internal;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.CompilationMode;
import com.android.tools.r8.D8;
import com.android.tools.r8.D8Command;
import com.android.tools.r8.DexIndexedConsumer;
import com.android.tools.r8.OutputMode;
import com.android.tools.r8.ProgramResource;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.AndroidAppConsumers;
import com.android.tools.r8.utils.ArtErrorParser;
import com.android.tools.r8.utils.ArtErrorParser.ArtErrorInfo;
import com.android.tools.r8.utils.ArtErrorParser.ArtErrorParserException;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.KeepingDiagnosticHandler;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.Reporter;
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.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ComparisonFailure;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
public abstract class CompilationTestBase extends TestBase {
protected KeepingDiagnosticHandler handler;
protected Reporter reporter;
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
@Before
public void reset() {
handler = new KeepingDiagnosticHandler();
reporter = new Reporter(handler);
}
public AndroidApp runAndCheckVerification(
CompilerUnderTest compiler,
CompilationMode mode,
String referenceApk,
List<String> pgConfs,
String input)
throws ExecutionException, IOException, CompilationFailedException {
return runAndCheckVerification(
compiler, mode, referenceApk, pgConfs, null, Collections.singletonList(input));
}
public AndroidApp runAndCheckVerification(D8Command.Builder builder, String referenceApk)
throws IOException, ExecutionException, CompilationFailedException {
AndroidAppConsumers appSink = new AndroidAppConsumers(builder);
D8.run(builder.build());
AndroidApp result = appSink.build();
checkVerification(result, referenceApk);
return result;
}
public void assertIdenticalZipFiles(File file1, File file2) throws IOException {
try (ZipFile zipFile1 = new ZipFile(file1); ZipFile zipFile2 = new ZipFile(file2)) {
final Enumeration<? extends ZipEntry> entries1 = zipFile1.entries();
final Enumeration<? extends ZipEntry> entries2 = zipFile2.entries();
while (entries1.hasMoreElements()) {
Assert.assertTrue(entries2.hasMoreElements());
ZipEntry entry1 = entries1.nextElement();
ZipEntry entry2 = entries2.nextElement();
Assert.assertEquals(entry1.getName(), entry2.getName());
Assert.assertEquals(entry1.getCrc(), entry2.getCrc());
Assert.assertEquals(entry1.getSize(), entry2.getSize());
}
}
}
public AndroidApp runAndCheckVerification(
CompilerUnderTest compiler,
CompilationMode mode,
String referenceApk,
List<String> pgConfs,
Consumer<InternalOptions> optionsConsumer,
List<String> inputs)
throws ExecutionException, IOException, CompilationFailedException {
return runAndCheckVerification(compiler, mode, referenceApk, pgConfs, optionsConsumer,
DexIndexedConsumer::emptyConsumer, inputs);
}
public AndroidApp runAndCheckVerification(
CompilerUnderTest compiler,
CompilationMode mode,
String referenceApk,
List<String> pgConfs,
Consumer<InternalOptions> optionsConsumer,
Supplier<DexIndexedConsumer> dexIndexedConsumerSupplier,
List<String> inputs)
throws ExecutionException, IOException, CompilationFailedException {
assertTrue(referenceApk == null || new File(referenceApk).exists());
AndroidAppConsumers outputApp;
if (compiler == CompilerUnderTest.R8) {
R8Command.Builder builder = R8Command.builder(reporter);
builder.addProgramFiles(ListUtils.map(inputs, Paths::get));
if (pgConfs != null) {
// Sanitize libraries for apps relying on the Proguard behaviour of lookup in program
// classes before library classes. See tools/sanitize_libraries.py for more information.
LibrarySanitizer librarySanitizer =
new LibrarySanitizer(temp)
.addProguardConfigurationFiles(
pgConfs.stream().map(Paths::get).collect(Collectors.toList()))
.sanitize();
builder.addLibraryFiles(librarySanitizer.getSanitizedLibrary());
builder.addProguardConfigurationFiles(librarySanitizer.getSanitizedProguardConfiguration());
} else {
builder.setDisableTreeShaking(true);
builder.setDisableMinification(true);
}
builder.setMode(mode);
builder.setProgramConsumer(dexIndexedConsumerSupplier.get());
builder.setMinApiLevel(AndroidApiLevel.L.getLevel());
ToolHelper.allowPartiallyImplementedProguardOptions(builder);
ToolHelper.addProguardConfigurationConsumer(
builder,
pgConfig -> {
pgConfig.setPrintSeeds(false);
pgConfig.setIgnoreWarnings(true);
});
outputApp = new AndroidAppConsumers(builder);
ToolHelper.runR8(builder.build(), optionsConsumer);
} else {
assert compiler == CompilerUnderTest.D8;
D8Command.Builder builder =
D8Command.builder()
.addProgramFiles(ListUtils.map(inputs, Paths::get))
.setMode(mode)
.setMinApiLevel(AndroidApiLevel.L.getLevel());
outputApp = new AndroidAppConsumers(builder);
D8.run(builder.build());
}
return checkVerification(outputApp.build(), referenceApk);
}
public AndroidApp checkVerification(AndroidApp outputApp, String referenceApk)
throws IOException, ExecutionException {
Path out = temp.getRoot().toPath().resolve("all.zip");
Path oatFile = temp.getRoot().toPath().resolve("all.oat");
outputApp.writeToZip(out, OutputMode.DexIndexed);
try {
ToolHelper.runDex2Oat(out, oatFile);
return outputApp;
} catch (AssertionError e) {
if (referenceApk == null) {
throw e;
}
CodeInspector theirs = new CodeInspector(Paths.get(referenceApk));
CodeInspector ours = new CodeInspector(out);
List<ArtErrorInfo> errors;
try {
errors = ArtErrorParser.parse(e.getMessage());
} catch (ArtErrorParserException parserException) {
System.err.println(parserException.toString());
throw e;
}
if (errors.isEmpty()) {
throw e;
}
for (ArtErrorInfo error : errors.subList(0, errors.size() - 1)) {
System.err.println(new ComparisonFailure(error.getMessage(),
"REFERENCE\n" + error.dump(theirs, false) + "\nEND REFERENCE",
"PROCESSED\n" + error.dump(ours, true) + "\nEND PROCESSED").toString());
}
ArtErrorInfo error = errors.get(errors.size() - 1);
throw new ComparisonFailure(error.getMessage(),
"REFERENCE\n" + error.dump(theirs, false) + "\nEND REFERENCE",
"PROCESSED\n" + error.dump(ours, true) + "\nEND PROCESSED");
}
}
public void assertIdenticalApplications(AndroidApp app1, AndroidApp app2)
throws IOException, ResourceException {
assertIdenticalApplications(app1, app2, false);
}
public void assertIdenticalApplications(AndroidApp app1, AndroidApp app2, boolean write)
throws IOException, ResourceException {
try (Closer closer = Closer.create()) {
if (write) {
app1.writeToDirectory(temp.newFolder("app1").toPath(), OutputMode.DexIndexed);
app2.writeToDirectory(temp.newFolder("app2").toPath(), OutputMode.DexIndexed);
}
List<ProgramResource> files1 = app1.getDexProgramResourcesForTesting();
List<ProgramResource> files2 = app2.getDexProgramResourcesForTesting();
assertEquals(files1.size(), files2.size());
for (int index = 0; index < files1.size(); index++) {
InputStream file1 = closer.register(files1.get(index).getByteStream());
InputStream file2 = closer.register(files2.get(index).getByteStream());
byte[] bytes1 = ByteStreams.toByteArray(file1);
byte[] bytes2 = ByteStreams.toByteArray(file2);
assertArrayEquals("File index " + index, bytes1, bytes2);
}
}
}
public void assertIdenticalApplicationsUpToCode(
AndroidApp app1, AndroidApp app2, boolean allowNewClassesInApp2)
throws IOException, ExecutionException {
CodeInspector inspect1 = new CodeInspector(app1);
CodeInspector inspect2 = new CodeInspector(app2);
class Pair<T> {
private T first;
private T second;
private void set(boolean selectFirst, T value) {
if (selectFirst) {
first = value;
} else {
second = value;
}
}
}
// Collect all classes from both inspectors, indexed by finalDescriptor.
Map<String, Pair<FoundClassSubject>> allClasses = new HashMap<>();
BiConsumer<CodeInspector, Boolean> collectClasses =
(inspector, selectFirst) -> {
inspector.forAllClasses(
clazz -> {
String finalDescriptor = clazz.getFinalDescriptor();
allClasses.compute(
finalDescriptor,
(k, v) -> {
if (v == null) {
v = new Pair<>();
}
v.set(selectFirst, clazz);
return v;
});
});
};
collectClasses.accept(inspect1, true);
collectClasses.accept(inspect2, false);
for (Map.Entry<String, Pair<FoundClassSubject>> classEntry : allClasses.entrySet()) {
String className = classEntry.getKey();
FoundClassSubject class1 = classEntry.getValue().first;
FoundClassSubject class2 = classEntry.getValue().second;
assert class1 != null || class2 != null;
if (!allowNewClassesInApp2) {
assertNotNull(String.format("Class %s is missing from the first app.", className), class1);
}
assertNotNull(String.format("Class %s is missing from the second app.", className), class2);
if (class1 == null) {
continue;
}
// Collect all methods for this class from both apps.
Map<MethodSignature, Pair<FoundMethodSubject>> allMethods = new HashMap<>();
BiConsumer<FoundClassSubject, Boolean> collectMethods =
(classSubject, selectFirst) -> {
classSubject.forAllMethods(
m -> {
MethodSignature fs = m.getFinalSignature();
allMethods.compute(
fs,
(k, v) -> {
if (v == null) {
v = new Pair<>();
}
v.set(selectFirst, m);
return v;
});
});
};
collectMethods.accept(class1, true);
collectMethods.accept(class2, false);
for (Map.Entry<MethodSignature, Pair<FoundMethodSubject>> methodEntry :
allMethods.entrySet()) {
MethodSignature signature = methodEntry.getKey();
FoundMethodSubject method1 = methodEntry.getValue().first;
FoundMethodSubject method2 = methodEntry.getValue().second;
assert method1 != null || method2 != null;
assertNotNull(
String.format(
"Method %s of class %s is missing from the first app.", signature, className),
method1);
assertNotNull(
String.format(
"Method %s of class %s is missing from the second app.", signature, className),
method2);
}
// Collect all fields for this class from both apps.
Map<FieldSignature, Pair<FoundFieldSubject>> allFields = new HashMap<>();
BiConsumer<FoundClassSubject, Boolean> collectFields =
(classSubject, selectFirst) -> {
classSubject.forAllFields(
f -> {
FieldSignature fs = f.getFinalSignature();
allFields.compute(
fs,
(k, v) -> {
if (v == null) {
v = new Pair<>();
}
v.set(selectFirst, f);
return v;
});
});
};
collectFields.accept(class1, true);
collectFields.accept(class2, false);
for (Map.Entry<FieldSignature, Pair<FoundFieldSubject>> fieldEntry : allFields.entrySet()) {
FieldSignature signature = fieldEntry.getKey();
FoundFieldSubject field1 = fieldEntry.getValue().first;
FoundFieldSubject field2 = fieldEntry.getValue().second;
assert field1 != null || field2 != null;
assertNotNull(
String.format(
"Field %s of class %s is missing from the first app.", signature, className),
field1);
assertNotNull(
String.format(
"Field %s of class %s is missing from the second app.", signature, className),
field2);
}
}
}
}