blob: bb0e4e89e6b074d0fdf908a90223734b63eb910e [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.build.gradle.internal.transforms;
import static com.android.build.api.transform.Status.CHANGED;
import static com.android.build.api.transform.Status.REMOVED;
import static com.android.testutils.truth.PathSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.transforms.testdata.Animal;
import com.android.build.gradle.internal.transforms.testdata.CarbonForm;
import com.android.build.gradle.internal.transforms.testdata.Cat;
import com.android.build.gradle.internal.transforms.testdata.Tiger;
import com.android.build.gradle.internal.transforms.testdata.Toy;
import com.android.builder.core.DefaultDexOptions;
import com.android.builder.dexing.DexerTool;
import com.android.builder.utils.FileCache;
import com.android.testutils.TestInputsGenerator;
import com.android.testutils.TestUtils;
import com.android.testutils.truth.MoreTruth;
import com.android.utils.FileUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class DexArchiveBuilderTransformDesugaringTest {
private TransformOutputProvider outputProvider;
private Path out;
@Rule public TemporaryFolder tmpDir = new TemporaryFolder();
@Before
public void setUp() throws IOException {
out = tmpDir.getRoot().toPath().resolve("out");
Files.createDirectories(out);
outputProvider = new TestTransformOutputProvider(out);
}
@Test
public void testLambdas() throws Exception {
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(
input, ImmutableSet.of(CarbonForm.class, Animal.class, Cat.class, Toy.class));
TransformInput dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(false)
.build();
getTransform(null, 15, true, false).transform(invocation);
// it should contain Cat and synthesized lambda class
MoreTruth.assertThatDex(getDex(Cat.class)).hasClassesCount(2);
}
interface WithDefault {
default void foo() {}
}
interface WithStatic {
static void bar() {}
}
static class ImplementsWithDefault implements WithDefault {}
static class InvokesDefault {
public static void main(String[] args) {
new ImplementsWithDefault().foo();
}
}
@Test
public void testDefaultMethods_minApiBelow24() throws Exception {
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(
input, ImmutableSet.of(WithDefault.class, WithStatic.class));
TransformInput dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(false)
.build();
getTransform(null, 23, true, false).transform(invocation);
// it contains both original and synthesized
MoreTruth.assertThatDex(getDex(WithDefault.class)).hasClassesCount(2);
MoreTruth.assertThatDex(getDex(WithStatic.class)).hasClassesCount(2);
}
@Test
public void testDefaultMethods_minApiAbove24() throws Exception {
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(
input, ImmutableSet.of(WithDefault.class, WithStatic.class));
TransformInput dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(false)
.build();
getTransform(null, 24, true, false).transform(invocation);
// it contains only the original class
MoreTruth.assertThatDex(getDex(WithDefault.class)).hasClassesCount(1);
MoreTruth.assertThatDex(getDex(WithStatic.class)).hasClassesCount(1);
}
@Test
public void testIncremental_lambdaClass() throws Exception {
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(
input, ImmutableSet.of(CarbonForm.class, Animal.class, Cat.class, Toy.class));
TransformInput dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(false)
.build();
getTransform(null).transform(invocation);
File catDex = getDex(Cat.class);
File animalDex = getDex(Animal.class);
long catTimestamp = catDex.lastModified();
long animalTimestamp = animalDex.lastModified();
TestUtils.waitForFileSystemTick();
dirInput =
TransformTestHelper.directoryBuilder(input.toFile())
.putChangedFiles(getChangedStatusMap(input, CHANGED, Toy.class))
.build();
invocation =
TransformTestHelper.invocationBuilder()
.setIncremental(true)
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.build();
getTransform(null).transform(invocation);
catDex = getDex(Cat.class);
animalDex = getDex(Animal.class);
assertThat(catTimestamp).isLessThan(catDex.lastModified());
assertThat(animalTimestamp).isEqualTo(animalDex.lastModified());
}
@Test
public void testIncremental_lambdaClass_removed() throws Exception {
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(
input, ImmutableSet.of(CarbonForm.class, Animal.class, Cat.class, Toy.class));
TransformInput dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(false)
.build();
getTransform(null).transform(invocation);
File catDex = getDex(Cat.class);
File animalDex = getDex(Animal.class);
long catTimestamp = catDex.lastModified();
long animalTimestamp = animalDex.lastModified();
TestUtils.waitForFileSystemTick();
dirInput =
TransformTestHelper.directoryBuilder(input.toFile())
.putChangedFiles(getChangedStatusMap(input, REMOVED, Toy.class))
.build();
invocation =
TransformTestHelper.invocationBuilder()
.setIncremental(true)
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.build();
File toyDex = getDex(Toy.class);
getTransform(null).transform(invocation);
catDex = getDex(Cat.class);
animalDex = getDex(Animal.class);
assertThat(catTimestamp).isLessThan(catDex.lastModified());
assertThat(animalTimestamp).isEqualTo(animalDex.lastModified());
assertThat(toyDex).doesNotExist();
}
@Test
public void testIncremental_changeSuperTypes() throws Exception {
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(
input,
ImmutableSet.of(CarbonForm.class, Animal.class, Cat.class, Tiger.class, Toy.class));
TransformInput dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(false)
.build();
getTransform(null).transform(invocation);
File tigerDex = getDex(Tiger.class);
File carbonFormDex = getDex(CarbonForm.class);
long tigerTimestamp = tigerDex.lastModified();
long carbonFormTimestamp = carbonFormDex.lastModified();
dirInput =
TransformTestHelper.directoryBuilder(input.toFile())
.putChangedFiles(getChangedStatusMap(input, CHANGED, Animal.class))
.build();
invocation =
TransformTestHelper.invocationBuilder()
.setIncremental(true)
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.build();
TestUtils.waitForFileSystemTick();
getTransform(null).transform(invocation);
tigerDex = getDex(Tiger.class);
carbonFormDex = getDex(CarbonForm.class);
assertThat(tigerTimestamp).isLessThan(tigerDex.lastModified());
assertThat(carbonFormTimestamp).isEqualTo(carbonFormDex.lastModified());
dirInput =
TransformTestHelper.directoryBuilder(input.toFile())
.putChangedFiles(getChangedStatusMap(input, CHANGED, Cat.class))
.build();
invocation =
TransformTestHelper.invocationBuilder()
.setIncremental(true)
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.build();
TestUtils.waitForFileSystemTick();
getTransform(null).transform(invocation);
tigerDex = getDex(Tiger.class);
carbonFormDex = getDex(CarbonForm.class);
assertThat(tigerTimestamp).isLessThan(tigerDex.lastModified());
assertThat(carbonFormTimestamp).isEqualTo(carbonFormDex.lastModified());
}
@Test
public void test_incremental_full_incremental()
throws IOException, TransformException, InterruptedException {
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(input, ImmutableSet.of(CarbonForm.class, Animal.class));
TransformInput dirInput =
TransformTestHelper.directoryBuilder(input.toFile())
.putChangedFiles(
getChangedStatusMap(
input, Status.ADDED, CarbonForm.class, Animal.class))
.build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(true)
.build();
getTransform(null).transform(invocation);
File animalDex = getDex(Animal.class);
dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(false)
.build();
getTransform(null).transform(invocation);
dirInput =
TransformTestHelper.directoryBuilder(input.toFile())
.putChangedFiles(getChangedStatusMap(input, Status.REMOVED, Animal.class))
.build();
invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.setIncremental(true)
.build();
getTransform(null).transform(invocation);
assertThat(getDex(CarbonForm.class)).exists();
assertThat(animalDex).doesNotExist();
}
@Test
public void test_incremental_jarAndDir()
throws IOException, TransformException, InterruptedException {
Path jar = tmpDir.getRoot().toPath().resolve("input.jar");
Path input = tmpDir.getRoot().toPath().resolve("input");
TestInputsGenerator.pathWithClasses(jar, ImmutableSet.of(CarbonForm.class, Animal.class));
TestInputsGenerator.pathWithClasses(
input, ImmutableSet.of(Toy.class, Cat.class, Tiger.class));
TransformInput dirInput = TransformTestHelper.directoryBuilder(input.toFile()).build();
TransformInput jarInput = TransformTestHelper.singleJarBuilder(jar.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.addInput(jarInput)
.setIncremental(false)
.build();
getTransform(null).transform(invocation);
long catTimestamp = getDex(Cat.class).lastModified();
long toyTimestamp = getDex(Toy.class).lastModified();
jarInput =
TransformTestHelper.singleJarBuilder(jar.toFile())
.setStatus(Status.CHANGED)
.build();
invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(dirInput)
.addInput(jarInput)
.setIncremental(true)
.build();
TestUtils.waitForFileSystemTick();
getTransform(null).transform(invocation);
assertThat(catTimestamp).isLessThan(getDex(Cat.class).lastModified());
assertThat(toyTimestamp).isEqualTo(getDex(Toy.class).lastModified());
}
/** Regression test to make sure we do not add unchanged files to cache. */
@Test
public void test_incremental_notChangedNotAddedToCache() throws Exception {
Path jar = tmpDir.getRoot().toPath().resolve("input.jar");
TestInputsGenerator.pathWithClasses(jar, ImmutableSet.of(CarbonForm.class, Animal.class));
TransformInput jarInput =
TransformTestHelper.singleJarBuilder(jar.toFile())
.setScopes(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
.setContentTypes(QualifiedContent.DefaultContentType.CLASSES)
.build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(jarInput)
.setIncremental(false)
.build();
FileCache cache = FileCache.getInstanceWithSingleProcessLocking(tmpDir.newFolder());
getTransform(cache).transform(invocation);
String[] numEntries = Objects.requireNonNull(cache.getCacheDirectory().list());
File referencedJar = tmpDir.newFile("referenced.jar");
TestInputsGenerator.jarWithEmptyClasses(referencedJar.toPath(), ImmutableList.of("A"));
TransformInput referencedInput =
TransformTestHelper.singleJarBuilder(referencedJar).build();
jarInput =
TransformTestHelper.singleJarBuilder(jar.toFile())
.setStatus(Status.NOTCHANGED)
.setScopes(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
.setContentTypes(QualifiedContent.DefaultContentType.CLASSES)
.build();
invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(jarInput)
.addReferenceInput(referencedInput)
.setIncremental(true)
.build();
getTransform(cache).transform(invocation);
assertThat(cache.getCacheDirectory().list()).named("cache entries").isEqualTo(numEntries);
}
@Test
public void test_duplicateClasspathEntries() throws Exception {
Path lib1 = tmpDir.getRoot().toPath().resolve("lib1.jar");
TestInputsGenerator.pathWithClasses(
lib1, ImmutableSet.of(ImplementsWithDefault.class, WithDefault.class));
Path lib2 = tmpDir.getRoot().toPath().resolve("lib2.jar");
TestInputsGenerator.pathWithClasses(lib2, ImmutableSet.of(WithDefault.class));
Path app = tmpDir.getRoot().toPath().resolve("app");
TestInputsGenerator.pathWithClasses(app, ImmutableSet.of(InvokesDefault.class));
TransformInput lib1Input =
TransformTestHelper.singleJarBuilder(lib1.toFile())
.setScopes(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
.setContentTypes(QualifiedContent.DefaultContentType.CLASSES)
.build();
TransformInput lib2Input =
TransformTestHelper.singleJarBuilder(lib2.toFile())
.setScopes(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
.setContentTypes(QualifiedContent.DefaultContentType.CLASSES)
.build();
TransformInput appInput = TransformTestHelper.directoryBuilder(app.toFile()).build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(lib1Input)
.addInput(lib2Input)
.addInput(appInput)
.setIncremental(false)
.build();
getTransform(null, 15, true, true).transform(invocation);
MoreTruth.assertThatDex(getDex(InvokesDefault.class)).hasClassesCount(1);
}
/** Regression test for b/117062425. */
@Test
public void test_incrementalDesugaringWithCaching() throws Exception {
Path lib1 = tmpDir.getRoot().toPath().resolve("lib1.jar");
TestInputsGenerator.pathWithClasses(lib1, ImmutableSet.of(ImplementsWithDefault.class));
Path lib2 = tmpDir.getRoot().toPath().resolve("lib2.jar");
TestInputsGenerator.pathWithClasses(lib2, ImmutableSet.of(WithDefault.class));
TransformInput lib1Input =
TransformTestHelper.singleJarBuilder(lib1.toFile())
.setScopes(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
.setContentTypes(QualifiedContent.DefaultContentType.CLASSES)
.build();
// Mimics dex that from cache for lib1.jar. Transform invocation should remove it.
Files.createFile(out.resolve("lib1.jar.jar"));
TransformInput lib2Input =
TransformTestHelper.singleJarBuilder(lib2.toFile())
.setScopes(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
.setContentTypes(QualifiedContent.DefaultContentType.CLASSES)
.setStatus(Status.CHANGED)
.build();
TransformInvocation invocation =
TransformTestHelper.invocationBuilder()
.setTransformOutputProvider(outputProvider)
.addInput(lib1Input)
.addInput(lib2Input)
.setIncremental(true)
.build();
getTransform(null, 15, true, true).transform(invocation);
List<Path> lib1DexOutputs =
Files.list(out)
.filter(p -> p.getFileName().toString().startsWith("lib1.jar"))
.collect(Collectors.toList());
assertThat(lib1DexOutputs).hasSize(1);
}
@NonNull
private DexArchiveBuilderTransform getTransform(
@Nullable FileCache userCache,
int minSdkVersion,
boolean isDebuggable,
boolean includeAndroidJar) {
List<File> androidJasClasspath =
includeAndroidJar
? ImmutableList.of(TestUtils.getPlatformFile("android.jar"))
: ImmutableList.of();
return new DexArchiveBuilderTransformBuilder()
.setAndroidJarClasspath(() -> androidJasClasspath)
.setDexOptions(new DefaultDexOptions())
.setMessageReceiver(new NoOpMessageReceiver())
.setUserLevelCache(userCache)
.setMinSdkVersion(minSdkVersion)
.setDexer(DexerTool.D8)
.setUseGradleWorkers(false)
.setInBufferSize(10)
.setOutBufferSize(10)
.setIsDebuggable(isDebuggable)
.setJava8LangSupportType(VariantScope.Java8LangSupport.D8)
.setProjectVariant("myVariant")
.setIncludeFeaturesInScope(false)
.setNumberOfBuckets(2)
.createDexArchiveBuilderTransform();
}
@NonNull
private DexArchiveBuilderTransform getTransform(@Nullable FileCache userCache) {
return getTransform(userCache, 15, true, false);
}
@NonNull
private File getDex(@NonNull Class<?> clazz) {
return Iterables.getOnlyElement(
FileUtils.find(
out.toFile(), Pattern.compile(".*" + clazz.getSimpleName() + "\\.dex")));
}
@NonNull
private Map<File, Status> getChangedStatusMap(
@NonNull Path root, @NonNull Status status, @NonNull Class<?>... classes) {
Map<File, Status> statusMap = new HashMap<>();
for (Class<?> clazz : classes) {
statusMap.put(root.resolve(TestInputsGenerator.getPath(clazz)).toFile(), status);
}
return statusMap;
}
}