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