blob: 6700ef5022243ce84d4b902e70b0b62b7cb29385 [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.integration.application;
import static com.android.build.gradle.integration.common.truth.GradleTaskSubject.assertThat;
import static com.android.testutils.truth.DexSubject.assertThat;
import static com.android.testutils.truth.PathSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import com.android.annotations.NonNull;
import com.android.build.gradle.integration.common.fixture.GradleBuildResult;
import com.android.build.gradle.integration.common.fixture.GradleTaskExecutor;
import com.android.build.gradle.integration.common.fixture.GradleTestProject;
import com.android.build.gradle.integration.common.fixture.app.HelloWorldApp;
import com.android.build.gradle.integration.common.utils.TestFileUtils;
import com.android.build.gradle.internal.scope.ArtifactTypeUtil;
import com.android.build.gradle.internal.scope.InternalArtifactType;
import com.android.build.gradle.options.BooleanOption;
import com.android.build.gradle.options.IntegerOption;
import com.android.testutils.TestUtils;
import com.android.testutils.apk.Dex;
import com.android.testutils.apk.Zip;
import com.android.utils.FileUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Rule;
import org.junit.Test;
/** Tests for incremental dexing using dex archives. */
@SuppressWarnings("OptionalGetWithoutIsPresent")
public class DexArchivesTest {
@Rule
public GradleTestProject project =
GradleTestProject.builder()
.fromTestApp(HelloWorldApp.forPlugin("com.android.application"))
.withGradleBuildCacheDirectory(new File("local-build-cache"))
.create();
@Test
public void testInitialBuild() throws Exception {
project.executor().run("assembleDebug");
checkIntermediaryDexArchives(getInitialFolderDexEntries(), getInitialJarDexClasses());
File merged = project.getIntermediateFile("dex/debug/mergeDexDebug/");
assertThat(merged).isDirectory();
assertThat(merged.list()).hasLength(1);
Dex mainDex = project.getApk(GradleTestProject.ApkType.DEBUG).getMainDexFile().get();
assertThat(mainDex).containsExactlyClassesIn(getApkDexClasses());
}
@Test
public void testChangingExistingFile() throws Exception {
project.executor().run("assembleDebug");
TestFileUtils.addMethod(
FileUtils.join(project.getMainSrcDir(), "com/example/helloworld/HelloWorld.java"),
"\npublic void addedMethod() {}");
if (!project.getIntermediateFile(
InternalArtifactType.COMPILE_BUILD_CONFIG_JAR.INSTANCE.getFolderName())
.exists()) {
long created = FileUtils.find(builderDir(), "BuildConfig.dex").get().lastModified();
TestUtils.waitForFileSystemTick();
project.executor().run("assembleDebug");
assertThat(FileUtils.find(builderDir(), "BuildConfig.dex").get())
.wasModifiedAt(created);
assertThat(FileUtils.find(builderDir(), "HelloWorld.dex").get().lastModified())
.isGreaterThan(created);
} else {
project.executor().run("assembleDebug");
}
Dex mainDex = project.getApk(GradleTestProject.ApkType.DEBUG).getMainDexFile().get();
assertThat(mainDex).containsExactlyClassesIn(getApkDexClasses());
assertThat(mainDex)
.containsClass("Lcom/example/helloworld/HelloWorld;")
.that()
.hasMethod("addedMethod");
}
@Test
public void testAddingFile() throws Exception {
project.executor().run("assembleDebug");
if (!project.getIntermediateFile(
InternalArtifactType.COMPILE_BUILD_CONFIG_JAR.INSTANCE.getFolderName())
.exists()) {
long created = FileUtils.find(builderDir(), "BuildConfig.dex").get().lastModified();
addNewClass();
TestUtils.waitForFileSystemTick();
project.executor().run("assembleDebug");
assertThat(FileUtils.find(builderDir(), "BuildConfig.dex").get())
.wasModifiedAt(created);
} else {
addNewClass();
TestUtils.waitForFileSystemTick();
project.executor().run("assembleDebug");
}
List<String> dexEntries = Lists.newArrayList("NewClass.dex");
dexEntries.addAll(getInitialFolderDexEntries());
checkIntermediaryDexArchives(dexEntries, getInitialJarDexClasses());
List<String> dexClasses = Lists.newArrayList("Lcom/example/helloworld/NewClass;");
dexClasses.addAll(getApkDexClasses());
assertThat(project.getApk(GradleTestProject.ApkType.DEBUG).getMainDexFile().get())
.containsExactlyClassesIn(dexClasses);
}
@Test
public void testRemovingFile() throws Exception {
String newClass = "package com.example.helloworld;\n" + "public class ToRemove {}";
File srcToRemove =
FileUtils.join(project.getMainSrcDir(), "com/example/helloworld/ToRemove.java");
Files.write(srcToRemove.toPath(), newClass.getBytes(Charsets.UTF_8));
project.executor().run("assembleDebug");
assertThat(FileUtils.find(builderDir(), "ToRemove.dex").get()).exists();
srcToRemove.delete();
project.executor().run("assembleDebug");
checkIntermediaryDexArchives(getInitialFolderDexEntries(), getInitialJarDexClasses());
assertThat(project.getApk(GradleTestProject.ApkType.DEBUG).getMainDexFile().get())
.containsExactlyClassesIn(getApkDexClasses());
}
@Test
public void testForReleaseVariants() throws IOException, InterruptedException {
GradleBuildResult result =
project.executor()
// http://b/162074215
.with(BooleanOption.INCLUDE_DEPENDENCY_INFO_IN_APKS, false)
.run("assembleRelease");
assertThat(result.getTask(":dexBuilderRelease")).didWork();
assertThat(result.getTask(":mergeDexRelease")).didWork();
assertThat(result.getTask(":mergeExtDexRelease")).didWork();
}
/** Regression test for http://b/68144982. */
@Test
public void testIncrementalDexingRemoteDependency() throws IOException, InterruptedException {
// Add an arbitrary external *AAR* dependency
TestFileUtils.appendToFile(
project.getBuildFile(),
"\n"
+ "android.defaultConfig.minSdkVersion=10\n"
+ "dependencies { implementation ('org.jdeferred:jdeferred-android-aar:1.2.2') { transitive = false } }");
project.executor().run("assembleDebug");
// Minor version update
TestFileUtils.appendToFile(
project.getBuildFile(),
"\ndependencies { implementation ('org.jdeferred:jdeferred-android-aar:1.2.3') { transitive = false } }");
project.executor().run("assembleDebug");
}
@Test
public void testWithCacheDoesNotLoadLocalState() throws IOException, InterruptedException {
GradleTaskExecutor executor = project.executor().withArgument("--build-cache");
executor.run("assembleDebug");
File inputJarHashes =
new File(
ArtifactTypeUtil.getOutputDir(
InternalArtifactType.DEX_ARCHIVE_INPUT_JAR_HASHES.INSTANCE,
project.getBuildDir()),
"debug/out");
assertThat(inputJarHashes).exists();
executor.run("clean");
GradleBuildResult result = executor.run("assembleDebug");
assertThat(result.getTask(":dexBuilderDebug")).wasFromCache();
assertThat(inputJarHashes).doesNotExist();
}
@Test
public void testDexingBucketsImpactOnCaching() throws IOException, InterruptedException {
// 1st build to cache
GradleTaskExecutor executor = project.executor().withArgument("--build-cache");
executor.with(IntegerOption.DEXING_NUMBER_OF_BUCKETS, 1).run("assembleDebug");
File previousRunDexBuckets =
new File(
ArtifactTypeUtil.getOutputDir(
InternalArtifactType.DEX_NUMBER_OF_BUCKETS_FILE.INSTANCE,
project.getBuildDir()),
"debug/out");
assertThat(previousRunDexBuckets).exists();
executor.run("clean");
// 2nd build should be a cache hit
GradleBuildResult result =
executor.with(IntegerOption.DEXING_NUMBER_OF_BUCKETS, 2).run("assembleDebug");
assertThat(previousRunDexBuckets).doesNotExist();
assertThat(result.getTask(":dexBuilderDebug")).wasFromCache();
// 3rd build adds a source file
if (!project.getIntermediateFile(
InternalArtifactType.COMPILE_BUILD_CONFIG_JAR.INSTANCE.getFolderName())
.exists()) {
long initialBuildTimestamp =
FileUtils.find(builderDir(), "BuildConfig.dex").get().lastModified();
addNewClass();
executor.with(IntegerOption.DEXING_NUMBER_OF_BUCKETS, 2).run("assembleDebug");
assertThat(FileUtils.find(builderDir(), "BuildConfig.dex").get().lastModified())
.isGreaterThan(initialBuildTimestamp);
assertThat(FileUtils.find(builderDir(), "NewClass.dex").get().lastModified())
.isGreaterThan(initialBuildTimestamp);
}
}
@Test
public void testIncrementalRunWithChangedBuckets() throws IOException, InterruptedException {
project.executor().with(IntegerOption.DEXING_NUMBER_OF_BUCKETS, 1).run("assembleDebug");
if (!project.getIntermediateFile(
InternalArtifactType.COMPILE_BUILD_CONFIG_JAR.INSTANCE.getFolderName())
.exists()) {
addNewClass();
long initialBuildTimestamp =
FileUtils.find(builderDir(), "BuildConfig.dex").get().lastModified();
project.executor().with(IntegerOption.DEXING_NUMBER_OF_BUCKETS, 2).run("assembleDebug");
assertThat(FileUtils.find(builderDir(), "BuildConfig.dex").get().lastModified())
.isGreaterThan(initialBuildTimestamp);
assertThat(FileUtils.find(builderDir(), "NewClass.dex").get().lastModified())
.isGreaterThan(initialBuildTimestamp);
}
}
private static Stream<String> getDexClasses(Path path) {
try {
return new Dex(path).getClasses().keySet().stream();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private void checkIntermediaryDexArchives(
@NonNull Collection<String> folderDexEntryNames,
@NonNull Collection<String> jarsDexClasses)
throws Exception {
File projectDexOut =
new File(
ArtifactTypeUtil.getOutputDir(
InternalArtifactType.PROJECT_DEX_ARCHIVE.INSTANCE,
project.getBuildDir()),
"debug/out");
List<String> dexInDirs = new ArrayList<>();
List<String> dexInJars = new ArrayList<>();
try (Stream<Path> files = Files.walk(projectDexOut.toPath())) {
files.forEach(
f -> {
if (f.toString().endsWith(".dex")) {
dexInDirs.add(f.getFileName().toString());
} else if (f.toString().endsWith(".jar")) {
try (Zip zip = new Zip(f)) {
dexInJars.addAll(
zip.getEntries().stream()
.flatMap(DexArchivesTest::getDexClasses)
.collect(Collectors.toList()));
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
assertThat(f)
.named("dexing output that is not dex nor jar")
.isDirectory();
}
});
}
assertThat(dexInDirs).containsExactlyElementsIn(folderDexEntryNames);
assertThat(dexInJars).containsExactlyElementsIn(jarsDexClasses);
}
@NonNull
private List<String> getInitialFolderDexEntries() {
if (project.getIntermediateFile(
InternalArtifactType.COMPILE_BUILD_CONFIG_JAR.INSTANCE.getFolderName())
.exists()) {
return Lists.newArrayList("HelloWorld.dex");
} else {
return Lists.newArrayList("BuildConfig.dex", "HelloWorld.dex");
}
}
@NonNull
private List<String> getInitialJarDexClasses() {
return Lists.newArrayList(
"Lcom/example/helloworld/R;",
"Lcom/example/helloworld/R$id;",
"Lcom/example/helloworld/R$layout;",
"Lcom/example/helloworld/R$string;");
}
@NonNull
private List<String> getApkDexClasses() {
if (project.getIntermediateFile(
InternalArtifactType.COMPILE_BUILD_CONFIG_JAR.INSTANCE.getFolderName())
.exists()) {
return Lists.newArrayList(
"Lcom/example/helloworld/HelloWorld;",
"Lcom/example/helloworld/R;",
"Lcom/example/helloworld/R$id;",
"Lcom/example/helloworld/R$layout;",
"Lcom/example/helloworld/R$string;");
} else {
return Lists.newArrayList(
"Lcom/example/helloworld/BuildConfig;",
"Lcom/example/helloworld/HelloWorld;",
"Lcom/example/helloworld/R;",
"Lcom/example/helloworld/R$id;",
"Lcom/example/helloworld/R$layout;",
"Lcom/example/helloworld/R$string;");
}
}
@NonNull
private File builderDir() {
return new File(
ArtifactTypeUtil.getOutputDir(
InternalArtifactType.PROJECT_DEX_ARCHIVE.INSTANCE, project.getBuildDir()),
"debug/out");
}
private void addNewClass() throws IOException {
String newClass = "package com.example.helloworld;\n" + "public class NewClass {}";
Files.write(
project.getMainSrcDir().toPath().resolve("com/example/helloworld/NewClass.java"),
newClass.getBytes(Charsets.UTF_8));
}
}