blob: d87784a6705aad98fbdb2dfe3ec40a81157d0658 [file] [log] [blame]
/*
* Copyright (C) 2013 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.rendering;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidProject;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceType;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.TestProjects;
import com.android.tools.idea.gradle.stubs.android.AndroidLibraryStub;
import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
import com.android.tools.idea.gradle.stubs.android.VariantStub;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleWithNameAlreadyExists;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
import com.intellij.testFramework.fixtures.TestFixtureBuilder;
import org.jetbrains.android.AndroidTestCase;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import java.io.File;
import java.util.*;
import static com.android.tools.idea.rendering.ModuleResourceRepositoryTest.getFirstItem;
public class ProjectResourceRepositoryTest extends AndroidTestCase {
private static final String LAYOUT = "resourceRepository/layout.xml";
private static final String VALUES = "resourceRepository/values.xml";
private static final String VALUES_OVERLAY1 = "resourceRepository/valuesOverlay1.xml";
private static final String VALUES_OVERLAY2 = "resourceRepository/valuesOverlay2.xml";
private static final String VALUES_OVERLAY2_NO = "resourceRepository/valuesOverlay2No.xml";
public void testStable() {
assertSame(ProjectResourceRepository.getProjectResources(myFacet, true), ProjectResourceRepository.getProjectResources(myFacet, true));
assertSame(ProjectResourceRepository.getProjectResources(myFacet, true), ProjectResourceRepository.getProjectResources(myModule, true));
}
// Ensure that we invalidate the id cache when the file is rescanned but ids don't change
// (this was broken)
public void testInvalidateIds() {
// Like testOverlayUpdates1, but rather than testing changes to layout resources (file-based resource)
// perform document edits in value-documents
VirtualFile layoutFile = myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
VirtualFile res1 = myFixture.copyFileToProject(VALUES, "res/values/values.xml").getParent().getParent();
VirtualFile res2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "res2/values/values.xml").getParent().getParent();
VirtualFile res3 = myFixture.copyFileToProject(VALUES_OVERLAY2, "res3/values/nameDoesNotMatter.xml").getParent().getParent();
myFixture.copyFileToProject(VALUES_OVERLAY2_NO, "res3/values-no/values.xml");
assertNotSame(res1, res2);
assertNotSame(res1, res3);
assertNotSame(res2, res3);
// Just need an empty repository to make it a real module -set-; otherwise with a single
// module we just get a module repository, not a module set repository
LocalResourceRepository other = new LocalResourceRepository("unit test") {
@NonNull
@Override
protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() {
return Collections.emptyMap();
}
@Nullable
@Override
protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) {
return ArrayListMultimap.create();
}
};
ModuleResourceRepository module = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3));
final ProjectResourceRepository resources = ProjectResourceRepository.createForTest(myFacet, Arrays.asList(module, other));
PsiFile layoutPsiFile = PsiManager.getInstance(getProject()).findFile(layoutFile);
assertNotNull(layoutPsiFile);
assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh"));
final PsiResourceItem item = getFirstItem(resources, ResourceType.ID, "btn_title_refresh");
final long generation = resources.getModificationCount();
final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
final Document document = documentManager.getDocument(layoutPsiFile);
assertNotNull(document);
WriteCommandAction.runWriteCommandAction(null, new Runnable() {
@Override
public void run() {
String string = "<ImageView style=\"@style/TitleBarSeparator\" />";
int offset = document.getText().indexOf(string);
document.deleteString(offset, offset + string.length());
documentManager.commitDocument(document);
}
});
assertTrue(resources.isScanPending(layoutPsiFile));
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
assertTrue(generation < resources.getModificationCount());
// Should still be defined:
assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh"));
PsiResourceItem newItem = getFirstItem(resources, ResourceType.ID, "btn_title_refresh");
assertNotNull(newItem.getSource());
// However, should be a different item
assertNotSame(item, newItem);
}
});
}
// Regression test for https://code.google.com/p/android/issues/detail?id=57090
public void testParents() {
myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
List<AndroidFacet> libraries = AndroidUtils.getAllAndroidDependencies(myModule, true);
assertEquals(2, libraries.size());
ModuleRootModificationUtil.addDependency(libraries.get(0).getModule(), libraries.get(1).getModule());
addArchiveLibraries();
ProjectResourceRepository repository = ProjectResourceRepository.create(myFacet);
assertEquals(3, repository.getChildCount());
Collection<String> items = repository.getItemsOfType(ResourceType.STRING);
assertTrue(items.isEmpty());
for (AndroidFacet facet : libraries) {
LocalResourceRepository moduleRepository = facet.getModuleResources(true);
assertNotNull(moduleRepository);
LocalResourceRepository moduleSetRepository = facet.getProjectResources(true);
assertNotNull(moduleSetRepository);
LocalResourceRepository librarySetRepository = facet.getAppResources(true);
assertNotNull(librarySetRepository);
}
myFacet.getModuleResources(true);
myFacet.getProjectResources(true);
myFacet.getAppResources(true);
}
private void addArchiveLibraries() {
// Add in some Android projects too
myFacet.getProperties().ALLOW_USER_CONFIGURATION = false; // make it a Gradle project
AndroidProjectStub androidProject = TestProjects.createFlavorsProject();
VariantStub variant = androidProject.getFirstVariant();
assertNotNull(variant);
File rootDir = androidProject.getRootDir();
IdeaAndroidProject androidModel = new IdeaAndroidProject(GradleConstants.SYSTEM_ID, androidProject.getName(), rootDir,
androidProject, variant.getName(), AndroidProject.ARTIFACT_ANDROID_TEST);
myFacet.setAndroidModel(androidModel);
File bundle = new File(rootDir, "bundle.aar");
File libJar = new File(rootDir, "bundle_aar" + File.separatorChar + "library.jar");
AndroidLibraryStub library = new AndroidLibraryStub(bundle, libJar);
variant.getMainArtifact().getDependencies().addLibrary(library);
}
@Override
protected void configureAdditionalModules(@NotNull TestFixtureBuilder<IdeaProjectTestFixture> projectBuilder,
@NotNull List<MyAdditionalModuleData> modules) {
final String testName = getTestName(true);
if (testName.equals("parents")) { // for unit test testDependencies
addModuleWithAndroidFacet(projectBuilder, modules, "plib1", true);
addModuleWithAndroidFacet(projectBuilder, modules, "plib2", true);
} else if (testName.equals("dependencies")) { // for unit test testDependencies
addModuleWithAndroidFacet(projectBuilder, modules, "sharedlib", true);
addModuleWithAndroidFacet(projectBuilder, modules, "lib1", true);
addModuleWithAndroidFacet(projectBuilder, modules, "lib2", true);
addModuleWithAndroidFacet(projectBuilder, modules, "app", true);
}
}
// Regression test for https://code.google.com/p/android/issues/detail?id=65140
public void testDependencies() throws Exception {
myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
Module lib1 = null;
Module lib2 = null;
Module sharedLib = null;
Module app = null;
for (Module module : ModuleManager.getInstance(getProject()).getModules()) {
if (module != myModule) {
VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots();
assertEquals(1, contentRoots.length);
String name = contentRoots[0].getName();
if (name.equals("lib1")) {
lib1 = module;
}
else if (name.equals("lib2")) {
lib2 = module;
}
else if (name.equals("sharedlib")) {
sharedLib = module;
}
else if (name.equals("app")) {
app = module;
} else {
fail(name);
}
}
}
assertNotNull(lib1);
assertNotNull(lib2);
assertNotNull(sharedLib);
assertNotNull(app);
renameModule(lib1, "lib1");
renameModule(lib2, "lib2");
renameModule(sharedLib, "sharedLib");
renameModule(app, "app");
AndroidFacet lib1Facet = AndroidFacet.getInstance(lib1);
AndroidFacet lib2Facet = AndroidFacet.getInstance(lib2);
AndroidFacet sharedLibFacet = AndroidFacet.getInstance(sharedLib);
AndroidFacet appFacet = AndroidFacet.getInstance(app);
assertNotNull(lib1Facet);
assertNotNull(lib2Facet);
assertNotNull(sharedLibFacet);
assertNotNull(appFacet);
// Set up project dependencies
addModuleDependency(lib1, sharedLib);
addModuleDependency(lib2, sharedLib);
addModuleDependency(app, lib1);
addModuleDependency(app, lib2);
assertTrue(ModuleRootManager.getInstance(app).isDependsOn(lib1));
assertTrue(ModuleRootManager.getInstance(lib1).isDependsOn(sharedLib));
assertFalse(ModuleRootManager.getInstance(sharedLib).isDependsOn(lib1));
assertFalse(ModuleRootManager.getInstance(lib2).isDependsOn(lib1));
// Note that these are currently direct dependencies only, so app.isDependsOn(sharedLib) is false
// Test AndroidUtils#getallAndroidDependencies
List<AndroidFacet> appDependsOn = AndroidUtils.getAllAndroidDependencies(app, true);
assertTrue(appDependsOn.contains(lib1Facet));
assertTrue(appDependsOn.contains(lib2Facet));
assertTrue(appDependsOn.contains(sharedLibFacet));
assertFalse(appDependsOn.contains(appFacet));
List<AndroidFacet> lib1DependsOn = AndroidUtils.getAllAndroidDependencies(lib1, true);
assertTrue(lib1DependsOn.contains(sharedLibFacet));
assertFalse(lib1DependsOn.contains(appFacet));
assertFalse(lib1DependsOn.contains(lib1Facet));
assertFalse(lib1DependsOn.contains(lib2Facet));
// Set up resources so we can check which values are inherited through module dependencies
VirtualFile v1 = myFixture.copyFileToProject(VALUES, "additionalModules/sharedlib/res/values/sharedvalues.xml");
VirtualFile v2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "additionalModules/lib2/res/values/lib2values.xml");
assertNotNull(v1);
assertNotNull(v2);
PsiManager manager = PsiManager.getInstance(getProject());
PsiFile sharedLibValues = manager.findFile(v1);
PsiFile lib2Values = manager.findFile(v2);
assertNotNull(sharedLibValues);
assertNotNull(lib2Values);
final AppResourceRepository lib1Resources = lib1Facet.getAppResources(true);
final AppResourceRepository lib2Resources = lib2Facet.getAppResources(true);
assertNotNull(lib1Resources);
assertNotNull(lib2Resources);
assertNotSame(lib1Resources, lib2Resources);
assertFalse(lib1Resources.isScanPending(sharedLibValues));
assertFalse(lib1Resources.isScanPending(lib2Values));
assertFalse(lib2Resources.isScanPending(sharedLibValues));
assertFalse(lib2Resources.isScanPending(lib2Values));
assertTrue(lib1Resources.hasResourceItem(ResourceType.PLURALS, "my_plural"));
assertTrue(lib1Resources.hasResourceItem(ResourceType.STRING, "ellipsis"));
assertTrue(lib1Resources.hasResourceItem(ResourceType.ARRAY, "security_questions"));
List<ResourceItem> items = lib1Resources.getResourceItem(ResourceType.STRING, "ellipsis");
assertNotNull(items);
ResourceValue firstValue = items.get(0).getResourceValue(false);
assertNotNull(firstValue);
assertEquals("Here it is: \u2026!", firstValue.getValue());
assertTrue(lib2Resources.hasResourceItem(ResourceType.ARRAY, "security_questions"));
assertTrue(lib2Resources.hasResourceItem(ResourceType.PLURALS, "my_plural"));
assertTrue(lib2Resources.hasResourceItem(ResourceType.STRING, "ellipsis"));
// ONLY defined in lib2: should not be visible from lib1
assertTrue(lib2Resources.hasResourceItem(ResourceType.STRING, "unique_string"));
items = lib2Resources.getResourceItem(ResourceType.STRING, "unique_string");
assertNotNull(items);
firstValue = items.get(0).getResourceValue(false);
assertNotNull(firstValue);
assertEquals("Unique", firstValue.getValue());
assertFalse(lib1Resources.hasResourceItem(ResourceType.STRING, "unique_string"));
}
private static void addModuleDependency(Module from, Module to) {
final ModifiableRootModel model = ModuleRootManager.getInstance(from).getModifiableModel();
model.addModuleOrderEntry(to);
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
model.commit();
}
});
}
private static void renameModule(Module from, String name) throws ModuleWithNameAlreadyExists {
final ModifiableModuleModel model = ModuleManager.getInstance(from.getProject()).getModifiableModel();
model.renameModule(from, name);
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
model.commit();
}
});
}
// Note that the project resource repository is also tested in the app resource repository test, which of course merges
// project resources with libraries
}