| /* |
| * 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.ide.common.rendering.api.ResourceValue; |
| import com.android.ide.common.res2.ResourceItem; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.resources.ResourceType; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.intellij.openapi.command.WriteCommandAction; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.PsiManager; |
| import org.jetbrains.android.AndroidTestCase; |
| import org.jetbrains.annotations.NotNull; |
| |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| @SuppressWarnings("SpellCheckingInspection") |
| public class ModuleResourceRepositoryTest extends AndroidTestCase { |
| private static final String LAYOUT = "resourceRepository/layout.xml"; |
| private static final String LAYOUT_OVERLAY = "resourceRepository/layoutOverlay.xml"; |
| private static final String LAYOUT_IDS_1 = "resourceRepository/layout_ids1.xml"; |
| private static final String LAYOUT_IDS_2 = "resourceRepository/layout_ids2.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(ModuleResourceRepository.getModuleResources(myFacet, true), ModuleResourceRepository.getModuleResources(myFacet, true)); |
| assertSame(ModuleResourceRepository.getModuleResources(myFacet, true), ModuleResourceRepository.getModuleResources(myModule, true)); |
| } |
| |
| public void testSingleResourceFolder() { |
| LocalResourceRepository repository = ModuleResourceRepository.create(myFacet); |
| assertTrue(repository instanceof ResourceFolderRepository); |
| } |
| |
| public void testOverlays() { |
| myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml"); |
| myFixture.copyFileToProject(LAYOUT_OVERLAY, "res2/layout/layout1.xml"); |
| myFixture.copyFileToProject(LAYOUT_IDS_1, "res2/layout/layout_ids1.xml"); |
| myFixture.copyFileToProject(LAYOUT_IDS_2, "res2/layout/layout_ids2.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); |
| |
| ModuleResourceRepository resources = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3)); |
| |
| // Check that values are handled correctly. First a plain value (not overridden anywhere). |
| assertStringIs(resources, "title_layout_changes", "Layout Changes"); |
| |
| // Check that an overridden key (overridden in just one flavor) is picked up |
| assertStringIs(resources, "title_crossfade", "Complex Crossfade"); // Overridden in res2 |
| assertStringIs(resources, "title_zoom", "Zoom!"); // Overridden in res3 |
| |
| // Make sure that new/unique strings from flavors are available |
| assertStringIs(resources, "unique_string", "Unique"); // Overridden in res2 |
| assertStringIs(resources, "another_unique_string", "Another Unique", false); // Overridden in res3 |
| |
| // Check that an overridden key (overridden in multiple flavors) picks the last one |
| assertStringIs(resources, "app_name", "Very Different App Name", false); // res3 (not unique because we have a values-no item too) |
| |
| // Layouts: Should only be offered id's from the overriding layout (plus those defined in values.xml) |
| assertTrue(resources.hasResourceItem(ResourceType.ID, "action_next")); // from values.xml |
| assertTrue(resources.hasResourceItem(ResourceType.ID, "noteArea")); // from res2 layout1.xml |
| |
| // Layout masking does not currently work. I'm not 100% certain what the intended behavior is |
| // here (e.g. res1's layout1 contains @+id/button1, res2's layout1 does not; should @+id/button1 be visible?) |
| //assertFalse(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh")); // masked in res1 by res2's layout replacement |
| |
| // Check that localized lookup (qualifier matching works) |
| List<ResourceItem> stringList = resources.getResourceItem(ResourceType.STRING, "another_unique_string"); |
| assertNotNull(stringList); |
| assertSize(2, stringList); |
| FolderConfiguration valueConfig = FolderConfiguration.getConfigForFolder("values-no"); |
| assertNotNull(valueConfig); |
| ResourceValue stringValue = resources.getConfiguredResources(ResourceType.STRING, valueConfig).get("another_unique_string"); |
| assertNotNull(stringValue); |
| assertEquals("En Annen", stringValue.getValue()); |
| |
| // Change flavor order and make sure things are updated and work correctly |
| resources.updateRoots(Arrays.asList(res1, res3, res2)); |
| |
| // Should now be picking app_name from res2 rather than res3 since it's now last |
| assertStringIs(resources, "app_name", "Different App Name", false); // res2 |
| |
| // Sanity check other merging |
| assertStringIs(resources, "title_layout_changes", "Layout Changes"); |
| assertStringIs(resources, "title_crossfade", "Complex Crossfade"); // Overridden in res2 |
| assertStringIs(resources, "title_zoom", "Zoom!"); // Overridden in res3 |
| assertStringIs(resources, "unique_string", "Unique"); // Overridden in res2 |
| assertStringIs(resources, "another_unique_string", "Another Unique", false); // Overridden in res3 |
| |
| // Hide a resource root (res2) |
| resources.updateRoots(Arrays.asList(res1, res3)); |
| |
| // No longer aliasing the main layout |
| assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh")); // res1 layout1.xml |
| assertTrue(resources.hasResourceItem(ResourceType.ID, "noteArea")); // from res1 layout1.xml |
| assertTrue(resources.hasResourceItem(ResourceType.ID, "action_next")); // from values.xml |
| |
| assertStringIs(resources, "title_crossfade", "Simple Crossfade"); // No longer overridden in res2 |
| |
| // Finally ensure that we can switch roots repeatedly (had some earlier bugs related to root unregistration) |
| resources.updateRoots(Arrays.asList(res1, res3, res2)); |
| resources.updateRoots(Arrays.asList(res1)); |
| resources.updateRoots(Arrays.asList(res1, res3, res2)); |
| resources.updateRoots(Arrays.asList(res1)); |
| resources.updateRoots(Arrays.asList(res1, res3, res2)); |
| resources.updateRoots(Arrays.asList(res2)); |
| resources.updateRoots(Arrays.asList(res1)); |
| resources.updateRoots(Arrays.asList(res1, res2, res3)); |
| assertStringIs(resources, "title_layout_changes", "Layout Changes"); |
| |
| // Make sure I get all the resource ids (there can be multiple; these are not replaced via overlays) |
| List<ResourceItem> ids = resources.getResourceItem(ResourceType.ID, "my_id"); |
| assertNotNull(ids); |
| assertSize(2, ids); |
| Collections.sort(ids, new Comparator<ResourceItem>() { |
| @SuppressWarnings("ConstantConditions") |
| @Override |
| public int compare(ResourceItem item1, ResourceItem item2) { |
| return item1.getSource().getFile().getName().compareTo(item2.getSource().getFile().getName()); |
| } |
| }); |
| //noinspection ConstantConditions |
| assertEquals("layout_ids1.xml", ids.get(0).getSource().getFile().getName()); |
| //noinspection ConstantConditions |
| assertEquals("layout_ids2.xml", ids.get(1).getSource().getFile().getName()); |
| } |
| |
| public void testOverlayUpdates1() { |
| final VirtualFile layout = myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml"); |
| final VirtualFile layoutOverlay = myFixture.copyFileToProject(LAYOUT_OVERLAY, "res2/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"); |
| ModuleResourceRepository resources = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3)); |
| assertStringIs(resources, "title_layout_changes", "Layout Changes"); // sanity check |
| |
| // Layout resource check: |
| // Check that our @/layout/layout1 resource currently refers to res2 override, |
| // then rename it to @layout/layout2, and verify that we have both, and then |
| // rename base to @layout/layout2 and verify that we are back to overriding. |
| |
| assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout1")); |
| assertFalse(resources.hasResourceItem(ResourceType.LAYOUT, "layout2")); |
| PsiResourceItem layout1 = getSingleItem(resources, ResourceType.LAYOUT, "layout1"); |
| assertItemIsInDir(res2, layout1); |
| |
| long generation = resources.getModificationCount(); |
| WriteCommandAction.runWriteCommandAction(null, new Runnable() { |
| @Override |
| public void run() { |
| try { |
| layoutOverlay.rename(this, "layout2.xml"); |
| } |
| catch (IOException e) { |
| fail(e.toString()); |
| } |
| } |
| }); |
| assertTrue(resources.getModificationCount() > generation); |
| |
| assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout2")); |
| assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout1")); |
| |
| // Layout should now be coming through from res1 since res2 is no longer overriding it |
| layout1 = getSingleItem(resources, ResourceType.LAYOUT, "layout1"); |
| assertItemIsInDir(res1, layout1); |
| |
| PsiResourceItem layout2 = getSingleItem(resources, ResourceType.LAYOUT, "layout2"); |
| assertItemIsInDir(res2, layout2); |
| |
| // Now rename layout1 to layout2 to hide it again |
| generation = resources.getModificationCount(); |
| WriteCommandAction.runWriteCommandAction(null, new Runnable() { |
| @Override |
| public void run() { |
| try { |
| layout.rename(this, "layout2.xml"); |
| } |
| catch (IOException e) { |
| fail(e.toString()); |
| } |
| } |
| }); |
| assertTrue(resources.getModificationCount() > generation); |
| |
| assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout2")); |
| assertFalse(resources.hasResourceItem(ResourceType.LAYOUT, "layout1")); |
| |
| layout2 = getSingleItem(resources, ResourceType.LAYOUT, "layout2"); |
| assertItemIsInDir(res2, layout2); |
| } |
| |
| public void testOverlayUpdates2() { |
| // Like testOverlayUpdates1, but rather than testing changes to layout resources (file-based resource) |
| // perform document edits in value-documents |
| |
| myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml"); |
| myFixture.copyFileToProject(LAYOUT_OVERLAY, "res2/layout/layout1.xml"); |
| VirtualFile values1 = myFixture.copyFileToProject(VALUES, "res/values/values.xml"); |
| VirtualFile values2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "res2/values/values.xml"); |
| VirtualFile values3 = myFixture.copyFileToProject(VALUES_OVERLAY2, "res3/values/nameDoesNotMatter.xml"); |
| final VirtualFile values3No = myFixture.copyFileToProject(VALUES_OVERLAY2_NO, "res3/values-no/values.xml"); |
| VirtualFile res1 = values1.getParent().getParent(); |
| VirtualFile res2 = values2.getParent().getParent(); |
| VirtualFile res3 = values3.getParent().getParent(); |
| ModuleResourceRepository resources = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3)); |
| PsiFile psiValues1 = PsiManager.getInstance(getProject()).findFile(values1); |
| assertNotNull(psiValues1); |
| PsiFile psiValues2 = PsiManager.getInstance(getProject()).findFile(values2); |
| assertNotNull(psiValues2); |
| PsiFile psiValues3 = PsiManager.getInstance(getProject()).findFile(values3); |
| assertNotNull(psiValues3); |
| PsiFile psiValues3No = PsiManager.getInstance(getProject()).findFile(values3No); |
| assertNotNull(psiValues3No); |
| |
| // Initial state; sanity check from #testOverlays() |
| assertStringIs(resources, "title_layout_changes", "Layout Changes"); |
| assertStringIs(resources, "title_crossfade", "Complex Crossfade"); // Overridden in res2 |
| assertStringIs(resources, "title_zoom", "Zoom!"); // Overridden in res3 |
| assertStringIs(resources, "unique_string", "Unique"); // Overridden in res2 |
| assertStringIs(resources, "another_unique_string", "Another Unique", false); // Overridden in res3 |
| assertStringIs(resources, "app_name", "Very Different App Name", false); // res3 (not unique because we have a values-no item too) |
| |
| // Value resource check: |
| // Verify that an edit in a value file, both in a non-overridden and an overridden |
| // value, is observed; and that an override in an overridden value is not observed. |
| |
| assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name")); |
| assertTrue(resources.hasResourceItem(ResourceType.STRING, "title_layout_changes")); |
| PsiResourceItem appName = getFirstItem(resources, ResourceType.STRING, "app_name"); |
| assertItemIsInDir(res3, appName); |
| assertStringIs(resources, "app_name", "Very Different App Name", false); // res3 (not unique because we have a values-no item too) |
| |
| long generation = resources.getModificationCount(); |
| final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject()); |
| final Document document = documentManager.getDocument(psiValues3); |
| assertNotNull(document); |
| WriteCommandAction.runWriteCommandAction(null, new Runnable() { |
| @Override |
| public void run() { |
| int offset = document.getText().indexOf("Very Different App Name"); |
| document.insertString(offset, "Not "); |
| documentManager.commitDocument(document); |
| } |
| }); |
| assertTrue(resources.getModificationCount() > generation); |
| |
| // Should still be defined in res3 but have new value |
| appName = getFirstItem(resources, ResourceType.STRING, "app_name"); |
| assertItemIsInDir(res3, appName); |
| assertStringIs(resources, "app_name", "Not Very Different App Name", false); |
| |
| generation = resources.getModificationCount(); |
| WriteCommandAction.runWriteCommandAction(null, new Runnable() { |
| @Override |
| public void run() { |
| int offset = document.getText().indexOf("app_name"); |
| document.insertString(offset, "r"); |
| documentManager.commitDocument(document); |
| } |
| }); |
| assertTrue(resources.getModificationCount() > generation); |
| assertTrue(resources.hasResourceItem(ResourceType.STRING, "rapp_name")); |
| |
| appName = getFirstItem(resources, ResourceType.STRING, "app_name"); |
| // The item is still under res3, but now it's in the Norwegian translation |
| assertEquals("no", appName.getSource().getQualifiers()); |
| assertStringIs(resources, "app_name", "Forskjellig Navn", false); |
| |
| // Delete that file: |
| generation = resources.getModificationCount(); |
| WriteCommandAction.runWriteCommandAction(null, new Runnable() { |
| @Override |
| public void run() { |
| try { |
| values3No.delete(this); |
| } |
| catch (IOException e) { |
| fail(e.toString()); |
| } |
| } |
| }); |
| assertTrue(resources.getModificationCount() > generation); |
| |
| // Now the item is no longer available in res3; should fallback to res 2 |
| appName = getFirstItem(resources, ResourceType.STRING, "app_name"); |
| assertItemIsInDir(res2, appName); |
| assertStringIs(resources, "app_name", "Different App Name", false); |
| |
| // Check that editing an overridden attribute does not count as a change |
| final Document document2 = documentManager.getDocument(psiValues1); |
| assertNotNull(document2); |
| WriteCommandAction.runWriteCommandAction(null, new Runnable() { |
| @Override |
| public void run() { |
| int offset = document2.getText().indexOf("Animations Demo"); |
| document2.insertString(offset, "Cool "); |
| documentManager.commitDocument(document2); |
| } |
| }); |
| // Unaffected by above change |
| assertStringIs(resources, "app_name", "Different App Name", false); |
| |
| // Finally check that editing an non-overridden attribute also gets picked up as a change |
| generation = resources.getModificationCount(); |
| WriteCommandAction.runWriteCommandAction(null, new Runnable() { |
| @Override |
| public void run() { |
| int offset = document2.getText().indexOf("Layout Changes"); |
| document2.insertString(offset, "New "); |
| documentManager.commitDocument(document2); |
| } |
| }); |
| assertTrue(resources.getModificationCount() > generation); |
| assertStringIs(resources, "title_layout_changes", "New Layout Changes", false); |
| } |
| |
| // Unit test support methods |
| |
| static void assertItemIsInDir(VirtualFile dir, PsiResourceItem item) { |
| PsiFile psiFile = item.getPsiFile(); |
| assertNotNull(psiFile); |
| VirtualFile parent = psiFile.getVirtualFile(); |
| assertNotNull(parent); |
| assertEquals(dir, parent.getParent().getParent()); |
| } |
| |
| static void assertStringIs(LocalResourceRepository repository, String key, String expected) { |
| assertStringIs(repository, key, expected, true); |
| } |
| |
| @NotNull |
| private static PsiResourceItem getSingleItem(LocalResourceRepository repository, ResourceType type, String key) { |
| List<ResourceItem> list = repository.getResourceItem(type, key); |
| assertNotNull(list); |
| assertSize(1, list); |
| ResourceItem item = list.get(0); |
| assertNotNull(item); |
| assertTrue(item instanceof PsiResourceItem); |
| return (PsiResourceItem)item; |
| } |
| |
| @NotNull |
| static PsiResourceItem getFirstItem(LocalResourceRepository repository, ResourceType type, String key) { |
| List<ResourceItem> list = repository.getResourceItem(type, key); |
| assertNotNull(list); |
| ResourceItem item = list.get(0); |
| assertNotNull(item); |
| assertTrue(item instanceof PsiResourceItem); |
| return (PsiResourceItem)item; |
| } |
| |
| static void assertStringIs(LocalResourceRepository repository, String key, String expected, boolean mustBeUnique) { |
| assertTrue(repository.hasResourceItem(ResourceType.STRING, key)); |
| List<ResourceItem> list = repository.getResourceItem(ResourceType.STRING, key); |
| assertNotNull(list); |
| |
| // generally we expect just one item (e.g. overlays should not visible, which is why we assert a single item, but for items |
| // that for example have translations there could be multiple items, and we test this, so allow assertion to specify whether it's |
| // expected) |
| if (mustBeUnique) { |
| assertSize(1, list); |
| } |
| |
| ResourceItem item = list.get(0); |
| ResourceValue resourceValue = item.getResourceValue(false); |
| assertNotNull(resourceValue); |
| assertEquals(expected, resourceValue.getValue()); |
| } |
| |
| public void testAllowEmpty() { |
| assertTrue(LintUtils.assertionsEnabled()); // this test should be run with assertions enabled! |
| LocalResourceRepository repository = ModuleResourceRepository.createForTest(myFacet, Collections.<VirtualFile>emptyList()); |
| assertNotNull(repository); |
| repository.getModificationCount(); |
| assertEmpty(repository.getItemsOfType(ResourceType.ID)); |
| } |
| } |