1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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 
17 package com.android.tools.metalava.apilevels
18 
19 import kotlin.test.Test
20 import kotlin.test.assertEquals
21 import kotlin.test.assertFailsWith
22 import kotlin.test.assertTrue
23 import org.junit.Assert
24 
25 class ApiToExtensionsMapTest {
26     @Test
empty inputnull27     fun `empty input`() {
28         val xml =
29             """
30             <?xml version="1.0" encoding="utf-8"?>
31             <!-- No rules is a valid (albeit weird). -->
32             <sdk-extensions-info>
33                 <sdk shortname="R-ext" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
34                 <sdk shortname="S-ext" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
35                 <sdk shortname="T-ext" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
36             </sdk-extensions-info>
37         """
38                 .trimIndent()
39         val map = ApiToExtensionsMap.fromXml("no-module", xml)
40 
41         assertTrue(map.getExtensions("com.foo.Bar").isEmpty())
42     }
43 
44     @Test
wildcardnull45     fun wildcard() {
46         val xml =
47             """
48             <?xml version="1.0" encoding="utf-8"?>
49             <!-- All APIs will default to extension SDK A. -->
50             <sdk-extensions-info>
51                 <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
52                 <symbol jar="mod" pattern="*" sdks="A" />
53             </sdk-extensions-info>
54         """
55                 .trimIndent()
56         val map = ApiToExtensionsMap.fromXml("mod", xml)
57 
58         assertEquals(map.getExtensions("com.foo.Bar"), listOf("A"))
59         assertEquals(map.getExtensions("com.foo.SomeOtherBar"), listOf("A"))
60     }
61 
62     @Test
single classnull63     fun `single class`() {
64         val xml =
65             """
66             <?xml version="1.0" encoding="utf-8"?>
67             <!-- A single class. The class, any internal classes, and any methods are allowed;
68                  everything else is denied -->
69             <sdk-extensions-info>
70                 <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
71                 <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
72             </sdk-extensions-info>
73         """
74                 .trimIndent()
75         val map = ApiToExtensionsMap.fromXml("mod", xml)
76 
77         assertEquals(map.getExtensions("com.foo.Bar"), listOf("A"))
78         assertEquals(map.getExtensions("com.foo.Bar#FIELD"), listOf("A"))
79         assertEquals(map.getExtensions("com.foo.Bar#method"), listOf("A"))
80         assertEquals(map.getExtensions("com.foo.Bar\$Inner"), listOf("A"))
81         assertEquals(map.getExtensions("com.foo.Bar\$Inner\$InnerInner"), listOf("A"))
82 
83         val clazz = ApiClass("com/foo/Bar", 1, false)
84         val method = ApiElement("method(Ljava.lang.String;I)V", 2, false)
85         assertEquals(map.getExtensions(clazz), listOf("A"))
86         assertEquals(map.getExtensions(clazz, method), listOf("A"))
87 
88         assertTrue(map.getExtensions("com.foo.SomeOtherClass").isEmpty())
89     }
90 
91     @Test
multiple extensionsnull92     fun `multiple extensions`() {
93         val xml =
94             """
95             <?xml version="1.0" encoding="utf-8"?>
96             <!-- Any number of white space separated extension SDKs may be listed. -->
97             <sdk-extensions-info>
98                 <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
99                 <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
100                 <sdk shortname="FOO" name="FOO Extensions" id="10" reference="android/os/Build${'$'}VERSION_CODES${'$'}FOO" />
101                 <sdk shortname="BAR" name="BAR Extensions" id="11" reference="android/os/Build${'$'}VERSION_CODES${'$'}BAR" />
102                 <symbol jar="mod" pattern="*" sdks="A,B,FOO,BAR" />
103             </sdk-extensions-info>
104         """
105                 .trimIndent()
106         val map = ApiToExtensionsMap.fromXml("mod", xml)
107 
108         assertEquals(listOf("A", "B", "FOO", "BAR"), map.getExtensions("com.foo.Bar"))
109     }
110 
111     @Test
precedencenull112     fun precedence() {
113         val xml =
114             """
115             <?xml version="1.0" encoding="utf-8"?>
116             <!-- Multiple classes, and multiple rules with different precedence. -->
117             <sdk-extensions-info>
118                 <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
119                 <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
120                 <sdk shortname="C" name="C Extensions" id="3" reference="android/os/Build${'$'}VERSION_CODES${'$'}C" />
121                 <sdk shortname="D" name="D Extensions" id="4" reference="android/os/Build${'$'}VERSION_CODES${'$'}D" />
122                 <symbol jar="mod" pattern="*" sdks="A" />
123                 <symbol jar="mod" pattern="com.foo.Bar" sdks="B" />
124                 <symbol jar="mod" pattern="com.foo.Bar${'$'}Inner#method" sdks="C" />
125                 <symbol jar="mod" pattern="com.bar.Foo" sdks="D" />
126             </sdk-extensions-info>
127         """
128                 .trimIndent()
129         val map = ApiToExtensionsMap.fromXml("mod", xml)
130 
131         assertEquals(map.getExtensions("anything"), listOf("A"))
132 
133         assertEquals(map.getExtensions("com.foo.Bar"), listOf("B"))
134         assertEquals(map.getExtensions("com.foo.Bar#FIELD"), listOf("B"))
135         assertEquals(map.getExtensions("com.foo.Bar\$Inner"), listOf("B"))
136 
137         assertEquals(map.getExtensions("com.foo.Bar\$Inner#method"), listOf("C"))
138 
139         assertEquals(map.getExtensions("com.bar.Foo"), listOf("D"))
140         assertEquals(map.getExtensions("com.bar.Foo#FIELD"), listOf("D"))
141     }
142 
143     @Test
multiple mainline modulesnull144     fun `multiple mainline modules`() {
145         val xml =
146             """
147             <?xml version="1.0" encoding="utf-8"?>
148             <!-- The allow list will only consider patterns that are marked with the given mainline module -->
149             <sdk-extensions-info>
150                 <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
151                 <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
152                 <symbol jar="foo" pattern="*" sdks="A" />
153                 <symbol jar="bar" pattern="*" sdks="B" />
154             </sdk-extensions-info>
155         """
156                 .trimIndent()
157         val allowListA = ApiToExtensionsMap.fromXml("foo", xml)
158         val allowListB = ApiToExtensionsMap.fromXml("bar", xml)
159         val allowListC = ApiToExtensionsMap.fromXml("baz", xml)
160 
161         assertEquals(allowListA.getExtensions("anything"), listOf("A"))
162         assertEquals(allowListB.getExtensions("anything"), listOf("B"))
163         assertTrue(allowListC.getExtensions("anything").isEmpty())
164     }
165 
166     @Test
declarations and rules can be mixednull167     fun `declarations and rules can be mixed`() {
168         val xml =
169             """
170             <?xml version="1.0" encoding="utf-8"?>
171             <!-- SDK declarations and rule lines can be mixed in any order -->
172             <sdk-extensions-info>
173                 <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
174                 <symbol jar="foo" pattern="*" sdks="A,B" />
175                 <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
176             </sdk-extensions-info>
177         """
178                 .trimIndent()
179         val map = ApiToExtensionsMap.fromXml("foo", xml)
180 
181         assertEquals(map.getExtensions("com.foo.Bar"), listOf("A", "B"))
182     }
183 
184     @Test
bad inputnull185     fun `bad input`() {
186         assertFailsWith<IllegalArgumentException> {
187             ApiToExtensionsMap.fromXml(
188                 "mod",
189                 """
190                     <?xml version="1.0" encoding="utf-8"?>
191                     <!-- Missing root element -->
192                     <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
193                     <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
194                 """
195                     .trimIndent()
196             )
197         }
198 
199         assertFailsWith<IllegalArgumentException> {
200             ApiToExtensionsMap.fromXml(
201                 "mod",
202                 """
203                     <?xml version="1.0" encoding="utf-8"?>
204                     <!-- <sdk> tag at unexpected depth  -->
205                     <sdk-extensions-info version="2">
206                         <foo>
207                             <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" >
208                         </foo>
209                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
210                     </sdk-extensions-info>
211                 """
212                     .trimIndent()
213             )
214         }
215 
216         assertFailsWith<IllegalArgumentException> {
217             ApiToExtensionsMap.fromXml(
218                 "mod",
219                 """
220                     <?xml version="1.0" encoding="utf-8"?>
221                     <!-- using 0 (reserved for the Android platform SDK) as ID -->
222                     <sdk-extensions-info>
223                         <sdk shortname="A" name="A Extensions" id="0" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
224                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
225                     </sdk-extensions-info>
226                 """
227                     .trimIndent()
228             )
229         }
230 
231         assertFailsWith<IllegalArgumentException> {
232             ApiToExtensionsMap.fromXml(
233                 "mod",
234                 """
235                     <?xml version="1.0" encoding="utf-8"?>
236                     <!-- missing module attribute -->
237                     <sdk-extensions-info>
238                         <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
239                         <symbol pattern="com.foo.Bar" sdks="A" />
240                     </sdk-extensions-info>
241                 """
242                     .trimIndent()
243             )
244         }
245 
246         assertFailsWith<IllegalArgumentException> {
247             ApiToExtensionsMap.fromXml(
248                 "mod",
249                 """
250                     <?xml version="1.0" encoding="utf-8"?>
251                     <!-- duplicate module+pattern pairs -->
252                     <sdk-extensions-info>
253                         <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
254                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
255                         <symbol jar="mod" pattern="com.foo.Bar" sdks="B" />
256                     </sdk-extensions-info>
257                 """
258                     .trimIndent()
259             )
260         }
261 
262         assertFailsWith<IllegalArgumentException> {
263             ApiToExtensionsMap.fromXml(
264                 "mod",
265                 """
266                     <?xml version="1.0" encoding="utf-8"?>
267                     <!-- sdks attribute refer to non-declared SDK -->
268                     <sdk-extensions-info>
269                         <sdk shortname="B" name="A Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
270                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
271                     </sdk-extensions-info>
272                 """
273                     .trimIndent()
274             )
275         }
276 
277         assertFailsWith<IllegalArgumentException> {
278             ApiToExtensionsMap.fromXml(
279                 "mod",
280                 """
281                     <?xml version="1.0" encoding="utf-8"?>
282                     <!-- duplicate numerical ID -->
283                     <sdk-extensions-info>
284                         <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
285                         <sdk shortname="B" name="B Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
286                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
287                     </sdk-extensions-info>
288                 """
289                     .trimIndent()
290             )
291         }
292 
293         assertFailsWith<IllegalArgumentException> {
294             ApiToExtensionsMap.fromXml(
295                 "mod",
296                 """
297                     <?xml version="1.0" encoding="utf-8"?>
298                     <!-- duplicate short SDK name -->
299                     <sdk-extensions-info>
300                         <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
301                         <sdk shortname="A" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
302                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
303                     </sdk-extensions-info>
304                 """
305                     .trimIndent()
306             )
307         }
308 
309         assertFailsWith<IllegalArgumentException> {
310             ApiToExtensionsMap.fromXml(
311                 "mod",
312                 """
313                     <?xml version="1.0" encoding="utf-8"?>
314                     <!-- duplicate long SDK name -->
315                     <sdk-extensions-info>
316                         <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
317                         <sdk shortname="B" name="A Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
318                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
319                     </sdk-extensions-info>
320                 """
321                     .trimIndent()
322             )
323         }
324 
325         assertFailsWith<IllegalArgumentException> {
326             ApiToExtensionsMap.fromXml(
327                 "mod",
328                 """
329                     <?xml version="1.0" encoding="utf-8"?>
330                     <!-- duplicate SDK reference -->
331                     <sdk-extensions-info version="1">
332                         <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
333                         <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
334                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
335                     </sdk-extensions-info>
336                 """
337                     .trimIndent()
338             )
339         }
340 
341         assertFailsWith<IllegalArgumentException> {
342             ApiToExtensionsMap.fromXml(
343                 "mod",
344                 """
345                     <?xml version="1.0" encoding="utf-8"?>
346                     <!-- duplicate SDK for same symbol -->
347                     <sdk-extensions-info>
348                         <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
349                         <sdk shortname="B" name="B Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
350                         <symbol jar="mod" pattern="com.foo.Bar" sdks="A,B,A" />
351                     </sdk-extensions-info>
352                 """
353                     .trimIndent()
354             )
355         }
356     }
357 
358     @Test
calculate sdks xml attributenull359     fun `calculate sdks xml attribute`() {
360         val xml =
361             """
362             <?xml version="1.0" encoding="utf-8"?>
363             <!-- Verify the calculateSdksAttr method -->
364             <sdk-extensions-info>
365                 <sdk shortname="R" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
366                 <sdk shortname="S" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
367                 <sdk shortname="T" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
368                 <sdk shortname="FOO" name="FOO Extensions" id="1000000" reference="android/os/Build${'$'}VERSION_CODES${'$'}FOO" />
369                 <sdk shortname="BAR" name="BAR Extensions" id="1000001" reference="android/os/Build${'$'}VERSION_CODES${'$'}BAR" />
370             </sdk-extensions-info>
371         """
372                 .trimIndent()
373         val filter = ApiToExtensionsMap.fromXml("mod", xml)
374 
375         Assert.assertEquals("0:34", filter.calculateSdksAttr(34, 34, listOf(), ApiElement.NEVER))
376 
377         Assert.assertEquals("30:4", filter.calculateSdksAttr(34, 34, listOf("R"), 4))
378 
379         Assert.assertEquals("30:4,31:4", filter.calculateSdksAttr(34, 34, listOf("R", "S"), 4))
380 
381         Assert.assertEquals("30:4,31:4,0:33", filter.calculateSdksAttr(33, 34, listOf("R", "S"), 4))
382 
383         Assert.assertEquals(
384             "30:4,31:4,1000000:4,0:33",
385             filter.calculateSdksAttr(33, 34, listOf("R", "S", "FOO"), 4)
386         )
387 
388         Assert.assertEquals(
389             "30:4,31:4,1000000:4,1000001:4,0:33",
390             filter.calculateSdksAttr(33, 34, listOf("R", "S", "FOO", "BAR"), 4)
391         )
392 
393         // Make sure that if it was released in dessert released R (30) that it is reported as being
394         // in both the extension SDK included in R (30:4) and in R itself (0:30) but not in S or T.
395         Assert.assertEquals("30:4,0:30", filter.calculateSdksAttr(30, 34, listOf("R", "S"), 4))
396 
397         // Make sure that if it was released in dessert released S (31) that it is reported as being
398         // in both the extension SDK included in R (30:4), S (31:4) and in S itself (0:30) but not
399         // in T.
400         Assert.assertEquals(
401             "30:4,31:4,0:31",
402             filter.calculateSdksAttr(31, 34, listOf("R", "S", "T"), 4)
403         )
404 
405         // Make sure that if it was released in dessert released S+ (32) that it is reported as
406         // being in both the extension SDK included in R (30:4), S (31:4) and in S itself (0:30) but
407         // not in T.
408         Assert.assertEquals(
409             "30:4,31:4,0:32",
410             filter.calculateSdksAttr(32, 34, listOf("R", "S", "T"), 4)
411         )
412 
413         // Make sure that if it was released in dessert released T (33) that it is reported as being
414         // in both the extension SDK included in R (30:4), S (31:4), T (33:4) and T itself.
415         Assert.assertEquals(
416             "30:4,31:4,33:4,0:33",
417             filter.calculateSdksAttr(33, 34, listOf("R", "S", "T"), 4)
418         )
419 
420         // Make sure that if it was released in dessert release before R (21) that it is not
421         // reported as being in any sdks; it will just have `since="21"`.
422         Assert.assertEquals("", filter.calculateSdksAttr(21, 34, listOf("R", "S"), 4))
423     }
424 }
425