| // 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.command.dexer.DxContext; | |
| import com.android.dx.dex.DexOptions; | |
| import com.android.dx.dex.cf.CfOptions; | |
| import com.android.dx.dex.code.PositionList; | |
| 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 DxContext(), 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()).isSameInstanceAs(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(); | |
| byte[] dexcode = f.get().getContent(); | |
| assertThat(f.get().getEntry().getName()).isEqualTo(filename + ".dex"); | |
| assertThat(f.get().getEntry().getTime()).isEqualTo(FILE_TIME); | |
| assertThat(f.get().getEntry().getSize()).isEqualTo(dexcode.length); | |
| assertThat(f.get().getEntry().getCompressedSize()).isEqualTo(dexcode.length); | |
| Dex dex = new Dex(dexcode); | |
| assertThat(dex.classDefs()).hasSize(1); | |
| assertThat(cache.getIfPresent(DexingKey.create(false, false, PositionList.LINES, 13, bytecode))) | |
| .isSameInstanceAs(dexcode); | |
| assertThat(cache.getIfPresent(DexingKey.create(false, false, PositionList.NONE, 13, bytecode))) | |
| .isNull(); | |
| assertThat(cache.getIfPresent(DexingKey.create(true, false, PositionList.LINES, 13, bytecode))) | |
| .isNull(); | |
| assertThat(cache.getIfPresent(DexingKey.create(false, true, PositionList.LINES, 13, bytecode))) | |
| .isNull(); | |
| assertThat(cache.getIfPresent(DexingKey.create(true, true, PositionList.LINES, 13, 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))); | |
| } | |
| } |