blob: 77c1e538a342bf122609e3a6c09c83ec64be1b27 [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.tracereferences;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.DiagnosticsChecker;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.tracereferences.TraceReferencesFormattingConsumer.OutputFormat;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import kotlin.text.Charsets;
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 TraceReferencesCommandTest extends TestBase {
@Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters().withNoneRuntime().build();
}
public TraceReferencesCommandTest(TestParameters parameters) {}
@Test(expected = CompilationFailedException.class)
public void emptyBuilder() throws Throwable {
TraceReferencesCommand.builder().build();
}
@Test(expected = CompilationFailedException.class)
public void emptyRun() throws Throwable {
DiagnosticsChecker.checkErrorsContains(
"No library specified",
handler -> {
TraceReferences.run(TraceReferencesCommand.builder(handler).build());
});
}
@Test(expected = CompilationFailedException.class)
public void emptyRunCommandLine() throws Throwable {
DiagnosticsChecker.checkErrorsContains(
"No library specified",
handler -> {
TraceReferences.run(
TraceReferencesCommand.parse(new String[] {""}, Origin.unknown(), handler).build());
});
}
@Test(expected = CompilationFailedException.class)
public void onlyLibrarySpecified() throws Throwable {
DiagnosticsChecker.checkErrorsContains(
"No source specified",
handler -> {
TraceReferences.run(
TraceReferencesCommand.builder(handler)
.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
.build());
});
}
@Test(expected = CompilationFailedException.class)
public void onlyLibrarySpecifiedCommandLine() throws Throwable {
DiagnosticsChecker.checkErrorsContains(
"No source specified",
handler -> {
TraceReferences.run(
TraceReferencesCommand.parse(
new String[] {
"--lib", ToolHelper.getAndroidJar(AndroidApiLevel.P).toString()
},
Origin.unknown(),
handler)
.build());
});
}
@Test(expected = CompilationFailedException.class)
public void invalidFormatCommandLine() throws Throwable {
DiagnosticsChecker.checkErrorsContains(
"Unsupported format 'xxx'",
handler -> {
TraceReferences.run(
TraceReferencesCommand.parse(
new String[] {"--format", "xxx"}, Origin.unknown(), handler)
.build());
});
}
@Test(expected = CompilationFailedException.class)
public void missingFormatCommandLine() throws Throwable {
DiagnosticsChecker.checkErrorsContains(
"Missing parameter for --format",
handler -> {
TraceReferences.run(
TraceReferencesCommand.parse(new String[] {"--format"}, Origin.unknown(), handler)
.build());
});
}
@Test(expected = CompilationFailedException.class)
public void multipleFormatsCommandLine() throws Throwable {
DiagnosticsChecker.checkErrorsContains(
"--format specified multiple times",
handler -> {
TraceReferences.run(
TraceReferencesCommand.parse(
new String[] {"--format", "printuses", "--format", "keep"},
Origin.unknown(),
handler)
.build());
});
}
private String formatName(OutputFormat format) {
if (format == OutputFormat.PRINTUSAGE) {
return "printuses";
}
if (format == OutputFormat.KEEP_RULES) {
return "keep";
}
assertSame(format, OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION);
return "keepallowobfuscation";
}
public void runAndCheckOutput(
Path targetJar,
Path sourceJar,
OutputFormat format,
String expected,
Consumer<DiagnosticsChecker> diagnosticsCheckerConsumer)
throws Throwable {
Path dir = temp.newFolder().toPath();
Path output = dir.resolve("output.txt");
DiagnosticsChecker diagnosticsChecker = new DiagnosticsChecker();
TraceReferencesFormattingConsumer consumer = new TraceReferencesFormattingConsumer(format);
try {
TraceReferences.run(
TraceReferencesCommand.builder(diagnosticsChecker)
.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
.addTargetFiles(targetJar)
.addSourceFiles(sourceJar)
.setConsumer(consumer)
.build());
assertEquals(expected, consumer.get());
if (diagnosticsCheckerConsumer != null) {
diagnosticsCheckerConsumer.accept(diagnosticsChecker);
} else {
assertEquals(0, diagnosticsChecker.errors.size());
assertEquals(0, diagnosticsChecker.warnings.size());
assertEquals(0, diagnosticsChecker.infos.size());
}
} catch (CompilationFailedException e) {
if (diagnosticsCheckerConsumer != null) {
diagnosticsCheckerConsumer.accept(diagnosticsChecker);
}
throw e;
}
TraceReferences.run(
TraceReferencesCommand.parse(
new String[] {
"--lib", ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
"--target", targetJar.toString(),
"--source", sourceJar.toString(),
"--output", output.toString(),
"--format", formatName(format)
},
Origin.unknown())
.build());
assertEquals(expected, FileUtils.readTextFile(output, Charsets.UTF_8));
}
public void runAndCheckOutput(
List<Class<?>> targetClasses,
List<Class<?>> sourceClasses,
OutputFormat format,
String expected)
throws Throwable {
runAndCheckOutput(targetClasses, sourceClasses, format, expected, null);
}
public void runAndCheckOutput(
List<Class<?>> targetClasses,
List<Class<?>> sourceClasses,
OutputFormat format,
String expected,
Consumer<DiagnosticsChecker> diagnosticsCheckerConsumer)
throws Throwable {
Path dir = temp.newFolder().toPath();
Path targetJar =
ZipBuilder.builder(dir.resolve("target.jar"))
.addFilesRelative(
ToolHelper.getClassPathForTests(),
targetClasses.stream()
.map(ToolHelper::getClassFileForTestClass)
.collect(Collectors.toList()))
.build();
Path sourceJar =
ZipBuilder.builder(dir.resolve("source.jar"))
.addFilesRelative(
ToolHelper.getClassPathForTests(),
sourceClasses.stream()
.map(ToolHelper::getClassFileForTestClass)
.collect(Collectors.toList()))
.build();
runAndCheckOutput(targetJar, sourceJar, format, expected, diagnosticsCheckerConsumer);
}
@Test
public void test_printUses() throws Throwable {
runAndCheckOutput(
ImmutableList.of(Target.class),
ImmutableList.of(Source.class),
OutputFormat.PRINTUSAGE,
StringUtils.lines(
ImmutableList.of(
"com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
"com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: void"
+ " method(int)",
"com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: int"
+ " field")));
}
@Test
public void test_keepRules() throws Throwable {
runAndCheckOutput(
ImmutableList.of(Target.class),
ImmutableList.of(Source.class),
OutputFormat.KEEP_RULES,
StringUtils.lines(
ImmutableList.of(
"-keep class com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target"
+ " {",
" public static void method(int);",
" int field;",
"}",
"-keeppackagenames com.android.tools.r8.tracereferences")));
}
@Test
public void test_keepRulesAllowObfuscation() throws Throwable {
runAndCheckOutput(
ImmutableList.of(Target.class),
ImmutableList.of(Source.class),
OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
StringUtils.lines(
ImmutableList.of(
"-keep,allowobfuscation class"
+ " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target {",
" public static void method(int);",
" int field;",
"}",
"-keeppackagenames com.android.tools.r8.tracereferences")));
}
private void checkTargetMissing(DiagnosticsChecker diagnosticsChecker) {
Field field;
Method method;
try {
field = Target.class.getField("field");
method = Target.class.getMethod("method", int.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
assertEquals(1, diagnosticsChecker.errors.size());
assertEquals(0, diagnosticsChecker.warnings.size());
assertEquals(0, diagnosticsChecker.infos.size());
diagnosticsChecker.checkErrorsContains(Reference.classFromClass(Target.class).toString());
diagnosticsChecker.checkErrorsContains(Reference.fieldFromField(field).toString());
diagnosticsChecker.checkErrorsContains(Reference.methodFromMethod(method).toString());
}
@Test
public void testNoReferences_printUses() throws Throwable {
try {
runAndCheckOutput(
ImmutableList.of(OtherTarget.class),
ImmutableList.of(Source.class),
OutputFormat.PRINTUSAGE,
StringUtils.lines(),
this::checkTargetMissing);
fail("Expected compilation to fail");
} catch (CompilationFailedException e) {
// Expected.
}
}
@Test
public void testMissingReference_keepRules() throws Throwable {
Field field = Target.class.getField("field");
Method method = Target.class.getMethod("method", int.class);
try {
runAndCheckOutput(
ImmutableList.of(OtherTarget.class),
ImmutableList.of(Source.class),
OutputFormat.KEEP_RULES,
StringUtils.lines(),
this::checkTargetMissing);
fail("Expected compilation to fail");
} catch (CompilationFailedException e) {
// Expected.
}
}
@Test
public void testMissingReference_keepRulesAllowObfuscation() throws Throwable {
try {
runAndCheckOutput(
ImmutableList.of(OtherTarget.class),
ImmutableList.of(Source.class),
OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
StringUtils.lines(),
this::checkTargetMissing);
fail("Expected compilation to fail");
} catch (CompilationFailedException e) {
// Expected.
}
}
@Test
public void testMissingReference_errorToWarning() throws Throwable {
Path dir = temp.newFolder().toPath();
Path targetJar =
ZipBuilder.builder(dir.resolve("target.jar"))
.addFilesRelative(
ToolHelper.getClassPathForTests(),
ToolHelper.getClassFileForTestClass(OtherTarget.class))
.build();
Path sourceJar =
ZipBuilder.builder(dir.resolve("source.jar"))
.addFilesRelative(
ToolHelper.getClassPathForTests(),
ToolHelper.getClassFileForTestClass(Source.class))
.build();
Path output = dir.resolve("output.txt");
DiagnosticsChecker diagnosticsChecker = new DiagnosticsChecker();
TraceReferences.run(
TraceReferencesCommand.parse(
new String[] {
"--lib",
ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
"--target",
targetJar.toString(),
"--source",
sourceJar.toString(),
"--output",
output.toString(),
"--format",
formatName(OutputFormat.PRINTUSAGE),
"--map-diagnostics:MissingDefinitionsDiagnostic",
"error",
"warning"
},
Origin.unknown(),
diagnosticsChecker)
.build());
assertEquals(0, diagnosticsChecker.errors.size());
assertEquals(1, diagnosticsChecker.warnings.size());
assertEquals(0, diagnosticsChecker.infos.size());
}
private void checkTargetPartlyMissing(DiagnosticsChecker diagnosticsChecker) {
Field field;
Method method;
try {
field = Target.class.getField("field");
method = Target.class.getMethod("method", int.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
assertEquals(1, diagnosticsChecker.errors.size());
assertEquals(0, diagnosticsChecker.warnings.size());
assertEquals(0, diagnosticsChecker.infos.size());
diagnosticsChecker.checkErrorsContains(Reference.fieldFromField(field).toString());
diagnosticsChecker.checkErrorsContains(Reference.methodFromMethod(method).toString());
}
@Test
public void testMissingDefinition_printUses() throws Throwable {
Path dir = temp.newFolder().toPath();
Path targetJar =
ZipBuilder.builder(dir.resolve("target.jar"))
.addBytes(
DescriptorUtils.getPathFromJavaType(Target.class), getClassWithTargetRemoved())
.build();
Path sourceJar =
ZipBuilder.builder(dir.resolve("source.jar"))
.addFilesRelative(
ToolHelper.getClassPathForTests(),
ToolHelper.getClassFileForTestClass(Source.class))
.build();
try {
runAndCheckOutput(
targetJar,
sourceJar,
OutputFormat.PRINTUSAGE,
StringUtils.lines(
ImmutableList.of(
"com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target")),
this::checkTargetPartlyMissing);
fail("Expected compilation to fail");
} catch (CompilationFailedException e) {
// Expected.
}
}
@Test
public void testMissingDefinition_keepRules() throws Throwable {
Path dir = temp.newFolder().toPath();
Path targetJar =
ZipBuilder.builder(dir.resolve("target.jar"))
.addBytes(
DescriptorUtils.getPathFromJavaType(Target.class), getClassWithTargetRemoved())
.build();
Path sourceJar =
ZipBuilder.builder(dir.resolve("source.jar"))
.addFilesRelative(
ToolHelper.getClassPathForTests(),
ToolHelper.getClassFileForTestClass(Source.class))
.build();
ZipUtils.zip(
sourceJar,
ToolHelper.getClassPathForTests(),
ToolHelper.getClassFileForTestClass(Source.class));
try {
runAndCheckOutput(
targetJar,
sourceJar,
OutputFormat.KEEP_RULES,
StringUtils.lines(
ImmutableList.of(
"-keep class"
+ " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target"
+ " {",
"}",
"-keeppackagenames com.android.tools.r8.tracereferences")),
this::checkTargetPartlyMissing);
fail("Expected compilation to fail");
} catch (CompilationFailedException e) {
// Expected.
}
}
@Test
public void testMissingDefinition_keepRulesAllowObfuscation() throws Throwable {
Path dir = temp.newFolder().toPath();
Path targetJar =
ZipBuilder.builder(dir.resolve("target.jar"))
.addBytes(
DescriptorUtils.getPathFromJavaType(Target.class), getClassWithTargetRemoved())
.build();
Path sourceJar =
ZipBuilder.builder(dir.resolve("source.jar"))
.addFilesRelative(
ToolHelper.getClassPathForTests(),
ToolHelper.getClassFileForTestClass(Source.class))
.build();
try {
runAndCheckOutput(
targetJar,
sourceJar,
OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
StringUtils.lines(
ImmutableList.of(
"-keep,allowobfuscation class"
+ " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target {",
"}",
"-keeppackagenames com.android.tools.r8.tracereferences")),
this::checkTargetPartlyMissing);
fail("Expected compilation to fail");
} catch (CompilationFailedException e) {
// Expected.
}
}
private byte[] getClassWithTargetRemoved() throws IOException {
return transformer(Target.class)
.removeMethods((access, name, descriptor, signature, exceptions) -> name.equals("method"))
.removeFields((access, name, descriptor, signature, value) -> name.equals("field"))
.transform();
}
static class Target {
public static int field;
public static void method(int i) {}
}
static class OtherTarget {
public static void method() {}
}
static class Source {
public static void source() {
Target.method(Target.field);
}
}
}