blob: 43300431a46c8d80df52ed4ca2b2f3fbc15b7d1a [file] [log] [blame]
/*
* Copyright (C) 2016 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.res;
import static com.android.builder.model.AndroidProject.PROJECT_TYPE_LIBRARY;
import static com.android.ide.common.rendering.api.ResourceNamespace.RES_AUTO;
import static com.android.tools.idea.res.ModuleResourceRepositoryTest.assertHasExactResourceTypes;
import static com.android.tools.idea.res.ModuleResourceRepositoryTest.getFirstItem;
import com.android.ide.common.gradle.model.level2.IdeDependenciesFactory;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.ResourceItem;
import com.android.ide.common.resources.ResourceRepository;
import com.android.resources.ResourceType;
import com.android.tools.idea.gradle.TestProjects;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
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.android.tools.idea.testing.Modules;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleOrderEntry;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
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 com.intellij.util.ui.UIUtil;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.intellij.lang.annotations.Language;
import org.jetbrains.android.AndroidTestCase;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.IdeaSourceProvider;
import org.jetbrains.android.facet.ResourceFolderManager;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
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(ResourceRepositoryManager.getProjectResources(myFacet), ResourceRepositoryManager.getProjectResources(myFacet));
assertSame(ResourceRepositoryManager.getProjectResources(myFacet), ResourceRepositoryManager.getProjectResources(myModule));
}
/**
* Like {@link ModuleResourceRepository#testOverlayUpdates1}, but rather than testing changes to layout
* resources (file-based resource) perform document edits in value-documents.
* <p>
* Ensure that we invalidate the id cache when the file is rescanned but ids don't change.
*/
public void testInvalidateIds() {
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 TestLocalResourceRepository(RES_AUTO);
ModuleResourceRepository module = ModuleResourceRepository.createForTest(myFacet, ImmutableList.of(res1, res2, res3), RES_AUTO, null);
ProjectResourceRepository resources = ProjectResourceRepository.createForTest(myFacet, ImmutableList.of(module, other));
PsiFile layoutPsiFile = PsiManager.getInstance(getProject()).findFile(layoutFile);
assertNotNull(layoutPsiFile);
assertTrue(resources.hasResources(RES_AUTO, ResourceType.ID, "btn_title_refresh"));
ResourceItem item = getFirstItem(resources, ResourceType.ID, "btn_title_refresh");
long generation = resources.getModificationCount();
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
Document document = documentManager.getDocument(layoutPsiFile);
assertNotNull(document);
WriteCommandAction.runWriteCommandAction(null, () -> {
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(() -> {
assertTrue(generation < resources.getModificationCount());
// Should still be defined:
assertTrue(resources.hasResources(RES_AUTO, ResourceType.ID, "btn_title_refresh"));
ResourceItem newItem = getFirstItem(resources, ResourceType.ID, "btn_title_refresh");
assertNotNull(newItem.getSource());
// However, should be a different item
assertNotSame(item, newItem);
});
UIUtil.dispatchAllInvocationEvents();
}
// 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.getChildren().size());
Collection<String> items = repository.getResources(RES_AUTO, ResourceType.STRING).keySet();
assertTrue(items.isEmpty());
for (AndroidFacet facet : libraries) {
LocalResourceRepository moduleRepository = ResourceRepositoryManager.getModuleResources(facet);
assertNotNull(moduleRepository);
LocalResourceRepository moduleSetRepository = ResourceRepositoryManager.getProjectResources(facet);
assertNotNull(moduleSetRepository);
LocalResourceRepository librarySetRepository = ResourceRepositoryManager.getAppResources(facet);
assertNotNull(librarySetRepository);
}
ResourceRepositoryManager.getModuleResources(myFacet);
ResourceRepositoryManager.getProjectResources(myFacet);
ResourceRepositoryManager.getAppResources(myFacet);
}
public void testGetResourceDirsAndUpdateRoots() {
myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
addArchiveLibraries();
List<VirtualFile> flavorDirs = Lists.newArrayList(ResourceFolderManager.getInstance(myFacet).getFolders());
ProjectResourceRepository repository = ProjectResourceRepository.create(myFacet);
List<LocalResourceRepository> originalChildren = repository.getLocalResources();
// Should have a bunch repository directories from the various flavors.
Set<VirtualFile> resourceDirs = repository.getResourceDirs();
assertNotEmpty(resourceDirs);
assertNotEmpty(flavorDirs);
assertSameElements(resourceDirs, flavorDirs);
// Now delete a directory, notify the folder manager and updateRoots.
VirtualFile firstFlavor = flavorDirs.remove(0);
WriteCommandAction.runWriteCommandAction(getProject(), new Runnable() {
@Override
public void run() {
try {
firstFlavor.delete(this);
}
catch (IOException e) {
assertFalse("delete failed " + e, false);
}
ResourceFolderManager.getInstance(myFacet).checkForChanges();
repository.updateRoots();
}
});
// The child repositories should be the same since the module structure didn't change.
List<LocalResourceRepository> newChildren = repository.getLocalResources();
Set<VirtualFile> newResourceDirs = repository.getResourceDirs();
assertEquals(newChildren, originalChildren);
// However, the resourceDirs should now be different, missing the first flavor directory.
assertSameElements(newResourceDirs, flavorDirs);
}
public void testRootChangeListener() {
MultiResourceRepository resources = (MultiResourceRepository)ResourceRepositoryManager.getProjectResources(myFacet);
List<LocalResourceRepository> originalChildren = resources.getLocalResources();
assertNotEmpty(originalChildren);
Collection<VirtualFile> originalDirs = resources.getResourceDirs();
assertNotEmpty(originalDirs);
Modules modules = new Modules(getProject());
// Now remove one of the modules, which should automatically cause the repo to have different roots.
WriteCommandAction.runWriteCommandAction(
getProject(), () -> removeModuleDependency(myModule, modules.getModule("plib2").getName()));
assertEquals(originalChildren.size() - 1, resources.getChildren().size());
assertEquals(originalDirs.size() - 1, resources.getResourceDirs().size());
}
public void testHasResourcesOfType() {
// Test hasResourcesOfType merging (which may be optimized to be lighter-weight than map merging).
VirtualFile res1 = myFixture.copyFileToProject(LAYOUT, "res/layout/layout.xml").getParent().getParent();
VirtualFile res2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "res2/values/values.xml").getParent().getParent();
VirtualFile values3 = myFixture.copyFileToProject(VALUES, "res3/values/many_more_values.xml");
VirtualFile res3 = values3.getParent().getParent();
assertNotSame(res1, res2);
assertNotSame(res1, res3);
assertNotSame(res2, res3);
// Test having some overlap between the modules.
LocalResourceRepository module1 = ModuleResourceRepository.createForTest(myFacet, ImmutableList.of(res1, res2), RES_AUTO, null);
LocalResourceRepository module2 = ModuleResourceRepository.createForTest(myFacet, ImmutableList.of(res2, res3), RES_AUTO, null);
ProjectResourceRepository resources = ProjectResourceRepository.createForTest(myFacet, ImmutableList.of(module1, module2));
// Create a repo with res1, res2, res3 and check types.
// After that, delete a file in res3 and check types again.
EnumSet<ResourceType> typesWithoutRes3 = EnumSet.of(ResourceType.ARRAY, ResourceType.ID, ResourceType.LAYOUT,
ResourceType.STRING, ResourceType.STYLE);
EnumSet<ResourceType> allTypes = EnumSet.copyOf(typesWithoutRes3);
allTypes.addAll(ImmutableList.of(ResourceType.ATTR, ResourceType.INTEGER, ResourceType.STYLEABLE, ResourceType.PLURALS));
assertHasExactResourceTypes(resources, allTypes);
// Now delete the values file and check again.
PsiFile psiValues3 = PsiManager.getInstance(getProject()).findFile(values3);
assertNotNull(psiValues3);
WriteCommandAction.runWriteCommandAction(null, psiValues3::delete);
assertHasExactResourceTypes(resources, typesWithoutRes3);
}
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();
AndroidModuleModel androidModel =
new AndroidModuleModel(androidProject.getName(), rootDir, androidProject, variant.getName(), new IdeDependenciesFactory());
myFacet.getConfiguration().setModel(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);
// Refresh temporary resource directories created by the model, so that they are accessible as VirtualFiles.
Collection<File> resourceDirs =
IdeaSourceProvider.getAllSourceProviders(myFacet)
.stream()
.flatMap(provider -> provider.getResDirectories().stream())
.collect(Collectors.toList());
refreshForVfs(resourceDirs);
}
private static void refreshForVfs(Collection<File> freshFiles) {
for (File file : freshFiles) {
String path = FileUtil.toSystemIndependentName(file.getPath());
VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
VfsUtil.markDirtyAndRefresh(false, true, true, virtualFile);
}
}
@Override
protected void configureAdditionalModules(@NotNull TestFixtureBuilder<IdeaProjectTestFixture> projectBuilder,
@NotNull List<MyAdditionalModuleData> modules) {
String testName = getTestName(true);
if (testName.equals("parents")) { // for unit test testDependencies
addModuleWithAndroidFacet(projectBuilder, modules, "plib1", PROJECT_TYPE_LIBRARY);
addModuleWithAndroidFacet(projectBuilder, modules, "plib2", PROJECT_TYPE_LIBRARY);
}
else if (testName.equals("dependencies")) { // for unit test testDependencies
addModuleWithAndroidFacet(projectBuilder, modules, "sharedlib", PROJECT_TYPE_LIBRARY);
addModuleWithAndroidFacet(projectBuilder, modules, "lib1", PROJECT_TYPE_LIBRARY);
addModuleWithAndroidFacet(projectBuilder, modules, "lib2", PROJECT_TYPE_LIBRARY);
}
else if (testName.equals("rootChangeListener")) {
addModuleWithAndroidFacet(projectBuilder, modules, "plib1", PROJECT_TYPE_LIBRARY);
addModuleWithAndroidFacet(projectBuilder, modules, "plib2", PROJECT_TYPE_LIBRARY);
}
else if (testName.equals("resourceOverride")) {
addModuleWithAndroidFacet(projectBuilder, modules, "level1", PROJECT_TYPE_LIBRARY, true);
addModuleWithAndroidFacet(projectBuilder, modules, "level2", PROJECT_TYPE_LIBRARY, false);
}
}
// 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");
Modules modules = new Modules(getProject());
Module lib1 = modules.getModule("lib1");
Module lib2 = modules.getModule("lib2");
Module sharedLib = modules.getModule("sharedlib");
Module app = modules.getAppModule();
assertNotNull(lib1);
assertNotNull(lib2);
assertNotNull(sharedLib);
assertNotNull(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);
LocalResourceRepository lib1Resources = ResourceRepositoryManager.getAppResources(lib1Facet);
LocalResourceRepository lib2Resources = ResourceRepositoryManager.getAppResources(lib2Facet);
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.hasResources(RES_AUTO, ResourceType.PLURALS, "my_plural"));
assertTrue(lib1Resources.hasResources(RES_AUTO, ResourceType.STRING, "ellipsis"));
assertTrue(lib1Resources.hasResources(RES_AUTO, ResourceType.ARRAY, "security_questions"));
List<ResourceItem> items = lib1Resources.getResources(RES_AUTO, ResourceType.STRING, "ellipsis");
assertNotNull(items);
ResourceValue firstValue = items.get(0).getResourceValue();
assertNotNull(firstValue);
assertEquals("Here it is: \u2026!", firstValue.getValue());
assertTrue(lib2Resources.hasResources(RES_AUTO, ResourceType.ARRAY, "security_questions"));
assertTrue(lib2Resources.hasResources(RES_AUTO, ResourceType.PLURALS, "my_plural"));
assertTrue(lib2Resources.hasResources(RES_AUTO, ResourceType.STRING, "ellipsis"));
// ONLY defined in lib2: should not be visible from lib1
assertTrue(lib2Resources.hasResources(RES_AUTO, ResourceType.STRING, "unique_string"));
items = lib2Resources.getResources(RES_AUTO, ResourceType.STRING, "unique_string");
assertNotNull(items);
firstValue = items.get(0).getResourceValue();
assertNotNull(firstValue);
assertEquals("Unique", firstValue.getValue());
assertFalse(lib1Resources.hasResources(RES_AUTO, ResourceType.STRING, "unique_string"));
}
// Regression test for https://issuetracker.google.com/issues/68799367
public void testResourceOverride() {
Modules modules = new Modules(getProject());
Module lib1 = modules.getModule("level1");
Module lib2 = modules.getModule("level2");
Module app = modules.getAppModule();
assertNotNull(lib1);
assertNotNull(lib2);
assertNotNull(app);
AndroidFacet appFacet = AndroidFacet.getInstance(app);
// Set up project dependencies
addModuleDependency(lib1, lib2);
// Dependencies: app -> lib1 -> lib2
assertTrue(ModuleRootManager.getInstance(lib1).isDependsOn(lib2));
assertTrue(ModuleRootManager.getInstance(app).isDependsOn(lib1));
assertFalse(ModuleRootManager.getInstance(app).isDependsOn(lib2));
assertFalse(ModuleRootManager.getInstance(lib2).isDependsOn(lib1));
@Language("XML")
String level1Strings = "<resources>\n" +
" <string name=\"test_string\">LEVEL 1</string>\n" +
"</resources>";
@Language("XML")
String level2Strings = "<resources>\n" +
" <string name=\"test_string\">LEVEL 2</string>\n" +
"</resources>";
// Set up string override
myFixture.addFileToProject("additionalModules/level1/res/values/strings.xml", level1Strings).getVirtualFile();
myFixture.addFileToProject("additionalModules/level2/res/values/strings.xml", level2Strings).getVirtualFile();
ResourceRepository appResources = ResourceRepositoryManager.getAppResources(appFacet);
List<ResourceItem> resolved = appResources.getResources(RES_AUTO, ResourceType.STRING, "test_string");
assertEquals(1, resolved.size());
assertEquals("LEVEL 1", resolved.get(0).getResourceValue().getValue());
// Retry reversing the library dependency to ensure the order does not depend on anything other than the
// dependency order (like for example, alphabetical order of the modules).
removeModuleDependency(lib1, lib2.getName());
removeModuleDependency(app, lib1.getName());
addModuleDependency(app, lib2);
addModuleDependency(lib2, lib1);
// app -> lib2 -> lib1
assertTrue(ModuleRootManager.getInstance(lib2).isDependsOn(lib1));
assertTrue(ModuleRootManager.getInstance(app).isDependsOn(lib2));
assertFalse(ModuleRootManager.getInstance(app).isDependsOn(lib1));
assertFalse(ModuleRootManager.getInstance(lib1).isDependsOn(lib2));
appResources = ResourceRepositoryManager.getAppResources(appFacet);
resolved = appResources.getResources(RES_AUTO, ResourceType.STRING, "test_string");
assertEquals(1, resolved.size());
assertEquals("LEVEL 2", resolved.get(0).getResourceValue().getValue());
}
private static void addModuleDependency(Module from, Module to) {
ModifiableRootModel model = ModuleRootManager.getInstance(from).getModifiableModel();
model.addModuleOrderEntry(to);
ApplicationManager.getApplication().runWriteAction(model::commit);
}
private static void removeModuleDependency(Module from, String name) {
boolean found = false;
ModifiableRootModel model = ModuleRootManager.getInstance(from).getModifiableModel();
for (OrderEntry orderEntry : model.getOrderEntries()) {
if (orderEntry instanceof ModuleOrderEntry) {
ModuleOrderEntry moduleOrderEntry = (ModuleOrderEntry)orderEntry;
if (moduleOrderEntry.getModuleName().equals(name)) {
assertFalse(found);
model.removeOrderEntry(orderEntry);
found = true;
}
}
}
assertTrue(found);
ApplicationManager.getApplication().runWriteAction(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.
}