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