blob: 93054295cb780fb9845d38503ea631d6895ed224 [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.tools.idea.gradle;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.BaseArtifact;
import com.android.builder.model.JavaArtifact;
import com.android.sdklib.IAndroidTarget;
import com.intellij.execution.JUnitPatcher;
import com.intellij.execution.configurations.JavaParameters;
import com.intellij.openapi.compiler.CompileScope;
import com.intellij.openapi.compiler.CompilerManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.util.PathsList;
import com.intellij.util.containers.ContainerUtil;
import org.gradle.tooling.model.UnsupportedMethodException;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
import static com.intellij.openapi.util.io.FileUtil.pathsEqual;
import static com.intellij.openapi.util.io.FileUtil.toSystemDependentName;
/**
* Implementation of {@link JUnitPatcher} that removes android.jar from the class path. It's only applicable to
* JUnit run configurations if the selected test artifact is "unit tests". In this case, the mockable android.jar is already in the
* dependencies (taken from the model).
*/
public class AndroidJunitPatcher extends JUnitPatcher {
@Override
public void patchJavaParameters(@Nullable Module module, @NotNull JavaParameters javaParameters) {
if (module == null) {
return;
}
AndroidFacet androidFacet = AndroidFacet.getInstance(module);
if (androidFacet == null) {
return;
}
IdeaAndroidProject androidModel = androidFacet.getAndroidModel();
if (androidModel == null) {
return;
}
BaseArtifact testArtifact = androidModel.findSelectedTestArtifactInSelectedVariant();
if (testArtifact == null) {
return;
}
// Modify the class path only if we're dealing with the unit test artifact.
if (!AndroidProject.ARTIFACT_UNIT_TEST.equals(testArtifact.getName()) || !(testArtifact instanceof JavaArtifact)) {
return;
}
PathsList classPath = javaParameters.getClassPath();
AndroidPlatform platform = AndroidPlatform.getInstance(module);
if (platform == null) {
return;
}
String originalClassPath = classPath.getPathsString();
try {
handlePlatformJar(classPath, platform, (JavaArtifact)testArtifact);
handleJavaResources(module, androidModel, classPath);
}
catch (Exception e) {
throw new RuntimeException(String.format("Error patching the JUnit class path. Original class path:%n%s", originalClassPath),
e);
}
}
// Removes real android.jar from the classpath and puts the mockable one at the end.
private static void handlePlatformJar(@NotNull PathsList classPath,
@NotNull AndroidPlatform platform,
@NotNull JavaArtifact artifact) {
String androidJarPath = platform.getTarget().getPath(IAndroidTarget.ANDROID_JAR);
for (String entry : classPath.getPathList()) {
if (pathsEqual(androidJarPath, entry)) {
classPath.remove(entry);
}
}
// Move the mockable android jar to the end. This is to make sure "empty" classes from android.jar don't end up shadowing real
// classes needed by the testing code (e.g. XML/JSON related). Since mockable jars were introduced in 1.1, they were put in the model
// as dependencies, which means a module which depends on Android libraries with different will end up with more than one mockable jar in the
// classpath.
List<String> mockableJars = ContainerUtil.newSmartList();
for (String path : classPath.getPathList()) {
if (new File(toSystemDependentName(path)).getName().startsWith("mockable-")) {
// PathsList stores strings - use the one that's actually stored there.
mockableJars.add(path);
}
}
// Remove all mockable android.jars.
for (String mockableJar : mockableJars) {
classPath.remove(mockableJar);
}
File mockableJar = getMockableJarFromModel(artifact);
if (mockableJar != null) {
classPath.addTail(mockableJar.getPath());
}
else {
// We're dealing with an old plugin, that puts the mockable jar in the dependencies. Just put the matching android.jar at the end of
// the classpath.
for (String mockableJarPath : mockableJars) {
if (mockableJarPath.endsWith("-" + platform.getApiLevel() + ".jar")) {
classPath.addTail(mockableJarPath);
return;
}
}
}
}
@Nullable
private static File getMockableJarFromModel(@NotNull JavaArtifact model) {
try {
return model.getMockablePlatformJar();
}
catch (UnsupportedMethodException e) {
// Older model.
return null;
}
}
/**
* Puts folders with merged java resources for the selected variant of every module on the classpath.
*
* <p>The problem we're solving here is that CompilerModuleExtension supports only one directory for "compiler output". When IJ compiles
* Java projects, it copies resources to the output classes dir. This is something our Gradle plugin doesn't do, so we need to add the
* resource directories to the classpath here.
*
* <p>We need to do this for every project dependency as well, since we're using classes and resources directories of these directly.
*
* @see <a href="http://b.android.com/172409">Bug 172409</a>
* @see com.android.tools.idea.gradle.customizer.android.CompilerOutputModuleCustomizer#customizeModule(Project, ModifiableRootModel, IdeaAndroidProject)
*/
private static void handleJavaResources(@NotNull Module module,
@NotNull IdeaAndroidProject androidModel,
@NotNull PathsList classPath) {
final CompilerManager compilerManager = CompilerManager.getInstance(module.getProject());
CompileScope scope = compilerManager.createModulesCompileScope(new Module[]{module}, true, true);
for (Module affectedModule : scope.getAffectedModules()) {
AndroidFacet facet = AndroidFacet.getInstance(affectedModule);
if (facet != null) {
IdeaAndroidProject affectedAndroidModel = facet.getAndroidModel();
if (affectedAndroidModel != null) {
try {
classPath.add(affectedAndroidModel.getMainArtifact().getJavaResourcesFolder());
}
catch (UnsupportedMethodException e) {
// Java resources were not present in older versions of the gradle plugin.
}
}
}
}
// The only test resources we want to use, are the ones from the module where the test is.
BaseArtifact testArtifact = androidModel.findSelectedTestArtifactInSelectedVariant();
if (testArtifact != null) {
try {
classPath.add(testArtifact.getJavaResourcesFolder());
}
catch (UnsupportedMethodException e) {
// Java resources were not present in older versions of the gradle plugin.
}
}
}
}