| // 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 com.google.common.base.Predicates.not; | 
 | 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.testing.AndroidBuildVersion; | 
 | 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.DescriptorUtils; | 
 | import com.android.tools.r8.utils.ForwardingOutputStream; | 
 | import com.android.tools.r8.utils.InternalOptions; | 
 | import com.android.tools.r8.utils.ThrowingOutputStream; | 
 | import com.android.tools.r8.utils.codeinspector.EnumUnboxingInspector; | 
 | import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector; | 
 | import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector; | 
 | 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.Collection; | 
 | import java.util.List; | 
 | import java.util.Set; | 
 | import java.util.concurrent.ExecutionException; | 
 | import java.util.function.Consumer; | 
 | import java.util.function.Function; | 
 | import java.util.function.Supplier; | 
 | import java.util.stream.Collectors; | 
 | import java.util.stream.Stream; | 
 |  | 
 | public abstract class TestCompilerBuilder< | 
 |         C extends BaseCompilerCommand, | 
 |         B extends BaseCompilerCommand.Builder<C, B>, | 
 |         CR extends TestCompileResult<CR, RR>, | 
 |         RR extends TestRunResult<RR>, | 
 |         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.allowUnusedDontWarnRules = false; | 
 |         options.testing.allowUnnecessaryDontWarnWildcards = false; | 
 |         options.testing.reportUnusedProguardConfigurationRules = true; | 
 |         options.horizontalClassMergerOptions().enable(); | 
 |       }; | 
 |  | 
 |   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 MainDexClassesCollector mainDexClassesCollector; | 
 |   private StringConsumer mainDexListConsumer; | 
 |   protected int minApiLevel = ToolHelper.getMinApiLevelForDexVm().getLevel(); | 
 |   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; | 
 |  | 
 |   private boolean isAndroidBuildVersionAdded = false; | 
 |  | 
 |   public boolean isTestShrinkerBuilder() { | 
 |     return false; | 
 |   } | 
 |  | 
 |   public T addAndroidBuildVersion() { | 
 |     addProgramClasses(AndroidBuildVersion.class); | 
 |     isAndroidBuildVersionAdded = true; | 
 |     return self(); | 
 |   } | 
 |  | 
 |   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(boolean skipReporting) { | 
 |     return addOptionsModification( | 
 |         options -> { | 
 |           options.testing.allowCheckDiscardedErrors = true; | 
 |           options.testing.dontReportFailingCheckDiscarded = skipReporting; | 
 |         }); | 
 |   } | 
 |  | 
 |   public T addEnumUnboxingInspector(Consumer<EnumUnboxingInspector> inspector) { | 
 |     return addOptionsModification( | 
 |         options -> | 
 |             options.testing.unboxedEnumsConsumer = | 
 |                 ((dexItemFactory, unboxedEnums) -> | 
 |                     inspector.accept(new EnumUnboxingInspector(dexItemFactory, unboxedEnums)))); | 
 |   } | 
 |  | 
 |   public T addHorizontallyMergedClassesInspector( | 
 |       ThrowableConsumer<HorizontallyMergedClassesInspector> inspector) { | 
 |     return addOptionsModification( | 
 |         options -> | 
 |             options.testing.horizontallyMergedClassesConsumer = | 
 |                 ((dexItemFactory, horizontallyMergedClasses) -> | 
 |                     inspector.acceptWithRuntimeException( | 
 |                         new HorizontallyMergedClassesInspector( | 
 |                             dexItemFactory, horizontallyMergedClasses)))); | 
 |   } | 
 |  | 
 |   public T addHorizontallyMergedClassesInspectorIf( | 
 |       boolean condition, ThrowableConsumer<HorizontallyMergedClassesInspector> inspector) { | 
 |     if (condition) { | 
 |       return addHorizontallyMergedClassesInspector(inspector); | 
 |     } | 
 |     return self(); | 
 |   } | 
 |  | 
 |   public T addVerticallyMergedClassesInspector( | 
 |       Consumer<VerticallyMergedClassesInspector> inspector) { | 
 |     return addOptionsModification( | 
 |         options -> | 
 |             options.testing.verticallyMergedClassesConsumer = | 
 |                 ((dexItemFactory, verticallyMergedClasses) -> | 
 |                     inspector.accept( | 
 |                         new VerticallyMergedClassesInspector( | 
 |                             dexItemFactory, verticallyMergedClasses)))); | 
 |   } | 
 |  | 
 |   public CR compile() throws CompilationFailedException { | 
 |     AndroidAppConsumers sink = new AndroidAppConsumers(); | 
 |     builder.setProgramConsumer(sink.wrapProgramConsumer(programConsumer)); | 
 |     if (mainDexClassesCollector != null || mainDexListConsumer != null) { | 
 |       builder.setMainDexListConsumer( | 
 |           ChainedStringConsumer.builder() | 
 |               .addIfNotNull(mainDexClassesCollector) | 
 |               .addIfNotNull(mainDexListConsumer) | 
 |               .build()); | 
 |     } | 
 |     if (backend.isDex() || !isTestShrinkerBuilder()) { | 
 |       assert !builder.isMinApiLevelSet() | 
 |           : "Don't set the API level directly through BaseCompilerCommand.Builder in tests"; | 
 |       builder.setMinApiLevel(minApiLevel); | 
 |     } | 
 |     if (useDefaultRuntimeLibrary) { | 
 |       if (backend == Backend.DEX) { | 
 |         assert builder.isMinApiLevelSet(); | 
 |         builder.addLibraryFiles( | 
 |             ToolHelper.getFirstSupportedAndroidJar( | 
 |                 AndroidApiLevel.getAndroidApiLevel(builder.getMinApiLevel()))); | 
 |       } else { | 
 |         builder.addLibraryFiles(TestBase.runtimeJar(backend)); | 
 |       } | 
 |     } | 
 |     List<String> mainDexClasses = null; | 
 |     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); | 
 |       if (isAndroidBuildVersionAdded) { | 
 |         cr.setSystemProperty(AndroidBuildVersion.PROPERTY, "" + builder.getMinApiLevel()); | 
 |       } | 
 |       return cr; | 
 |     } finally { | 
 |       if (mainDexClassesCollector != null) { | 
 |         getState().setMainDexClasses(mainDexClassesCollector.getMainDexClasses()); | 
 |       } | 
 |       if (stdout != null) { | 
 |         getState().setStdout(stdout.toString()); | 
 |       } | 
 |       System.setOut(oldStdout); | 
 |       if (stderr != null) { | 
 |         getState().setStderr(stderr.toString()); | 
 |       } | 
 |       System.setErr(oldStderr); | 
 |     } | 
 |   } | 
 |  | 
 |   public T enableExperimentalMapFileVersion() { | 
 |     addOptionsModification(o -> o.testing.enableExperimentalMapFileVersion = true); | 
 |     return self(); | 
 |   } | 
 |  | 
 |   @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 setMinApi(AndroidApiLevel minApiLevel) { | 
 |     return setMinApi(minApiLevel.getLevel()); | 
 |   } | 
 |  | 
 |   public T setMinApi(int minApiLevel) { | 
 |     this.minApiLevel = 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() { | 
 |     builder.setDisableDesugaring(true); | 
 |     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; | 
 |     assert backend == Backend.fromConsumer(programConsumer); | 
 |     this.programConsumer = programConsumer; | 
 |     return self(); | 
 |   } | 
 |  | 
 |   public T collectMainDexClasses() { | 
 |     assert mainDexClassesCollector == null; | 
 |     mainDexClassesCollector = new MainDexClassesCollector(); | 
 |     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 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.getDesugarLibJsonForTesting())); | 
 |     // 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(); | 
 |   } | 
 |  | 
 |   private static class ChainedStringConsumer implements StringConsumer { | 
 |  | 
 |     private final List<StringConsumer> consumers; | 
 |  | 
 |     ChainedStringConsumer(List<StringConsumer> consumers) { | 
 |       this.consumers = consumers; | 
 |     } | 
 |  | 
 |     static Builder builder() { | 
 |       return new Builder(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void accept(String string, DiagnosticsHandler handler) { | 
 |       consumers.forEach(consumer -> consumer.accept(string, handler)); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void finished(DiagnosticsHandler handler) { | 
 |       consumers.forEach(consumer -> consumer.finished(handler)); | 
 |     } | 
 |  | 
 |     static class Builder { | 
 |  | 
 |       private final List<StringConsumer> consumers = new ArrayList<>(); | 
 |  | 
 |       Builder add(StringConsumer consumer) { | 
 |         assert consumer != null; | 
 |         consumers.add(consumer); | 
 |         return this; | 
 |       } | 
 |  | 
 |       Builder addIfNotNull(StringConsumer consumer) { | 
 |         return consumer != null ? add(consumer) : this; | 
 |       } | 
 |  | 
 |       ChainedStringConsumer build() { | 
 |         return new ChainedStringConsumer(consumers); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   private static class MainDexClassesCollector implements StringConsumer { | 
 |  | 
 |     private StringBuilder builder = new StringBuilder(); | 
 |     private Set<String> mainDexClasses; | 
 |  | 
 |     public Set<String> getMainDexClasses() { | 
 |       assert mainDexClasses != null; | 
 |       return mainDexClasses; | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void accept(String string, DiagnosticsHandler handler) { | 
 |       builder.append(string); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void finished(DiagnosticsHandler handler) { | 
 |       mainDexClasses = | 
 |           Stream.of(builder.toString().split(System.lineSeparator())) | 
 |               .filter(not(String::isEmpty)) | 
 |               .map( | 
 |                   line -> { | 
 |                     assert line.endsWith(".class"); | 
 |                     return line.substring(0, line.length() - ".class".length()); | 
 |                   }) | 
 |               .map(DescriptorUtils::getJavaTypeFromBinaryName) | 
 |               .collect(Collectors.toSet()); | 
 |       builder = null; | 
 |     } | 
 |   } | 
 | } |