/* * 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.internal.CommandLineRunner; import com.android.ide.common.internal.LoggedErrorException; import com.android.sdklib.BuildToolInfo; import com.android.sdklib.repository.FullRevision; import com.android.utils.ILogger; import com.android.utils.StdLogger; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.io.Files; import junit.framework.TestCase; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class PreDexCacheTest extends TestCase { private static final String DEX_DATA = "**"; /** * Override the command line runner to intercept the call to dex and replace it * with something else. */ private static class FakeCommandLineRunner extends CommandLineRunner { public FakeCommandLineRunner(ILogger logger) { super(logger); } @Override public void runCmdLine(@NonNull String[] command, @Nullable Map<String, String> envVariableMap) throws IOException, InterruptedException, LoggedErrorException { // small delay to test multi-threading. Thread.sleep(1000); // input file is the last file in the command File input = new File(command[command.length - 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.length ; i++) { if ("--output".equals(command[i])) { output = command[i+1]; break; } } if (output == null) { throw new IOException("Failed to find output in dex commands"); } // read the source content List<String> lines = Files.readLines(input, Charsets.UTF_8); // modify the lines List<String> dexedLines = Lists.newArrayListWithCapacity(lines.size()); for (String line : lines) { dexedLines.add(DEX_DATA + line + DEX_DATA); } // combine the lines String content = Joiner.on('\n').join(dexedLines); // write it Files.write(content, new File(output), Charsets.UTF_8); } } /** * Override the command line runner to simulate error during the dexing */ private static class FakeCommandLineRunner2 extends CommandLineRunner { public FakeCommandLineRunner2(ILogger logger) { super(logger); } @Override public void runCmdLine(@NonNull String[] command, @Nullable Map<String, String> envVariableMap) throws IOException, InterruptedException, LoggedErrorException { Thread.sleep(1000); throw new IOException("foo"); } } 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 public String getJavaMaxHeapSize() { return null; } @Override public int getThreadCount() { return 1; } } 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, LoggedErrorException, 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 FakeCommandLineRunner(new StdLogger(StdLogger.Level.INFO))); 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 CommandLineRunner clr = new FakeCommandLineRunner(new StdLogger(StdLogger.Level.INFO)); 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*/, clr); } 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 CommandLineRunner clr = new FakeCommandLineRunner(new StdLogger(StdLogger.Level.INFO)); final CommandLineRunner clrWithError = new FakeCommandLineRunner2(new StdLogger(StdLogger.Level.INFO)); 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 ? clrWithError : clr); } 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, LoggedErrorException, InterruptedException { final CommandLineRunner clr = new FakeCommandLineRunner(new StdLogger(StdLogger.Level.INFO)); 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*/, clr); 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*/, clr); // 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(); Files.write(content, input, Charsets.UTF_8); 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 file. File dx = new File(toolDir, FN_DX); Files.write("dx!", dx, Charsets.UTF_8); return new BuildToolInfo( new FullRevision(1), toolDir, new File(toolDir, FN_AAPT), new File(toolDir, FN_AIDL), dx, new File(toolDir, FN_DX_JAR), 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(); } }