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 android.platform.coverage 18 19 import com.google.common.truth.extensions.proto.ProtoTruth.assertThat 20 import com.google.protobuf.TextFormat 21 import java.nio.file.Files 22 import java.nio.file.Path 23 import java.nio.file.StandardOpenOption 24 import org.junit.After 25 import org.junit.Before 26 import org.junit.Test 27 import org.junit.runner.RunWith 28 import org.junit.runners.JUnit4 29 30 @RunWith(JUnit4::class) 31 class ExtractFlaggedApisTest { 32 33 companion object { 34 const val COMMAND = "java -jar extract-flagged-apis.jar %s %s" 35 } 36 37 private var apiTextFile: Path = Files.createTempFile("current", ".txt") 38 private var flagToApiMap: Path = Files.createTempFile("flag_api_map", ".textproto") 39 40 @Before setupnull41 fun setup() { 42 apiTextFile = Files.createTempFile("current", ".txt") 43 flagToApiMap = Files.createTempFile("flag_api_map", ".textproto") 44 } 45 46 @After cleanupnull47 fun cleanup() { 48 Files.deleteIfExists(apiTextFile) 49 Files.deleteIfExists(flagToApiMap) 50 } 51 52 @Test extractFlaggedApis_onlyMethodFlag_useMethodFlagnull53 fun extractFlaggedApis_onlyMethodFlag_useMethodFlag() { 54 val apiText = 55 """ 56 // Signature format: 2.0 57 package android.net.ipsec.ike { 58 public final class IkeSession implements java.lang.AutoCloseable { 59 method @FlaggedApi("com.android.ipsec.flags.dumpsys_api") public void dump(@NonNull java.io.PrintWriter); 60 } 61 } 62 """ 63 .trimIndent() 64 Files.write(apiTextFile, apiText.toByteArray(Charsets.UTF_8), StandardOpenOption.APPEND) 65 66 val process = Runtime.getRuntime().exec(createCommand()) 67 process.waitFor() 68 69 val content = Files.readAllBytes(flagToApiMap).toString(Charsets.UTF_8) 70 val result = TextFormat.parse(content, FlagApiMap::class.java) 71 72 val expected = FlagApiMap.newBuilder() 73 val api = 74 JavaMethod.newBuilder() 75 .setPackageName("android.net.ipsec.ike") 76 .setClassName("IkeSession") 77 .setMethodName("dump") 78 api.addParameters("java.io.PrintWriter") 79 addFlaggedApi(expected, api, "com.android.ipsec.flags.dumpsys_api") 80 assertThat(result).isEqualTo(expected.build()) 81 } 82 83 @Test extractFlaggedApis_onlyClassFlag_useClassFlagnull84 fun extractFlaggedApis_onlyClassFlag_useClassFlag() { 85 val apiText = 86 """ 87 // Signature format: 2.0 88 package android.net.ipsec.ike { 89 @FlaggedApi("com.android.ipsec.flags.dumpsys_api") public final class IkeSession implements java.lang.AutoCloseable { 90 method public void dump(@NonNull java.io.PrintWriter); 91 } 92 } 93 """ 94 .trimIndent() 95 Files.write(apiTextFile, apiText.toByteArray(Charsets.UTF_8), StandardOpenOption.APPEND) 96 97 val process = Runtime.getRuntime().exec(createCommand()) 98 process.waitFor() 99 100 val content = Files.readAllBytes(flagToApiMap).toString(Charsets.UTF_8) 101 val result = TextFormat.parse(content, FlagApiMap::class.java) 102 103 val expected = FlagApiMap.newBuilder() 104 val api = 105 JavaMethod.newBuilder() 106 .setPackageName("android.net.ipsec.ike") 107 .setClassName("IkeSession") 108 .setMethodName("dump") 109 api.addParameters("java.io.PrintWriter") 110 addFlaggedApi(expected, api, "com.android.ipsec.flags.dumpsys_api") 111 assertThat(result).isEqualTo(expected.build()) 112 } 113 114 @Test extractFlaggedApis_flaggedConstructorsAreFlaggedApisnull115 fun extractFlaggedApis_flaggedConstructorsAreFlaggedApis() { 116 val apiText = 117 """ 118 // Signature format: 2.0 119 package android.app.pinner { 120 @FlaggedApi("android.app.pinner_service_client_api") public class PinnerServiceClient { 121 ctor @FlaggedApi("android.app.pinner_service_client_api") public PinnerServiceClient(); 122 } 123 } 124 """ 125 .trimIndent() 126 Files.write(apiTextFile, apiText.toByteArray(Charsets.UTF_8), StandardOpenOption.APPEND) 127 128 val process = Runtime.getRuntime().exec(createCommand()) 129 process.waitFor() 130 131 val content = Files.readAllBytes(flagToApiMap).toString(Charsets.UTF_8) 132 val result = TextFormat.parse(content, FlagApiMap::class.java) 133 134 val expected = FlagApiMap.newBuilder() 135 val api = 136 JavaMethod.newBuilder() 137 .setPackageName("android.app.pinner") 138 .setClassName("PinnerServiceClient") 139 .setMethodName("PinnerServiceClient") 140 addFlaggedApi(expected, api, "android.app.pinner_service_client_api") 141 assertThat(result).isEqualTo(expected.build()) 142 } 143 144 @Test extractFlaggedApis_unflaggedNestedClassShouldUseOuterClassFlagnull145 fun extractFlaggedApis_unflaggedNestedClassShouldUseOuterClassFlag() { 146 val apiText = 147 """ 148 // Signature format: 2.0 149 package android.location.provider { 150 @FlaggedApi(Flags.FLAG_NEW_GEOCODER) public final class ForwardGeocodeRequest implements android.os.Parcelable { 151 method public int describeContents(); 152 } 153 public static final class ForwardGeocodeRequest.Builder { 154 method @NonNull public android.location.provider.ForwardGeocodeRequest build(); 155 } 156 } 157 """ 158 .trimIndent() 159 Files.write(apiTextFile, apiText.toByteArray(Charsets.UTF_8), StandardOpenOption.APPEND) 160 161 val process = Runtime.getRuntime().exec(createCommand()) 162 process.waitFor() 163 164 val content = Files.readAllBytes(flagToApiMap).toString(Charsets.UTF_8) 165 val result = TextFormat.parse(content, FlagApiMap::class.java) 166 167 val expected = FlagApiMap.newBuilder() 168 val api1 = 169 JavaMethod.newBuilder() 170 .setPackageName("android.location.provider") 171 .setClassName("ForwardGeocodeRequest") 172 .setMethodName("describeContents") 173 addFlaggedApi(expected, api1, "Flags.FLAG_NEW_GEOCODER") 174 val api2 = 175 JavaMethod.newBuilder() 176 .setPackageName("android.location.provider") 177 .setClassName("ForwardGeocodeRequest.Builder") 178 .setMethodName("build") 179 addFlaggedApi(expected, api2, "Flags.FLAG_NEW_GEOCODER") 180 assertThat(result).ignoringRepeatedFieldOrder().isEqualTo(expected.build()) 181 } 182 183 @Test extractFlaggedApis_unflaggedNestedClassShouldUseOuterClassFlag_deeplyNestednull184 fun extractFlaggedApis_unflaggedNestedClassShouldUseOuterClassFlag_deeplyNested() { 185 val apiText = 186 """ 187 // Signature format: 2.0 188 package android.package.xyz { 189 @FlaggedApi(outer_class_flag) public final class OuterClass { 190 method public int apiInOuterClass(); 191 } 192 public final class OuterClass.Deeply.NestedClass { 193 method public void apiInNestedClass(); 194 } 195 } 196 """ 197 .trimIndent() 198 Files.write(apiTextFile, apiText.toByteArray(Charsets.UTF_8), StandardOpenOption.APPEND) 199 200 val process = Runtime.getRuntime().exec(createCommand()) 201 process.waitFor() 202 203 val content = Files.readAllBytes(flagToApiMap).toString(Charsets.UTF_8) 204 val result = TextFormat.parse(content, FlagApiMap::class.java) 205 206 val expected = FlagApiMap.newBuilder() 207 val api1 = 208 JavaMethod.newBuilder() 209 .setPackageName("android.package.xyz") 210 .setClassName("OuterClass") 211 .setMethodName("apiInOuterClass") 212 addFlaggedApi(expected, api1, "outer_class_flag") 213 val api2 = 214 JavaMethod.newBuilder() 215 .setPackageName("android.package.xyz") 216 .setClassName("OuterClass.Deeply.NestedClass") 217 .setMethodName("apiInNestedClass") 218 addFlaggedApi(expected, api2, "outer_class_flag") 219 assertThat(result).ignoringRepeatedFieldOrder().isEqualTo(expected.build()) 220 } 221 addFlaggedApinull222 private fun addFlaggedApi(builder: FlagApiMap.Builder, api: JavaMethod.Builder, flag: String) { 223 if (builder.containsFlagToApi(flag)) { 224 val updatedApis = 225 builder.getFlagToApiOrThrow(flag).toBuilder().addJavaMethods(api).build() 226 builder.putFlagToApi(flag, updatedApis) 227 } else { 228 val apis = FlaggedApis.newBuilder().addJavaMethods(api).build() 229 builder.putFlagToApi(flag, apis) 230 } 231 } 232 createCommandnull233 private fun createCommand(): Array<String> { 234 val command = 235 String.format(COMMAND, apiTextFile.toAbsolutePath(), flagToApiMap.toAbsolutePath()) 236 return command.split(" ").toTypedArray() 237 } 238 } 239