diff options
Diffstat (limited to 'src/test/java/com/google/devtools/build')
11 files changed, 894 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/android/dexer/AllTests.java b/src/test/java/com/google/devtools/build/android/dexer/AllTests.java new file mode 100644 index 0000000000..0e6ea791c9 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/AllTests.java @@ -0,0 +1,21 @@ +// 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 com.google.devtools.build.lib.testutil.ClasspathSuite; +import org.junit.runner.RunWith; + +/** Test suite for dexer tests. */ +@RunWith(ClasspathSuite.class) +public class AllTests {} diff --git a/src/test/java/com/google/devtools/build/android/dexer/BUILD b/src/test/java/com/google/devtools/build/android/dexer/BUILD new file mode 100644 index 0000000000..4f8f600272 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/BUILD @@ -0,0 +1,45 @@ +# Description: +# Tests for the blaze dx bridge code. + +java_library( + name = "tests", + srcs = [ + "AllTests.java", + ] + select({ + "//external:has_androidsdk": glob( + ["*Test.java"], + exclude = [ + "NoAndroidSdkStubTest.java", + "AllTests.java", + ], + ), + "//conditions:default": ["NoAndroidSdkStubTest.java"], + }), + resources = ["testresource.txt"], + deps = [ + "//src/test/java/com/google/devtools/build/lib:testutil", + "//src/tools/android/java/com/google/devtools/build/android/dexer", + "//third_party:guava", + "//third_party:junit4", + "//third_party:mockito", + "//third_party:truth", + ] + select({ + "//external:has_androidsdk": ["//external:android/dx_jar_import"], + "//conditions:default": [], + }), +) + +java_test( + name = "AllTests", + data = [ + "test_main_dex_list.txt", + ":tests", + ], + jvm_flags = [ + "-Dtestmaindexlist=$(location :test_main_dex_list.txt)", + "-Dtestinputjar=$(location :tests)", + ], + runtime_deps = [ + ":tests", + ], +) diff --git a/src/test/java/com/google/devtools/build/android/dexer/DexBuilderTest.java b/src/test/java/com/google/devtools/build/android/dexer/DexBuilderTest.java new file mode 100644 index 0000000000..5adca31806 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/DexBuilderTest.java @@ -0,0 +1,71 @@ +// 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 java.nio.charset.StandardCharsets.UTF_8; + +import com.android.dex.Dex; +import com.google.common.io.ByteStreams; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DexBuilder}. */ +@RunWith(JUnit4.class) +public class DexBuilderTest { + + private static final Path WORKING_DIR = Paths.get(System.getProperty("user.dir")); + + @Test + public void testBuildDexArchive() throws Exception { + DexBuilder.Options options = new DexBuilder.Options(); + // Use Jar file that has this test in it as the input Jar + options.inputJar = WORKING_DIR.resolve(System.getProperty("testinputjar")); + options.outputZip = + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR"), "dex_builder_test.zip"); + options.maxThreads = 1; + DexBuilder.buildDexArchive(options, new Dexing.DexingOptions()); + assertThat(options.outputZip.toFile().exists()).isTrue(); + + HashSet<String> files = new HashSet<>(); + try (ZipFile zip = new ZipFile(options.outputZip.toFile())) { + Enumeration<? extends ZipEntry> entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + files.add(entry.getName()); + if (entry.getName().endsWith(".dex")) { + Dex dex = new Dex(zip.getInputStream(entry)); + assertThat(dex.classDefs()).named(entry.getName()).hasSize(1); + } else if (entry.getName().endsWith("/testresource.txt")) { + byte[] content = ByteStreams.toByteArray(zip.getInputStream(entry)); + assertThat(content).named(entry.getName()).isEqualTo("test".getBytes(UTF_8)); + } + } + } + // Make sure this test is in the Zip file, which also means we parsed its dex code above + assertThat(files).contains(getClass().getName().replace('.', '/') + ".class.dex"); + // Make sure test resource is in the Zip file, which also means it had the expected content + assertThat(files) + .contains(getClass().getPackage().getName().replace('.', '/') + "/testresource.txt"); + } +} + diff --git a/src/test/java/com/google/devtools/build/android/dexer/DexConversionEnqueuerTest.java b/src/test/java/com/google/devtools/build/android/dexer/DexConversionEnqueuerTest.java new file mode 100644 index 0000000000..b71020740f --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/DexConversionEnqueuerTest.java @@ -0,0 +1,196 @@ +// 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 com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import com.android.dex.Dex; +import com.android.dx.dex.DexOptions; +import com.android.dx.dex.cf.CfOptions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import com.google.devtools.build.android.dexer.Dexing.DexingKey; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.concurrent.Future; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link DexConversionEnqueuer}. */ +@RunWith(JUnit4.class) +public class DexConversionEnqueuerTest { + + private static final long FILE_TIME = 12345678987654321L; + + @Mock private ZipFile zip; + + private DexConversionEnqueuer stuffer; + private final Cache<DexingKey, byte[]> cache = CacheBuilder.newBuilder().build(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + makeStuffer(); + } + + private void makeStuffer() { + stuffer = + new DexConversionEnqueuer( + zip, + newDirectExecutorService(), + new DexConverter(new Dexing(new DexOptions(), new CfOptions())), + cache); + } + + /** Makes sure there's always a future returning {@code null} at the end. */ + @After + public void assertEndOfStreamMarker() throws Exception { + Future<ZipEntryContent> f = stuffer.getFiles().remove(); + assertThat(f.isDone()).isTrue(); + assertThat(f.get()).isNull(); + assertThat(stuffer.getFiles()).isEmpty(); + } + + @Test + public void testEmptyZip() throws Exception { + mockEntries(); + stuffer.call(); + } + + @Test + public void testDirectory_copyEmptyBuffer() throws Exception { + ZipEntry entry = newZipEntry("dir/", 0); + assertThat(entry.isDirectory()).isTrue(); // test sanity + mockEntries(entry); + + stuffer.call(); + Future<ZipEntryContent> f = stuffer.getFiles().remove(); + assertThat(f.isDone()).isTrue(); + assertThat(f.get().getEntry()).isEqualTo(entry); + assertThat(f.get().getContent()).isEmpty(); + assertThat(entry.getCompressedSize()).isEqualTo(0); + } + + @Test + public void testFile_copyContent() throws Exception { + byte[] content = "Hello".getBytes(UTF_8); + ZipEntry entry = newZipEntry("file", content.length); + mockEntries(entry); + when(zip.getInputStream(entry)).thenReturn(new ByteArrayInputStream(content)); + + stuffer.call(); + Future<ZipEntryContent> f = stuffer.getFiles().remove(); + assertThat(f.isDone()).isTrue(); + assertThat(f.get().getEntry()).isEqualTo(entry); + assertThat(f.get().getContent()).isEqualTo(content); + assertThat(cache.size()).isEqualTo(0); // don't cache resource files + assertThat(entry.getCompressedSize()).isEqualTo(-1); // we don't know how the file will compress + } + + @Test + public void testClass_convertToDex() throws Exception { + testConvertClassToDex(); + } + + @Test + public void testClass_cachedResult() throws Exception { + byte[] dexcode = testConvertClassToDex(); + + makeStuffer(); + String filename = getClass().getName().replace('.', '/') + ".class"; + mockClassFile(filename); + stuffer.call(); + Future<ZipEntryContent> f = stuffer.getFiles().remove(); + assertThat(f.isDone()).isTrue(); + assertThat(f.get().getEntry().getName()).isEqualTo(filename + ".dex"); + assertThat(f.get().getEntry().getTime()).isEqualTo(FILE_TIME); + assertThat(f.get().getContent()).isSameAs(dexcode); + } + + private byte[] testConvertClassToDex() throws Exception { + String filename = getClass().getName().replace('.', '/') + ".class"; + byte[] bytecode = mockClassFile(filename); + + stuffer.call(); + Future<ZipEntryContent> f = stuffer.getFiles().remove(); + assertThat(f.isDone()).isTrue(); + assertThat(f.get().getEntry().getName()).isEqualTo(filename + ".dex"); + assertThat(f.get().getEntry().getTime()).isEqualTo(FILE_TIME); + assertThat(f.get().getEntry().getSize()).isEqualTo(-1); + assertThat(f.get().getEntry().getCompressedSize()).isEqualTo(-1); + byte[] dexcode = f.get().getContent(); + Dex dex = new Dex(dexcode); + assertThat(dex.classDefs()).hasSize(1); + assertThat(cache.getIfPresent(DexingKey.create(false, false, bytecode))).isSameAs(dexcode); + assertThat(cache.getIfPresent(DexingKey.create(true, false, bytecode))).isNull(); + assertThat(cache.getIfPresent(DexingKey.create(false, true, bytecode))).isNull(); + assertThat(cache.getIfPresent(DexingKey.create(true, true, bytecode))).isNull(); + return dexcode; + } + + private byte[] mockClassFile(String filename) throws IOException { + byte[] bytecode = ByteStreams.toByteArray( + Thread.currentThread().getContextClassLoader().getResourceAsStream(filename)); + ZipEntry entry = newZipEntry(filename, bytecode.length); + assertThat(entry.isDirectory()).isFalse(); // test sanity + mockEntries(entry); + when(zip.getInputStream(entry)).thenReturn(new ByteArrayInputStream(bytecode)); + return bytecode; + } + + @Test + public void testException_stillEnqueueEndOfStreamMarker() throws Exception { + when(zip.entries()).thenThrow(new IllegalStateException("test")); + try { + stuffer.call(); + fail("IllegalStateException expected"); + } catch (IllegalStateException expected) { + } + // assertEndOfStreamMarker() makes sure the end-of-stream marker is there + } + + private ZipEntry newZipEntry(String name, long size) { + ZipEntry result = new ZipEntry(name); + // Class under test needs sizing information so we need to set it for the test. These values + // are always set when reading zip entries from an existing zip file. + result.setSize(size); + result.setCompressedSize(size); + result.setTime(FILE_TIME); + return result; + } + + // thenReturn expects a generic type that uses the unknown ? extends ZipEntry "returned" + // by entries(). Since we can't come up with an unknown type, use raw type to make this typecheck. + // Note this is safe: actual entries() callers expect ZipEntries and they get them. + @SuppressWarnings({"rawtypes", "unchecked"}) + private void mockEntries(ZipEntry... entries) { + when(zip.entries()) + .thenReturn((Enumeration) Collections.enumeration(ImmutableList.copyOf(entries))); + } +} diff --git a/src/test/java/com/google/devtools/build/android/dexer/DexFileAggregatorTest.java b/src/test/java/com/google/devtools/build/android/dexer/DexFileAggregatorTest.java new file mode 100644 index 0000000000..7bc9ded10d --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/DexFileAggregatorTest.java @@ -0,0 +1,123 @@ +// 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.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.android.dex.Dex; +import com.android.dx.dex.DexOptions; +import com.android.dx.dex.cf.CfOptions; +import com.android.dx.dex.file.DexFile; +import com.google.common.collect.Iterables; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link DexFileAggregator}. */ +@RunWith(JUnit4.class) +public class DexFileAggregatorTest { + + /** Standard .dex file limit on methods and fields. */ + private static final int DEX_LIMIT = 265 * 265; + private static final int WASTE = 1; + + @Mock private DexFileArchive dest; + @Captor private ArgumentCaptor<Dex> written; + + private Dex dex; + + @Before + public void setUp() throws IOException { + dex = DexFiles.toDex(convertClass(DexFileAggregatorTest.class)); + MockitoAnnotations.initMocks(this); + } + + @Test + public void testClose_emptyWritesNothing() throws Exception { + DexFileAggregator dexer = + new DexFileAggregator(dest, MultidexStrategy.MINIMAL, DEX_LIMIT, WASTE); + dexer.close(); + verify(dest, times(0)).addFile(any(ZipEntry.class), any(Dex.class)); + } + + @Test + public void testAddAndClose_singleInputWritesThatInput() throws Exception { + DexFileAggregator dexer = new DexFileAggregator(dest, MultidexStrategy.MINIMAL, 0, WASTE); + dexer.add(dex); + dexer.close(); + verify(dest).addFile(any(ZipEntry.class), eq(dex)); + } + + @Test + public void testMultidex_underLimitWritesOneShard() throws Exception { + DexFileAggregator dexer = + new DexFileAggregator(dest, MultidexStrategy.BEST_EFFORT, DEX_LIMIT, WASTE); + Dex dex2 = DexFiles.toDex(convertClass(ByteStreams.class)); + dexer.add(dex); + dexer.add(dex2); + verify(dest, times(0)).addFile(any(ZipEntry.class), any(Dex.class)); + dexer.close(); + verify(dest).addFile(any(ZipEntry.class), written.capture()); + assertThat(Iterables.size(written.getValue().classDefs())).isEqualTo(2); + } + + @Test + public void testMultidex_overLimitWritesSecondShard() throws Exception { + DexFileAggregator dexer = new DexFileAggregator(dest, MultidexStrategy.BEST_EFFORT, + 2 /* dex has more than 2 methods and fields */, WASTE); + Dex dex2 = DexFiles.toDex(convertClass(ByteStreams.class)); + dexer.add(dex); // classFile is already over limit but we take anything in empty shard + dexer.add(dex2); // this should start a new shard + // Make sure there was one file written and that file is dex + verify(dest).addFile(any(ZipEntry.class), written.capture()); + assertThat(written.getValue()).isSameAs(dex); + dexer.close(); + verify(dest).addFile(any(ZipEntry.class), eq(dex2)); + } + + @Test + public void testMonodex_alwaysWritesSingleShard() throws Exception { + DexFileAggregator dexer = new DexFileAggregator(dest, MultidexStrategy.OFF, + 2 /* dex has more than 2 methods and fields */, WASTE); + Dex dex2 = DexFiles.toDex(convertClass(ByteStreams.class)); + dexer.add(dex); + dexer.add(dex2); + verify(dest, times(0)).addFile(any(ZipEntry.class), any(Dex.class)); + dexer.close(); + verify(dest).addFile(any(ZipEntry.class), written.capture()); + assertThat(Iterables.size(written.getValue().classDefs())).isEqualTo(2); + } + + private static DexFile convertClass(Class<?> clazz) throws IOException { + String path = clazz.getName().replace('.', '/') + ".class"; + try (InputStream in = + Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) { + return new DexConverter(new Dexing(new DexOptions(), new CfOptions())) + .toDexFile(ByteStreams.toByteArray(in), path); + } + } +} diff --git a/src/test/java/com/google/devtools/build/android/dexer/DexFileArchiveTest.java b/src/test/java/com/google/devtools/build/android/dexer/DexFileArchiveTest.java new file mode 100644 index 0000000000..7aaceadd44 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/DexFileArchiveTest.java @@ -0,0 +1,56 @@ +// 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.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; + +import com.android.dex.Dex; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link DexFileArchive}. */ +@RunWith(JUnit4.class) +public class DexFileArchiveTest { + + @Mock private ZipOutputStream out; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testAddDex() throws Exception { + ZipEntry entry = new ZipEntry("test.dex"); + Dex dex = new Dex(1); + try (DexFileArchive archive = new DexFileArchive(out)) { + archive.addFile(entry, dex); + } + assertThat(entry.getSize()).isEqualTo(1L); + InOrder order = inOrder(out); + order.verify(out).putNextEntry(entry); + order.verify(out).write(any(byte[].class), eq(0), eq(1)); + order.verify(out).close(); + } +} diff --git a/src/test/java/com/google/devtools/build/android/dexer/DexFileMergerTest.java b/src/test/java/com/google/devtools/build/android/dexer/DexFileMergerTest.java new file mode 100644 index 0000000000..226561b559 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/DexFileMergerTest.java @@ -0,0 +1,273 @@ +// 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 java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; + +import com.android.dex.ClassDef; +import com.android.dex.Dex; +import com.google.common.base.Function; +import com.google.common.base.Predicates; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterators; +import com.google.common.collect.Multimap; +import com.google.common.collect.SetMultimap; +import com.google.common.collect.Sets; +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.Enumeration; +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 DexFileMerger}. */ +@RunWith(JUnit4.class) +public class DexFileMergerTest { + + 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 MAIN_DEX_LIST_FILE = + WORKING_DIR.resolve(System.getProperty("testmaindexlist")); + + /** + * Exercises DexFileMerger like Bazel would in the ideal case, namely with a dex archive as input. + * DexFileMerger may in practice see a mixed input file containing .dex and .class files, but this + * test uses only .dex files in the input. + */ + @Test + public void testMergeDexArchive_singleOutputDex() throws Exception { + Path dexArchive = buildDexArchive(); + Path outputArchive = runDexFileMerger(dexArchive, 256 * 256, "from_dex_archive.dex.zip"); + + int expectedClassCount = matchingFileCount(dexArchive, ".*\\.class.dex$"); + assertSingleDexOutput(expectedClassCount, outputArchive); + } + + /** + * Similar to {@link #testMergeDexArchive_singleOutputDex} but forces multiple output dex files. + */ + @Test + public void testMergeDexArchive_multidex() throws Exception { + Path dexArchive = buildDexArchive(); + Path outputArchive = runDexFileMerger(dexArchive, 20, "multidex_from_dex_archive.dex.zip"); + + int expectedClassCount = matchingFileCount(dexArchive, ".*\\.class.dex$"); + assertMultidexOutput(expectedClassCount, outputArchive, ImmutableSet.<String>of()); + } + + @Test + public void testMergeDexArchive_mainDexList() throws Exception { + Path dexArchive = buildDexArchive(); + Path outputArchive = + runDexFileMerger( + dexArchive, + 200, + "main_dex_list.dex.zip", + MultidexStrategy.MINIMAL, + MAIN_DEX_LIST_FILE, + /*minimalMainDex=*/ false); + + int expectedClassCount = matchingFileCount(dexArchive, ".*\\.class.dex$"); + assertMainDexOutput(expectedClassCount, outputArchive, false); + } + + @Test + public void testMergeDexArchive_minimalMainDex() throws Exception { + Path dexArchive = buildDexArchive(); + Path outputArchive = + runDexFileMerger( + dexArchive, + 256 * 256, + "minimal_main_dex.dex.zip", + MultidexStrategy.MINIMAL, + MAIN_DEX_LIST_FILE, + /*minimalMainDex=*/ true); + + int expectedClassCount = matchingFileCount(dexArchive, ".*\\.class.dex$"); + assertMainDexOutput(expectedClassCount, outputArchive, true); + } + + @Test + public void testMultidexOffWithMultidexFlags() throws Exception { + Path dexArchive = buildDexArchive(); + try { + runDexFileMerger( + dexArchive, + 200, + "classes.dex.zip", + MultidexStrategy.OFF, + /*mainDexList=*/ null, + /*minimalMainDex=*/ true); + fail("Expected DexFileMerger to fail"); + } catch (IllegalArgumentException e) { + assertThat(e) + .hasMessage( + "--minimal-main-dex is only supported with multidex enabled, but mode is: OFF"); + } + try { + runDexFileMerger( + dexArchive, + 200, + "classes.dex.zip", + MultidexStrategy.OFF, + MAIN_DEX_LIST_FILE, + /*minimalMainDex=*/ false); + fail("Expected DexFileMerger to fail"); + } catch (IllegalArgumentException e) { + assertThat(e) + .hasMessage("--main-dex-list is only supported with multidex enabled, but mode is: OFF"); + } + } + + private void assertSingleDexOutput(int expectedClassCount, Path outputArchive) + throws IOException { + try (ZipFile output = new ZipFile(outputArchive.toFile())) { + ZipEntry entry = Iterators.getOnlyElement(Iterators.forEnumeration(output.entries())); + assertThat(entry.getName()).isEqualTo("classes.dex"); + Dex dex = new Dex(output.getInputStream(entry)); + assertThat(dex.classDefs()).hasSize(expectedClassCount); + } + } + + private Multimap<String, String> assertMultidexOutput(int expectedClassCount, + Path outputArchive, Set<String> mainDexList) throws IOException { + SetMultimap<String, String> dexFiles = HashMultimap.create(); + try (ZipFile output = new ZipFile(outputArchive.toFile())) { + Enumeration<? extends ZipEntry> entries = output.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + assertThat(entry.getName()).containsMatch("classes[2-9]?.dex"); + Dex dex = new Dex(output.getInputStream(entry)); + for (ClassDef clazz : dex.classDefs()) { + dexFiles.put(entry.getName(), + toSlashedClassName(dex.typeNames().get(clazz.getTypeIndex()))); + } + } + } + assertThat(dexFiles.keySet().size()).isAtLeast(2); // test sanity + assertThat(dexFiles.size()).isAtLeast(1); // test sanity + assertThat(dexFiles).hasSize(expectedClassCount); + for (int i = 0; i < dexFiles.keySet().size(); ++i) { + assertThat(dexFiles).containsKey(DexFileAggregator.getDexFileName(i)); + } + for (int i = 1; i < dexFiles.keySet().size(); ++i) { + Set<String> prev = dexFiles.get(DexFileAggregator.getDexFileName(i - 1)); + if (i == 1) { + prev = Sets.difference(prev, mainDexList); + } + Set<String> shard = dexFiles.get(DexFileAggregator.getDexFileName(i)); + for (String c1 : prev) { + for (String c2 : shard) { + assertThat(DexFileMerger.compareClassNames(c2, c1)) + .named(c2 + " in shard " + i + " should compare as larger than " + c1 + + "; list of all shards for reference: " + dexFiles) + .isGreaterThan(0); + } + } + } + return dexFiles; + } + + private void assertMainDexOutput(int expectedClassCount, Path outputArchive, + boolean minimalMainDex) throws IOException { + HashSet<String> mainDexList = new HashSet<>(); + for (String filename : Files.readAllLines(MAIN_DEX_LIST_FILE, UTF_8)) { + mainDexList.add( + filename.endsWith(".class") ? filename.substring(0, filename.length() - 6) : filename); + } + Multimap<String, String> dexFiles = + assertMultidexOutput(expectedClassCount, outputArchive, mainDexList); + assertThat(dexFiles.keySet()).hasSize(2); + if (minimalMainDex) { + assertThat(dexFiles.get("classes.dex")).containsExactlyElementsIn(mainDexList); + } else { + assertThat(dexFiles.get("classes.dex")).containsAllIn(mainDexList); + } + } + + /** Converts signature classes, eg., "Lpath/to/Class;", to regular names like "path/to/Class". */ + private static String toSlashedClassName(String signatureClassname) { + return signatureClassname.substring(1, signatureClassname.length() - 1); + } + + private int matchingFileCount(Path dexArchive, String filenameFilter) throws IOException { + try (ZipFile input = new ZipFile(dexArchive.toFile())) { + return Iterators.size(Iterators.filter( + Iterators.transform(Iterators.forEnumeration(input.entries()), ZipEntryName.INSTANCE), + Predicates.containsPattern(filenameFilter))); + } + } + + private Path runDexFileMerger(Path dexArchive, int maxNumberOfIdxPerDex, String outputBasename) + throws IOException { + return runDexFileMerger( + dexArchive, + maxNumberOfIdxPerDex, + outputBasename, + MultidexStrategy.MINIMAL, + /*mainDexList=*/ null, + /*minimalMainDex=*/ false); + } + + private Path runDexFileMerger( + Path dexArchive, + int maxNumberOfIdxPerDex, + String outputBasename, + MultidexStrategy multidexMode, + @Nullable Path mainDexList, + boolean minimalMainDex) + throws IOException { + DexFileMerger.Options options = new DexFileMerger.Options(); + options.inputArchive = dexArchive; + options.outputArchive = + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR"), outputBasename); + options.multidexMode = multidexMode; + options.maxNumberOfIdxPerDex = maxNumberOfIdxPerDex; + options.mainDexListFile = mainDexList; + options.minimalMainDex = minimalMainDex; + DexFileMerger.buildMergedDexFiles(options); + assertThat(options.outputArchive.toFile().exists()).isTrue(); + return options.outputArchive; + } + + private Path buildDexArchive() throws Exception { + DexBuilder.Options options = new DexBuilder.Options(); + // Use Jar file that has this test in it as the input Jar + options.inputJar = INPUT_JAR; + options.outputZip = + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR"), "libtests.dex.zip"); + options.maxThreads = 1; + DexBuilder.buildDexArchive(options, new Dexing.DexingOptions()); + return options.outputZip; + } + + private enum ZipEntryName implements Function<ZipEntry, String> { + INSTANCE; + @Override + public String apply(ZipEntry input) { + return input.getName(); + } + } +} diff --git a/src/test/java/com/google/devtools/build/android/dexer/DexingKeyTest.java b/src/test/java/com/google/devtools/build/android/dexer/DexingKeyTest.java new file mode 100644 index 0000000000..f7250c9bbb --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/DexingKeyTest.java @@ -0,0 +1,77 @@ +// 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 com.google.devtools.build.android.dexer.Dexing.DexingKey; +import com.google.devtools.build.android.dexer.Dexing.DexingOptions; +import java.lang.reflect.Member; +import java.util.HashSet; +import java.util.Set; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DexingKeyTest}. */ +@RunWith(JUnit4.class) +public class DexingKeyTest { + + @Test + public void testOrderMatters() { + DexingKey key = DexingKey.create(false, true, new byte[0]); + assertThat(key.localInfo()).isFalse(); + assertThat(key.optimize()).isTrue(); + } + + /** + * Makes sure that arrays are compared by content. Auto-value promises that but I want to be + * really sure as we'd never get any cache hits if arrays were compared by reference. + */ + @Test + public void testContentMatters() { + assertThat(DexingKey.create(false, false, new byte[] { 1, 2, 3 })) + .isEqualTo(DexingKey.create(false, false, new byte[] { 1, 2, 3 })); + assertThat(DexingKey.create(false, false, new byte[] { 1, 2, 3 })) + .isNotEqualTo(DexingKey.create(false, false, new byte[] { 1, 3, 3 })); + } + + /** + * Makes sure that all {@link DexingOptions} (that can affect resulting {@code .dex} files) are + * reflected in {@link DexingKey}. Forgetting to reflect new options in {@link DexingKey} could + * result in spurious cache hits when {@link DexingKey} is used as cache key for dexing results + * (a {@code .dex} file created with different options would be used where it shouldn't), so we + * have this test to make sure that doesn't happen. + */ + @Test + public void testFieldsCoverDexingOptions() { + Set<String> keyMethods = names(DexingKey.class.getDeclaredMethods()); + Set<String> optionsFields = names(DexingOptions.class.getDeclaredFields()); + keyMethods.remove("create"); // Ignore factory method (we just want accessors) + keyMethods.remove("classfileContent"); // Ignore classfile content (we just want options) + keyMethods.remove("$jacocoInit"); // Ignore extra method generated in coverage builds + optionsFields.remove("printWarnings"); // Doesn't affect resulting dex files + optionsFields.remove("$jacocoData"); // Ignore extra field generated in coverage builds + assertThat(keyMethods).containsExactlyElementsIn(optionsFields); + } + + private static Set<String> names(Member... members) { + HashSet<String> result = new HashSet<>(); + for (Member member : members) { + result.add(member.getName()); + } + return result; + } +} + diff --git a/src/test/java/com/google/devtools/build/android/dexer/NoAndroidSdkStubTest.java b/src/test/java/com/google/devtools/build/android/dexer/NoAndroidSdkStubTest.java new file mode 100644 index 0000000000..e26acb3dec --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/NoAndroidSdkStubTest.java @@ -0,0 +1,29 @@ +// 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 org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Stub test to run if {@code android_sdk_repository} is not in the {@code WORKSPACE} file. */ +@RunWith(JUnit4.class) +public class NoAndroidSdkStubTest { + @Test + public void printWarningMessageTest() { + System.out.println( + "Android tests are being skipped because no android_sdk_repository rule is set up in the " + + "WORKSPACE file."); + } +} diff --git a/src/test/java/com/google/devtools/build/android/dexer/test_main_dex_list.txt b/src/test/java/com/google/devtools/build/android/dexer/test_main_dex_list.txt new file mode 100644 index 0000000000..48aa3d0731 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/test_main_dex_list.txt @@ -0,0 +1,2 @@ +com/google/devtools/build/android/dexer/DexFileMergerTest.class +com/google/devtools/build/android/dexer/DexFileAggregatorTest.class diff --git a/src/test/java/com/google/devtools/build/android/dexer/testresource.txt b/src/test/java/com/google/devtools/build/android/dexer/testresource.txt new file mode 100644 index 0000000000..30d74d2584 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/dexer/testresource.txt @@ -0,0 +1 @@ +test
\ No newline at end of file |