1 /*
2  * Copyright (C) 2024 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.cli.compatibility
18 
19 import com.android.tools.metalava.cli.common.BaseOptionGroupTest
20 import com.android.tools.metalava.cli.common.SignatureBasedApi
21 import com.android.tools.metalava.model.api.surface.ApiVariantType
22 import com.android.tools.metalava.model.visitors.ApiType
23 import com.android.tools.metalava.testing.signature
24 import com.android.tools.metalava.testing.source
25 import com.google.common.truth.Truth.assertThat
26 import org.junit.Assert.assertThrows
27 import org.junit.Test
28 
29 val COMPATIBILITY_CHECK_OPTIONS_HELP =
30     """
31 Compatibility Checks:
32 
33   Options controlling which, if any, compatibility checks are performed against a previously released API.
34 
35   --check-compatibility:api:released <file>  Check compatibility of the previously released API.
36 
37                                              When multiple files are provided any files that are a delta on another file
38                                              must come after the other file, e.g. if `system` is a delta on `public`
39                                              then `public` must come first, then `system`. Or, in other words, they must
40                                              be provided in order from the narrowest API to the widest API.
41   --check-compatibility:removed:released <file>
42                                              Check compatibility of the previously released but since removed APIs.
43 
44                                              When multiple files are provided any files that are a delta on another file
45                                              must come after the other file, e.g. if `system` is a delta on `public`
46                                              then `public` must come first, then `system`. Or, in other words, they must
47                                              be provided in order from the narrowest API to the widest API.
48   --api-compat-annotation <annotation>       Specify an annotation important for API compatibility.
49 
50                                              Adding/removing this annotation will be considered an incompatible change.
51                                              The fully qualified name of the annotation should be passed.
52   --error-message:compatibility:released <message>
53                                              If set, this is output when errors are detected in
54                                              --check-compatibility:api:released or
55                                              --check-compatibility:removed:released.
56   --baseline:compatibility:released <file>   An optional baseline file that contains a list of known compatibility
57                                              issues which should be ignored. If this does not exist and
58                                              --update-baseline:compatibility:released is not specified then it will be
59                                              created and populated with all the known compatibility issues.
60   --update-baseline:compatibility:released <file>
61                                              An optional file into which a list of the latest compatibility issues found
62                                              will be written. If --baseline:compatibility:released is specified then any
63                                              issues listed in there will be copied into this file; that minimizes the
64                                              amount of churn in the baseline file when updating by not removing legacy
65                                              issues that have been fixed. If --delete-empty-baselines is specified and
66                                              this baseline is empty then the file will be deleted.
67     """
68         .trimIndent()
69 
70 class CompatibilityCheckOptionsTest :
71     BaseOptionGroupTest<CompatibilityCheckOptions>(
72         COMPATIBILITY_CHECK_OPTIONS_HELP,
73     ) {
74 
createOptionsnull75     override fun createOptions(): CompatibilityCheckOptions = CompatibilityCheckOptions()
76 
77     @Test
78     fun `check compatibility api released`() {
79         val file =
80             signature("released.txt", "// Signature format: 2.0\n").createFile(temporaryFolder.root)
81         runTest(ARG_CHECK_COMPATIBILITY_API_RELEASED, file.path) {
82             assertThat(options.compatibilityChecks)
83                 .isEqualTo(
84                     listOf(
85                         CompatibilityCheckOptions.CheckRequest(
86                             previouslyReleasedApi = SignatureBasedApi.fromFiles(listOf(file)),
87                             apiType = ApiType.PUBLIC_API,
88                         ),
89                     )
90                 )
91         }
92     }
93 
94     @Test
check compatibility api released multiple filesnull95     fun `check compatibility api released multiple files`() {
96         val file1 =
97             signature("released1.txt", "// Signature format: 2.0\n")
98                 .createFile(temporaryFolder.root)
99         val file2 =
100             signature("released2.txt", "// Signature format: 2.0\n")
101                 .createFile(temporaryFolder.root)
102         runTest(
103             ARG_CHECK_COMPATIBILITY_API_RELEASED,
104             file1.path,
105             ARG_CHECK_COMPATIBILITY_API_RELEASED,
106             file2.path,
107         ) {
108             assertThat(options.compatibilityChecks)
109                 .isEqualTo(
110                     listOf(
111                         CompatibilityCheckOptions.CheckRequest(
112                             previouslyReleasedApi =
113                                 SignatureBasedApi.fromFiles(listOf(file1, file2)),
114                             apiType = ApiType.PUBLIC_API,
115                         ),
116                     )
117                 )
118         }
119     }
120 
121     @Test
check compatibility removed api releasednull122     fun `check compatibility removed api released`() {
123         val file =
124             signature("removed.txt", "// Signature format: 2.0\n").createFile(temporaryFolder.root)
125         runTest(ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED, file.path) {
126             assertThat(options.compatibilityChecks)
127                 .isEqualTo(
128                     listOf(
129                         CompatibilityCheckOptions.CheckRequest(
130                             previouslyReleasedApi =
131                                 SignatureBasedApi.fromFiles(
132                                     listOf(file),
133                                     apiVariantType = ApiVariantType.REMOVED,
134                                 ),
135                             apiType = ApiType.REMOVED,
136                         ),
137                     )
138                 )
139         }
140     }
141 
142     /**
143      * Create a fake jar file. It is ok that it is not actually a jar file as its contents are not
144      * read.
145      */
fakeJarnull146     private fun fakeJar() = source("some.jar", "PK...").createFile(temporaryFolder.root)
147 
148     @Test
149     fun `check compatibility api released from jar`() {
150         val jarFile = fakeJar()
151         val exception =
152             assertThrows(IllegalStateException::class.java) {
153                 runTest(ARG_CHECK_COMPATIBILITY_API_RELEASED, jarFile.path) {
154                     options.compatibilityChecks
155                 }
156             }
157 
158         assertThat(exception.message)
159             .isEqualTo(
160                 "$ARG_CHECK_COMPATIBILITY_API_RELEASED: Can no longer check compatibility against jar files like $jarFile please use equivalent signature files"
161             )
162     }
163 
164     @Test
check compatibility api released mixture of signature and jarnull165     fun `check compatibility api released mixture of signature and jar`() {
166         val jarFile = fakeJar()
167         val signatureFile =
168             signature("removed.txt", "// Signature format: 2.0\n").createFile(temporaryFolder.root)
169 
170         val exception =
171             assertThrows(IllegalStateException::class.java) {
172                 runTest(
173                     ARG_CHECK_COMPATIBILITY_API_RELEASED,
174                     jarFile.path,
175                     ARG_CHECK_COMPATIBILITY_API_RELEASED,
176                     signatureFile.path,
177                 ) {
178                     options.compatibilityChecks
179                 }
180             }
181 
182         assertThat(exception.message)
183             .isEqualTo(
184                 "$ARG_CHECK_COMPATIBILITY_API_RELEASED: Can no longer check compatibility against jar files like $jarFile please use equivalent signature files"
185             )
186     }
187 
188     @Test
check compatibility api removed does not support jar filenull189     fun `check compatibility api removed does not support jar file`() {
190         val jarFile = fakeJar()
191 
192         val exception =
193             assertThrows(IllegalStateException::class.java) {
194                 runTest(ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED, jarFile.path) {
195                     options.compatibilityChecks
196                 }
197             }
198         assertThat(exception.message)
199             .isEqualTo(
200                 "$ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED: Can no longer check compatibility against jar files like $jarFile please use equivalent signature files"
201             )
202     }
203 
204     @Test
api compat annotations multiple valuesnull205     fun `api compat annotations multiple values`() {
206         runTest(
207             ARG_API_COMPAT_ANNOTATION,
208             "com.example.MyAnnotation",
209             ARG_API_COMPAT_ANNOTATION,
210             "com.example.MyOtherAnnotation",
211         ) {
212             assertThat(options.apiCompatAnnotations)
213                 .containsExactly("com.example.MyAnnotation", "com.example.MyOtherAnnotation")
214         }
215     }
216 }
217