blob: e638abf46282f137767c1a3d461ccda206b71fe0 [file] [log] [blame]
// Copyright (c) 2017, 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.android.tools.r8.TestBuilder.getTestingAnnotations;
import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
import static com.google.common.collect.Lists.cartesianProduct;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
import com.android.tools.r8.DataResourceProvider.Visitor;
import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
import com.android.tools.r8.TestRuntime.CfRuntime;
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.cf.CfVersion;
import com.android.tools.r8.code.Instruction;
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.features.ClassToFeatureSplitMap;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppServices;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexCode;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.SmaliWriter;
import com.android.tools.r8.graph.SubtypingInfo;
import com.android.tools.r8.jasmin.JasminBuilder;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.references.ClassReference;
import com.android.tools.r8.references.FieldReference;
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.references.TypeReference;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.EnqueuerFactory;
import com.android.tools.r8.shaking.EnqueuerResult;
import com.android.tools.r8.shaking.MainDexInfo;
import com.android.tools.r8.shaking.NoHorizontalClassMergingRule;
import com.android.tools.r8.shaking.NoVerticalClassMergingRule;
import com.android.tools.r8.shaking.ProguardClassNameList;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.shaking.ProguardKeepRule;
import com.android.tools.r8.shaking.ProguardKeepRule.Builder;
import com.android.tools.r8.shaking.ProguardKeepRuleType;
import com.android.tools.r8.shaking.ProguardMemberRule;
import com.android.tools.r8.shaking.ProguardMemberType;
import com.android.tools.r8.shaking.ProguardTypeMatcher;
import com.android.tools.r8.shaking.RootSetUtils.RootSet;
import com.android.tools.r8.shaking.serviceloader.ServiceLoaderMultipleTest.Greeter;
import com.android.tools.r8.transformers.ClassFileTransformer;
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.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.Pair;
import com.android.tools.r8.utils.PreloadedClassFileProvider;
import com.android.tools.r8.utils.ReflectiveBuildPathUtils;
import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesClass;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.android.tools.r8.utils.Timing;
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.MethodSubject;
import com.google.common.base.Predicates;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
public class TestBase {
public enum Backend {
CF,
DEX;
public boolean isCf() {
return this == CF;
}
public boolean isDex() {
return this == DEX;
}
public static Backend fromConsumer(ProgramConsumer consumer) {
return consumer instanceof ClassFileConsumer ? CF : DEX;
}
}
public static R8FullTestBuilder testForR8(TemporaryFolder temp, Backend backend) {
return R8FullTestBuilder.create(new TestState(temp), backend);
}
public static R8CompatTestBuilder testForR8Compat(
TemporaryFolder temp, Backend backend, boolean forceProguardCompatibility) {
return R8CompatTestBuilder.create(new TestState(temp), backend, forceProguardCompatibility);
}
public static ExternalR8TestBuilder testForExternalR8(
TemporaryFolder temp, Backend backend, TestRuntime runtime) {
return ExternalR8TestBuilder.create(new TestState(temp), backend, runtime);
}
public static D8TestBuilder testForD8(TemporaryFolder temp, Backend backend) {
return D8TestBuilder.create(new TestState(temp), backend);
}
public static D8TestBuilder testForD8(TemporaryFolder temp) {
return D8TestBuilder.create(new TestState(temp), Backend.DEX);
}
public static DXTestBuilder testForDX(TemporaryFolder temp) {
return DXTestBuilder.create(new TestState(temp));
}
public static JvmTestBuilder testForJvm(TemporaryFolder temp) {
return JvmTestBuilder.create(new TestState(temp));
}
public static ProguardTestBuilder testForProguard(ProguardVersion version, TemporaryFolder temp) {
return ProguardTestBuilder.create(new TestState(temp), version);
}
public static GenerateMainDexListTestBuilder testForMainDexListGenerator(TemporaryFolder temp) {
return GenerateMainDexListTestBuilder.create(new TestState(temp));
}
public R8FullTestBuilder testForR8(Backend backend) {
return testForR8(temp, backend);
}
public R8CompatTestBuilder testForR8Compat(Backend backend) {
return testForR8Compat(backend, true);
}
public R8CompatTestBuilder testForR8Compat(Backend backend, boolean forceProguardCompatibility) {
return testForR8Compat(temp, backend, forceProguardCompatibility);
}
public ExternalR8TestBuilder testForExternalR8(Backend backend, TestRuntime runtime) {
return testForExternalR8(temp, backend, runtime);
}
public D8TestBuilder testForD8() {
return testForD8(temp, Backend.DEX);
}
public D8TestBuilder testForD8(Backend backend) {
return testForD8(temp, backend);
}
public DXTestBuilder testForDX() {
return testForDX(temp);
}
public JvmTestBuilder testForJvm() {
return testForJvm(temp);
}
public TestBuilder<? extends SingleTestRunResult<?>, ?> testForRuntime(
TestRuntime runtime, Consumer<D8TestBuilder> d8TestBuilderConsumer) {
if (runtime.isCf()) {
return testForJvm();
} else {
assert runtime.isDex();
D8TestBuilder d8TestBuilder = testForD8();
d8TestBuilderConsumer.accept(d8TestBuilder);
return d8TestBuilder;
}
}
public TestBuilder<? extends SingleTestRunResult<?>, ?> testForRuntime(
TestRuntime runtime, AndroidApiLevel apiLevel) {
return testForRuntime(runtime, d8TestBuilder -> d8TestBuilder.setMinApi(apiLevel));
}
public TestBuilder<? extends SingleTestRunResult<?>, ?> testForRuntime(
TestParameters parameters) {
return testForRuntime(parameters.getRuntime(), parameters.getApiLevel());
}
public TestBuilder<DesugarTestRunResult, ?> testForDesugaring(TestParameters parameters) {
return testForDesugaring(
parameters.getRuntime().getBackend(),
parameters.getApiLevel(),
o -> {},
Predicates.alwaysTrue());
}
public TestBuilder<DesugarTestRunResult, ?> testForDesugaring(
TestParameters parameters, Consumer<InternalOptions> optionsModification) {
return testForDesugaring(
parameters.getRuntime().getBackend(),
parameters.getApiLevel(),
optionsModification,
Predicates.alwaysTrue());
}
@Deprecated
// This is not supposed to be used for tests. It is here for debugging where filtering to run
// only some (typically one) test configuration is helpful.
public TestBuilder<DesugarTestRunResult, ?> testForDesugaring(
TestParameters parameters,
Consumer<InternalOptions> optionsModification,
Predicate<DesugarTestConfiguration> filter) {
return testForDesugaring(
parameters.getRuntime().getBackend(),
parameters.getApiLevel(),
optionsModification,
filter);
}
private TestBuilder<DesugarTestRunResult, ?> testForDesugaring(
Backend backend,
AndroidApiLevel apiLevel,
Consumer<InternalOptions> optionsModification,
Predicate<DesugarTestConfiguration> filter) {
assert apiLevel != null : "No API level. Add .withAllApiLevelsAlsoForCf() to test parameters?";
TestState state = new TestState(temp);
ImmutableList.Builder<
Pair<DesugarTestConfiguration, TestBuilder<? extends TestRunResult<?>, ?>>>
builders = ImmutableList.builder();
if (backend == Backend.CF) {
if (filter.test(DesugarTestConfiguration.JAVAC)) {
builders.add(new Pair<>(DesugarTestConfiguration.JAVAC, JvmTestBuilder.create(state)));
}
if (filter.test(DesugarTestConfiguration.D8_CF)) {
builders.add(
new Pair<>(
DesugarTestConfiguration.D8_CF,
D8TestBuilder.create(state, Backend.CF)
.setMinApi(apiLevel)
.addOptionsModification(optionsModification)));
}
} else {
assert backend == Backend.DEX;
if (filter.test(DesugarTestConfiguration.D8_DEX)) {
builders.add(
new Pair<>(
DesugarTestConfiguration.D8_DEX,
D8TestBuilder.create(state, Backend.DEX)
.setMinApi(apiLevel)
.addOptionsModification(optionsModification)));
}
if (filter.test(DesugarTestConfiguration.D8_CF_D8_DEX)) {
builders.add(
new Pair<>(
DesugarTestConfiguration.D8_CF_D8_DEX,
IntermediateCfD8TestBuilder.create(state, apiLevel)
.addOptionsModification(optionsModification)));
}
}
return DesugarTestBuilder.create(state, builders.build());
}
/** @deprecated use {@link #testForProguard(ProguardVersion)} instead. */
@Deprecated
public ProguardTestBuilder testForProguard() {
return testForProguard(ProguardVersion.V6_0_1);
}
public ProguardTestBuilder testForProguard(ProguardVersion version) {
return testForProguard(version, temp);
}
public GenerateMainDexListTestBuilder testForMainDexListGenerator() {
return testForMainDexListGenerator(temp);
}
public JavaCompilerTool javac(CfRuntime jdk) {
return JavaCompilerTool.create(jdk, temp);
}
public static JavaCompilerTool javac(CfRuntime jdk, TemporaryFolder temp) {
return JavaCompilerTool.create(jdk, temp);
}
public static KotlinCompilerTool kotlinc(
CfRuntime jdk,
TemporaryFolder temp,
KotlinCompiler kotlinCompiler,
KotlinTargetVersion kotlinTargetVersion) {
return KotlinCompilerTool.create(jdk, temp, kotlinCompiler, kotlinTargetVersion);
}
public static KotlinCompilerTool kotlinc(
KotlinCompiler kotlinCompiler, KotlinTargetVersion kotlinTargetVersion) {
return kotlinc(TestRuntime.getCheckedInJdk9(), staticTemp, kotlinCompiler, kotlinTargetVersion);
}
public KotlinCompilerTool kotlinc(
CfRuntime jdk, KotlinCompiler kotlinCompiler, KotlinTargetVersion kotlinTargetVersion) {
return KotlinCompilerTool.create(jdk, temp, kotlinCompiler, kotlinTargetVersion);
}
public static ClassFileTransformer transformer(Class<?> clazz) throws IOException {
return ClassFileTransformer.create(clazz);
}
public static ClassFileTransformer transformer(Path path, ClassReference classReference)
throws IOException {
return ClassFileTransformer.create(Files.readAllBytes(path), classReference);
}
public static ClassFileTransformer transformer(byte[] bytes, ClassReference classReference) {
return ClassFileTransformer.create(bytes, classReference);
}
// Actually running Proguard should only be during development.
private static final boolean RUN_PROGUARD = System.getProperty("run_proguard") != null;
@Rule public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
@Rule
public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
private static TemporaryFolder staticTemp = null;
@BeforeClass
public static void testBaseBeforeClassSetup() throws IOException {
assert staticTemp == null;
staticTemp = ToolHelper.getTemporaryFolderForTest();
staticTemp.create();
}
@AfterClass
public static void testBaseBeforeClassTearDown() {
assert staticTemp != null;
staticTemp.delete();
staticTemp = null;
}
public static TemporaryFolder getStaticTemp() {
return staticTemp;
}
public static TestParametersBuilder getTestParameters() {
return TestParametersBuilder.builder();
}
public static KotlinTestParameters.Builder getKotlinTestParameters() {
return KotlinTestParameters.builder();
}
public static <S, T, E extends Throwable> Function<S, T> memoizeFunction(
ThrowingFunction<S, T, E> fn) {
return CacheBuilder.newBuilder()
.build(
CacheLoader.from(
b -> {
try {
return fn.applyWithRuntimeException(b);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}));
}
public static <S, T, U, E extends Throwable> BiFunction<S, T, U> memoizeBiFunction(
ThrowingBiFunction<S, T, U, E> fn) {
class Pair {
final S first;
final T second;
public Pair(S first, T second) {
this.first = first;
this.second = second;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Pair)) {
return false;
}
Pair other = (Pair) obj;
return Objects.equals(first, other.first) && Objects.equals(second, other.second);
}
@Override
public int hashCode() {
return Objects.hash(first, second);
}
}
final Function<Pair, U> memoizedFn = memoizeFunction(pair -> fn.apply(pair.first, pair.second));
return (a, b) -> memoizedFn.apply(new Pair(a, b));
}
/**
* Check if tests should also run Proguard when applicable.
*/
protected boolean isRunProguard() {
return RUN_PROGUARD;
}
/**
* Write lines of text to a temporary file.
*
* The file will include a line separator after the last line.
*/
protected Path writeTextToTempFile(String... lines) throws IOException {
return writeTextToTempFile(System.lineSeparator(), Arrays.asList(lines));
}
protected void writeTextToTempFile(Path file, String... lines) throws IOException {
writeTextToTempFile(file, System.lineSeparator(), Arrays.asList(lines));
}
/**
* Write lines of text to a temporary file, along with the specified line separator.
*
* The file will include a line separator after the last line.
*/
protected Path writeTextToTempFile(String lineSeparator, List<String> lines)
throws IOException {
return writeTextToTempFile(lineSeparator, lines, true);
}
protected void writeTextToTempFile(Path file, String lineSeparator, List<String> lines)
throws IOException {
writeTextToTempFile(file, lineSeparator, lines, true);
}
/**
* Write lines of text to a temporary file, along with the specified line separator.
*
* The argument <code>includeTerminatingLineSeparator</code> control if the file will include
* a line separator after the last line.
*/
protected Path writeTextToTempFile(
String lineSeparator, List<String> lines, boolean includeTerminatingLineSeparator)
throws IOException {
Path file = temp.newFile().toPath();
writeTextToTempFile(file, lineSeparator, lines, includeTerminatingLineSeparator);
return file;
}
protected void writeTextToTempFile(
Path file,
String lineSeparator,
List<String> lines,
boolean includeTerminatingLineSeparator)
throws IOException {
String contents = String.join(lineSeparator, lines);
if (includeTerminatingLineSeparator) {
contents += lineSeparator;
}
Files.write(file, contents.getBytes(StandardCharsets.UTF_8));
}
/** Build an AndroidApp with the specified test classes as byte array. */
protected AndroidApp buildAndroidApp(byte[]... classes) {
return buildAndroidApp(Arrays.asList(classes));
}
/** Build an AndroidApp with the specified test classes as byte array. */
protected AndroidApp buildAndroidApp(List<byte[]> classes) {
AndroidApp.Builder builder = AndroidApp.builder();
for (byte[] clazz : classes) {
builder.addClassProgramData(clazz, Origin.unknown());
}
builder.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.N.getLevel()));
return builder.build();
}
/**
* Build an AndroidApp with the specified jar.
*/
protected AndroidApp readJar(Path jar) {
return AndroidApp.builder()
.addProgramResourceProvider(ArchiveProgramResourceProvider.fromArchive(jar))
.build();
}
protected List<String> classNamesFromDexFile(Path dexFile) throws IOException {
return new CodeInspector(dexFile)
.allClasses().stream().map(FoundClassSubject::toString).collect(Collectors.toList());
}
/**
* Build an AndroidApp with the specified test classes.
*/
protected static AndroidApp readClasses(Class... classes) throws IOException {
return readClasses(Arrays.asList(classes));
}
/** Build an AndroidApp with the specified test classes. */
protected static AndroidApp readClasses(List<Class<?>> classes) throws IOException {
return readClasses(classes, Collections.emptyList());
}
/** Build an AndroidApp with the specified test classes. */
protected static AndroidApp readClasses(
List<Class<?>> programClasses, List<Class<?>> libraryClasses) throws IOException {
return buildClasses(programClasses, libraryClasses).build();
}
protected static AndroidApp.Builder buildClasses(Class<?>... programClasses) throws IOException {
return buildClasses(Arrays.asList(programClasses));
}
protected static AndroidApp.Builder buildClasses(Collection<Class<?>> programClasses)
throws IOException {
return buildClasses(programClasses, Collections.emptyList());
}
protected static AndroidApp.Builder buildClassesWithTestingAnnotations(Class<?>... programClasses)
throws IOException {
return buildClassesWithTestingAnnotations(Arrays.asList(programClasses));
}
protected static AndroidApp.Builder buildClassesWithTestingAnnotations(
Collection<Class<?>> programClasses) throws IOException {
AndroidApp.Builder builder = buildClasses(programClasses, Collections.emptyList());
for (Class<?> testingAnnotation : getTestingAnnotations()) {
builder.addProgramFile(ToolHelper.getClassFileForTestClass(testingAnnotation));
}
return builder;
}
protected static AndroidApp.Builder buildClasses(
Collection<Class<?>> programClasses, Collection<Class<?>> libraryClasses) throws IOException {
AndroidApp.Builder builder = AndroidApp.builder();
for (Class<?> clazz : programClasses) {
builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz));
}
if (!libraryClasses.isEmpty()) {
PreloadedClassFileProvider.Builder libraryBuilder = PreloadedClassFileProvider.builder();
for (Class<?> clazz : libraryClasses) {
Path file = ToolHelper.getClassFileForTestClass(clazz);
libraryBuilder.addResource(DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()),
Files.readAllBytes(file));
}
builder.addLibraryResourceProvider(libraryBuilder.build());
}
return builder;
}
protected static AndroidApp.Builder buildInnerClasses(Class<?> clazz) throws IOException {
AndroidApp.Builder builder = AndroidApp.builder();
builder.addProgramFiles(ToolHelper.getClassFilesForInnerClasses(clazz));
return builder;
}
protected static AndroidApp readClassesAndRuntimeJar(
List<Class<?>> programClasses, Backend backend) throws IOException {
AndroidApp.Builder builder = AndroidApp.builder();
for (Class<?> clazz : programClasses) {
builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz));
}
if (backend == Backend.DEX) {
AndroidApiLevel androidLibrary = ToolHelper.getMinApiLevelForDexVm();
builder.addLibraryFiles(ToolHelper.getAndroidJar(androidLibrary));
} else {
assert backend == Backend.CF;
builder.addLibraryFiles(ToolHelper.getJava8RuntimeJar());
}
return builder.build();
}
/** Build an AndroidApp from the specified program files. */
protected AndroidApp readProgramFiles(Path... programFiles) throws IOException {
return AndroidApp.builder().addProgramFiles(programFiles).build();
}
/**
* Copy test classes to the specified directory.
*/
protected void copyTestClasses(Path dest, Class... classes) throws IOException {
for (Class<?> clazz : classes) {
Path path = dest.resolve(clazz.getCanonicalName().replace('.', '/') + ".class");
Files.createDirectories(path.getParent());
Files.copy(ToolHelper.getClassFileForTestClass(clazz), path);
}
}
/** Create a temporary JAR file containing the specified test classes. */
protected Path jarTestClasses(Class<?>... classes) throws IOException {
return jarTestClasses(Arrays.asList(classes), null);
}
/** Create a temporary JAR file containing the specified test classes. */
protected Path jarTestClasses(Iterable<Class<?>> classes) throws IOException {
return jarTestClasses(classes, null);
}
/** Create a temporary JAR file containing the specified test classes and data resources. */
protected Path jarTestClasses(Iterable<Class<?>> classes, List<DataResource> dataResources)
throws IOException {
Path jar = File.createTempFile("junit", ".jar", temp.getRoot()).toPath();
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
addTestClassesToJar(out, classes);
if (dataResources != null) {
addDataResourcesToJar(out, dataResources);
}
}
return jar;
}
/** Create a temporary JAR file containing the specified test classes. */
protected void addTestClassesToJar(JarOutputStream out, Iterable<Class<?>> classes)
throws IOException {
for (Class<?> clazz : classes) {
try (FileInputStream in =
new FileInputStream(ToolHelper.getClassFileForTestClass(clazz).toFile())) {
out.putNextEntry(new ZipEntry(ToolHelper.getJarEntryForTestClass(clazz)));
ByteStreams.copy(in, out);
out.closeEntry();
}
}
}
/** Create a temporary JAR file containing the specified data resources. */
protected void addDataResourcesToJar(
JarOutputStream out, List<? extends DataResource> dataResources) throws IOException {
try {
for (DataResource dataResource : dataResources) {
String name = dataResource.getName();
boolean isDirectory = dataResource instanceof DataDirectoryResource;
if (isDirectory && !name.endsWith("/")) {
// Directory entries must end with a slash. Otherwise they will be empty files.
name += "/";
}
out.putNextEntry(new ZipEntry(name));
if (!isDirectory) {
ByteStreams.copy(((DataEntryResource) dataResource).getByteStream(), out);
}
out.closeEntry();
}
} catch (ResourceException e) {
throw new IOException("Resource error", e);
}
}
/**
* Creates a new, temporary JAR that contains all the entries from the given JAR as well as the
* specified data resources. The given JAR is left unchanged.
*/
protected Path addDataResourcesToExistingJar(
Path existingJar, List<? extends DataResource> dataResources) throws IOException {
Path newJar = File.createTempFile("app", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(newJar.toFile()))) {
ArchiveProgramResourceProvider.fromArchive(existingJar)
.readArchive(
(entry, stream) -> {
out.putNextEntry(new ZipEntry(entry.getEntryName()));
ByteStreams.copy(stream, out);
out.closeEntry();
});
addDataResourcesToJar(out, dataResources);
}
return newJar;
}
private static DexApplication readApplicationForDexOutput(AndroidApp app, InternalOptions options)
throws Exception {
assert options.programConsumer == null;
options.programConsumer = DexIndexedConsumer.emptyConsumer();
return new ApplicationReader(app, options, Timing.empty()).read();
}
protected static AppView<AppInfo> computeAppView(AndroidApp app) throws Exception {
AppInfo appInfo =
AppInfo.createInitialAppInfo(readApplicationForDexOutput(app, new InternalOptions()));
return AppView.createForD8(appInfo);
}
protected static AppInfoWithClassHierarchy computeAppInfoWithClassHierarchy(AndroidApp app)
throws Exception {
return AppInfoWithClassHierarchy.createInitialAppInfoWithClassHierarchy(
readApplicationForDexOutput(app, new InternalOptions()),
ClassToFeatureSplitMap.createEmptyClassToFeatureSplitMap(),
MainDexInfo.none());
}
protected static AppView<AppInfoWithClassHierarchy> computeAppViewWithClassHierachy(
AndroidApp app) throws Exception {
return computeAppViewWithClassHierachy(app, null);
}
private static AppView<AppInfoWithClassHierarchy> computeAppViewWithClassHierachy(
AndroidApp app, Function<DexItemFactory, ProguardConfiguration> keepConfig) throws Exception {
return computeAppViewWithClassHierachy(app, keepConfig, null);
}
private static AppView<AppInfoWithClassHierarchy> computeAppViewWithClassHierachy(
AndroidApp app,
Function<DexItemFactory, ProguardConfiguration> keepConfig,
Consumer<InternalOptions> optionsConsumer)
throws Exception {
DexItemFactory dexItemFactory = new DexItemFactory();
if (keepConfig == null) {
keepConfig =
factory ->
buildConfigForRules(
factory, ImmutableList.of(ProguardKeepRule.defaultKeepAllRule(unused -> {})));
}
InternalOptions options = new InternalOptions(keepConfig.apply(dexItemFactory), new Reporter());
if (optionsConsumer != null) {
optionsConsumer.accept(options);
}
DexApplication dexApplication = readApplicationForDexOutput(app, options);
AppView<AppInfoWithClassHierarchy> appView = AppView.createForR8(dexApplication.toDirect());
appView.setAppServices(AppServices.builder(appView).build());
return appView;
}
protected static AppView<AppInfoWithLiveness> computeAppViewWithLiveness(AndroidApp app)
throws Exception {
return computeAppViewWithLiveness(app, null, null);
}
protected static AppView<AppInfoWithLiveness> computeAppViewWithLiveness(
AndroidApp app, Class<?> mainClass) throws Exception {
return computeAppViewWithLiveness(
app,
factory ->
buildConfigForRules(factory, buildKeepRuleForClassAndMethods(mainClass, factory)));
}
protected static AppView<AppInfoWithLiveness> computeAppViewWithLiveness(
AndroidApp app, Function<DexItemFactory, ProguardConfiguration> keepConfig) throws Exception {
return computeAppViewWithLiveness(app, keepConfig, null);
}
protected static AppView<AppInfoWithLiveness> computeAppViewWithLiveness(
AndroidApp app,
Function<DexItemFactory, ProguardConfiguration> keepConfig,
Consumer<InternalOptions> optionsConsumer)
throws Exception {
AppView<AppInfoWithClassHierarchy> appView =
computeAppViewWithClassHierachy(app, keepConfig, optionsConsumer);
// Run the tree shaker to compute an instance of AppInfoWithLiveness.
ExecutorService executor = Executors.newSingleThreadExecutor();
SubtypingInfo subtypingInfo = new SubtypingInfo(appView);
RootSet rootSet =
RootSet.builder(
appView, subtypingInfo, appView.options().getProguardConfiguration().getRules())
.build(executor);
appView.setRootSet(rootSet);
EnqueuerResult enqueuerResult =
EnqueuerFactory.createForInitialTreeShaking(appView, executor, subtypingInfo)
.traceApplication(rootSet, executor, Timing.empty());
// We do not run the tree pruner to ensure that the hierarchy is as designed and not modified
// due to liveness.
return appView.setAppInfo(enqueuerResult.getAppInfo());
}
protected static DexType buildType(Class<?> clazz, DexItemFactory factory) {
return buildType(Reference.classFromClass(clazz), factory);
}
protected static DexType buildType(TypeReference type, DexItemFactory factory) {
return factory.createType(type.getDescriptor());
}
protected static DexField buildField(Field field, DexItemFactory factory) {
return buildField(Reference.fieldFromField(field), factory);
}
protected static DexField buildField(FieldReference field, DexItemFactory factory) {
return factory.createField(
buildType(field.getHolderClass(), factory),
buildType(field.getFieldType(), factory),
field.getFieldName());
}
protected static DexMethod buildMethod(Method method, DexItemFactory factory) {
return buildMethod(Reference.methodFromMethod(method), factory);
}
protected static DexMethod buildMethod(MethodReference method, DexItemFactory factory) {
return factory.createMethod(
buildType(method.getHolderClass(), factory),
buildProto(method.getReturnType(), method.getFormalTypes(), factory),
method.getMethodName());
}
protected static DexMethod buildNullaryVoidMethod(
Class<?> clazz, String name, DexItemFactory factory) {
return buildMethod(
Reference.method(Reference.classFromClass(clazz), name, Collections.emptyList(), null),
factory);
}
protected static DexProto buildProto(
TypeReference returnType, List<TypeReference> formalTypes, DexItemFactory factory) {
return factory.createProto(
returnType == null ? factory.voidType : buildType(returnType, factory),
ListUtils.map(formalTypes, type -> buildType(type, factory)));
}
protected static List<ProguardConfigurationRule> buildKeepRuleForClass(
Class<?> clazz, DexItemFactory factory) {
Builder keepRuleBuilder = ProguardKeepRule.builder();
keepRuleBuilder.setSource("buildKeepRuleForClass " + clazz.getTypeName());
keepRuleBuilder.setType(ProguardKeepRuleType.KEEP);
keepRuleBuilder.setClassNames(
ProguardClassNameList.singletonList(
ProguardTypeMatcher.create(
factory.createType(DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName())))));
return Collections.singletonList(keepRuleBuilder.build());
}
protected static List<ProguardConfigurationRule> buildKeepRuleForClassAndMethods(
Class<?> clazz, DexItemFactory factory) {
Builder keepRuleBuilder = ProguardKeepRule.builder();
keepRuleBuilder.setSource("buildKeepRuleForClassAndMethods " + clazz.getTypeName());
keepRuleBuilder.setType(ProguardKeepRuleType.KEEP);
keepRuleBuilder.setClassNames(
ProguardClassNameList.singletonList(
ProguardTypeMatcher.create(
factory.createType(DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName())))));
keepRuleBuilder.setMemberRules(
Lists.newArrayList(
ProguardMemberRule.builder().setRuleType(ProguardMemberType.ALL_METHODS).build()));
return Collections.singletonList(keepRuleBuilder.build());
}
protected static ProguardConfiguration buildConfigForRules(
DexItemFactory factory, Collection<ProguardConfigurationRule> rules) {
return buildConfigForRules(factory, new Reporter(), rules);
}
protected static ProguardConfiguration buildConfigForRules(
DexItemFactory factory, Reporter reporter, Collection<ProguardConfigurationRule> rules) {
ProguardConfiguration.Builder builder = ProguardConfiguration.builder(factory, reporter);
rules.forEach(builder::addRule);
return builder.build();
}
/** Returns a list containing all the data resources in the given app. */
public static List<DataEntryResource> getDataResources(AndroidApp app) throws ResourceException {
List<DataEntryResource> dataResources = new ArrayList<>();
for (ProgramResourceProvider programResourceProvider : app.getProgramResourceProviders()) {
dataResources.addAll(getDataResources(programResourceProvider.getDataResourceProvider()));
}
return dataResources;
}
public static List<DataEntryResource> getDataResources(DataResourceProvider dataResourceProvider)
throws ResourceException {
List<DataEntryResource> dataResources = new ArrayList<>();
if (dataResourceProvider != null) {
dataResourceProvider.accept(
new Visitor() {
@Override
public void visit(DataDirectoryResource directory) {}
@Override
public void visit(DataEntryResource file) {
dataResources.add(file);
}
});
}
return dataResources;
}
protected static Path getFileInTest(String folder, String fileName) {
return Paths.get(ToolHelper.TESTS_DIR, "java", folder, fileName);
}
/**
* Create a temporary JAR file containing all test classes in a package.
*/
protected Path jarTestClassesInPackage(Package pkg) throws IOException {
Path jar = File.createTempFile("junit", ".jar", temp.getRoot()).toPath();
String zipEntryPrefix = ToolHelper.getJarEntryForTestPackage(pkg) + "/";
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
for (Path file : ToolHelper.getClassFilesForTestPackage(pkg)) {
try (FileInputStream in = new FileInputStream(file.toFile())) {
out.putNextEntry(new ZipEntry(zipEntryPrefix + file.getFileName()));
ByteStreams.copy(in, out);
out.closeEntry();
}
}
}
return jar;
}
/** Create a temporary JAR file containing the specified test classes. */
protected Path jarTestClasses(List<Class<?>> classes) throws IOException {
return jarTestClasses(classes.toArray(new Class<?>[]{}));
}
protected static List<Object[]> buildParameters(Object... arraysOrIterables) {
Function<Object, List<Object>> arrayOrIterableToList =
arrayOrIterable -> {
if (arrayOrIterable.getClass().isArray()) {
Object[] array = (Object[]) arrayOrIterable;
return Arrays.asList(array);
} else {
assert arrayOrIterable instanceof Iterable<?>;
Iterable<?> iterable = (Iterable) arrayOrIterable;
return ImmutableList.builder().addAll(iterable).build();
}
};
List<List<Object>> lists =
Arrays.stream(arraysOrIterables).map(arrayOrIterableToList).collect(Collectors.toList());
return cartesianProduct(lists).stream().map(List::toArray).collect(Collectors.toList());
}
/** Compile an application with D8. */
protected AndroidApp compileWithD8(AndroidApp app) throws CompilationFailedException {
D8Command.Builder builder = ToolHelper.prepareD8CommandBuilder(app);
AndroidAppConsumers appSink = new AndroidAppConsumers(builder);
D8.run(builder.build());
return appSink.build();
}
/** Compile an application with D8. */
protected AndroidApp compileWithD8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
throws CompilationFailedException {
return ToolHelper.runD8(app, optionsConsumer);
}
/** Compile an application with R8. */
protected AndroidApp compileWithR8(Class... classes)
throws IOException, CompilationFailedException {
return ToolHelper.runR8(readClasses(classes));
}
/** Compile an application with R8. */
protected AndroidApp compileWithR8(List<Class<?>> classes)
throws IOException, CompilationFailedException {
R8Command command = ToolHelper.prepareR8CommandBuilder(readClasses(classes)).build();
return ToolHelper.runR8(command);
}
/** Compile an application with R8. */
protected AndroidApp compileWithR8(
List<Class<?>> classes, Consumer<InternalOptions> optionsConsumer)
throws IOException, CompilationFailedException {
R8Command command = ToolHelper.prepareR8CommandBuilder(readClasses(classes)).build();
return ToolHelper.runR8(command, optionsConsumer);
}
/** Compile an application with R8. */
protected AndroidApp compileWithR8(AndroidApp app) throws CompilationFailedException {
R8Command command = ToolHelper.prepareR8CommandBuilder(app).build();
return ToolHelper.runR8(command);
}
/** Compile an application with R8. */
protected AndroidApp compileWithR8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
throws IOException, CompilationFailedException {
R8Command command = ToolHelper.prepareR8CommandBuilder(app)
.setDisableTreeShaking(true)
.setDisableMinification(true)
.addProguardConfiguration(ImmutableList.of("-keepattributes *"), Origin.unknown())
.build();
return ToolHelper.runR8(command, optionsConsumer);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(List<Class<?>> classes, String proguardConfig)
throws IOException, CompilationFailedException {
return compileWithR8(readClasses(classes), proguardConfig);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(
List<Class<?>> classes, String proguardConfig, Consumer<InternalOptions> optionsConsumer)
throws IOException, CompilationFailedException {
return compileWithR8(readClasses(classes), proguardConfig, optionsConsumer);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(List<Class<?>> classes, Path proguardConfig)
throws IOException, CompilationFailedException {
return compileWithR8(readClasses(classes), proguardConfig);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(AndroidApp app, Path proguardConfig)
throws IOException, CompilationFailedException {
R8Command command =
ToolHelper.prepareR8CommandBuilder(app)
.addProguardConfigurationFiles(proguardConfig)
.build();
return ToolHelper.runR8(command);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(AndroidApp app, String proguardConfig)
throws IOException, CompilationFailedException {
return compileWithR8(app, proguardConfig, null, Backend.DEX);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(AndroidApp app, String proguardConfig, Backend backend)
throws IOException, CompilationFailedException {
return compileWithR8(app, proguardConfig, null, backend);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(
AndroidApp app, String proguardConfig, Consumer<InternalOptions> optionsConsumer)
throws IOException, CompilationFailedException {
return compileWithR8(app, proguardConfig, optionsConsumer, Backend.DEX);
}
/** Compile an application with R8 using the supplied proguard configuration and backend. */
protected AndroidApp compileWithR8(
AndroidApp app,
String proguardConfig,
Consumer<InternalOptions> optionsConsumer,
Backend backend)
throws CompilationFailedException {
R8Command command =
ToolHelper.prepareR8CommandBuilder(app, emptyConsumer(backend))
.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown())
.addLibraryFiles(runtimeJar(backend))
.build();
return ToolHelper.runR8(command, optionsConsumer);
}
/** Compile an application with R8 using the supplied proguard configuration. */
protected AndroidApp compileWithR8(
AndroidApp app, Path proguardConfig, Consumer<InternalOptions> optionsConsumer)
throws CompilationFailedException {
R8Command command =
ToolHelper.prepareR8CommandBuilder(app)
.addProguardConfigurationFiles(proguardConfig)
.build();
return ToolHelper.runR8(command, optionsConsumer);
}
/**
* Generate a Proguard configuration for keeping the "static void main(String[])" method of the
* specified class.
*/
public static String keepMainProguardConfiguration(Class<?> clazz) {
return keepMainProguardConfiguration(clazz.getTypeName());
}
/**
* Generate a Proguard configuration for keeping the "static void main(String[])" method of the
* specified class.
*/
public static String keepMainProguardConfiguration(Class<?> clazz, List<String> additionalLines) {
return keepMainProguardConfiguration(clazz.getTypeName()) + StringUtils.lines(additionalLines);
}
/**
* Generate a Proguard configuration for keeping the "public static void main(String[])" method of
* the specified class.
*
* The class is assumed to be public.
*/
public static String keepMainProguardConfiguration(String clazz) {
return StringUtils.lines(
"-keep class " + clazz + " {", " public static void main(java.lang.String[]);", "}");
}
public static String noShrinkingNoMinificationProguardConfiguration() {
return StringUtils.lines("-dontshrink", "-dontobfuscate");
}
/**
* Generate a Proguard configuration for keeping the "static void main(String[])" method of the
* specified class and specify if -allowaccessmodification and -dontobfuscate are added as well.
*/
public static String keepMainProguardConfiguration(
Class<?> clazz, boolean allowaccessmodification, boolean obfuscate) {
return keepMainProguardConfiguration(clazz)
+ (allowaccessmodification ? "-allowaccessmodification\n" : "")
+ (obfuscate ? "-printmapping\n" : "-dontobfuscate\n");
}
public static String keepMainProguardConfiguration(
String clazz, boolean allowaccessmodification, boolean obfuscate) {
return keepMainProguardConfiguration(clazz)
+ (allowaccessmodification ? "-allowaccessmodification\n" : "")
+ (obfuscate ? "-printmapping\n" : "-dontobfuscate\n");
}
/**
* Generate a Proguard configuration for keeping the "static void main(String[])" method of the
* specified class and add rules to inline methods with the inlining annotation.
*/
public static String keepMainProguardConfigurationWithInliningAnnotation(Class<?> clazz) {
return "-forceinline class * { @com.android.tools.r8.ForceInline *; }"
+ System.lineSeparator()
+ "-neverinline class * { @com.android.tools.r8.NeverInline *; }"
+ System.lineSeparator()
+ keepMainProguardConfiguration(clazz);
}
@Deprecated
private static String matchInterfaceRule(String name, Class matchInterface) {
return "-" + name + " @" + matchInterface.getTypeName() + " class *";
}
@Deprecated
public static String noVerticalClassMergingRule() {
return matchInterfaceRule(NoVerticalClassMergingRule.RULE_NAME, NoVerticalClassMerging.class);
}
@Deprecated
public static String noHorizontalClassMerging() {
return matchInterfaceRule(
NoHorizontalClassMergingRule.RULE_NAME, NoHorizontalClassMerging.class);
}
/**
* Run application on the specified version of Art with the specified main class.
*/
protected ProcessResult runOnArtRaw(AndroidApp app, String mainClass,
Consumer<ArtCommandBuilder> cmdBuilder, DexVm version) throws IOException {
Path out = File.createTempFile("junit", ".zip", temp.getRoot()).toPath();
app.writeToZip(out, OutputMode.DexIndexed);
return ToolHelper.runArtRaw(
ImmutableList.of(out.toString()), mainClass, cmdBuilder, version, false);
}
/**
* Run application on the specified version of Art with the specified main class.
*/
protected ProcessResult runOnArtRaw(AndroidApp app, String mainClass, DexVm version)
throws IOException {
return runOnArtRaw(app, mainClass, null, version);
}
/**
* Run application on Art with the specified main class.
*/
protected ProcessResult runOnArtRaw(AndroidApp app, String mainClass) throws IOException {
return runOnArtRaw(app, mainClass, null);
}
/**
* Run application on Art with the specified main class.
*/
protected ProcessResult runOnArtRaw(AndroidApp app, Class mainClass) throws IOException {
return runOnArtRaw(app, mainClass.getTypeName());
}
/**
* Run application on Art with the specified main class and provided arguments.
*/
protected String runOnArt(AndroidApp app, Class mainClass, String... args) throws IOException {
return runOnArt(app, mainClass, Arrays.asList(args));
}
/**
* Run application on Art with the specified main class and provided arguments.
*/
protected String runOnArt(AndroidApp app, String mainClass, List<String> args)
throws IOException {
return runOnArt(app, mainClass, args, null);
}
/**
* Run application on Art with the specified main class, provided arguments, and specified VM
* version.
*/
protected String runOnArt(AndroidApp app, String mainClass, List<String> args, DexVm dexVm)
throws IOException {
Path out = File.createTempFile("junit", ".zip", temp.getRoot()).toPath();
app.writeToZip(out, OutputMode.DexIndexed);
return ToolHelper.runArtNoVerificationErrors(
ImmutableList.of(out.toString()), mainClass,
builder -> {
builder.appendArtOption("-ea");
for (String arg : args) {
builder.appendProgramArgument(arg);
}
},
dexVm);
}
/**
* Run application on Art with the specified main class and provided arguments.
*/
protected String runOnArt(AndroidApp app, Class mainClass, List<String> args) throws IOException {
return runOnArt(app, mainClass.getCanonicalName(), args);
}
/**
* Run application on Art with the specified main class and provided arguments.
*/
protected String runOnArt(AndroidApp app, String mainClass, String... args) throws IOException {
return runOnArt(app, mainClass, Arrays.asList(args));
}
/**
* Run a single class application on Java.
*/
protected String runOnJava(Class mainClass) throws Exception {
ProcessResult result = ToolHelper.runJava(mainClass);
ToolHelper.failOnProcessFailure(result);
ToolHelper.failOnVerificationErrors(result);
return result.stdout;
}
/** Run application on Java with the specified main class and provided arguments. */
protected String runOnJava(AndroidApp app, Class mainClass, String... args) throws IOException {
return runOnJava(app, mainClass, Arrays.asList(args));
}
/** Run application on Java with the specified main class and provided arguments. */
protected String runOnJava(AndroidApp app, Class mainClass, List<String> args)
throws IOException {
return runOnJava(app, mainClass.getCanonicalName(), args);
}
/** Run application on Java with the specified main class and provided arguments. */
protected String runOnJava(AndroidApp app, String mainClass, String... args) throws IOException {
return runOnJava(app, mainClass, Arrays.asList(args));
}
/** Run application on Java with the specified main class and provided arguments. */
protected String runOnJava(AndroidApp app, String mainClass, List<String> args)
throws IOException {
ProcessResult result = runOnJavaRaw(app, mainClass, args);
ToolHelper.failOnProcessFailure(result);
ToolHelper.failOnVerificationErrors(result);
return result.stdout;
}
protected ProcessResult runOnJavaRawNoVerify(String main, byte[]... classes) throws IOException {
return runOnJavaRawNoVerify(main, Arrays.asList(classes), Collections.emptyList());
}
protected ProcessResult runOnJavaRawNoVerify(String main, List<byte[]> classes, List<String> args)
throws IOException {
return ToolHelper.runJavaNoVerify(Collections.singletonList(writeToJar(classes)), main, args);
}
protected ProcessResult runOnJavaRaw(String main, byte[]... classes) throws IOException {
return runOnJavaRaw(main, Arrays.asList(classes), Collections.emptyList());
}
protected ProcessResult runOnJavaRaw(String main, List<byte[]> classes, List<String> args)
throws IOException {
List<String> mainAndArgs = new ArrayList<>();
mainAndArgs.add(main);
mainAndArgs.addAll(args);
return ToolHelper.runJava(
Collections.singletonList(writeToJar(classes)),
mainAndArgs.toArray(StringUtils.EMPTY_ARRAY));
}
protected ProcessResult runOnJavaRaw(AndroidApp app, String mainClass, List<String> args)
throws IOException {
Path out = File.createTempFile("junit", ".zip", temp.getRoot()).toPath();
app.writeToZip(out, OutputMode.ClassFile);
List<String> mainAndArgs = new ArrayList<>();
mainAndArgs.add(mainClass);
mainAndArgs.addAll(args);
return ToolHelper.runJava(out, mainAndArgs.toArray(StringUtils.EMPTY_ARRAY));
}
protected ProcessResult runOnJavaRawNoVerify(AndroidApp app, String mainClass, List<String> args)
throws IOException {
Path out = File.createTempFile("junit", ".zip", temp.getRoot()).toPath();
app.writeToZip(out, OutputMode.ClassFile);
return ToolHelper.runJavaNoVerify(out, mainClass, args.toArray(StringUtils.EMPTY_ARRAY));
}
/** Run application on Art or Java with the specified main class. */
protected String runOnVM(AndroidApp app, Class mainClass, Backend backend) throws IOException {
return runOnVM(app, mainClass.getName(), backend);
}
/** Run application on Art or Java with the specified main class. */
protected String runOnVM(AndroidApp app, String mainClass, Backend backend) throws IOException {
switch (backend) {
case CF:
return runOnJava(app, mainClass);
case DEX:
return runOnArt(app, mainClass);
default:
throw new Unreachable("Unexpected backend: " + backend);
}
}
protected ProcessResult runOnVMRaw(AndroidApp app, Class<?> mainClass, Backend backend)
throws IOException {
return runOnVMRaw(app, mainClass.getTypeName(), backend);
}
protected ProcessResult runOnVMRaw(AndroidApp app, String mainClass, Backend backend)
throws IOException {
switch (backend) {
case CF:
return runOnJavaRaw(app, mainClass, ImmutableList.of());
case DEX:
return runOnArtRaw(app, mainClass);
default:
throw new Unreachable("Unexpected backend: " + backend);
}
}
public static String extractClassName(byte[] ccc) {
return DescriptorUtils.descriptorToJavaType(extractClassDescriptor(ccc));
}
public static String extractClassDescriptor(byte[] ccc) {
return "L" + extractClassInternalType(ccc) + ";";
}
private static String extractClassInternalType(byte[] ccc) {
class ClassNameExtractor extends ClassVisitor {
private String className;
private ClassNameExtractor() {
super(ASM_VERSION);
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
className = name;
}
String getClassInternalType() {
return className;
}
}
ClassReader reader = new ClassReader(ccc);
ClassNameExtractor extractor = new ClassNameExtractor();
reader.accept(
extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return extractor.getClassInternalType();
}
protected static void writeClassesToJar(Path output, Collection<Class<?>> classes)
throws IOException {
ClassFileConsumer consumer = new ArchiveConsumer(output);
for (Class<?> clazz : classes) {
consumer.accept(
ByteDataView.of(Files.readAllBytes(ToolHelper.getClassFileForTestClass(clazz))),
DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()),
null);
}
consumer.finished(null);
}
protected static void writeClassesToJar(Path output, Class<?>... classes) throws IOException {
writeClassesToJar(output, Arrays.asList(classes));
}
protected static void writeClassFileDataToJar(Path output, Collection<byte[]> classes) {
ClassFileConsumer consumer = new ArchiveConsumer(output);
for (byte[] clazz : classes) {
consumer.accept(ByteDataView.of(clazz), extractClassDescriptor(clazz), null);
}
consumer.finished(null);
}
protected static void writeClassFilesToJar(Path output, Collection<Path> classes)
throws IOException {
List<byte[]> bytes = new LinkedList<>();
for (Path classPath : classes) {
byte[] classBytes = Files.readAllBytes(Paths.get(classPath.toString()));
bytes.add(classBytes);
}
writeClassFileDataToJar(output, bytes);
}
protected Path writeToJar(List<byte[]> classes) throws IOException {
Path result = File.createTempFile("junit", ".jar", temp.getRoot()).toPath();
writeClassFileDataToJar(result, classes);
return result;
}
protected Path writeToJar(JasminBuilder jasminBuilder) throws Exception {
return writeToJar(jasminBuilder.buildClasses());
}
/**
* Disassemble the content of an application. Only works for an application with only dex code.
*/
protected void disassemble(AndroidApp app) throws Exception {
InternalOptions options = new InternalOptions();
System.out.println(SmaliWriter.smali(app, options));
}
protected MethodSubject getMethodSubject(
CodeInspector inspector,
String className,
String returnType,
String methodName,
List<String> parameters) {
ClassSubject clazz = inspector.clazz(className);
assertTrue(clazz.isPresent());
MethodSubject method = clazz.method(returnType, methodName, parameters);
assertTrue(method.isPresent());
return method;
}
protected MethodSubject getMethodSubject(
AndroidApp application,
String className,
String returnType,
String methodName,
List<String> parameters) {
try {
CodeInspector inspector = new CodeInspector(application);
return getMethodSubject(inspector, className, returnType, methodName, parameters);
} catch (Exception e) {
return null;
}
}
protected DexEncodedMethod getMethod(
AndroidApp application,
String className,
String returnType,
String methodName,
List<String> parameters) {
return getMethodSubject(application, className, returnType, methodName, parameters).getMethod();
}
protected ProgramMethod getMethod(
CodeInspector inspector,
String className,
String returnType,
String methodName,
List<String> parameters) {
return getMethodSubject(inspector, className, returnType, methodName, parameters)
.getProgramMethod();
}
protected static void checkInstructions(
DexCode code, List<Class<? extends Instruction>> instructions) {
assertEquals(instructions.size(), code.instructions.length);
for (int i = 0; i < instructions.size(); ++i) {
assertEquals("Unexpected instruction at index " + i,
instructions.get(i), code.instructions[i].getClass());
}
}
protected Stream<Instruction> filterInstructionKind(
DexCode dexCode, Class<? extends Instruction> kind) {
return Arrays.stream(dexCode.instructions)
.filter(kind::isInstance)
.map(kind::cast);
}
protected long countCall(MethodSubject method, String className, String methodName) {
return method.streamInstructions().filter(instructionSubject -> {
if (instructionSubject.isInvoke()) {
DexMethod invokedMethod = instructionSubject.getMethod();
return invokedMethod.holder.toString().contains(className)
&& invokedMethod.name.toString().contains(methodName);
}
return false;
}).count();
}
protected long countCall(MethodSubject method, String methodName) {
return method.streamInstructions().filter(instructionSubject -> {
if (instructionSubject.isInvoke()) {
DexMethod invokedMethod = instructionSubject.getMethod();
return invokedMethod.name.toString().contains(methodName);
}
return false;
}).count();
}
public enum MinifyMode {
NONE,
JAVA,
AGGRESSIVE;
public boolean isMinify() {
return this != NONE;
}
public boolean isAggressive() {
return this == AGGRESSIVE;
}
public static MinifyMode[] withoutNone() {
return new MinifyMode[] {JAVA, AGGRESSIVE};
}
}
public static ProgramConsumer emptyConsumer(Backend backend) {
if (backend == Backend.DEX) {
return DexIndexedConsumer.emptyConsumer();
} else {
assert backend == Backend.CF;
return ClassFileConsumer.emptyConsumer();
}
}
public static OutputMode outputMode(Backend backend) {
if (backend == Backend.DEX) {
return OutputMode.DexIndexed;
} else {
assert backend == Backend.CF;
return OutputMode.ClassFile;
}
}
@Deprecated
public static Path runtimeJar(TestParameters parameters) {
if (parameters.isDexRuntime()) {
return ToolHelper.getAndroidJar(parameters.getRuntime().asDex().getMinApiLevel());
} else {
assert parameters.isCfRuntime();
return ToolHelper.getJava8RuntimeJar();
}
}
@Deprecated
public static Path runtimeJar(Backend backend) {
if (backend == Backend.DEX) {
return ToolHelper.getDefaultAndroidJar();
} else {
assert backend == Backend.CF;
return ToolHelper.getJava8RuntimeJar();
}
}
public static class JarBuilder {
final Path jar;
final ZipOutputStream stream;
final Set<Class<?>> servicesAdded = Sets.newIdentityHashSet();
private JarBuilder(TemporaryFolder temp) throws IOException {
jar = temp.newFolder().toPath().resolve("a.jar");
stream = new ZipOutputStream(Files.newOutputStream(jar));
}
public static JarBuilder builder(TemporaryFolder temp) throws IOException {
return new JarBuilder(temp);
}
public JarBuilder addClass(Class<?> clazz) throws IOException {
stream.putNextEntry(new ZipEntry(DescriptorUtils.getPathFromJavaType(clazz)));
stream.write(Files.readAllBytes(ToolHelper.getClassFileForTestClass(clazz)));
stream.closeEntry();
return this;
}
public JarBuilder addResource(String path, String content) throws IOException {
stream.putNextEntry(new ZipEntry(path));
stream.write(content.getBytes(StandardCharsets.UTF_8));
stream.closeEntry();
return this;
}
public JarBuilder addServiceWithImplementations(
Class<?> service, List<Class<?>> implementations) throws IOException {
boolean added = servicesAdded.add(service);
assert added : "Currently each service can only be added once";
addResource(
"META-INF/services/" + Greeter.class.getTypeName(),
StringUtils.lines(
implementations.stream().map(Class::getTypeName).collect(Collectors.toList())));
return this;
}
public Path build() throws IOException {
stream.close();
return jar;
}
}
public JarBuilder jarBuilder() throws IOException {
return JarBuilder.builder(temp);
}
public List<Path> buildOnDexRuntime(TestParameters parameters, List<Path> paths)
throws CompilationFailedException, IOException {
if (parameters.isCfRuntime()) {
return paths;
}
return Collections.singletonList(
testForD8()
.addProgramFiles(paths)
.setMinApi(parameters.getApiLevel())
.compile()
.writeToZip());
}
public List<Path> buildOnDexRuntime(TestParameters parameters, Path... paths)
throws IOException, CompilationFailedException {
return buildOnDexRuntime(parameters, Arrays.asList(paths));
}
public Path buildOnDexRuntime(TestParameters parameters, Class<?>... classes)
throws IOException, CompilationFailedException {
if (parameters.isDexRuntime()) {
return testForD8()
.addProgramClasses(classes)
.setMinApi(parameters.getApiLevel())
.compile()
.writeToZip();
}
Path path = temp.newFolder().toPath().resolve("classes.jar");
ArchiveConsumer consumer = new ArchiveConsumer(path);
for (Class<?> clazz : classes) {
consumer.accept(
ByteDataView.of(ToolHelper.getClassAsBytes(clazz)),
DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()),
null);
}
consumer.finished(null);
return path;
}
public Path buildOnDexRuntime(TestParameters parameters, byte[]... classes)
throws IOException, CompilationFailedException {
if (parameters.isDexRuntime()) {
return testForD8()
.addProgramClassFileData(classes)
.setMinApi(parameters.getApiLevel())
.compile()
.writeToZip();
}
Path path = temp.newFolder().toPath().resolve("classes.jar");
ArchiveConsumer consumer = new ArchiveConsumer(path);
for (byte[] clazz : classes) {
consumer.accept(ByteDataView.of(clazz), extractClassDescriptor(clazz), null);
}
consumer.finished(null);
return path;
}
public static DexType toDexType(Class<?> clazz, DexItemFactory dexItemFactory) {
return dexItemFactory.createType(descriptor(clazz));
}
public static DexType toDexType(ClassReference classReference, DexItemFactory dexItemFactory) {
return dexItemFactory.createType(classReference.getDescriptor());
}
public static String binaryName(Class<?> clazz) {
return DescriptorUtils.getBinaryNameFromJavaType(typeName(clazz));
}
public static String descriptor(Class<?> clazz) {
return DescriptorUtils.javaTypeToDescriptor(typeName(clazz));
}
public static PathOrigin getOrigin(Class<?> clazz) {
return new PathOrigin(ToolHelper.getClassFileForTestClass(clazz));
}
public static String typeName(Class<?> clazz) {
return clazz.getTypeName();
}
public static String examplesTypeName(Class<? extends ExamplesClass> clazz) throws Exception {
return ReflectiveBuildPathUtils.resolveClassName(clazz);
}
public static AndroidApiLevel apiLevelWithDefaultInterfaceMethodsSupport() {
return AndroidApiLevel.N;
}
public static boolean hasDefaultInterfaceMethodsSupport(TestParameters parameters) {
return parameters.isCfRuntime()
|| parameters
.getApiLevel()
.isGreaterThanOrEqualTo(apiLevelWithDefaultInterfaceMethodsSupport());
}
public static AndroidApiLevel apiLevelWithStaticInterfaceMethodsSupport() {
return AndroidApiLevel.N;
}
public static AndroidApiLevel apiLevelWithInvokeCustomSupport() {
return AndroidApiLevel.O;
}
public static AndroidApiLevel apiLevelWithNativeMultiDexSupport() {
return AndroidApiLevel.L;
}
public static AndroidApiLevel apiLevelWithTwrCloseResourceSupport() {
return AndroidApiLevel.K;
}
public static boolean canUseJavaUtilObjects(TestParameters parameters) {
return parameters.isCfRuntime()
|| parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
}
public static boolean canUseRequireNonNull(TestParameters parameters) {
return parameters.isDexRuntime()
&& parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
}
public Path compileToZip(
TestParameters parameters, Collection<Class<?>> classPath, Class<?>... compilationUnit)
throws Exception {
return compileToZip(parameters, classPath, Arrays.asList(compilationUnit));
}
public Path compileToZip(
TestParameters parameters,
Collection<Class<?>> classpath,
Collection<Class<?>> compilationUnit)
throws Exception {
if (parameters.isCfRuntime()) {
Path out = temp.newFolder().toPath().resolve("out.jar");
writeClassesToJar(out, compilationUnit);
return out;
}
return testForD8()
.setMinApi(parameters.getApiLevel())
.addProgramClasses(compilationUnit)
.addClasspathClasses(classpath)
.compile()
.writeToZip();
}
protected static CfVersion extractClassFileVersion(byte[] classFileBytes) {
class ClassFileVersionExtractor extends ClassVisitor {
private int version;
private ClassFileVersionExtractor() {
super(ASM_VERSION);
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
this.version = version;
}
CfVersion getClassFileVersion() {
return CfVersion.fromRaw(version);
}
}
ClassReader reader = new ClassReader(classFileBytes);
ClassFileVersionExtractor extractor = new ClassFileVersionExtractor();
reader.accept(
extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return extractor.getClassFileVersion();
}
}