aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test/java/com/google/devtools/build
diff options
context:
space:
mode:
authorGravatar ajmichael <ajmichael@google.com>2017-06-29 00:49:36 +0200
committerGravatar Marcel Hlopko <hlopko@google.com>2017-06-29 09:33:53 +0200
commitc1e0d7b9f6bfa8df950980f9370c638443d361e1 (patch)
treea26e56ca9a9dc86d118219ba8cedd15f7aada9fe /src/test/java/com/google/devtools/build
parent199624bdc59a36cda9920f804e9933954de6ce95 (diff)
Open source dexer tests.
RELNOTES: None PiperOrigin-RevId: 160461708
Diffstat (limited to 'src/test/java/com/google/devtools/build')
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/AllTests.java21
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/BUILD45
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/DexBuilderTest.java71
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/DexConversionEnqueuerTest.java196
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/DexFileAggregatorTest.java123
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/DexFileArchiveTest.java56
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/DexFileMergerTest.java273
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/DexingKeyTest.java77
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/NoAndroidSdkStubTest.java29
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/test_main_dex_list.txt2
-rw-r--r--src/test/java/com/google/devtools/build/android/dexer/testresource.txt1
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