blob: b9b87accb49e925d4c09b6355cfef783e1388f2a [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.metalava.apilevels
import com.android.sdklib.SdkVersionInfo
import com.android.tools.lint.detector.api.ApiConstraint
import com.android.tools.metalava.ARG_ANDROID_JAR_PATTERN
import com.android.tools.metalava.ARG_API_VERSION_NAMES
import com.android.tools.metalava.ARG_API_VERSION_SIGNATURE_FILES
import com.android.tools.metalava.ARG_CURRENT_CODENAME
import com.android.tools.metalava.ARG_CURRENT_VERSION
import com.android.tools.metalava.ARG_FIRST_VERSION
import com.android.tools.metalava.ARG_GENERATE_API_LEVELS
import com.android.tools.metalava.ARG_GENERATE_API_VERSION_HISTORY
import com.android.tools.metalava.ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS
import com.android.tools.metalava.ARG_SDK_INFO_FILE
import com.android.tools.metalava.ARG_SDK_JAR_ROOT
import com.android.tools.metalava.DriverTest
import com.android.tools.metalava.getApiLookup
import com.android.tools.metalava.java
import com.android.tools.metalava.minApiLevel
import com.google.common.truth.Truth.assertThat
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import java.io.File
import kotlin.text.Charsets.UTF_8
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.BeforeClass
import org.junit.Test
class ApiGeneratorTest : DriverTest() {
companion object {
// As per ApiConstraint that uses a bit vector, API has to be between 1..61.
private const val MAGIC_VERSION_INT = 57 // [SdkVersionInfo.MAX_LEVEL] - 4
private const val MAGIC_VERSION_STR = MAGIC_VERSION_INT.toString()
@JvmStatic
@BeforeClass
fun beforeClass() {
assert(MAGIC_VERSION_INT > SdkVersionInfo.HIGHEST_KNOWN_API)
// Trigger <clinit> of [SdkApiConstraint] to call `isValidApiLevel` in its companion
ApiConstraint.UNKNOWN
// This checks if MAGIC_VERSION_INT is not bigger than [SdkVersionInfo.MAX_LEVEL]
assert(ApiConstraint.SdkApiConstraint.isValidApiLevel(MAGIC_VERSION_INT))
}
}
@Test
fun `Extract API levels`() {
var oldSdkJars = File("prebuilts/tools/common/api-versions")
if (!oldSdkJars.isDirectory) {
oldSdkJars = File("../../prebuilts/tools/common/api-versions")
if (!oldSdkJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?"
)
return
}
}
var platformJars = File("prebuilts/sdk")
if (!platformJars.isDirectory) {
platformJars = File("../../prebuilts/sdk")
if (!platformJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars"
)
return
}
}
val output = File.createTempFile("api-info", "xml")
output.deleteOnExit()
val outputPath = output.path
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_LEVELS,
outputPath,
ARG_ANDROID_JAR_PATTERN,
"${oldSdkJars.path}/android-%/android.jar",
ARG_ANDROID_JAR_PATTERN,
"${platformJars.path}/%/public/android.jar",
ARG_CURRENT_CODENAME,
"Z",
ARG_CURRENT_VERSION,
MAGIC_VERSION_STR // not real api level of Z
),
sourceFiles =
arrayOf(
java(
"""
package android.pkg;
public class MyTest {
}
"""
)
)
)
assertTrue(output.isFile)
val xml = output.readText(UTF_8)
val nextVersion = MAGIC_VERSION_INT + 1
assertTrue(xml.contains("<class name=\"android/Manifest\$permission\" since=\"1\">"))
assertTrue(
xml.contains(
"<field name=\"BIND_CARRIER_MESSAGING_SERVICE\" since=\"22\" deprecated=\"23\"/>"
)
)
assertTrue(xml.contains("<class name=\"android/pkg/MyTest\" since=\"$nextVersion\""))
assertFalse(xml.contains("<implements name=\"java/lang/annotation/Annotation\" removed=\""))
assertFalse(xml.contains("<extends name=\"java/lang/Enum\" removed=\""))
assertFalse(xml.contains("<method name=\"append(C)Ljava/lang/AbstractStringBuilder;\""))
// Also make sure package private super classes are pruned
assertFalse(xml.contains("<extends name=\"android/icu/util/CECalendar\""))
val apiLookup = getApiLookup(output, temporaryFolder.newFolder())
// Make sure we're really using the correct database, not the SDK one. (This placeholder
// class is provided as a source file above.)
@Suppress("DEPRECATION")
assertEquals(nextVersion, apiLookup.getClassVersion("android.pkg.MyTest"))
@Suppress("DEPRECATION") apiLookup.getClassVersion("android.v")
@Suppress("DEPRECATION")
assertEquals(
5,
apiLookup.getFieldVersion("android.Manifest\$permission", "AUTHENTICATE_ACCOUNTS")
)
@Suppress("DEPRECATION")
val methodVersion =
apiLookup.getMethodVersion("android/icu/util/CopticCalendar", "computeTime", "()")
assertEquals(24, methodVersion)
}
@Test
fun `Extract System API`() {
// These are the wrong jar paths but this test doesn't actually care what the
// content of the jar files, just checking the logic of starting the database
// at some higher number than 1
var platformJars = File("prebuilts/sdk")
if (!platformJars.isDirectory) {
platformJars = File("../../prebuilts/sdk")
if (!platformJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars"
)
return
}
}
var extensionSdkJars = File("prebuilts/sdk/extensions")
if (!extensionSdkJars.isDirectory) {
extensionSdkJars = File("../../prebuilts/sdk/extensions")
if (!extensionSdkJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $extensionSdkJars"
)
return
}
}
val filter = File.createTempFile("filter", "txt")
filter.deleteOnExit()
filter.writeText(
"""
<sdk-extensions-info>
<!-- SDK definitions -->
<sdk shortname="R" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
<sdk shortname="S" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
<sdk shortname="T" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
<!-- Rules -->
<symbol jar="art.module.public.api" pattern="*" sdks="R" />
<symbol jar="conscrypt.module.intra.core.api " pattern="" sdks="R" />
<symbol jar="conscrypt.module.platform.api" pattern="*" sdks="R" />
<symbol jar="conscrypt.module.public.api" pattern="*" sdks="R" />
<symbol jar="framework-mediaprovider" pattern="*" sdks="R" />
<symbol jar="framework-mediaprovider" pattern="android.provider.MediaStore#canManageMedia" sdks="T" />
<symbol jar="framework-permission-s" pattern="*" sdks="R" />
<symbol jar="framework-permission" pattern="*" sdks="R" />
<symbol jar="framework-sdkextensions" pattern="*" sdks="R" />
<symbol jar="framework-scheduling" pattern="*" sdks="R" />
<symbol jar="framework-statsd" pattern="*" sdks="R" />
<symbol jar="framework-tethering" pattern="*" sdks="R" />
<symbol jar="legacy.art.module.platform.api" pattern="*" sdks="R" />
<symbol jar="service-media-s" pattern="*" sdks="R" />
<symbol jar="service-permission" pattern="*" sdks="R" />
<!-- use framework-permissions-s to test the order of multiple SDKs is respected -->
<symbol jar="android.net.ipsec.ike" pattern="android.net.eap.EapAkaInfo" sdks="R,S,T" />
<symbol jar="android.net.ipsec.ike" pattern="android.net.eap.EapInfo" sdks="T,S,R" />
<symbol jar="android.net.ipsec.ike" pattern="*" sdks="R" />
<!-- framework-connectivity: only android.net.CaptivePortal should have the 'sdks' attribute -->
<symbol jar="framework-connectivity" pattern="android.net.CaptivePortal" sdks="R" />
<!-- framework-media explicitly omitted: nothing in this module should have the 'sdks' attribute -->
</sdk-extensions-info>
"""
.trimIndent()
)
val output = File.createTempFile("api-info", "xml")
output.deleteOnExit()
val outputPath = output.path
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_LEVELS,
outputPath,
ARG_ANDROID_JAR_PATTERN,
"${platformJars.path}/%/public/android.jar",
ARG_SDK_JAR_ROOT,
"$extensionSdkJars",
ARG_SDK_INFO_FILE,
filter.path,
ARG_FIRST_VERSION,
"21",
ARG_CURRENT_VERSION,
"33"
)
)
assertTrue(output.isFile)
val xml = output.readText(UTF_8)
assertTrue(xml.contains("<api version=\"3\" min=\"21\">"))
assertTrue(
xml.contains(
"<sdk id=\"30\" shortname=\"R\" name=\"R Extensions\" reference=\"android/os/Build\$VERSION_CODES\$R\"/>"
)
)
assertTrue(
xml.contains(
"<sdk id=\"31\" shortname=\"S\" name=\"S Extensions\" reference=\"android/os/Build\$VERSION_CODES\$S\"/>"
)
)
assertTrue(
xml.contains(
"<sdk id=\"33\" shortname=\"T\" name=\"T Extensions\" reference=\"android/os/Build\$VERSION_CODES\$T\"/>"
)
)
assertTrue(xml.contains("<class name=\"android/Manifest\" since=\"21\">"))
assertTrue(xml.contains("<field name=\"showWhenLocked\" since=\"27\"/>"))
// top level class marked as since=21 and R=1, implemented in the framework-mediaprovider
// mainline module
assertTrue(
xml.contains(
"<class name=\"android/provider/MediaStore\" module=\"framework-mediaprovider\" since=\"21\" sdks=\"30:1,0:21\">"
)
)
// method with identical sdks attribute as containing class: sdks attribute should be
// omitted
assertTrue(xml.contains("<method name=\"getMediaScannerUri()Landroid/net/Uri;\"/>"))
// method with different sdks attribute than containing class
assertTrue(
xml.contains(
"<method name=\"canManageMedia(Landroid/content/Context;)Z\" since=\"31\" sdks=\"33:1,0:31\"/>"
)
)
val apiLookup = getApiLookup(output)
@Suppress("DEPRECATION") apiLookup.getClassVersion("android.v")
// This field was added in API level 5, but when we're starting the count higher
// (as in the system API), the first introduced API level is the one we use
@Suppress("DEPRECATION")
assertEquals(
21,
apiLookup.getFieldVersion("android.Manifest\$permission", "AUTHENTICATE_ACCOUNTS")
)
@Suppress("DEPRECATION")
val methodVersion =
apiLookup.getMethodVersion("android/icu/util/CopticCalendar", "computeTime", "()")
assertEquals(24, methodVersion)
// The filter says 'framework-permission-s * R' so RoleManager should exist
// and should have a module/sdks attributes
assertTrue(apiLookup.containsClass("android/app/role/RoleManager"))
assertTrue(
xml.contains(
"<method name=\"canManageMedia(Landroid/content/Context;)Z\" since=\"31\" sdks=\"33:1,0:31\"/>"
)
)
// The filter doesn't mention framework-media, so no class in that module should have a
// module/sdks attributes
assertTrue(xml.contains("<class name=\"android/media/MediaFeature\" since=\"31\">"))
// The filter only defines a single API in framework-connectivity: verify that only that API
// has the module/sdks attributes
assertTrue(
xml.contains(
"<class name=\"android/net/CaptivePortal\" module=\"framework-connectivity\" since=\"23\" sdks=\"30:1,0:23\">"
)
)
assertTrue(
xml.contains("<class name=\"android/net/ConnectivityDiagnosticsManager\" since=\"30\">")
)
// The order of the SDKs should be respected
// android.net.eap.EapAkaInfo R S T -> 0,30,31,33
assertTrue(
xml.contains(
"<class name=\"android/net/eap/EapAkaInfo\" module=\"android.net.ipsec.ike\" since=\"33\" sdks=\"30:3,31:3,33:3,0:33\">"
)
)
// android.net.eap.EapInfo T S R -> 0,33,31,30
assertTrue(
xml.contains(
"<class name=\"android/net/eap/EapInfo\" module=\"android.net.ipsec.ike\" since=\"33\" sdks=\"33:3,31:3,30:3,0:33\">"
)
)
// Verify historical backfill
assertEquals(30, apiLookup.getClassVersions("android/os/ext/SdkExtensions").minApiLevel())
assertEquals(
30,
apiLookup
.getMethodVersions("android/os/ext/SdkExtensions", "getExtensionVersion", "(I)I")
.minApiLevel()
)
assertEquals(
31,
apiLookup
.getMethodVersions(
"android/os/ext/SdkExtensions",
"getAllExtensionVersions",
"()Ljava/util/Map;"
)
.minApiLevel()
)
// Verify there's no extension versions listed for SdkExtensions
val sdkExtClassLine =
xml.lines().first { it.contains("<class name=\"android/os/ext/SdkExtensions\"") }
assertFalse(sdkExtClassLine.contains("sdks="))
}
@Test
fun `Correct API Level for release`() {
var oldSdkJars = File("prebuilts/tools/common/api-versions")
if (!oldSdkJars.isDirectory) {
oldSdkJars = File("../../prebuilts/tools/common/api-versions")
if (!oldSdkJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?"
)
return
}
}
var platformJars = File("prebuilts/sdk")
if (!platformJars.isDirectory) {
platformJars = File("../../prebuilts/sdk")
if (!platformJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars"
)
return
}
}
val output = File.createTempFile("api-info", "xml")
output.deleteOnExit()
val outputPath = output.path
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_LEVELS,
outputPath,
ARG_ANDROID_JAR_PATTERN,
"${oldSdkJars.path}/android-%/android.jar",
ARG_ANDROID_JAR_PATTERN,
"${platformJars.path}/%/public/android.jar",
ARG_CURRENT_CODENAME,
"REL",
ARG_CURRENT_VERSION,
MAGIC_VERSION_STR // not real api level
),
sourceFiles =
arrayOf(
java(
"""
package android.pkg;
public class MyTest {
}
"""
)
)
)
assertTrue(output.isFile)
// Anything with a REL codename is in the current API level
val xml = output.readText(UTF_8)
assertTrue(xml.contains("<class name=\"android/pkg/MyTest\" since=\"$MAGIC_VERSION_STR\""))
val apiLookup = getApiLookup(output, temporaryFolder.newFolder())
@Suppress("DEPRECATION")
assertEquals(MAGIC_VERSION_INT, apiLookup.getClassVersion("android.pkg.MyTest"))
}
@Test
fun `Correct API Level for non-release`() {
var oldSdkJars = File("prebuilts/tools/common/api-versions")
if (!oldSdkJars.isDirectory) {
oldSdkJars = File("../../prebuilts/tools/common/api-versions")
if (!oldSdkJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?"
)
return
}
}
var platformJars = File("prebuilts/sdk")
if (!platformJars.isDirectory) {
platformJars = File("../../prebuilts/sdk")
if (!platformJars.isDirectory) {
println(
"Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars"
)
return
}
}
val output = File.createTempFile("api-info", "xml")
output.deleteOnExit()
val outputPath = output.path
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_LEVELS,
outputPath,
ARG_ANDROID_JAR_PATTERN,
"${oldSdkJars.path}/android-%/android.jar",
ARG_ANDROID_JAR_PATTERN,
"${platformJars.path}/%/public/android.jar",
ARG_CURRENT_CODENAME,
"ZZZ", // not just Z, but very ZZZ
ARG_CURRENT_VERSION,
MAGIC_VERSION_STR // not real api level
),
sourceFiles =
arrayOf(
java(
"""
package android.pkg;
public class MyTest {
}
"""
)
)
)
assertTrue(output.isFile)
// Metalava should understand that a codename means "current api + 1"
val nextVersion = MAGIC_VERSION_INT + 1
val xml = output.readText(UTF_8)
assertTrue(xml.contains("<class name=\"android/pkg/MyTest\" since=\"$nextVersion\""))
val apiLookup = getApiLookup(output, temporaryFolder.newFolder())
@Suppress("DEPRECATION")
assertEquals(nextVersion, apiLookup.getClassVersion("android.pkg.MyTest"))
}
@Test
fun `Generate API for test prebuilts`() {
var testPrebuiltsRoot = File(System.getenv("METALAVA_TEST_PREBUILTS_SDK_ROOT"))
if (!testPrebuiltsRoot.isDirectory) {
fail("test prebuilts not found: $testPrebuiltsRoot")
}
val api_versions_xml = File.createTempFile("api-versions", "xml")
api_versions_xml.deleteOnExit()
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_LEVELS,
api_versions_xml.path,
ARG_ANDROID_JAR_PATTERN,
"${testPrebuiltsRoot.path}/%/public/android.jar",
ARG_SDK_JAR_ROOT,
"${testPrebuiltsRoot.path}/extensions",
ARG_SDK_INFO_FILE,
"${testPrebuiltsRoot.path}/sdk-extensions-info.xml",
ARG_FIRST_VERSION,
"30",
ARG_CURRENT_VERSION,
"32",
ARG_CURRENT_CODENAME,
"Foo"
),
sourceFiles =
arrayOf(
java(
"""
package android.test;
public class ClassAddedInApi31AndExt2 {
private ClassAddedInApi31AndExt2() {}
public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 1;
public static final int FIELD_ADDED_IN_EXT_3 = 2;
public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
public void methodAddedInExt3() { throw new RuntimeException("Stub!"); };
public void methodNotFinalized() { throw new RuntimeException("Stub!"); }
}
"""
)
)
)
assertTrue(api_versions_xml.isFile)
val xml = api_versions_xml.readText(UTF_8)
val expected =
"""
<?xml version="1.0" encoding="utf-8"?>
<api version="3" min="30">
<sdk id="30" shortname="R-ext" name="R Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}R"/>
<sdk id="31" shortname="S-ext" name="S Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}S"/>
<class name="android/test/ClassAddedInApi30" since="30">
<extends name="java/lang/Object"/>
<method name="methodAddedInApi30()V"/>
<method name="methodAddedInApi31()V" since="31"/>
</class>
<class name="android/test/ClassAddedInApi31AndExt2" module="framework-ext" since="31" sdks="30:2,31:2,0:31">
<extends name="java/lang/Object"/>
<method name="methodAddedInApi31AndExt2()V"/>
<method name="methodAddedInExt3()V" since="33" sdks="30:3,31:3"/>
<method name="methodNotFinalized()V" since="33" sdks="0:33"/>
<field name="FIELD_ADDED_IN_API_31_AND_EXT_2"/>
<field name="FIELD_ADDED_IN_EXT_3" since="33" sdks="30:3,31:3"/>
</class>
<class name="android/test/ClassAddedInExt1" module="framework-ext" since="31" sdks="30:1,31:1,0:31">
<extends name="java/lang/Object"/>
<method name="methodAddedInApi31AndExt2()V" sdks="30:2,31:2,0:31"/>
<method name="methodAddedInExt1()V"/>
<method name="methodAddedInExt3()V" since="33" sdks="30:3,31:3"/>
<field name="FIELD_ADDED_IN_API_31_AND_EXT_2" sdks="30:2,31:2,0:31"/>
<field name="FIELD_ADDED_IN_EXT_1"/>
<field name="FIELD_ADDED_IN_EXT_3" since="33" sdks="30:3,31:3"/>
</class>
<class name="android/test/ClassAddedInExt3" module="framework-ext" since="33" sdks="30:3,31:3">
<extends name="java/lang/Object"/>
<method name="methodAddedInExt3()V"/>
<field name="FIELD_ADDED_IN_EXT_3"/>
</class>
<class name="java/lang/Object" since="30">
<method name="&lt;init>()V"/>
</class>
</api>
"""
fun String.trimEachLine(): String =
lines().map { it.trim() }.filter { it.isNotEmpty() }.joinToString("\n")
assertEquals(expected.trimEachLine(), xml.trimEachLine())
}
@Test
fun `Generate API while removing missing class references`() {
val api_versions_xml = File.createTempFile("api-versions", "xml")
api_versions_xml.deleteOnExit()
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_LEVELS,
api_versions_xml.path,
ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS,
ARG_FIRST_VERSION,
"30",
ARG_CURRENT_VERSION,
"32",
ARG_CURRENT_CODENAME,
"Foo"
),
sourceFiles =
arrayOf(
java(
"""
package android.test;
public class ClassThatImplementsMethodFromApex implements ClassFromApex {
}
"""
)
)
)
assertTrue(api_versions_xml.isFile)
val xml = api_versions_xml.readText(UTF_8)
val expected =
"""
<?xml version="1.0" encoding="utf-8"?>
<api version="3" min="30">
<class name="android/test/ClassThatImplementsMethodFromApex" since="33">
<method name="&lt;init>()V"/>
</class>
</api>
"""
fun String.trimEachLine(): String =
lines().map { it.trim() }.filter { it.isNotEmpty() }.joinToString("\n")
assertEquals(expected.trimEachLine(), xml.trimEachLine())
}
@Test
fun `Generate API finds missing class references`() {
var testPrebuiltsRoot = File(System.getenv("METALAVA_TEST_PREBUILTS_SDK_ROOT"))
if (!testPrebuiltsRoot.isDirectory) {
fail("test prebuilts not found: $testPrebuiltsRoot")
}
val api_versions_xml = File.createTempFile("api-versions", "xml")
api_versions_xml.deleteOnExit()
var exception: IllegalStateException? = null
try {
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_LEVELS,
api_versions_xml.path,
ARG_FIRST_VERSION,
"30",
ARG_CURRENT_VERSION,
"32",
ARG_CURRENT_CODENAME,
"Foo"
),
sourceFiles =
arrayOf(
java(
"""
package android.test;
// Really this class should implement some interface that doesn't exist,
// but that's hard to set up in the test harness, so just verify that
// metalava complains about java/lang/Object not existing because we didn't
// include the testdata prebuilt jars.
public class ClassThatImplementsMethodFromApex {
}
"""
)
)
)
} catch (e: IllegalStateException) {
exception = e
}
assertNotNull(exception)
assertThat(exception?.message ?: "")
.contains(
"There are classes in this API that reference other classes that do not exist in this API."
)
assertThat(exception?.message ?: "")
.contains(
"java/lang/Object referenced by:\n android/test/ClassThatImplementsMethodFromApex"
)
}
@Test
fun `Create API levels from signature files`() {
val output = File.createTempFile("api-info", ".json")
output.deleteOnExit()
val outputPath = output.path
val pastVersions =
listOf(
createTextFile(
"1.1.0",
"""
package test.pkg {
public class Foo {
method public <T extends java.lang.String> void methodV1(T);
field public int fieldV1;
}
public class Foo.Bar {
}
}
"""
.trimIndent()
),
createTextFile(
"1.2.0",
"""
package test.pkg {
public class Foo {
method public <T extends java.lang.String> void methodV1(T);
method @Deprecated public <T> void methodV2(String, int);
field public int fieldV1;
field public int fieldV2;
}
public class Foo.Bar {
}
}
"""
.trimIndent()
),
createTextFile(
"1.3.0",
"""
package test.pkg {
public class Foo {
method @Deprecated public <T extends java.lang.String> void methodV1(T);
method public void methodV3();
field public int fieldV1;
field public int fieldV2;
}
@Deprecated public class Foo.Bar {
}
}
"""
.trimIndent()
)
)
val currentVersion =
"""
package test.pkg {
public class Foo {
method @Deprecated public <T extends java.lang.String> void methodV1(T);
method public void methodV3();
method public void methodV4();
field public int fieldV1;
field public int fieldV2;
}
@Deprecated public class Foo.Bar {
}
}
"""
.trimIndent()
check(
signatureSource = currentVersion,
extraArguments =
arrayOf(
ARG_GENERATE_API_VERSION_HISTORY,
outputPath,
ARG_API_VERSION_SIGNATURE_FILES,
pastVersions.joinToString(":") { it.absolutePath },
ARG_API_VERSION_NAMES,
listOf("1.1.0", "1.2.0", "1.3.0", "1.4.0").joinToString(" "),
)
)
assertTrue(output.isFile)
// Read output and reprint with pretty printing enabled to make test failures easier to read
val gson = GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create()
val outputJson = gson.fromJson(output.readText(), JsonElement::class.java)
val prettyOutput = gson.toJson(outputJson)
assertEquals(
"""
[
{
"class": "test.pkg.Foo.Bar",
"addedIn": "1.1.0",
"deprecatedIn": "1.3.0",
"methods": [],
"fields": []
},
{
"class": "test.pkg.Foo",
"addedIn": "1.1.0",
"methods": [
{
"method": "methodV1<T extends java.lang.String>(T)",
"addedIn": "1.1.0",
"deprecatedIn": "1.3.0"
},
{
"method": "methodV2<T>(java.lang.String,int)",
"addedIn": "1.2.0",
"deprecatedIn": "1.2.0"
},
{
"method": "methodV3()",
"addedIn": "1.3.0"
},
{
"method": "methodV4()",
"addedIn": "1.4.0"
}
],
"fields": [
{
"field": "fieldV2",
"addedIn": "1.2.0"
},
{
"field": "fieldV1",
"addedIn": "1.1.0"
}
]
}
]
"""
.trimIndent(),
prettyOutput
)
}
@Test
fun `Correct error with different number of API signature files and API version names`() {
val output = File.createTempFile("api-info", ".json")
output.deleteOnExit()
val outputPath = output.path
val filePaths =
listOf("1.1.0", "1.2.0", "1.3.0").map { name ->
val file = File.createTempFile(name, ".txt")
file.deleteOnExit()
file.path
}
check(
extraArguments =
arrayOf(
ARG_GENERATE_API_VERSION_HISTORY,
outputPath,
ARG_API_VERSION_SIGNATURE_FILES,
filePaths.joinToString(":"),
ARG_API_VERSION_NAMES,
listOf("1.1.0", "1.2.0").joinToString(" ")
),
expectedFail =
"Aborting: --api-version-names must have one more version than --api-version-signature-files to include the current version name"
)
}
@Test
fun `Kotlin-style nulls with old signature format is parsed`() {
val output = File.createTempFile("api-info", ".json")
output.deleteOnExit()
val outputPath = output.path
val api =
"""
// Signature format: 2.0
package test.pkg {
public class Foo {
method public void foo(String?);
}
}
"""
.trimIndent()
val input = createTextFile("0.0.0", api)
check(
signatureSource = api,
inputKotlinStyleNulls = true,
extraArguments =
arrayOf(
ARG_GENERATE_API_VERSION_HISTORY,
outputPath,
ARG_API_VERSION_SIGNATURE_FILES,
input.absolutePath,
ARG_API_VERSION_NAMES,
listOf("0.0.0", "0.1.0").joinToString(" ")
)
)
val expectedJson =
"[{\"class\":\"test.pkg.Foo\",\"addedIn\":\"0.0.0\",\"methods\":[{\"method\":\"foo(java.lang.String)\",\"addedIn\":\"0.0.0\"}],\"fields\":[]}]"
assertEquals(expectedJson, output.readText())
}
@Test
fun `API levels can be generated from just the current codebase`() {
val output = File.createTempFile("api-info", ".json")
output.deleteOnExit()
val outputPath = output.path
val api =
"""
// Signature format: 3.0
package test.pkg {
public class Foo {
method public void foo(String?);
}
}
"""
.trimIndent()
check(
signatureSource = api,
extraArguments =
arrayOf(
ARG_GENERATE_API_VERSION_HISTORY,
outputPath,
ARG_API_VERSION_NAMES,
"0.0.0"
)
)
val expectedJson =
"[{\"class\":\"test.pkg.Foo\",\"addedIn\":\"0.0.0\",\"methods\":[{\"method\":\"foo(java.lang.String)\",\"addedIn\":\"0.0.0\"}],\"fields\":[]}]"
assertEquals(expectedJson, output.readText())
}
private fun createTextFile(name: String, contents: String): File {
val file = File.createTempFile(name, ".txt")
file.deleteOnExit()
file.writeText(contents)
return file
}
}