blob: ce03733167a41feed3072ac66f6b93fb4ba6b892 [file] [log] [blame]
// Copyright (c) 2018, 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;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.TestBase.Backend;
import com.android.tools.r8.debug.DebugTestConfig;
import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase.KeepRuleConsumer;
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.ForwardingOutputStream;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ThrowingOutputStream;
import com.google.common.base.Suppliers;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public abstract class TestCompilerBuilder<
C extends BaseCompilerCommand,
B extends BaseCompilerCommand.Builder<C, B>,
CR extends TestCompileResult<CR, RR>,
RR extends TestRunResult,
T extends TestCompilerBuilder<C, B, CR, RR, T>>
extends TestBaseBuilder<C, B, CR, RR, T> {
public static final Consumer<InternalOptions> DEFAULT_OPTIONS =
options -> {
options.testing.allowClassInlinerGracefulExit = false;
options.testing.reportUnusedProguardConfigurationRules = true;
};
final Backend backend;
// Default initialized setup. Can be overwritten if needed.
private boolean allowStdoutMessages = false;
private boolean allowStderrMessages = false;
private boolean useDefaultRuntimeLibrary = true;
private final List<Path> additionalRunClassPath = new ArrayList<>();
private ProgramConsumer programConsumer;
private StringConsumer mainDexListConsumer;
private AndroidApiLevel defaultMinApiLevel = ToolHelper.getMinApiLevelForDexVm();
private Consumer<InternalOptions> optionsConsumer = DEFAULT_OPTIONS;
private ByteArrayOutputStream stdout = null;
private PrintStream oldStdout = null;
private ByteArrayOutputStream stderr = null;
private PrintStream oldStderr = null;
protected OutputMode outputMode = OutputMode.DexIndexed;
TestCompilerBuilder(TestState state, B builder, Backend backend) {
super(state, builder);
this.backend = backend;
if (backend == Backend.DEX) {
setOutputMode(OutputMode.DexIndexed);
} else {
assert backend == Backend.CF;
setOutputMode(OutputMode.ClassFile);
}
}
abstract CR internalCompile(
B builder, Consumer<InternalOptions> optionsConsumer, Supplier<AndroidApp> app)
throws CompilationFailedException;
public T addOptionsModification(Consumer<InternalOptions> optionsConsumer) {
if (optionsConsumer != null) {
this.optionsConsumer = this.optionsConsumer.andThen(optionsConsumer);
}
return self();
}
public T allowCheckDiscardedErrors() {
return addOptionsModification(options -> options.testing.allowCheckDiscardedErrors = true);
}
public CR compile() throws CompilationFailedException {
AndroidAppConsumers sink = new AndroidAppConsumers();
builder.setProgramConsumer(sink.wrapProgramConsumer(programConsumer));
builder.setMainDexListConsumer(mainDexListConsumer);
if (backend == Backend.DEX && defaultMinApiLevel != null) {
assert !builder.isMinApiLevelSet()
: "Don't set the API level directly through BaseCompilerCommand.Builder in tests";
builder.setMinApiLevel(defaultMinApiLevel.getLevel());
}
if (useDefaultRuntimeLibrary) {
if (backend == Backend.DEX && builder.isMinApiLevelSet()) {
builder.addLibraryFiles(
ToolHelper.getFirstSupportedAndroidJar(
AndroidApiLevel.getAndroidApiLevel(builder.getMinApiLevel())));
} else {
builder.addLibraryFiles(TestBase.runtimeJar(backend));
}
}
assertNull(oldStdout);
oldStdout = System.out;
assertNull(oldStderr);
oldStderr = System.err;
CR cr;
try {
if (stdout != null) {
assertTrue(allowStdoutMessages);
System.setOut(new PrintStream(new ForwardingOutputStream(stdout, System.out)));
} else if (!allowStdoutMessages) {
System.setOut(
new PrintStream(
new ThrowingOutputStream<>(
() -> new AssertionError("Unexpected print to stdout"))));
}
if (stderr != null) {
assertTrue(allowStderrMessages);
System.setErr(new PrintStream(new ForwardingOutputStream(stderr, System.err)));
} else if (!allowStderrMessages) {
System.setErr(
new PrintStream(
new ThrowingOutputStream<>(
() -> new AssertionError("Unexpected print to stderr"))));
}
cr =
internalCompile(builder, optionsConsumer, Suppliers.memoize(sink::build))
.addRunClasspathFiles(additionalRunClassPath);
return cr;
} finally {
if (stdout != null) {
getState().setStdout(stdout.toString());
}
System.setOut(oldStdout);
if (stderr != null) {
getState().setStderr(stderr.toString());
}
System.setErr(oldStderr);
}
}
@FunctionalInterface
public interface DiagnosticsConsumer {
void accept(TestDiagnosticMessages diagnostics);
}
public CR compileWithExpectedDiagnostics(DiagnosticsConsumer diagnosticsConsumer)
throws CompilationFailedException {
TestDiagnosticMessages diagnosticsHandler = getState().getDiagnosticsMessages();
try {
CR result = compile();
diagnosticsConsumer.accept(diagnosticsHandler);
return result;
} catch (CompilationFailedException e) {
diagnosticsConsumer.accept(diagnosticsHandler);
throw e;
}
}
@Override
@Deprecated
public RR run(String mainClass)
throws CompilationFailedException, ExecutionException, IOException {
return compile().run(mainClass);
}
@Override
public RR run(TestRuntime runtime, String mainClass, String... args)
throws CompilationFailedException, ExecutionException, IOException {
return compile().run(runtime, mainClass, args);
}
@Override
public DebugTestConfig debugConfig() {
// Rethrow exceptions since debug config is usually used in a delayed wrapper which
// does not declare exceptions.
try {
return compile().debugConfig();
} catch (CompilationFailedException e) {
throw new RuntimeException(e);
}
}
public T setMode(CompilationMode mode) {
builder.setMode(mode);
return self();
}
public T debug() {
return setMode(CompilationMode.DEBUG);
}
public T release() {
return setMode(CompilationMode.RELEASE);
}
public T setMinApiThreshold(AndroidApiLevel minApiThreshold) {
assert backend == Backend.DEX;
AndroidApiLevel minApi = ToolHelper.getMinApiLevelForDexVmNoHigherThan(minApiThreshold);
return setMinApi(minApi);
}
public T setMinApiThreshold(TestRuntime runtime) {
if (runtime.isDex()) {
setMinApiThreshold(runtime.asDex().getMinApiLevel());
}
return self();
}
public T setMinApi(AndroidApiLevel minApiLevel) {
if (backend == Backend.DEX) {
return setMinApi(minApiLevel.getLevel());
}
return self();
}
public T setMinApi(int minApiLevel) {
assert builder.getMinApiLevel() > 0 || this.defaultMinApiLevel != null
: "Tests must use this method to set min API level, and not"
+ " BaseCompilerCommand.Builder.setMinApiLevel()";
if (backend == Backend.DEX) {
this.defaultMinApiLevel = null;
builder.setMinApiLevel(minApiLevel);
}
return self();
}
/** @deprecated use {@link #setMinApi(AndroidApiLevel)} instead. */
@Deprecated
public T setMinApi(TestRuntime runtime) {
if (runtime.isDex()) {
setMinApi(runtime.asDex().getMinApiLevel());
}
return self();
}
public T disableDesugaring() {
return setDisableDesugaring(true);
}
public T setDisableDesugaring(boolean disableDesugaring) {
builder.setDisableDesugaring(disableDesugaring);
return self();
}
public OutputMode getOutputMode() {
if (programConsumer instanceof DexIndexedConsumer) {
return OutputMode.DexIndexed;
}
if (programConsumer instanceof DexFilePerClassFileConsumer) {
return ((DexFilePerClassFileConsumer) programConsumer)
.combineSyntheticClassesWithPrimaryClass()
? OutputMode.DexFilePerClassFile
: OutputMode.DexFilePerClass;
}
assert programConsumer instanceof ClassFileConsumer;
return OutputMode.ClassFile;
}
public T setOutputMode(OutputMode outputMode) {
assert ToolHelper.verifyValidOutputMode(backend, outputMode);
switch (outputMode) {
case DexIndexed:
programConsumer = DexIndexedConsumer.emptyConsumer();
break;
case DexFilePerClassFile:
programConsumer = DexFilePerClassFileConsumer.emptyConsumer();
break;
case DexFilePerClass:
programConsumer =
new DexFilePerClassFileConsumer.ForwardingConsumer(null) {
@Override
public boolean combineSyntheticClassesWithPrimaryClass() {
return false;
}
};
break;
case ClassFile:
programConsumer = ClassFileConsumer.emptyConsumer();
break;
}
return self();
}
public T setProgramConsumer(ProgramConsumer programConsumer) {
assert programConsumer != null;
this.programConsumer = programConsumer;
return self();
}
public T setMainDexListConsumer(StringConsumer consumer) {
assert consumer != null;
this.mainDexListConsumer = consumer;
return self();
}
public T setIncludeClassesChecksum(boolean include) {
builder.setIncludeClassesChecksum(include);
return self();
}
@Override
public T addLibraryFiles(Collection<Path> files) {
useDefaultRuntimeLibrary = false;
return super.addLibraryFiles(files);
}
@Override
public T addLibraryClasses(Collection<Class<?>> classes) {
useDefaultRuntimeLibrary = false;
return super.addLibraryClasses(classes);
}
@Override
public T addLibraryProvider(ClassFileResourceProvider provider) {
useDefaultRuntimeLibrary = false;
return super.addLibraryProvider(provider);
}
public T addClasspathClasses(Class<?>... classes) {
return addClasspathClasses(Arrays.asList(classes));
}
public abstract T addClasspathClasses(Collection<Class<?>> classes);
public T addClasspathFiles(Path... files) {
return addClasspathFiles(Arrays.asList(files));
}
public abstract T addClasspathFiles(Collection<Path> files);
public T noDesugaring() {
builder.setDisableDesugaring(true);
return self();
}
public T allowStdoutMessages() {
allowStdoutMessages = true;
return self();
}
public T collectStdout() {
assert stdout == null;
stdout = new ByteArrayOutputStream();
return allowStdoutMessages();
}
/**
* If {@link #allowStdoutMessages} is false, then {@link System#out} will be replaced temporarily
* by a {@link ThrowingOutputStream}. To allow the testing infrastructure to print messages to the
* terminal, this method provides a reference to the original {@link System#out}.
*/
public PrintStream getStdoutForTesting() {
assertNotNull(oldStdout);
return oldStdout;
}
public T allowStderrMessages() {
allowStderrMessages = true;
return self();
}
public T collectStderr() {
assert stderr == null;
stderr = new ByteArrayOutputStream();
return allowStderrMessages();
}
public T enableCoreLibraryDesugaring(AndroidApiLevel minAPILevel) {
return enableCoreLibraryDesugaring(minAPILevel, null);
}
public T enableCoreLibraryDesugaring(
AndroidApiLevel minApiLevel, KeepRuleConsumer keepRuleConsumer) {
assert minApiLevel.getLevel() < AndroidApiLevel.O.getLevel();
builder.addDesugaredLibraryConfiguration(
StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING));
// TODO(b/158543446): This should not be setting an implicit library file. Doing so causes
// inconsistent library setup depending on the api level and makes tests hard to read and
// reason about.
// Use P to mimic current Android Studio.
builder.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P));
return self();
}
@Override
public T addRunClasspathFiles(Collection<Path> files) {
additionalRunClassPath.addAll(files);
return self();
}
public T addAssertionsConfiguration(
Function<AssertionsConfiguration.Builder, AssertionsConfiguration>
assertionsConfigurationGenerator) {
builder.addAssertionsConfiguration(assertionsConfigurationGenerator);
return self();
}
}