// 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 java.io.IOException; import java.nio.file.DirectoryStream; 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 outputArchives = runDexSplitter(256 * 256, "from_single", dexArchive); assertThat(outputArchives).hasSize(1); ImmutableSet expectedFiles = dexEntries(dexArchive); assertThat(dexEntries(outputArchives.get(0))).containsExactlyElementsIn(expectedFiles); } @Test public void testDuplicateInputIgnored() throws Exception { Path dexArchive = buildDexArchive(); ImmutableList outputArchives = runDexSplitter(256 * 256, "from_duplicate", dexArchive, dexArchive); assertThat(outputArchives).hasSize(1); ImmutableSet expectedFiles = dexEntries(dexArchive); assertThat(dexEntries(outputArchives.get(0))).containsExactlyElementsIn(expectedFiles); } @Test public void testSingleInputMultidexOutput() throws Exception { Path dexArchive = buildDexArchive(); ImmutableList outputArchives = runDexSplitter(200, "multidex_from_single", dexArchive); assertThat(outputArchives.size()).isGreaterThan(1); // test sanity ImmutableSet expectedEntries = dexEntries(dexArchive); assertExpectedEntries(outputArchives, expectedEntries); } @Test public void testMultipleInputsMultidexOutput() throws Exception { Path dexArchive = buildDexArchive(); Path dexArchive2 = buildDexArchive(INPUT_JAR2, "jar2.dex.zip"); ImmutableList outputArchives = runDexSplitter(200, "multidex", dexArchive, dexArchive2); assertThat(outputArchives.size()).isGreaterThan(1); // test sanity HashSet expectedEntries = new HashSet<>(); expectedEntries.addAll(dexEntries(dexArchive)); expectedEntries.addAll(dexEntries(dexArchive2)); assertExpectedEntries(outputArchives, expectedEntries); } /** * Tests that the same input creates identical output in 2 runs. Flakiness here would indicate * race conditions or other concurrency issues. */ @Test public void testDeterminism() throws Exception { Path dexArchive = buildDexArchive(); Path dexArchive2 = buildDexArchive(INPUT_JAR2, "jar2.dex.zip"); ImmutableList outputArchives = runDexSplitter(200, "det1", dexArchive, dexArchive2); assertThat(outputArchives.size()).isGreaterThan(1); // test sanity ImmutableList outputArchives2 = runDexSplitter(200, "det2", dexArchive, dexArchive2); assertThat(outputArchives2).hasSize(outputArchives.size()); // paths differ though Path outputRoot2 = outputArchives2.get(0).getParent(); for (Path outputArchive : outputArchives) { ImmutableList expectedEntries; try (ZipFile zip = new ZipFile(outputArchive.toFile())) { expectedEntries = zip.stream().collect(ImmutableList.toImmutableList()); } ImmutableList actualEntries; try (ZipFile zip2 = new ZipFile(outputRoot2.resolve(outputArchive.getFileName()).toFile())) { actualEntries = zip2.stream().collect(ImmutableList.toImmutableList()); } int len = expectedEntries.size(); assertThat(actualEntries).hasSize(len); for (int i = 0; i < len; ++i) { ZipEntry expected = expectedEntries.get(i); ZipEntry actual = actualEntries.get(i); assertThat(actual.getName()).named(actual.getName()).isEqualTo(expected.getName()); assertThat(actual.getSize()).named(actual.getName()).isEqualTo(expected.getSize()); assertThat(actual.getCrc()).named(actual.getName()).isEqualTo(expected.getCrc()); } } } @Test public void testMainDexList() throws Exception { Path dexArchive = buildDexArchive(); ImmutableList outputArchives = runDexSplitter( 200, /*inclusionFilterJar=*/ null, "main_dex_list", MAIN_DEX_LIST_FILE, /*minimalMainDex=*/ false, dexArchive); ImmutableSet 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 outputArchives = runDexSplitter( 256 * 256, /*inclusionFilterJar=*/ null, "minimal_main_dex", MAIN_DEX_LIST_FILE, /*minimalMainDex=*/ true, dexArchive); ImmutableSet expectedEntries = dexEntries(dexArchive); assertThat(outputArchives.size()).isGreaterThan(1); // test sanity assertThat(dexEntries(outputArchives.get(0))) .containsExactlyElementsIn(expectedMainDexEntries()); assertExpectedEntries(outputArchives, expectedEntries); } @Test public void testInclusionFilterJar() throws Exception { Path dexArchive = buildDexArchive(); Path dexArchive2 = buildDexArchive(INPUT_JAR2, "jar2.dex.zip"); ImmutableList outputArchives = runDexSplitter( 256 * 256, INPUT_JAR2, "filtered", /*mainDexList=*/ null, /*minimalMainDex=*/ false, dexArchive, dexArchive2); // Only expect entries from the Jar we filtered by assertExpectedEntries(outputArchives, dexEntries(dexArchive2)); } private static Iterable expectedMainDexEntries() throws IOException { return Iterables.transform( Files.readAllLines(MAIN_DEX_LIST_FILE), new Function() { @Override public String apply(String input) { return input + ".dex"; } }); } @Test public void testMultidexOffWithMultidexFlags() throws Exception { Path dexArchive = buildDexArchive(); try { runDexSplitter( 200, /*inclusionFilterJar=*/ null, "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 outputArchives, Set expectedEntries) throws IOException { ImmutableSet.Builder 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 dexEntries(Path dexArchive) throws IOException { try (ZipFile input = new ZipFile(dexArchive.toFile())) { ImmutableSet result = input .stream() .map(ZipEntryName.INSTANCE) .filter(Predicates.containsPattern(".*\\.class.dex$")) .collect(ImmutableSet.toImmutableSet()); assertThat(result).isNotEmpty(); // test sanity return result; } } private ImmutableList runDexSplitter(int maxNumberOfIdxPerDex, String outputRoot, Path... dexArchives) throws IOException { return runDexSplitter( maxNumberOfIdxPerDex, /*inclusionFilterJar=*/ null, outputRoot, /*mainDexList=*/ null, /*minimalMainDex=*/ false, dexArchives); } private ImmutableList runDexSplitter( int maxNumberOfIdxPerDex, @Nullable Path inclusionFilterJar, 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; options.inclusionFilterJar = inclusionFilterJar; DexFileSplitter.splitIntoShards(options); assertThat(options.outputDirectory.toFile().exists()).isTrue(); ImmutableSet files = readFiles(options.outputDirectory, "*.zip"); ImmutableList.Builder 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 static ImmutableSet readFiles(Path directory, String glob) throws IOException { try (DirectoryStream stream = Files.newDirectoryStream(directory, glob)) { return ImmutableSet.copyOf(stream); } } 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 { INSTANCE; @Override public String apply(ZipEntry input) { return input.getName(); } } }