/* * Copyright (C) 2014 The Android Open Source Project * * 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.android.builder.internal.compiler; import static com.android.SdkConstants.FN_AAPT; import static com.android.SdkConstants.FN_AIDL; import static com.android.SdkConstants.FN_BCC_COMPAT; import static com.android.SdkConstants.FN_DX; import static com.android.SdkConstants.FN_DX_JAR; import static com.android.SdkConstants.FN_RENDERSCRIPT; import static com.android.SdkConstants.FN_ZIPALIGN; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.builder.core.DexOptions; import com.android.ide.common.process.JavaProcessExecutor; import com.android.ide.common.process.JavaProcessInfo; import com.android.ide.common.process.ProcessException; import com.android.ide.common.process.ProcessOutput; import com.android.ide.common.process.ProcessOutputHandler; import com.android.ide.common.process.ProcessResult; import com.android.sdklib.BuildToolInfo; import com.android.sdklib.repository.FullRevision; import com.google.common.base.Charsets; import com.google.common.io.Files; import junit.framework.TestCase; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; public class PreDexCacheTest extends TestCase { private static final String DEX_DATA = "**"; /** * implement a fake java process executor to intercept the call to dex and replace it * with something else. */ private static class FakeJavaProcessExecutor implements JavaProcessExecutor { @NonNull @Override public ProcessResult execute( @NonNull JavaProcessInfo javaProcessInfo, @NonNull ProcessOutputHandler processOutputHandler) { List<String> command = javaProcessInfo.getArgs(); ProcessException processException = null; try { // small delay to test multi-threading. Thread.sleep(1000); // input file is the last file in the command File input = new File(command.get(command.size() - 1)); if (!input.isFile()) { throw new FileNotFoundException(input.getPath()); } // loop on the command to find --output String output = null; for (int i = 0; i < command.size(); i++) { if ("--output".equals(command.get(i))) { output = command.get(i + 1); break; } } if (output == null) { throw new IOException("Failed to find output in dex commands"); } // read the source content JarFile jarFile = new JarFile(input); JarEntry jarEntry = jarFile.getJarEntry("content.class"); assert jarEntry != null; InputStream contentStream = jarFile.getInputStream(jarEntry); byte[] content = new byte[256]; int read = contentStream.read(content); contentStream.close(); jarFile.close(); String line = new String(content, 0, read, Charsets.UTF_8); // write it Files.write(DEX_DATA + line + DEX_DATA, new File(output), Charsets.UTF_8); } catch (Exception e) { //noinspection ThrowableInstanceNeverThrown processException = new ProcessException(null, e); } final ProcessException rethrow = processException; return new ProcessResult() { @Override public ProcessResult assertNormalExitValue() throws ProcessException { return this; } @Override public int getExitValue() { return 0; } @Override public ProcessResult rethrowFailure() throws ProcessException { if (rethrow != null) { throw rethrow; } return this; } }; } } /** * Fake executor that fails to execute */ private static class FailingExecutor implements JavaProcessExecutor { @NonNull @Override public ProcessResult execute(@NonNull JavaProcessInfo javaProcessInfo, @NonNull ProcessOutputHandler processOutputHandler) { try { Thread.sleep(1000); throw new IOException("foo"); } catch (final Exception e) { return new ProcessResult() { @Override public ProcessResult assertNormalExitValue() throws ProcessException { return this; } @Override public int getExitValue() { return 0; } @Override public ProcessResult rethrowFailure() throws ProcessException { throw new ProcessException(null, e); } }; } } } private static class FakeProcessOutputHandler implements ProcessOutputHandler { @NonNull @Override public ProcessOutput createOutput() { return null; } @Override public void handleOutput(@NonNull ProcessOutput processOutput) throws ProcessException { } } private static class FakeDexOptions implements DexOptions { @Override public boolean getIncremental() { return false; } @Override public boolean getPreDexLibraries() { return false; } @Override public boolean getJumboMode() { return false; } @Override @Nullable public String getJavaMaxHeapSize() { return null; } @Override @Nullable public Integer getThreadCount() { return null; } } private BuildToolInfo mBuildToolInfo; @Override protected void setUp() throws Exception { super.setUp(); mBuildToolInfo = getBuildToolInfo(); } @Override protected void tearDown() throws Exception { File toolFolder = mBuildToolInfo.getLocation(); deleteFolder(toolFolder); PreDexCache.getCache().clear(null, null); super.tearDown(); } public void testSinglePreDexLibrary() throws IOException, ProcessException, InterruptedException { String content = "Some Content"; File input = createInputFile(content); File output = File.createTempFile("predex", ".jar"); output.deleteOnExit(); PreDexCache.getCache().preDexLibrary( input, output, false /*multidex*/, new FakeDexOptions(), mBuildToolInfo, false /*verbose*/, new FakeJavaProcessExecutor(), new FakeProcessOutputHandler()); checkOutputFile(content, output); } public void testThreadedPreDexLibrary() throws IOException, InterruptedException { String content = "Some Content"; final File input = createInputFile(content); input.deleteOnExit(); Thread[] threads = new Thread[3]; final File[] outputFiles = new File[threads.length]; final JavaProcessExecutor javaProcessExecutor = new FakeJavaProcessExecutor(); final DexOptions dexOptions = new FakeDexOptions(); for (int i = 0 ; i < threads.length ; i++) { final int ii = i; threads[i] = new Thread() { @Override public void run() { try { File output = File.createTempFile("predex", ".jar"); output.deleteOnExit(); outputFiles[ii] = output; PreDexCache.getCache().preDexLibrary( input, output, false /*multidex*/, dexOptions, mBuildToolInfo, false /*verbose*/, javaProcessExecutor, new FakeProcessOutputHandler()); } catch (Exception ignored) { } } }; threads[i].start(); } // wait on the threads. for (Thread thread : threads) { thread.join(); } // check the output. for (File outputFile : outputFiles) { checkOutputFile(content, outputFile); } // now check the cache PreDexCache cache = PreDexCache.getCache(); assertEquals(1, cache.getMisses()); assertEquals(threads.length - 1, cache.getHits()); } public void testThreadedPreDexLibraryWithError() throws IOException, InterruptedException { String content = "Some Content"; final File input = createInputFile(content); input.deleteOnExit(); Thread[] threads = new Thread[3]; final File[] outputFiles = new File[threads.length]; final JavaProcessExecutor javaProcessExecutor = new FakeJavaProcessExecutor(); final JavaProcessExecutor javaProcessExecutorWithError = new FailingExecutor(); final DexOptions dexOptions = new FakeDexOptions(); final AtomicInteger threadDoneCount = new AtomicInteger(); for (int i = 0 ; i < threads.length ; i++) { final int ii = i; threads[i] = new Thread() { @Override public void run() { try { File output = File.createTempFile("predex", ".jar"); output.deleteOnExit(); outputFiles[ii] = output; PreDexCache.getCache().preDexLibrary( input, output, false /*multidex*/, dexOptions, mBuildToolInfo, false /*verbose*/, ii == 0 ? javaProcessExecutorWithError : javaProcessExecutor, new FakeProcessOutputHandler()); } catch (Exception ignored) { } threadDoneCount.incrementAndGet(); } }; threads[i].start(); } // wait on the threads, long enough but stop after a while for (Thread thread : threads) { thread.join(5000); } // if the test fail, we'll have two threads still blocked on the countdownlatch. assertEquals(3, threadDoneCount.get()); } public void testReload() throws IOException, ProcessException, InterruptedException { final JavaProcessExecutor javaProcessExecutor = new FakeJavaProcessExecutor(); final DexOptions dexOptions = new FakeDexOptions(); // convert one file. String content = "Some Content"; File input = createInputFile(content); File output = File.createTempFile("predex", ".jar"); output.deleteOnExit(); PreDexCache.getCache().preDexLibrary( input, output, false /*multidex*/, dexOptions, mBuildToolInfo, false /*verbose*/, javaProcessExecutor, new FakeProcessOutputHandler()); checkOutputFile(content, output); // store the cache File cacheXml = File.createTempFile("predex", ".xml"); cacheXml.deleteOnExit(); PreDexCache.getCache().clear(cacheXml, null); // reload. PreDexCache.getCache().load(cacheXml); // re-pre-dex into another file. File output2 = File.createTempFile("predex", ".jar"); output2.deleteOnExit(); PreDexCache.getCache().preDexLibrary( input, output2, false /*multidex*/, dexOptions, mBuildToolInfo, false /*verbose*/, javaProcessExecutor, new FakeProcessOutputHandler()); // check the output checkOutputFile(content, output2); // check the hit/miss PreDexCache cache = PreDexCache.getCache(); assertEquals(0, cache.getMisses()); assertEquals(1, cache.getHits()); } private static File createInputFile(String content) throws IOException { File input = File.createTempFile("predex", ".jar"); input.deleteOnExit(); JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(input)); try { jarOutputStream.putNextEntry(new ZipEntry("content.class")); jarOutputStream.write(content.getBytes(Charsets.UTF_8)); jarOutputStream.closeEntry(); } finally { jarOutputStream.close(); } return input; } private static void checkOutputFile(String content, File output) throws IOException { List<String> lines = Files.readLines(output, Charsets.UTF_8); assertEquals(1, lines.size()); assertEquals(DEX_DATA + content + DEX_DATA, lines.get(0)); } /** * Create a fake build tool info where the dx tool actually exists (even if it's not used). */ private static BuildToolInfo getBuildToolInfo() throws IOException { File toolDir = Files.createTempDir(); // create a dx.jar file. File dx = new File(toolDir, FN_DX_JAR); Files.write("dx!", dx, Charsets.UTF_8); return new BuildToolInfo( new FullRevision(1), toolDir, new File(toolDir, FN_AAPT), new File(toolDir, FN_AIDL), new File(toolDir, FN_DX), dx, new File(toolDir, FN_RENDERSCRIPT), new File(toolDir, "include"), new File(toolDir, "clang-include"), new File(toolDir, FN_BCC_COMPAT), new File(toolDir, "arm-linux-androideabi-ld"), new File(toolDir, "i686-linux-android-ld"), new File(toolDir, "mipsel-linux-android-ld"), new File(toolDir, FN_ZIPALIGN)); } private static void deleteFolder(File folder) { File[] files = folder.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { deleteFolder(file); } else { file.delete(); } } } folder.delete(); } }