1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.common.resources.platform;
17 
18 import static com.android.SdkConstants.ANDROID_URI;
19 import static com.android.SdkConstants.DOT_XML;
20 
21 import com.android.SdkConstants;
22 import com.android.annotations.NonNull;
23 import com.android.ide.common.api.IAttributeInfo.Format;
24 import com.android.ide.common.resources.ResourceItem;
25 import com.android.ide.common.resources.ResourceRepository;
26 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
27 import com.android.ide.eclipse.mock.Mocks;
28 import com.android.io.IAbstractFolder;
29 import com.android.io.IAbstractResource;
30 import com.android.resources.ResourceType;
31 import com.android.utils.StdLogger;
32 import com.google.common.base.Charsets;
33 import com.google.common.collect.ArrayListMultimap;
34 import com.google.common.collect.Multimap;
35 import com.google.common.collect.Sets;
36 import com.google.common.io.Files;
37 
38 import junit.framework.TestCase;
39 
40 import org.w3c.dom.Attr;
41 import org.w3c.dom.Document;
42 import org.w3c.dom.Element;
43 import org.w3c.dom.NamedNodeMap;
44 
45 import java.io.File;
46 import java.io.IOException;
47 import java.util.Collection;
48 import java.util.EnumSet;
49 import java.util.Map;
50 import java.util.Set;
51 
52 @SuppressWarnings("javadoc")
53 public class AttributeInfoTest extends TestCase {
testSimple()54     public void testSimple() throws Exception {
55         AttributeInfo info = new AttributeInfo("test", EnumSet.noneOf(Format.class));
56         assertTrue(info.isValid("", null, null));
57         assertTrue(info.isValid("a b c", null, null));
58         assertTrue(info.isValid("@android foo bar", null, null));
59     }
60 
testIsValidString()61     public void testIsValidString() throws Exception {
62         AttributeInfo info = new AttributeInfo("test", Format.STRING_SET);
63         assertTrue(info.isValid("", null, null));
64         assertTrue(info.isValid("a b c", null, null));
65         assertTrue(info.isValid("@android foo bar", null, null));
66     }
67 
testIsValidBoolean()68     public void testIsValidBoolean() throws Exception {
69         AttributeInfo info = new AttributeInfo("test", Format.BOOLEAN_SET);
70         assertTrue(info.isValid("true", null, null));
71         assertTrue(info.isValid("false", null, null));
72         assertFalse(info.isValid("", null, null));
73         assertTrue(info.isValid("TRUE", null, null));
74         assertTrue(info.isValid("True", null, null));
75         assertTrue(info.isValid("FALSE", null, null));
76         assertTrue(info.isValid("False", null, null));
77     }
78 
testIsValidInteger()79     public void testIsValidInteger() throws Exception {
80         AttributeInfo info = new AttributeInfo("test", Format.INTEGER_SET);
81         assertTrue(info.isValid("0", null, null));
82         assertTrue(info.isValid("1", null, null));
83         assertTrue(info.isValid("10", null, null));
84         assertTrue(info.isValid("-10", null, null));
85         assertTrue(info.isValid(Integer.toString(Integer.MAX_VALUE), null, null));
86 
87         assertFalse(info.isValid("", null, null));
88         assertFalse(info.isValid("a", null, null));
89         assertFalse(info.isValid("a1", null, null));
90         assertFalse(info.isValid("1a", null, null));
91         assertFalse(info.isValid("1.0", null, null));
92     }
93 
testIsValidFloat()94     public void testIsValidFloat() throws Exception {
95         AttributeInfo info = new AttributeInfo("test", Format.FLOAT_SET);
96         assertTrue(info.isValid("0", null, null));
97         assertTrue(info.isValid("1", null, null));
98         assertTrue(info.isValid("10", null, null));
99         assertTrue(info.isValid("-10", null, null));
100         assertTrue(info.isValid("-10.1234", null, null));
101         assertTrue(info.isValid(".1", null, null));
102         assertTrue(info.isValid("-.1", null, null));
103         assertTrue(info.isValid("1.5e22", null, null));
104         assertTrue(info.isValid(Integer.toString(Integer.MAX_VALUE), null, null));
105 
106         assertFalse(info.isValid("", null, null));
107         assertFalse(info.isValid(".", null, null));
108         assertFalse(info.isValid("-.", null, null));
109         assertFalse(info.isValid("a", null, null));
110         assertFalse(info.isValid("a1", null, null));
111         assertFalse(info.isValid("1a", null, null));
112     }
113 
testIsValidDimension()114     public void testIsValidDimension() throws Exception {
115         AttributeInfo info = new AttributeInfo("test", Format.DIMENSION_SET);
116         assertTrue(info.isValid("0dp", null, null));
117         assertTrue(info.isValid("1dp", null, null));
118         assertTrue(info.isValid("10dip", null, null));
119         assertTrue(info.isValid("-10px", null, null));
120         assertTrue(info.isValid("-10.1234mm", null, null));
121         assertTrue(info.isValid("14sp", null, null));
122         assertTrue(info.isValid("72pt", null, null));
123 
124         assertFalse(info.isValid("", null, null));
125         assertFalse(info.isValid("5", null, null));
126         assertFalse(info.isValid("50ps", null, null));
127         // Since we allow resources even when not specified in format, don't assert
128         // this:
129         //assertFalse(info.isValid("@dimen/foo"));
130     }
131 
testIsValidColor()132     public void testIsValidColor() throws Exception {
133         AttributeInfo info = new AttributeInfo("test", Format.COLOR_SET);
134         assertTrue(info.isValid("#fff", null, null));
135         assertTrue(info.isValid("#ffffff", null, null));
136         assertTrue(info.isValid("#12345678", null, null));
137         assertTrue(info.isValid("#abcdef00", null, null));
138 
139         assertFalse(info.isValid("", null, null));
140         assertFalse(info.isValid("#fffffffff", null, null));
141         assertFalse(info.isValid("red", null, null));
142         assertFalse(info.isValid("rgb(1,2,3)", null, null));
143     }
144 
testIsValidFraction()145     public void testIsValidFraction() throws Exception {
146         AttributeInfo info = new AttributeInfo("test", EnumSet.<Format>of(Format.FRACTION));
147         assertTrue(info.isValid("5%", null, null));
148         assertTrue(info.isValid("25%p", null, null));
149 
150         // We don't validate fractions accurately yet
151         //assertFalse(info.isValid(""));
152         //assertFalse(info.isValid("50%%"));
153         //assertFalse(info.isValid("50"));
154         //assertFalse(info.isValid("-2%"));
155     }
156 
testIsValidReference()157     public void testIsValidReference() throws Exception {
158         AttributeInfo info = new AttributeInfo("test", Format.REFERENCE_SET);
159         assertTrue(info.isValid("@android:string/foo", null, null));
160         assertTrue(info.isValid("@string/foo", null, null));
161         assertTrue(info.isValid("@dimen/foo", null, null));
162         assertTrue(info.isValid("@color/foo", null, null));
163         assertTrue(info.isValid("@animator/foo", null, null));
164         assertTrue(info.isValid("@anim/foo", null, null));
165         assertTrue(info.isValid("?android:attr/textAppearanceMedium", null, null));
166         assertTrue(info.isValid("?textAppearanceMedium", null, null));
167 
168         assertFalse(info.isValid("", null, null));
169         assertFalse(info.isValid("foo", null, null));
170         assertFalse(info.isValid("3.4", null, null));
171     }
172 
testIsValidEnum()173     public void testIsValidEnum() throws Exception {
174         AttributeInfo info = new AttributeInfo("test", Format.ENUM_SET);
175         info.setEnumValues(new String[] { "wrap_content", "match_parent" });
176         assertTrue(info.isValid("wrap_content", null, null));
177         assertTrue(info.isValid("match_parent", null, null));
178         assertFalse(info.isValid("", null, null));
179         assertFalse(info.isValid("other", null, null));
180         assertFalse(info.isValid("50", null, null));
181     }
182 
testIsValidFlag()183     public void testIsValidFlag() throws Exception {
184         AttributeInfo info = new AttributeInfo("test", Format.FLAG_SET);
185         info.setFlagValues(new String[] { "left", "top", "right", "bottom" });
186         assertTrue(info.isValid("left", null, null));
187         assertTrue(info.isValid("top", null, null));
188         assertTrue(info.isValid("left|top", null, null));
189         assertTrue(info.isValid("", null, null));
190 
191         assertFalse(info.isValid("other", null, null));
192         assertFalse(info.isValid("50", null, null));
193     }
194 
testCombined1()195     public void testCombined1() throws Exception {
196         AttributeInfo info = new AttributeInfo("test", EnumSet.<Format>of(Format.INTEGER,
197                 Format.REFERENCE));
198         assertTrue(info.isValid("1", null, null));
199         assertTrue(info.isValid("@dimen/foo", null, null));
200         assertFalse(info.isValid("foo", null, null));
201     }
202 
testCombined2()203     public void testCombined2() throws Exception {
204         AttributeInfo info = new AttributeInfo("test", EnumSet.<Format>of(Format.COLOR,
205                 Format.REFERENCE));
206         assertTrue(info.isValid("#ff00ff00", null, null));
207         assertTrue(info.isValid("@color/foo", null, null));
208         assertFalse(info.isValid("foo", null, null));
209     }
210 
testCombined3()211     public void testCombined3() throws Exception {
212         AttributeInfo info = new AttributeInfo("test", EnumSet.<Format>of(Format.STRING,
213                 Format.REFERENCE));
214         assertTrue(info.isValid("test", null, null));
215         assertTrue(info.isValid("@color/foo", null, null));
216     }
217 
testCombined4()218     public void testCombined4() throws Exception {
219         AttributeInfo info = new AttributeInfo("test", EnumSet.<Format>of(Format.ENUM,
220                 Format.DIMENSION));
221         info.setEnumValues(new String[] { "wrap_content", "match_parent" });
222         assertTrue(info.isValid("wrap_content", null, null));
223         assertTrue(info.isValid("match_parent", null, null));
224         assertTrue(info.isValid("50dp", null, null));
225         assertFalse(info.isValid("50", null, null));
226         assertFalse(info.isValid("test", null, null));
227     }
228 
testResourcesExist()229     public void testResourcesExist() throws Exception {
230         IAbstractFolder folder = Mocks.createAbstractFolder(
231                 SdkConstants.FD_RESOURCES, new IAbstractResource[0]);
232 
233         AttributeInfo info = new AttributeInfo("test", Format.REFERENCE_SET);
234         TestResourceRepository projectResources = new TestResourceRepository(folder,false);
235         projectResources.addResource(ResourceType.STRING, "mystring");
236         projectResources.addResource(ResourceType.DIMEN, "mydimen");
237         TestResourceRepository frameworkResources = new TestResourceRepository(folder, true);
238         frameworkResources.addResource(ResourceType.LAYOUT, "mylayout");
239 
240         assertTrue(info.isValid("@string/mystring", null, null));
241         assertTrue(info.isValid("@dimen/mydimen", null, null));
242         assertTrue(info.isValid("@android:layout/mylayout", null, null));
243         assertTrue(info.isValid("?android:attr/listPreferredItemHeigh", null, null));
244 
245         assertTrue(info.isValid("@string/mystring", projectResources, frameworkResources));
246         assertTrue(info.isValid("@dimen/mydimen", projectResources, frameworkResources));
247         assertTrue(info.isValid("@android:layout/mylayout", projectResources, frameworkResources));
248 
249         assertFalse(info.isValid("@android:string/mystring", projectResources,
250                 frameworkResources));
251         assertFalse(info.isValid("@android:dimen/mydimen", projectResources, frameworkResources));
252         assertFalse(info.isValid("@layout/mylayout", projectResources, frameworkResources));
253         assertFalse(info.isValid("@layout/foo", projectResources, frameworkResources));
254         assertFalse(info.isValid("@anim/foo", projectResources, frameworkResources));
255         assertFalse(info.isValid("@android:anim/foo", projectResources, frameworkResources));
256     }
257 
258     private class TestResourceRepository extends ResourceRepository {
259         private Multimap<ResourceType, String> mResources = ArrayListMultimap.create();
260 
TestResourceRepository(IAbstractFolder resFolder, boolean isFrameworkRepository)261         protected TestResourceRepository(IAbstractFolder resFolder, boolean isFrameworkRepository) {
262             super(resFolder, isFrameworkRepository);
263         }
264 
addResource(ResourceType type, String name)265         void addResource(ResourceType type, String name) {
266             mResources.put(type, name);
267         }
268 
269         @Override
270         @NonNull
createResourceItem(@onNull String name)271         protected ResourceItem createResourceItem(@NonNull String name) {
272             fail("Not used in test");
273             return null;
274         }
275 
276         @Override
hasResourceItem(@onNull ResourceType type, @NonNull String name)277         public boolean hasResourceItem(@NonNull ResourceType type, @NonNull String name) {
278             Collection<String> names = mResources.get(type);
279             if (names != null) {
280                 return names.contains(name);
281             }
282 
283             return false;
284         }
285     };
286 
287 
testIsValid()288     public void testIsValid() throws Exception {
289         // This test loads the full attrs.xml file and then processes a bunch of platform
290         // resource file and makes sure that they're all considered valid. This helps
291         // make sure that isValid() closely matches what aapt accepts.
292         String sdkPath = System.getenv("ADT_SDK_SOURCE_PATH");
293         assertNotNull("This test requires ADT_SDK_SOURCE_PATH to be set to point to the" +
294                 "SDK git repository", sdkPath);
295         File sdk = new File(sdkPath);
296         assertNotNull("$ADT_SDK_SOURCE_PATH (" + sdk.getPath() + ") is not a directory",
297                 sdk.isDirectory());
298         File git = sdk.getParentFile();
299         File attrsPath = new File(git, "frameworks" + File.separator + "base"
300                 + File.separator + "core" + File.separator + "res" + File.separator + "res"
301                 + File.separator + "values" + File.separator + "attrs.xml");
302         assertTrue(attrsPath.getPath(), attrsPath.exists());
303         AttrsXmlParser parser = new AttrsXmlParser(attrsPath.getPath(),
304                 new StdLogger(StdLogger.Level.VERBOSE), 1100);
305         parser.preload();
306         Map<String, AttributeInfo> attributeMap = parser.getAttributeMap();
307         assertNotNull(attributeMap);
308         assertNotNull(attributeMap.get("layout_width"));
309         Set<String> seen = Sets.newHashSet();
310 
311         checkDir(new File(git, "packages" + File.separator + "apps"), false, attributeMap, seen);
312     }
313 
checkDir(File dir, boolean isResourceDir, Map<String, AttributeInfo> map, Set<String> seen)314     private void checkDir(File dir, boolean isResourceDir,
315             Map<String, AttributeInfo> map, Set<String> seen) throws IOException {
316         assertTrue(dir.isDirectory());
317         File[] list = dir.listFiles();
318         if (list != null) {
319             for (File file : list) {
320                 if (isResourceDir && file.isFile() && file.getPath().endsWith(DOT_XML)) {
321                     checkXmlFile(file, map, seen);
322                 } else if (file.isDirectory()) {
323                     checkDir(file, isResourceDir || file.getName().equals("res"), map, seen);
324                 }
325             }
326         }
327     }
328 
checkXmlFile(File file, Map<String, AttributeInfo> map, Set<String> seen)329     private void checkXmlFile(File file, Map<String, AttributeInfo> map,
330             Set<String> seen) throws IOException {
331         String xml = Files.toString(file, Charsets.UTF_8);
332         if (xml != null) {
333             //Document doc = DomUtilities.parseStructuredDocument(xml);
334             Document doc = DomUtilities.parseDocument(xml, false);
335             if (doc != null && doc.getDocumentElement() != null) {
336                 checkElement(file, doc.getDocumentElement(), map, seen);
337             }
338         }
339     }
340 
checkElement(File file, Element element, Map<String, AttributeInfo> map, Set<String> seen)341     private void checkElement(File file, Element element, Map<String, AttributeInfo> map,
342             Set<String> seen) {
343         NamedNodeMap attributes = element.getAttributes();
344         for (int i = 0, n = attributes.getLength(); i < n; i++) {
345             Attr attribute = (Attr) attributes.item(i);
346 
347             String uri = attribute.getNamespaceURI();
348             String name = attribute.getLocalName();
349             String value = attribute.getValue();
350             if (ANDROID_URI.equals(uri)) {
351                 AttributeInfo info = map.get(name);
352                 if (info == null) {
353                     System.out.println("Warning: Unknown attribute '" + name + "' in " + file);
354                             return;
355                 }
356                 if (!info.isValid(value, null, null)) {
357                     if (name.equals("duration") || name.equals("exitFadeDuration")) {
358                         // Already known
359                         return;
360                     }
361                     String message = "In file " +
362                             file.getPath() + ":\nCould not validate value \"" + value
363                             + "\" for attribute '"
364                             + name + "' where the attribute info has formats " + info.getFormats()
365                             + "\n";
366                     System.out.println("\n" + message);
367                     fail(message);
368                 }
369                 if ((value.startsWith("@") || value.startsWith("?")) &&
370                         !info.getFormats().contains(Format.REFERENCE)) {
371                     // Print out errors in attrs.xml
372 
373                     if (!seen.contains(name)) {
374                         seen.add(name);
375                         System.out.println("\"" + name + "\" with formats " + info.getFormats()
376                                 + " was passed a reference (" + value + ") in file " + file);
377                     }
378                 }
379             }
380         }
381     }
382 }
383