blob: c2354ca134ff0c696cd9318e14d386e4bbe08300 [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.android.dexer;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import com.android.dx.command.dexer.DxContext;
import com.android.dx.dex.code.PositionList;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link DexFileSplitter}. */
@RunWith(JUnit4.class)
public class DexFileSplitterTest {
private static final Path WORKING_DIR = Paths.get(System.getProperty("user.dir"));
private static final Path INPUT_JAR = WORKING_DIR.resolve(System.getProperty("testinputjar"));
private static final Path INPUT_JAR2 = WORKING_DIR.resolve(System.getProperty("testinputjar2"));
private static final Path MAIN_DEX_LIST_FILE =
WORKING_DIR.resolve(System.getProperty("testmaindexlist"));
static final String DEX_PREFIX = "classes";
@Test
public void testSingleInputSingleOutput() throws Exception {
Path dexArchive = buildDexArchive();
ImmutableList<Path> outputArchives = runDexSplitter(256 * 256, "from_single", dexArchive);
assertThat(outputArchives).hasSize(1);
ImmutableSet<String> expectedFiles = dexEntries(dexArchive);
assertThat(dexEntries(outputArchives.get(0))).containsExactlyElementsIn(expectedFiles);
}
@Test
public void testDuplicateInputIgnored() throws Exception {
Path dexArchive = buildDexArchive();
ImmutableList<Path> outputArchives =
runDexSplitter(256 * 256, "from_duplicate", dexArchive, dexArchive);
assertThat(outputArchives).hasSize(1);
ImmutableSet<String> expectedFiles = dexEntries(dexArchive);
assertThat(dexEntries(outputArchives.get(0))).containsExactlyElementsIn(expectedFiles);
}
@Test
public void testSingleInputMultidexOutput() throws Exception {
Path dexArchive = buildDexArchive();
ImmutableList<Path> outputArchives = runDexSplitter(200, "multidex_from_single", dexArchive);
assertThat(outputArchives.size()).isGreaterThan(1); // test sanity
ImmutableSet<String> expectedEntries = dexEntries(dexArchive);
assertExpectedEntries(outputArchives, expectedEntries);
}
@Test
public void testMultipleInputsMultidexOutput() throws Exception {
Path dexArchive = buildDexArchive();
Path dexArchive2 = buildDexArchive(INPUT_JAR2, "jar2.dex.zip");
ImmutableList<Path> outputArchives = runDexSplitter(200, "multidex", dexArchive, dexArchive2);
assertThat(outputArchives.size()).isGreaterThan(1); // test sanity
HashSet<String> expectedEntries = new HashSet<>();
expectedEntries.addAll(dexEntries(dexArchive));
expectedEntries.addAll(dexEntries(dexArchive2));
assertExpectedEntries(outputArchives, expectedEntries);
}
@Test
public void testMainDexList() throws Exception {
Path dexArchive = buildDexArchive();
ImmutableList<Path> outputArchives =
runDexSplitter(
200,
"main_dex_list",
MAIN_DEX_LIST_FILE,
/*minimalMainDex=*/ false,
dexArchive);
ImmutableSet<String> expectedEntries = dexEntries(dexArchive);
assertThat(outputArchives.size()).isGreaterThan(1); // test sanity
assertThat(dexEntries(outputArchives.get(0)))
.containsAllIn(expectedMainDexEntries());
assertExpectedEntries(outputArchives, expectedEntries);
}
@Test
public void testMinimalMainDex() throws Exception {
Path dexArchive = buildDexArchive();
ImmutableList<Path> outputArchives =
runDexSplitter(
256 * 256,
"minimal_main_dex",
MAIN_DEX_LIST_FILE,
/*minimalMainDex=*/ true,
dexArchive);
ImmutableSet<String> expectedEntries = dexEntries(dexArchive);
assertThat(outputArchives.size()).isGreaterThan(1); // test sanity
assertThat(dexEntries(outputArchives.get(0)))
.containsExactlyElementsIn(expectedMainDexEntries());
assertExpectedEntries(outputArchives, expectedEntries);
}
private static Iterable<String> expectedMainDexEntries() throws IOException {
return Iterables.transform(
Files.readAllLines(MAIN_DEX_LIST_FILE),
new Function<String, String>() {
@Override
public String apply(String input) {
return input + ".dex";
}
});
}
@Test
public void testMultidexOffWithMultidexFlags() throws Exception {
Path dexArchive = buildDexArchive();
try {
runDexSplitter(
200,
"should_fail",
/*mainDexList=*/ null,
/*minimalMainDex=*/ true,
dexArchive);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertThat(e)
.hasMessageThat()
.isEqualTo("--minimal-main-dex not allowed without --main-dex-list");
}
}
private void assertExpectedEntries(
ImmutableList<Path> outputArchives, Set<String> expectedEntries) throws IOException {
ImmutableSet.Builder<String> actualFiles = ImmutableSet.builder();
for (Path outputArchive : outputArchives) {
actualFiles.addAll(dexEntries(outputArchive));
}
// ImmutableSet.Builder.build would fail if there were duplicates. Additionally we make sure
// all expected files are here
assertThat(actualFiles.build()).containsExactlyElementsIn(expectedEntries);
}
private ImmutableSet<String> dexEntries(Path dexArchive) throws IOException {
try (ZipFile input = new ZipFile(dexArchive.toFile())) {
ImmutableSet<String> result = ImmutableSet.copyOf(Iterators.filter(
Iterators.transform(Iterators.forEnumeration(input.entries()), ZipEntryName.INSTANCE),
Predicates.containsPattern(".*\\.class.dex$")));
assertThat(result).isNotEmpty(); // test sanity
return result;
}
}
private ImmutableList<Path> runDexSplitter(int maxNumberOfIdxPerDex, String outputRoot,
Path... dexArchives) throws IOException {
return runDexSplitter(
maxNumberOfIdxPerDex,
outputRoot,
/*mainDexList=*/ null,
/*minimalMainDex=*/ false,
dexArchives);
}
private ImmutableList<Path> runDexSplitter(
int maxNumberOfIdxPerDex,
String outputRoot,
@Nullable Path mainDexList,
boolean minimalMainDex,
Path... dexArchives)
throws IOException {
DexFileSplitter.Options options = new DexFileSplitter.Options();
options.inputArchives = ImmutableList.copyOf(dexArchives);
options.outputDirectory =
FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR"), outputRoot);
options.maxNumberOfIdxPerDex = maxNumberOfIdxPerDex;
options.mainDexListFile = mainDexList;
options.minimalMainDex = minimalMainDex;
DexFileSplitter.splitIntoShards(options);
assertThat(options.outputDirectory.toFile().exists()).isTrue();
ImmutableSet<Path> files =
ImmutableSet.copyOf(Files.newDirectoryStream(options.outputDirectory, "*.zip"));
ImmutableList.Builder<Path> result = ImmutableList.builder();
for (int i = 1; i <= files.size(); ++i) {
Path path = options.outputDirectory.resolve(i + ".shard.zip");
assertThat(files).contains(path);
result.add(path);
}
return result.build(); // return expected files in sorted order
}
private Path buildDexArchive() throws Exception {
return buildDexArchive(INPUT_JAR, "libtests.dex.zip");
}
private Path buildDexArchive(Path inputJar, String outputZip) throws Exception {
DexBuilder.Options options = new DexBuilder.Options();
// Use Jar file that has this test in it as the input Jar
options.inputJar = inputJar;
options.outputZip =
FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR"), outputZip);
options.maxThreads = 1;
Dexing.DexingOptions dexingOptions = new Dexing.DexingOptions();
dexingOptions.optimize = true;
dexingOptions.positionInfo = PositionList.LINES;
DexBuilder.buildDexArchive(options, new Dexing(new DxContext(), dexingOptions));
return options.outputZip;
}
// Can't use lambda for Java 7 compatibility so we can run this Jar through dx without desugaring.
private enum ZipEntryName implements Function<ZipEntry, String> {
INSTANCE;
@Override
public String apply(ZipEntry input) {
return input.getName();
}
}
}