| // 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))); |
| } |
| } |