blob: 6221a91d8ce1cc0ad23cabbfe74cc51b8129e4b1 [file] [log] [blame]
/*
* Copyright (C) 2015 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.build.gradle.internal.transforms;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.Context;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.incremental.InstantRunBuildContext;
import com.android.build.gradle.internal.incremental.InstantRunBuildMode;
import com.android.build.gradle.internal.incremental.InstantRunVerifierStatus;
import com.android.build.gradle.internal.pipeline.TransformInvocationBuilder;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.options.ProjectOptions;
import com.android.builder.core.AndroidBuilder;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.utils.FileUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Set;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/**
* Tests for the {@link InstantRunTransform} class
*/
public class InstantRunTransformTest {
@Mock
Context context;
@Mock
VariantScope variantScope;
@Mock
GlobalScope globalScope;
@Mock InstantRunBuildContext buildContext;
@Mock WaitableExecutor executor;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void setUpMock() {
MockitoAnnotations.initMocks(this);
AndroidBuilder mockBuilder = Mockito.mock(AndroidBuilder.class);
when(mockBuilder.getBootClasspath(true)).thenReturn(ImmutableList.of());
when(globalScope.getAndroidBuilder()).thenReturn(mockBuilder);
when(globalScope.getProjectOptions()).thenReturn(new ProjectOptions(ImmutableMap.of()));
when(variantScope.getGlobalScope()).thenReturn(globalScope);
when(variantScope.getInstantRunBuildContext()).thenReturn(buildContext);
when(variantScope.getInstantRunBootClasspath()).thenReturn(ImmutableList.of());
when(buildContext.getBuildMode()).thenReturn(InstantRunBuildMode.HOT_WARM);
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
Callable callable = (Callable) args[0];
callable.call();
return null;
}).when(executor).execute(anyCallable());
}
@Test
public void classLoaderOverrideTest()
throws TransformException, InterruptedException, IOException {
// now set up a funky classloader.
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
InstantRunTransform transform = new InstantRunTransform(executor, variantScope) {
@Nullable
@Override
protected Void transformToClasses2Format(@NonNull File inputDir,
@NonNull File inputFile, @NonNull File outputDir,
@NonNull Status change) throws IOException {
assertNotEquals(classLoader, Thread.currentThread().getContextClassLoader());
return null;
}
@Nullable
@Override
protected Void transformToClasses3Format(File inputDir, File inputFile, File outputDir)
throws IOException {
assertNotEquals(classLoader, Thread.currentThread().getContextClassLoader());
return null;
}
};
TransformInput transformInput =
TransformTestHelper.directoryBuilder(new File("/tmp"))
.setScope(Scope.PROJECT)
.setContentType(QualifiedContent.DefaultContentType.CLASSES)
.putChangedFiles(
ImmutableMap.<File, Status>builder()
.put(new File("/tmp/foo/bar/Changed.class"), Status.CHANGED)
.put(new File("/tmp/foo/bar/Added.class"), Status.ADDED)
.build())
.build();
// make sure our executor is called with the right class loader context.
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
Callable callable = (Callable) args[0];
callable.call();
return null;
}).when(executor).execute(anyCallable());
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(classLoader);
try {
TransformOutputProvider transformOutput =
createTransformOutputProvider(new File("out"), new File("out.3"));
assertEquals(classLoader, Thread.currentThread().getContextClassLoader());
transform.transform(
new TransformInvocationBuilder(context)
.addInputs(ImmutableList.of(transformInput))
.addOutputProvider(transformOutput)
.setIncrementalMode(true)
.build());
// make sure any thread class loader set during the transform execution has been reset
assertEquals(classLoader, Thread.currentThread().getContextClassLoader());
} finally {
Thread.currentThread().setContextClassLoader(currentClassLoader);
}
}
@Test
public void incrementalModeTest() throws TransformException, InterruptedException, IOException {
final ImmutableList.Builder<File> filesElectedForClasses2Transformation = ImmutableList.builder();
final ImmutableList.Builder<File> filesElectedForClasses3Transformation = ImmutableList.builder();
InstantRunTransform transform = createTransform(
filesElectedForClasses2Transformation, filesElectedForClasses3Transformation);
TransformInput transformInput =
TransformTestHelper.directoryBuilder(new File("/tmp"))
.setContentType(QualifiedContent.DefaultContentType.CLASSES)
.setScope(Scope.PROJECT)
.putChangedFiles(
ImmutableMap.<File, Status>builder()
.put(new File("/tmp/foo/bar/Changed.class"), Status.CHANGED)
.put(new File("/tmp/foo/bar/Added.class"), Status.ADDED)
.build())
.build();
TransformOutputProvider transformOutput =
createTransformOutputProvider(temporaryFolder.newFolder("out"),
temporaryFolder.newFolder("out.3"));
transform.transform(
new TransformInvocationBuilder(context)
.addInputs(ImmutableList.of(transformInput))
.addOutputProvider(transformOutput)
.setIncrementalMode(true)
.build());
ImmutableList<File> processedFiles = filesElectedForClasses2Transformation.build();
assertEquals("Wrong number of files elected for classes 2 processing", 2, processedFiles.size());
assertEquals("Output File path",
FileUtils.toSystemDependentPath("/tmp/foo/bar/Changed.class"),
processedFiles.get(0).getPath());
assertEquals("Output File path",
FileUtils.toSystemDependentPath("/tmp/foo/bar/Added.class"),
processedFiles.get(1).getPath());
processedFiles = filesElectedForClasses3Transformation.build();
assertEquals("Wrong number of files elected for classes 3 processing", 1, processedFiles.size());
assertEquals("Output File path",
FileUtils.toSystemDependentPath("/tmp/foo/bar/Changed.class"),
processedFiles.get(0).getPath());
}
@Test
public void fullModeTest()
throws IOException, TransformException, InterruptedException {
final File inputFolder = temporaryFolder.newFolder("input");
final File originalFile = createEmptyFile(inputFolder, "com/example/A.class");
final File otherFile = createEmptyFile(inputFolder, "com/other/B.class");
final ImmutableList.Builder<File> filesElectedForClasses2Transformation = ImmutableList.builder();
final ImmutableList.Builder<File> filesElectedForClasses3Transformation = ImmutableList.builder();
when(buildContext.getVerifierResult()).thenReturn(
InstantRunVerifierStatus.COLD_SWAP_REQUESTED);
when(buildContext.getBuildMode()).thenReturn(InstantRunBuildMode.COLD);
InstantRunTransform transform = createTransform(
filesElectedForClasses2Transformation, filesElectedForClasses2Transformation);
TransformInput transformInput =
TransformTestHelper.directoryBuilder(inputFolder)
.setScope(Scope.PROJECT)
.setContentType(QualifiedContent.DefaultContentType.CLASSES)
.build();
TransformOutputProvider transformOutputProvider =
createTransformOutputProvider(temporaryFolder.newFolder("output"),
temporaryFolder.newFolder("outputEnhancedFolder"));
transform.transform(
new TransformInvocationBuilder(context)
.addOutputProvider(transformOutputProvider)
.addInputs(ImmutableList.of(transformInput))
.setIncrementalMode(false)
.build());
verify(buildContext).setVerifierStatus(
Mockito.eq(InstantRunVerifierStatus.BUILD_NOT_INCREMENTAL));
ImmutableList<File> processedFiles = filesElectedForClasses2Transformation.build();
assertEquals("Wrong number of files elected for processing", 2, processedFiles.size());
// check produced files.
assertThat(processedFiles).containsAllOf(originalFile, otherFile);
assertThat(filesElectedForClasses3Transformation.build()).isEmpty();
}
@Test
public void dirtyFullModeTest()
throws IOException, TransformException, InterruptedException {
final File inputFolder = temporaryFolder.newFolder("input");
final File originalFile = createEmptyFile(inputFolder, "com/example/A.class");
final File otherFile = createEmptyFile(inputFolder, "com/other/B.class");
final File outputFolder = temporaryFolder.newFolder("output");
final File outputFile = createEmptyFile(outputFolder, "com/example/A.class");
final File outputEnhancedFolder = temporaryFolder.newFolder("outputEnhanced");
final File outputEnhancedFile =
createEmptyFile(outputEnhancedFolder, "com/example/A$override.class");
assertTrue(outputFile.exists());
assertTrue(outputEnhancedFile.exists());
final ImmutableList.Builder<File> filesElectedForClasses2Transformation = ImmutableList.builder();
final ImmutableList.Builder<File> filesElectedForClasses3Transformation = ImmutableList.builder();
when(buildContext.getVerifierResult()).thenReturn(
InstantRunVerifierStatus.COLD_SWAP_REQUESTED);
when(buildContext.getBuildMode()).thenReturn(InstantRunBuildMode.COLD);
InstantRunTransform transform = createTransform(
filesElectedForClasses2Transformation, filesElectedForClasses2Transformation);
TransformInput transformInput =
TransformTestHelper.directoryBuilder(inputFolder)
.setContentType(QualifiedContent.DefaultContentType.CLASSES)
.setScope(Scope.PROJECT)
.build();
TransformOutputProvider transformOutputProvider =
createTransformOutputProvider(outputFolder, outputEnhancedFolder);
transform.transform(
new TransformInvocationBuilder(context)
.addOutputProvider(transformOutputProvider)
.addInputs(ImmutableList.of(transformInput))
.setIncrementalMode(false)
.build());
verify(buildContext).setVerifierStatus(
Mockito.eq(InstantRunVerifierStatus.BUILD_NOT_INCREMENTAL));
ImmutableList<File> processedFiles = filesElectedForClasses2Transformation.build();
assertEquals("Wrong number of files elected for processing", 2, processedFiles.size());
// check produced files.
assertThat(processedFiles).containsAllOf(originalFile, otherFile);
assertThat(filesElectedForClasses3Transformation.build()).isEmpty();
assertThat(outputEnhancedFile.listFiles()).isNull();
}
@Test
public void fileDeletionInIncrementalModeTest()
throws TransformException, InterruptedException, IOException {
fileDeletionTest(true /* incrementalMode */);
}
@Test
public void fileDeletionInFullModeTest()
throws TransformException, InterruptedException, IOException {
fileDeletionTest(false /* incrementalMode */);
}
private void fileDeletionTest(boolean incrementalMode)
throws IOException, TransformException, InterruptedException {
final File inputFolder = temporaryFolder.newFolder("input");
final File originalFile = createEmptyFile(inputFolder, "com/example/A.class");
final File otherFile = createEmptyFile(inputFolder, "com/other/B.class");
final File outputFolder = temporaryFolder.newFolder("output");
final File outputFile = createEmptyFile(outputFolder, "com/example/A.class");
final File outputEnhancedFolder = temporaryFolder.newFolder("outputEnhanced");
final File outputEnhancedFile =
createEmptyFile(outputEnhancedFolder, "com/example/A$override.class");
assertTrue(outputFile.exists());
assertTrue(outputEnhancedFile.exists());
final ImmutableList.Builder<File> filesElectedForClasses2Transformation = ImmutableList.builder();
final ImmutableList.Builder<File> filesElectedForClasses3Transformation = ImmutableList.builder();
InstantRunTransform transform = createTransform(
filesElectedForClasses2Transformation, filesElectedForClasses3Transformation);
TransformInput transformInput =
TransformTestHelper.directoryBuilder(inputFolder)
.setScope(Scope.PROJECT)
.setContentType(QualifiedContent.DefaultContentType.CLASSES)
.putChangedFiles(
incrementalMode
? ImmutableMap.<File, Status>builder()
.put(otherFile, Status.ADDED)
.put(originalFile, Status.REMOVED)
.build()
: ImmutableMap.of())
.build();
TransformOutputProvider transformOutputProvider =
createTransformOutputProvider(outputFolder, outputEnhancedFolder);
// delete the "deleted" file.
FileUtils.delete(originalFile);
if (!incrementalMode) {
when(buildContext.getBuildMode()).thenReturn(InstantRunBuildMode.COLD);
}
transform.transform(
new TransformInvocationBuilder(context)
.addOutputProvider(transformOutputProvider)
.addInputs(ImmutableList.of(transformInput))
.setIncrementalMode(incrementalMode)
.build());
if (!incrementalMode) {
verify(buildContext).setVerifierStatus(
Mockito.eq(InstantRunVerifierStatus.BUILD_NOT_INCREMENTAL));
}
ImmutableList<File> processedFiles = filesElectedForClasses2Transformation.build();
assertEquals("Wrong number of files elected for processing", 1, processedFiles.size());
assertThat(processedFiles.get(0).getAbsolutePath()).contains(
FileUtils.join("com", "other", "B.class"));
assertFalse("Incremental support class file should have been deleted.", outputFile.exists());
assertFalse("Enhanced class file should have been deleted.", outputEnhancedFile.exists());
}
private static File createEmptyFile(File folder, String path)
throws IOException {
File file = new File(folder, path);
Files.createParentDirs(file);
Files.touch(file);
return file;
}
private static TransformOutputProvider createTransformOutputProvider(File classes2, File classes3) {
return new TransformOutputProvider() {
@Override
public void deleteAll() throws IOException {
}
@NonNull
@Override
public File getContentLocation(@NonNull String name,
@NonNull Set<ContentType> types,
@NonNull Set<? super Scope> scopes, @NonNull Format format) {
assertThat(types).hasSize(1);
if (types.iterator().next().equals(QualifiedContent.DefaultContentType.CLASSES)) {
return classes2;
} else {
return classes3;
}
}
};
}
private InstantRunTransform createTransform(
ImmutableList.Builder<File> filesElectedForClasses2Transformation,
ImmutableList.Builder<File> filesElectedForClasses3Transformation) {
return new InstantRunTransform(executor, variantScope) {
@Override
@Nullable
protected Void transformToClasses2Format(
@NonNull File inputDir,
@NonNull File inputFile,
@NonNull File outputDir,
@NonNull Status status)
throws IOException {
filesElectedForClasses2Transformation.add(inputFile);
return null;
}
@Override
@Nullable
protected Void transformToClasses3Format(File inputDir, File inputFile, File outputDir)
throws IOException {
filesElectedForClasses3Transformation.add(inputFile);
return null;
}
@Override
protected void wrapUpOutputs(File classes2Folder, File classes3Folder)
throws IOException {
}
};
}
@SuppressWarnings({"unchecked"})
private static <T> Callable<T> anyCallable() {
return (Callable<T>) any(Callable.class);
}
}