| /* |
| * Copyright (C) 2017 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 |
| |
| import com.android.SdkConstants |
| import com.android.SdkConstants.DOT_JAVA |
| import com.android.SdkConstants.DOT_KT |
| import com.android.ide.common.process.DefaultProcessExecutor |
| import com.android.ide.common.process.LoggedProcessOutputHandler |
| import com.android.ide.common.process.ProcessException |
| import com.android.ide.common.process.ProcessInfoBuilder |
| import com.android.tools.lint.checks.ApiLookup |
| import com.android.tools.lint.checks.infrastructure.LintDetectorTest |
| import com.android.tools.lint.checks.infrastructure.TestFile |
| import com.android.tools.lint.checks.infrastructure.TestFiles |
| import com.android.tools.lint.checks.infrastructure.TestFiles.java |
| import com.android.tools.lint.checks.infrastructure.stripComments |
| import com.android.tools.metalava.doclava1.ApiFile |
| import com.android.tools.metalava.doclava1.Errors |
| import com.android.utils.FileUtils |
| import com.android.utils.SdkUtils |
| import com.android.utils.StdLogger |
| import com.android.utils.XmlUtils |
| import com.google.common.base.Charsets |
| import com.google.common.io.ByteStreams |
| import com.google.common.io.Closeables |
| import com.google.common.io.Files |
| import org.intellij.lang.annotations.Language |
| import org.junit.Assert.assertEquals |
| import org.junit.Assert.assertNotNull |
| import org.junit.Assert.assertTrue |
| import org.junit.Assert.fail |
| import org.junit.Before |
| import org.junit.Rule |
| import org.junit.rules.TemporaryFolder |
| import java.io.File |
| import java.io.FileNotFoundException |
| import java.io.PrintWriter |
| import java.io.StringWriter |
| import java.net.URL |
| |
| const val CHECK_OLD_DOCLAVA_TOO = false |
| const val CHECK_STUB_COMPILATION = false |
| |
| abstract class DriverTest { |
| @get:Rule |
| var temporaryFolder = TemporaryFolder() |
| |
| @Before |
| fun setup() { |
| System.setProperty(ENV_VAR_METALAVA_TESTS_RUNNING, SdkConstants.VALUE_TRUE) |
| } |
| |
| private fun createProject(vararg files: TestFile): File { |
| val dir = temporaryFolder.newFolder("project") |
| |
| files |
| .map { it.createFile(dir) } |
| .forEach { assertNotNull(it) } |
| |
| return dir |
| } |
| |
| protected fun runDriver(vararg args: String, expectedFail: String = ""): String { |
| resetTicker() |
| |
| val sw = StringWriter() |
| val writer = PrintWriter(sw) |
| if (!com.android.tools.metalava.run(arrayOf(*args), writer, writer)) { |
| val actualFail = cleanupString(sw.toString(), null) |
| if (expectedFail != actualFail.replace(".", "").trim()) { |
| if (expectedFail == "Aborting: Found compatibility problems with --check-compatibility" && |
| actualFail.startsWith("Aborting: Found compatibility problems checking the ") |
| ) { |
| // Special case for compat checks; we don't want to force each one of them |
| // to pass in the right string (which may vary based on whether writing out |
| // the signature was passed at the same time |
| // ignore |
| } else { |
| fail(actualFail) |
| } |
| } |
| } |
| |
| return sw.toString() |
| } |
| |
| private fun findKotlinStdlibPath(): List<String> { |
| val classPath: String = System.getProperty("java.class.path") |
| val paths = mutableListOf<String>() |
| for (path in classPath.split(':')) { |
| val file = File(path) |
| val name = file.name |
| if (name.startsWith("kotlin-stdlib") || |
| name.startsWith("kotlin-reflect") || |
| name.startsWith("kotlin-script-runtime") |
| ) { |
| paths.add(file.path) |
| } |
| } |
| if (paths.isEmpty()) { |
| error("Did not find kotlin-stdlib-jre8 in $PROGRAM_NAME classpath: $classPath") |
| } |
| return paths |
| } |
| |
| protected fun getJdkPath(): String? { |
| val javaHome = System.getProperty("java.home") |
| if (javaHome != null) { |
| var javaHomeFile = File(javaHome) |
| if (File(javaHomeFile, "bin${File.separator}javac").exists()) { |
| return javaHome |
| } else if (javaHomeFile.name == "jre") { |
| javaHomeFile = javaHomeFile.parentFile |
| if (javaHomeFile != null && File(javaHomeFile, "bin${File.separator}javac").exists()) { |
| return javaHomeFile.path |
| } |
| } |
| } |
| return System.getenv("JAVA_HOME") |
| } |
| |
| protected fun check( |
| /** The source files to pass to the analyzer */ |
| vararg sourceFiles: TestFile, |
| /** Any jars to add to the class path */ |
| classpath: Array<TestFile>? = null, |
| /** The API signature content (corresponds to --api) */ |
| @Language("TEXT") |
| api: String? = null, |
| /** The API signature content (corresponds to --api-xml) */ |
| @Language("XML") |
| apiXml: String? = null, |
| /** The exact API signature content (corresponds to --exact-api) */ |
| exactApi: String? = null, |
| /** The removed API (corresponds to --removed-api) */ |
| removedApi: String? = null, |
| /** The removed dex API (corresponds to --removed-dex-api) */ |
| removedDexApi: String? = null, |
| /** The private API (corresponds to --private-api) */ |
| privateApi: String? = null, |
| /** The private DEX API (corresponds to --private-dex-api) */ |
| privateDexApi: String? = null, |
| /** The DEX API (corresponds to --dex-api) */ |
| dexApi: String? = null, |
| /** The DEX mapping API (corresponds to --dex-api-mapping) */ |
| dexApiMapping: String? = null, |
| /** Expected stubs (corresponds to --stubs) */ |
| @Language("JAVA") stubs: Array<String> = emptyArray(), |
| /** Stub source file list generated */ |
| stubsSourceList: String? = null, |
| /** Whether the stubs should be written as documentation stubs instead of plain stubs. Decides |
| * whether the stubs include @doconly elements, uses rewritten/migration annotations, etc */ |
| docStubs: Boolean = false, |
| /** Whether to run in doclava1 compat mode */ |
| compatibilityMode: Boolean = true, |
| /** Whether to trim the output (leading/trailing whitespace removal) */ |
| trim: Boolean = true, |
| /** Whether to remove blank lines in the output (the signature file usually contains a lot of these) */ |
| stripBlankLines: Boolean = true, |
| /** Warnings expected to be generated when analyzing these sources */ |
| warnings: String? = "", |
| /** Whether to run doclava1 on the test output and assert that the output is identical */ |
| checkDoclava1: Boolean = compatibilityMode, |
| checkCompilation: Boolean = false, |
| /** Annotations to merge in (in .xml format) */ |
| @Language("XML") mergeXmlAnnotations: String? = null, |
| /** Annotations to merge in (in .txt/.signature format) */ |
| @Language("TEXT") mergeSignatureAnnotations: String? = null, |
| /** Qualifier annotations to merge in (in Java stub format) */ |
| @Language("JAVA") mergeJavaStubAnnotations: String? = null, |
| /** Inclusion annotations to merge in (in Java stub format) */ |
| @Language("JAVA") mergeInclusionAnnotations: String? = null, |
| /** An optional API signature file content to load **instead** of Java/Kotlin source files */ |
| @Language("TEXT") signatureSource: String? = null, |
| /** An optional API jar file content to load **instead** of Java/Kotlin source files */ |
| apiJar: File? = null, |
| /** An optional API signature to check the current API's compatibility with */ |
| @Language("TEXT") checkCompatibilityApi: String? = null, |
| /** An optional API signature to check the last released API's compatibility with */ |
| @Language("TEXT") checkCompatibilityApiReleased: String? = null, |
| /** An optional API signature to check the current removed API's compatibility with */ |
| @Language("TEXT") checkCompatibilityRemovedApiCurrent: String? = null, |
| /** An optional API signature to check the last released removed API's compatibility with */ |
| @Language("TEXT") checkCompatibilityRemovedApiReleased: String? = null, |
| /** An optional API signature to compute nullness migration status from */ |
| @Language("TEXT") migrateNullsApi: String? = null, |
| /** An optional Proguard keep file to generate */ |
| @Language("Proguard") proguard: String? = null, |
| /** Show annotations (--show-annotation arguments) */ |
| showAnnotations: Array<String> = emptyArray(), |
| /** Hide annotations (--hideAnnotation arguments) */ |
| hideAnnotations: Array<String> = emptyArray(), |
| /** If using [showAnnotations], whether to include unannotated */ |
| showUnannotated: Boolean = false, |
| /** Additional arguments to supply */ |
| extraArguments: Array<String> = emptyArray(), |
| /** Whether we should emit Kotlin-style null signatures */ |
| outputKotlinStyleNulls: Boolean = !compatibilityMode, |
| /** Whether we should interpret API files being read as having Kotlin-style nullness types */ |
| inputKotlinStyleNulls: Boolean = false, |
| /** Whether we should omit java.lang. etc from signature files */ |
| omitCommonPackages: Boolean = !compatibilityMode, |
| /** Expected output (stdout and stderr combined). If null, don't check. */ |
| expectedOutput: String? = null, |
| /** List of extra jar files to record annotation coverage from */ |
| coverageJars: Array<TestFile>? = null, |
| /** Optional manifest to load and associate with the codebase */ |
| @Language("XML") |
| manifest: String? = null, |
| /** Packages to pre-import (these will therefore NOT be included in emitted stubs, signature files etc */ |
| importedPackages: List<String> = emptyList(), |
| /** Packages to skip emitting signatures/stubs for even if public (typically used for unit tests |
| * referencing to classpath classes that aren't part of the definitions and shouldn't be part of the |
| * test output; e.g. a test may reference java.lang.Enum but we don't want to start reporting all the |
| * public APIs in the java.lang package just because it's indirectly referenced via the "enum" superclass |
| */ |
| skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"), |
| /** Whether we should include --showAnnotations=android.annotation.SystemApi */ |
| includeSystemApiAnnotations: Boolean = false, |
| /** Whether we should warn about super classes that are stripped because they are hidden */ |
| includeStrippedSuperclassWarnings: Boolean = false, |
| /** Apply level to XML */ |
| applyApiLevelsXml: String? = null, |
| /** Corresponds to SDK constants file broadcast_actions.txt */ |
| sdk_broadcast_actions: String? = null, |
| /** Corresponds to SDK constants file activity_actions.txt */ |
| sdk_activity_actions: String? = null, |
| /** Corresponds to SDK constants file service_actions.txt */ |
| sdk_service_actions: String? = null, |
| /** Corresponds to SDK constants file categories.txt */ |
| sdk_categories: String? = null, |
| /** Corresponds to SDK constants file features.txt */ |
| sdk_features: String? = null, |
| /** Corresponds to SDK constants file widgets.txt */ |
| sdk_widgets: String? = null, |
| /** Map from artifact id to artifact descriptor */ |
| artifacts: Map<String, String>? = null, |
| /** Extract annotations and check that the given packages contain the given extracted XML files */ |
| extractAnnotations: Map<String, String>? = null, |
| /** |
| * Whether to include source retention annotations in the stubs (in that case they do not |
| * go into the extracted annotations zip file) |
| */ |
| includeSourceRetentionAnnotations: Boolean = true, |
| /** |
| * Whether to include the signature version in signatures |
| */ |
| includeSignatureVersion: Boolean = false, |
| /** |
| * List of signature files to convert to JDiff XML and the |
| * expected XML output |
| */ |
| convertToJDiff: List<Pair<String, String>> = emptyList() |
| ) { |
| // Ensure different API clients don't interfere with each other |
| try { |
| val method = ApiLookup::class.java.getDeclaredMethod("dispose") |
| method.isAccessible = true |
| method.invoke(null) |
| } catch (ignore: Throwable) { |
| ignore.printStackTrace() |
| } |
| |
| if (compatibilityMode && mergeXmlAnnotations != null) { |
| fail( |
| "Can't specify both compatibilityMode and mergeXmlAnnotations: there were no " + |
| "annotations output in doclava1" |
| ) |
| } |
| if (compatibilityMode && mergeSignatureAnnotations != null) { |
| fail( |
| "Can't specify both compatibilityMode and mergeSignatureAnnotations: there were no " + |
| "annotations output in doclava1" |
| ) |
| } |
| if (compatibilityMode && mergeJavaStubAnnotations != null) { |
| fail( |
| "Can't specify both compatibilityMode and mergeJavaStubAnnotations: there were no " + |
| "annotations output in doclava1" |
| ) |
| } |
| if (compatibilityMode && mergeInclusionAnnotations != null) { |
| fail( |
| "Can't specify both compatibilityMode and mergeInclusionAnnotations" |
| ) |
| } |
| Errors.resetLevels() |
| |
| /** Expected output if exiting with an error code */ |
| val expectedFail = if (checkCompatibilityApi != null || |
| checkCompatibilityApiReleased != null || |
| checkCompatibilityRemovedApiCurrent != null || |
| checkCompatibilityRemovedApiReleased != null |
| ) { |
| "Aborting: Found compatibility problems with --check-compatibility" |
| } else { |
| "" |
| } |
| |
| // Unit test which checks that a signature file is as expected |
| val androidJar = getPlatformFile("android.jar") |
| |
| val project = createProject(*sourceFiles) |
| |
| val packages = sourceFiles.asSequence().map { findPackage(it.getContents()!!) }.filterNotNull().toSet() |
| |
| val sourcePathDir = File(project, "src") |
| if (!sourcePathDir.isDirectory) { |
| sourcePathDir.mkdirs() |
| } |
| val sourcePath = sourcePathDir.path |
| val sourceList = |
| if (signatureSource != null) { |
| sourcePathDir.mkdirs() |
| assert(sourceFiles.isEmpty()) { "Shouldn't combine sources with signature file loads" } |
| val signatureFile = File(project, "load-api.txt") |
| Files.asCharSink(signatureFile, Charsets.UTF_8).write(signatureSource.trimIndent()) |
| if (includeStrippedSuperclassWarnings) { |
| arrayOf(signatureFile.path) |
| } else { |
| arrayOf( |
| signatureFile.path, |
| ARG_HIDE, |
| "HiddenSuperclass" |
| ) // Suppress warning #111 |
| } |
| } else if (apiJar != null) { |
| sourcePathDir.mkdirs() |
| assert(sourceFiles.isEmpty()) { "Shouldn't combine sources with API jar file loads" } |
| arrayOf(apiJar.path) |
| } else { |
| sourceFiles.asSequence().map { File(project, it.targetPath).path }.toList().toTypedArray() |
| } |
| |
| val classpathArgs: Array<String> = if (classpath != null) { |
| val classpathString = classpath |
| .map { it.createFile(project) } |
| .map { it.path } |
| .joinToString(separator = File.pathSeparator) { it } |
| |
| arrayOf(ARG_CLASS_PATH, classpathString) |
| } else { |
| emptyArray() |
| } |
| |
| val reportedWarnings = StringBuilder() |
| reporter = object : Reporter(project) { |
| override fun print(message: String) { |
| reportedWarnings.append(cleanupString(message, project).trim()).append('\n') |
| } |
| } |
| |
| val mergeAnnotationsArgs = if (mergeXmlAnnotations != null) { |
| val merged = File(project, "merged-annotations.xml") |
| Files.asCharSink(merged, Charsets.UTF_8).write(mergeXmlAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val signatureAnnotationsArgs = if (mergeSignatureAnnotations != null) { |
| val merged = File(project, "merged-annotations.txt") |
| Files.asCharSink(merged, Charsets.UTF_8).write(mergeSignatureAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val javaStubAnnotationsArgs = if (mergeJavaStubAnnotations != null) { |
| val merged = File(project, "merged-qualifier-annotations.java") |
| Files.asCharSink(merged, Charsets.UTF_8).write(mergeJavaStubAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val inclusionAnnotationsArgs = if (mergeInclusionAnnotations != null) { |
| val merged = File(project, "merged-inclusion-annotations.java") |
| Files.asCharSink(merged, Charsets.UTF_8).write(mergeInclusionAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_INCLUSION_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityApiFile = if (checkCompatibilityApi != null) { |
| val jar = File(checkCompatibilityApi) |
| if (jar.isFile) { |
| jar |
| } else { |
| val file = File(project, "current-api.txt") |
| Files.asCharSink(file, Charsets.UTF_8).write(checkCompatibilityApi.trimIndent()) |
| file |
| } |
| } else { |
| null |
| } |
| |
| val checkCompatibilityApiReleasedFile = if (checkCompatibilityApiReleased != null) { |
| val jar = File(checkCompatibilityApiReleased) |
| if (jar.isFile) { |
| jar |
| } else { |
| val file = File(project, "released-api.txt") |
| Files.asCharSink(file, Charsets.UTF_8).write(checkCompatibilityApiReleased.trimIndent()) |
| file |
| } |
| } else { |
| null |
| } |
| |
| val checkCompatibilityRemovedApiCurrentFile = if (checkCompatibilityRemovedApiCurrent != null) { |
| val jar = File(checkCompatibilityRemovedApiCurrent) |
| if (jar.isFile) { |
| jar |
| } else { |
| val file = File(project, "removed-current-api.txt") |
| Files.asCharSink(file, Charsets.UTF_8).write(checkCompatibilityRemovedApiCurrent.trimIndent()) |
| file |
| } |
| } else { |
| null |
| } |
| |
| val checkCompatibilityRemovedApiReleasedFile = if (checkCompatibilityRemovedApiReleased != null) { |
| val jar = File(checkCompatibilityRemovedApiReleased) |
| if (jar.isFile) { |
| jar |
| } else { |
| val file = File(project, "removed-released-api.txt") |
| Files.asCharSink(file, Charsets.UTF_8).write(checkCompatibilityRemovedApiReleased.trimIndent()) |
| file |
| } |
| } else { |
| null |
| } |
| |
| val migrateNullsApiFile = if (migrateNullsApi != null) { |
| val jar = File(migrateNullsApi) |
| if (jar.isFile) { |
| jar |
| } else { |
| val file = File(project, "stable-api.txt") |
| Files.asCharSink(file, Charsets.UTF_8).write(migrateNullsApi.trimIndent()) |
| file |
| } |
| } else { |
| null |
| } |
| |
| val manifestFileArgs = if (manifest != null) { |
| val file = File(project, "manifest.xml") |
| Files.asCharSink(file, Charsets.UTF_8).write(manifest.trimIndent()) |
| arrayOf(ARG_MANIFEST, file.path) |
| } else { |
| emptyArray() |
| } |
| |
| val migrateNullsArguments = if (migrateNullsApiFile != null) { |
| arrayOf(ARG_MIGRATE_NULLNESS, migrateNullsApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityArguments = if (checkCompatibilityApiFile != null) { |
| arrayOf(ARG_CHECK_COMPATIBILITY_API_CURRENT, checkCompatibilityApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityApiReleasedArguments = if (checkCompatibilityApiReleasedFile != null) { |
| arrayOf(ARG_CHECK_COMPATIBILITY_API_RELEASED, checkCompatibilityApiReleasedFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityRemovedCurrentArguments = if (checkCompatibilityRemovedApiCurrentFile != null) { |
| arrayOf(ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT, checkCompatibilityRemovedApiCurrentFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityRemovedReleasedArguments = if (checkCompatibilityRemovedApiReleasedFile != null) { |
| arrayOf(ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED, checkCompatibilityRemovedApiReleasedFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val quiet = if (expectedOutput != null && !extraArguments.contains(ARG_VERBOSE)) { |
| // If comparing output, avoid noisy output such as the banner etc |
| arrayOf(ARG_QUIET) |
| } else { |
| emptyArray() |
| } |
| |
| val coverageStats = if (coverageJars != null && coverageJars.isNotEmpty()) { |
| val sb = StringBuilder() |
| val root = File(project, "coverageJars") |
| root.mkdirs() |
| for (jar in coverageJars) { |
| if (sb.isNotEmpty()) { |
| sb.append(File.pathSeparator) |
| } |
| val file = jar.createFile(root) |
| sb.append(file.path) |
| } |
| arrayOf(ARG_ANNOTATION_COVERAGE_OF, sb.toString()) |
| } else { |
| emptyArray() |
| } |
| |
| var proguardFile: File? = null |
| val proguardKeepArguments = if (proguard != null) { |
| proguardFile = File(project, "proguard.cfg") |
| arrayOf(ARG_PROGUARD, proguardFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val showAnnotationArguments = if (showAnnotations.isNotEmpty() || includeSystemApiAnnotations) { |
| val args = mutableListOf<String>() |
| for (annotation in showAnnotations) { |
| args.add(ARG_SHOW_ANNOTATION) |
| args.add(annotation) |
| } |
| if (includeSystemApiAnnotations && !args.contains("android.annotation.SystemApi")) { |
| args.add(ARG_SHOW_ANNOTATION) |
| args.add("android.annotation.SystemApi") |
| } |
| if (includeSystemApiAnnotations && !args.contains("android.annotation.TestApi")) { |
| args.add(ARG_SHOW_ANNOTATION) |
| args.add("android.annotation.TestApi") |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| val hideAnnotationArguments = if (hideAnnotations.isNotEmpty()) { |
| val args = mutableListOf<String>() |
| for (annotation in hideAnnotations) { |
| args.add(ARG_HIDE_ANNOTATION) |
| args.add(annotation) |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| val showUnannotatedArgs = |
| if (showUnannotated) { |
| arrayOf(ARG_SHOW_UNANNOTATED) |
| } else { |
| emptyArray() |
| } |
| |
| val includeSourceRetentionAnnotationArgs = |
| if (includeSourceRetentionAnnotations) { |
| arrayOf(ARG_INCLUDE_SOURCE_RETENTION) |
| } else { |
| emptyArray() |
| } |
| |
| var removedApiFile: File? = null |
| val removedArgs = if (removedApi != null) { |
| removedApiFile = temporaryFolder.newFile("removed.txt") |
| arrayOf(ARG_REMOVED_API, removedApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var removedDexApiFile: File? = null |
| val removedDexArgs = if (removedDexApi != null) { |
| removedDexApiFile = temporaryFolder.newFile("removed-dex.txt") |
| arrayOf(ARG_REMOVED_DEX_API, removedDexApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var apiFile: File? = null |
| val apiArgs = if (api != null) { |
| apiFile = temporaryFolder.newFile("public-api.txt") |
| arrayOf(ARG_API, apiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var exactApiFile: File? = null |
| val exactApiArgs = if (exactApi != null) { |
| exactApiFile = temporaryFolder.newFile("exact-api.txt") |
| arrayOf(ARG_EXACT_API, exactApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var apiXmlFile: File? = null |
| val apiXmlArgs = if (apiXml != null) { |
| apiXmlFile = temporaryFolder.newFile("public-api-xml.txt") |
| arrayOf(ARG_XML_API, apiXmlFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var privateApiFile: File? = null |
| val privateApiArgs = if (privateApi != null) { |
| privateApiFile = temporaryFolder.newFile("private.txt") |
| arrayOf(ARG_PRIVATE_API, privateApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var dexApiFile: File? = null |
| val dexApiArgs = if (dexApi != null) { |
| dexApiFile = temporaryFolder.newFile("public-dex.txt") |
| arrayOf(ARG_DEX_API, dexApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var dexApiMappingFile: File? = null |
| val dexApiMappingArgs = if (dexApiMapping != null) { |
| dexApiMappingFile = temporaryFolder.newFile("api-mapping.txt") |
| arrayOf(ARG_DEX_API_MAPPING, dexApiMappingFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| var privateDexApiFile: File? = null |
| val privateDexApiArgs = if (privateDexApi != null) { |
| privateDexApiFile = temporaryFolder.newFile("private-dex.txt") |
| arrayOf(ARG_PRIVATE_DEX_API, privateDexApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val convertToJDiffFiles = mutableListOf<Pair<File, File>>() |
| val convertToJDiffArgs = if (convertToJDiff.isNotEmpty()) { |
| val args = mutableListOf<String>() |
| var index = 1 |
| for ((signatures, _) in convertToJDiff) { |
| val convertSig = temporaryFolder.newFile("jdiff-signatures$index.txt") |
| convertSig.writeText(signatures.trimIndent(), Charsets.UTF_8) |
| val output = temporaryFolder.newFile("jdiff-output$index.xml") |
| convertToJDiffFiles += Pair(convertSig, output) |
| index++ |
| |
| args += ARG_CONVERT_TO_JDIFF |
| args += convertSig.path |
| args += output.path |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| var stubsDir: File? = null |
| val stubsArgs = if (stubs.isNotEmpty()) { |
| stubsDir = temporaryFolder.newFolder("stubs") |
| if (docStubs) { |
| arrayOf(ARG_DOC_STUBS, stubsDir.path) |
| } else { |
| arrayOf(ARG_STUBS, stubsDir.path) |
| } |
| } else { |
| emptyArray() |
| } |
| |
| var stubsSourceListFile: File? = null |
| val stubsSourceListArgs = if (stubsSourceList != null) { |
| stubsSourceListFile = temporaryFolder.newFile("droiddoc-src-list") |
| arrayOf(ARG_STUBS_SOURCE_LIST, stubsSourceListFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val applyApiLevelsXmlFile: File? |
| val applyApiLevelsXmlArgs = if (applyApiLevelsXml != null) { |
| ApiLookup::class.java.getDeclaredMethod("dispose").apply { isAccessible = true }.invoke(null) |
| applyApiLevelsXmlFile = temporaryFolder.newFile("api-versions.xml") |
| Files.asCharSink(applyApiLevelsXmlFile!!, Charsets.UTF_8).write(applyApiLevelsXml.trimIndent()) |
| arrayOf(ARG_APPLY_API_LEVELS, applyApiLevelsXmlFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val importedPackageArgs = mutableListOf<String>() |
| importedPackages.forEach { |
| importedPackageArgs.add("--stub-import-packages") |
| importedPackageArgs.add(it) |
| } |
| |
| val skipEmitPackagesArgs = mutableListOf<String>() |
| skipEmitPackages.forEach { |
| skipEmitPackagesArgs.add("--skip-emit-packages") |
| skipEmitPackagesArgs.add(it) |
| } |
| |
| val kotlinPath = findKotlinStdlibPath() |
| val kotlinPathArgs = |
| if (kotlinPath.isNotEmpty() && |
| sourceList.asSequence().any { it.endsWith(DOT_KT) } |
| ) { |
| arrayOf(ARG_CLASS_PATH, kotlinPath.joinToString(separator = File.pathSeparator) { it }) |
| } else { |
| emptyArray() |
| } |
| |
| val sdkFilesDir: File? |
| val sdkFilesArgs: Array<String> |
| if (sdk_broadcast_actions != null || |
| sdk_activity_actions != null || |
| sdk_service_actions != null || |
| sdk_categories != null || |
| sdk_features != null || |
| sdk_widgets != null |
| ) { |
| val dir = File(project, "sdk-files") |
| sdkFilesArgs = arrayOf(ARG_SDK_VALUES, dir.path) |
| sdkFilesDir = dir |
| } else { |
| sdkFilesArgs = emptyArray() |
| sdkFilesDir = null |
| } |
| |
| val artifactArgs = if (artifacts != null) { |
| val args = mutableListOf<String>() |
| var index = 1 |
| for ((artifactId, signatures) in artifacts) { |
| val signatureFile = temporaryFolder.newFile("signature-file-$index.txt") |
| Files.asCharSink(signatureFile, Charsets.UTF_8).write(signatures.trimIndent()) |
| index++ |
| |
| args.add(ARG_REGISTER_ARTIFACT) |
| args.add(signatureFile.path) |
| args.add(artifactId) |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| val extractedAnnotationsZip: File? |
| val extractAnnotationsArgs = if (extractAnnotations != null) { |
| extractedAnnotationsZip = temporaryFolder.newFile("extracted-annotations.zip") |
| arrayOf(ARG_EXTRACT_ANNOTATIONS, extractedAnnotationsZip.path) |
| } else { |
| extractedAnnotationsZip = null |
| emptyArray() |
| } |
| |
| val actualOutput = runDriver( |
| ARG_NO_COLOR, |
| ARG_NO_BANNER, |
| |
| // Tell metalava where to store temp folder: place them under the |
| // test root folder such that we clean up the output strings referencing |
| // paths to the temp folder |
| "--temp-folder", |
| temporaryFolder.newFolder("temp").path, |
| |
| // For the tests we want to treat references to APIs like java.io.Closeable |
| // as a class that is part of the API surface, not as a hidden class as would |
| // be the case when analyzing a complete API surface |
| // ARG_UNHIDE_CLASSPATH_CLASSES, |
| ARG_ALLOW_REFERENCING_UNKNOWN_CLASSES, |
| |
| // Annotation generation temporarily turned off by default while integrating with |
| // SDK builds; tests need these |
| ARG_INCLUDE_ANNOTATIONS, |
| |
| ARG_SOURCE_PATH, |
| sourcePath, |
| ARG_CLASS_PATH, |
| androidJar.path, |
| *classpathArgs, |
| *kotlinPathArgs, |
| *removedArgs, |
| *removedDexArgs, |
| *apiArgs, |
| *apiXmlArgs, |
| *exactApiArgs, |
| *privateApiArgs, |
| *dexApiArgs, |
| *privateDexApiArgs, |
| *dexApiMappingArgs, |
| *stubsArgs, |
| *stubsSourceListArgs, |
| "$ARGS_COMPAT_OUTPUT=${if (compatibilityMode) "yes" else "no"}", |
| "$ARG_OUTPUT_KOTLIN_NULLS=${if (outputKotlinStyleNulls) "yes" else "no"}", |
| "$ARG_INPUT_KOTLIN_NULLS=${if (inputKotlinStyleNulls) "yes" else "no"}", |
| "$ARG_OMIT_COMMON_PACKAGES=${if (omitCommonPackages) "yes" else "no"}", |
| "$ARG_INCLUDE_SIG_VERSION=${if (includeSignatureVersion) "yes" else "no"}", |
| *coverageStats, |
| *quiet, |
| *mergeAnnotationsArgs, |
| *signatureAnnotationsArgs, |
| *javaStubAnnotationsArgs, |
| *inclusionAnnotationsArgs, |
| *migrateNullsArguments, |
| *checkCompatibilityArguments, |
| *checkCompatibilityApiReleasedArguments, |
| *checkCompatibilityRemovedCurrentArguments, |
| *checkCompatibilityRemovedReleasedArguments, |
| *proguardKeepArguments, |
| *manifestFileArgs, |
| *convertToJDiffArgs, |
| *applyApiLevelsXmlArgs, |
| *showAnnotationArguments, |
| *hideAnnotationArguments, |
| *showUnannotatedArgs, |
| *includeSourceRetentionAnnotationArgs, |
| *sdkFilesArgs, |
| *importedPackageArgs.toTypedArray(), |
| *skipEmitPackagesArgs.toTypedArray(), |
| *artifactArgs, |
| *extractAnnotationsArgs, |
| *sourceList, |
| *extraArguments, |
| expectedFail = expectedFail |
| ) |
| |
| if (expectedOutput != null) { |
| assertEquals(expectedOutput.trimIndent().trim(), actualOutput.trim()) |
| } |
| |
| if (api != null && apiFile != null) { |
| assertTrue("${apiFile.path} does not exist even though --api was used", apiFile.exists()) |
| val actualText = readFile(apiFile, stripBlankLines, trim) |
| assertEquals(stripComments(api, stripLineComments = false).trimIndent(), actualText) |
| // Make sure we can read back the files we write |
| ApiFile.parseApi(apiFile, options.outputKotlinStyleNulls, true) |
| } |
| |
| if (apiXml != null && apiXmlFile != null) { |
| assertTrue("${apiXmlFile.path} does not exist even though $ARG_XML_API was used", |
| apiXmlFile.exists()) |
| val actualText = readFile(apiXmlFile, stripBlankLines, trim) |
| assertEquals(stripComments(apiXml, stripLineComments = false).trimIndent(), actualText) |
| // Make sure we can read back the files we write |
| XmlUtils.parseDocument(apiXmlFile.readText(Charsets.UTF_8), false) |
| } |
| |
| if (convertToJDiffFiles.isNotEmpty()) { |
| for (i in 0 until convertToJDiff.size) { |
| val expected = convertToJDiff[i].second |
| val converted = convertToJDiffFiles[i].second |
| assertTrue("${converted.path} does not exist even though $ARG_CONVERT_TO_JDIFF was used", |
| converted.exists()) |
| val actualText = readFile(converted, stripBlankLines, trim) |
| XmlUtils.parseDocument(converted.readText(Charsets.UTF_8), false) |
| assertEquals(stripComments(expected, stripLineComments = false).trimIndent(), actualText) |
| // Make sure we can read back the files we write |
| } |
| } |
| |
| if (removedApi != null && removedApiFile != null) { |
| assertTrue( |
| "${removedApiFile.path} does not exist even though --removed-api was used", |
| removedApiFile.exists() |
| ) |
| val actualText = readFile(removedApiFile, stripBlankLines, trim) |
| assertEquals(stripComments(removedApi, stripLineComments = false).trimIndent(), actualText) |
| // Make sure we can read back the files we write |
| ApiFile.parseApi(removedApiFile, options.outputKotlinStyleNulls, true) |
| } |
| |
| if (removedDexApi != null && removedDexApiFile != null) { |
| assertTrue( |
| "${removedDexApiFile.path} does not exist even though --removed-dex-api was used", |
| removedDexApiFile.exists() |
| ) |
| val actualText = readFile(removedDexApiFile, stripBlankLines, trim) |
| assertEquals(stripComments(removedDexApi, stripLineComments = false).trimIndent(), actualText) |
| } |
| |
| if (exactApi != null && exactApiFile != null) { |
| assertTrue( |
| "${exactApiFile.path} does not exist even though --exact-api was used", |
| exactApiFile.exists() |
| ) |
| val actualText = readFile(exactApiFile, stripBlankLines, trim) |
| assertEquals(stripComments(exactApi, stripLineComments = false).trimIndent(), actualText) |
| // Make sure we can read back the files we write |
| ApiFile.parseApi(exactApiFile, options.outputKotlinStyleNulls, true) |
| } |
| |
| if (privateApi != null && privateApiFile != null) { |
| assertTrue( |
| "${privateApiFile.path} does not exist even though --private-api was used", |
| privateApiFile.exists() |
| ) |
| val actualText = readFile(privateApiFile, stripBlankLines, trim) |
| assertEquals(stripComments(privateApi, stripLineComments = false).trimIndent(), actualText) |
| // Make sure we can read back the files we write |
| ApiFile.parseApi(privateApiFile, options.outputKotlinStyleNulls, true) |
| } |
| |
| if (dexApi != null && dexApiFile != null) { |
| assertTrue( |
| "${dexApiFile.path} does not exist even though --dex-api was used", |
| dexApiFile.exists() |
| ) |
| val actualText = readFile(dexApiFile, stripBlankLines, trim) |
| assertEquals(stripComments(dexApi, stripLineComments = false).trimIndent(), actualText) |
| } |
| |
| if (privateDexApi != null && privateDexApiFile != null) { |
| assertTrue( |
| "${privateDexApiFile.path} does not exist even though --private-dex-api was used", |
| privateDexApiFile.exists() |
| ) |
| val actualText = readFile(privateDexApiFile, stripBlankLines, trim) |
| assertEquals(stripComments(privateDexApi, stripLineComments = false).trimIndent(), actualText) |
| } |
| |
| if (dexApiMapping != null && dexApiMappingFile != null) { |
| assertTrue( |
| "${dexApiMappingFile.path} does not exist even though --dex-api-maping was used", |
| dexApiMappingFile.exists() |
| ) |
| val actualText = readFile(dexApiMappingFile, stripBlankLines, trim) |
| assertEquals(stripComments(dexApiMapping, stripLineComments = false).trimIndent(), actualText) |
| } |
| |
| if (proguard != null && proguardFile != null) { |
| val expectedProguard = readFile(proguardFile) |
| assertTrue( |
| "${proguardFile.path} does not exist even though --proguard was used", |
| proguardFile.exists() |
| ) |
| assertEquals(stripComments(proguard, stripLineComments = false).trimIndent(), expectedProguard.trim()) |
| } |
| |
| if (sdk_broadcast_actions != null) { |
| val actual = readFile(File(sdkFilesDir, "broadcast_actions.txt"), stripBlankLines, trim) |
| assertEquals(sdk_broadcast_actions.trimIndent().trim(), actual.trim()) |
| } |
| |
| if (sdk_activity_actions != null) { |
| val actual = readFile(File(sdkFilesDir, "activity_actions.txt"), stripBlankLines, trim) |
| assertEquals(sdk_activity_actions.trimIndent().trim(), actual.trim()) |
| } |
| |
| if (sdk_service_actions != null) { |
| val actual = readFile(File(sdkFilesDir, "service_actions.txt"), stripBlankLines, trim) |
| assertEquals(sdk_service_actions.trimIndent().trim(), actual.trim()) |
| } |
| |
| if (sdk_categories != null) { |
| val actual = readFile(File(sdkFilesDir, "categories.txt"), stripBlankLines, trim) |
| assertEquals(sdk_categories.trimIndent().trim(), actual.trim()) |
| } |
| |
| if (sdk_features != null) { |
| val actual = readFile(File(sdkFilesDir, "features.txt"), stripBlankLines, trim) |
| assertEquals(sdk_features.trimIndent().trim(), actual.trim()) |
| } |
| |
| if (sdk_widgets != null) { |
| val actual = readFile(File(sdkFilesDir, "widgets.txt"), stripBlankLines, trim) |
| assertEquals(sdk_widgets.trimIndent().trim(), actual.trim()) |
| } |
| |
| if (warnings != null) { |
| assertEquals( |
| warnings.trimIndent().trim(), |
| cleanupString(reportedWarnings.toString(), project) |
| ) |
| } |
| |
| if (extractAnnotations != null && extractedAnnotationsZip != null) { |
| assertTrue( |
| "Using --extract-annotations but $extractedAnnotationsZip was not created", |
| extractedAnnotationsZip.isFile |
| ) |
| for ((pkg, xml) in extractAnnotations) { |
| assertPackageXml(pkg, extractedAnnotationsZip, xml) |
| } |
| } |
| |
| if (stubs.isNotEmpty() && stubsDir != null) { |
| for (i in 0 until stubs.size) { |
| var stub = stubs[i].trimIndent() |
| val sourceFile = sourceFiles[i] |
| val targetPath = if (sourceFile.targetPath.endsWith(DOT_KT)) { |
| // Kotlin source stubs are rewritten as .java files for now |
| sourceFile.targetPath.substring(0, sourceFile.targetPath.length - 3) + DOT_JAVA |
| } else { |
| sourceFile.targetPath |
| } |
| var stubFile = File(stubsDir, targetPath.substring("src/".length)) |
| if (!stubFile.isFile) { |
| if (stub.startsWith("[") && stub.contains("]")) { |
| val pathEnd = stub.indexOf("]\n") |
| val path = stub.substring(1, pathEnd) |
| stubFile = File(stubsDir, path) |
| if (stubFile.isFile) { |
| stub = stub.substring(pathEnd + 2) |
| } |
| } |
| if (!stubFile.exists()) { |
| /* Example: |
| stubs = arrayOf( |
| """ |
| [test/visible/package-info.java] |
| <html>My package docs</html> |
| package test.visible; |
| """, |
| ... |
| Here the stub will be read from $stubsDir/test/visible/package-info.java. |
| */ |
| throw FileNotFoundException( |
| "Could not find generated stub for $targetPath; consider " + |
| "setting target relative path in stub header as prefix surrounded by []" |
| ) |
| } |
| } |
| val actualText = readFile(stubFile, stripBlankLines, trim) |
| assertEquals(stub, actualText) |
| } |
| } |
| |
| if (stubsSourceList != null && stubsSourceListFile != null) { |
| assertTrue( |
| "${stubsSourceListFile.path} does not exist even though --write-stubs-source-list was used", |
| stubsSourceListFile.exists() |
| ) |
| val actualText = readFile(stubsSourceListFile, stripBlankLines, trim) |
| assertEquals(stripComments(stubsSourceList, stripLineComments = false).trimIndent(), actualText) |
| } |
| |
| if (checkCompilation && stubsDir != null && CHECK_STUB_COMPILATION) { |
| val generated = gatherSources(listOf(stubsDir)).asSequence().map { it.path }.toList().toTypedArray() |
| |
| // Also need to include on the compile path annotation classes referenced in the stubs |
| val extraAnnotationsDir = File("stub-annotations/src/main/java") |
| if (!extraAnnotationsDir.isDirectory) { |
| fail("Couldn't find $extraAnnotationsDir: Is the pwd set to the root of the metalava source code?") |
| fail("Couldn't find $extraAnnotationsDir: Is the pwd set to the root of an Android source tree?") |
| } |
| val extraAnnotations = |
| gatherSources(listOf(extraAnnotationsDir)).asSequence().map { it.path }.toList().toTypedArray() |
| |
| if (!runCommand( |
| "${getJdkPath()}/bin/javac", arrayOf( |
| "-d", project.path, *generated, *extraAnnotations |
| ) |
| ) |
| ) { |
| fail("Couldn't compile stub file -- compilation problems") |
| return |
| } |
| } |
| |
| if (checkDoclava1 && !CHECK_OLD_DOCLAVA_TOO) { |
| println( |
| "This test requested diffing with doclava1, but doclava1 testing was disabled with the " + |
| "DriverTest#CHECK_OLD_DOCLAVA_TOO = false" |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && |
| api != null && apiFile != null |
| ) { |
| apiFile.delete() |
| checkSignaturesWithDoclava( |
| api = api, |
| argument = "-api", |
| output = apiFile, |
| expected = apiFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && apiXml != null && apiXmlFile != null) { |
| apiXmlFile.delete() |
| |
| // Either we write the signatureSource, or you must have specified an API report |
| val signatureFile: File = |
| apiFile ?: if (signatureSource != null) { |
| val temp = temporaryFolder.newFile("jdiff-doclava-api.txt") |
| temp.writeText(signatureSource.trimIndent(), Charsets.UTF_8) |
| temp |
| } else { |
| fail("When verifying XML files with doclava you must either specify signatureSource or api") |
| error("Unreachable") |
| } |
| |
| // Need to emit the codebase |
| generateJDiffXmlWithDoclava1(signatureFile, apiXmlFile) |
| |
| val actualText = cleanupString(readFile(apiXmlFile, stripBlankLines, trim), project, true) |
| assertEquals(stripComments(apiXml, stripLineComments = false).trimIndent(), actualText) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && |
| exactApi != null && exactApiFile != null |
| ) { |
| exactApiFile.delete() |
| checkSignaturesWithDoclava( |
| api = exactApi, |
| argument = "-exactApi", |
| output = exactApiFile, |
| expected = exactApiFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && |
| removedApi != null && removedApiFile != null |
| ) { |
| removedApiFile.delete() |
| checkSignaturesWithDoclava( |
| api = removedApi, |
| argument = "-removedApi", |
| output = removedApiFile, |
| expected = removedApiFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && stubsDir != null) { |
| stubsDir.deleteRecursively() |
| val firstFile = File(stubsDir, sourceFiles[0].targetPath.substring("src/".length)) |
| checkSignaturesWithDoclava( |
| api = stubs[0], |
| argument = "-stubs", |
| output = stubsDir, |
| expected = firstFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && proguard != null && proguardFile != null) { |
| proguardFile.delete() |
| checkSignaturesWithDoclava( |
| api = proguard, |
| argument = "-proguard", |
| output = proguardFile, |
| expected = proguardFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && |
| privateApi != null && privateApiFile != null |
| ) { |
| privateApiFile.delete() |
| checkSignaturesWithDoclava( |
| api = privateApi, |
| argument = "-privateApi", |
| output = privateApiFile, |
| expected = privateApiFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| // Workaround: -privateApi is a no-op if you don't also provide -api |
| extraDoclavaArguments = arrayOf("-api", File(privateApiFile.parentFile, "dummy-api.txt").path), |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && |
| privateDexApi != null && privateDexApiFile != null |
| ) { |
| privateDexApiFile.delete() |
| checkSignaturesWithDoclava( |
| api = privateDexApi, |
| argument = "-privateDexApi", |
| output = privateDexApiFile, |
| expected = privateDexApiFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| // Workaround: -privateDexApi is a no-op if you don't also provide -api |
| extraDoclavaArguments = arrayOf("-api", File(privateDexApiFile.parentFile, "dummy-api.txt").path), |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && |
| dexApi != null && dexApiFile != null |
| ) { |
| dexApiFile.delete() |
| checkSignaturesWithDoclava( |
| api = dexApi, |
| argument = "-dexApi", |
| output = dexApiFile, |
| expected = dexApiFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| // Workaround: -dexApi is a no-op if you don't also provide -api |
| extraDoclavaArguments = arrayOf("-api", File(dexApiFile.parentFile, "dummy-api.txt").path), |
| showUnannotated = showUnannotated, |
| project = project, |
| extraArguments = extraArguments |
| ) |
| } |
| |
| if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && |
| dexApiMapping != null && dexApiMappingFile != null |
| ) { |
| dexApiMappingFile.delete() |
| checkSignaturesWithDoclava( |
| api = dexApiMapping, |
| argument = "-apiMapping", |
| output = dexApiMappingFile, |
| expected = dexApiMappingFile, |
| sourceList = sourceList, |
| sourcePath = sourcePath, |
| packages = packages, |
| androidJar = androidJar, |
| trim = trim, |
| stripBlankLines = stripBlankLines, |
| showAnnotationArgs = showAnnotationArguments, |
| stubImportPackages = importedPackages, |
| // Workaround: -apiMapping is a no-op if you don't also provide -api |
| extraDoclavaArguments = arrayOf("-api", File(dexApiMappingFile.parentFile, "dummy-api.txt").path), |
| showUnannotated = showUnannotated, |
| project = project, |
| skipTestRoot = true, |
| extraArguments = extraArguments |
| ) |
| } |
| } |
| |
| /** Checks that the given zip annotations file contains the given XML package contents */ |
| private fun assertPackageXml(pkg: String, output: File, @Language("XML") expected: String) { |
| assertNotNull(output) |
| assertTrue(output.exists()) |
| val url = URL( |
| "jar:" + SdkUtils.fileToUrlString(output) + "!/" + pkg.replace('.', '/') + |
| "/annotations.xml" |
| ) |
| val stream = url.openStream() |
| try { |
| val bytes = ByteStreams.toByteArray(stream) |
| assertNotNull(bytes) |
| val xml = String(bytes, Charsets.UTF_8).replace("\r\n", "\n") |
| assertEquals(expected.trimIndent().trim(), xml.trimIndent().trim()) |
| } finally { |
| Closeables.closeQuietly(stream) |
| } |
| } |
| |
| /** Hides path prefixes from /tmp folders used by the testing infrastructure */ |
| private fun cleanupString(string: String, project: File?, dropTestRoot: Boolean = false): String { |
| var s = string |
| |
| if (project != null) { |
| s = s.replace(project.path, "TESTROOT") |
| s = s.replace(project.canonicalPath, "TESTROOT") |
| } |
| |
| s = s.replace(temporaryFolder.root.path, "TESTROOT") |
| |
| val tmp = System.getProperty("java.io.tmpdir") |
| if (tmp != null) { |
| s = s.replace(tmp, "TEST") |
| } |
| |
| s = s.trim() |
| |
| if (dropTestRoot) { |
| s = s.replace("TESTROOT/", "") |
| } |
| |
| return s |
| } |
| |
| private fun generateJDiffXmlWithDoclava1(signatureFile: File, xmlOutput: File) { |
| val docLava1 = findDoclava() |
| |
| val args = arrayOf( |
| "-convert2xml", |
| signatureFile.path, |
| xmlOutput.path |
| ) |
| |
| val message = "\n${args.joinToString(separator = "\n") { "\"$it\"," }}" |
| println("Running doclava1 with the following args:\n$message") |
| |
| val jdkPath = findJdk() |
| if (!runCommand( |
| "$jdkPath/bin/java", |
| arrayOf( |
| "-classpath", |
| "${docLava1.path}:$jdkPath/lib/tools.jar", |
| "com.google.doclava.apicheck.ApiCheck", |
| *args |
| ) |
| ) |
| ) { |
| return |
| } |
| } |
| |
| private fun checkSignaturesWithDoclava( |
| api: String, |
| argument: String, |
| output: File, |
| expected: File = output, |
| sourceList: Array<String>, |
| sourcePath: String, |
| packages: Set<String>, |
| androidJar: File, |
| trim: Boolean = true, |
| stripBlankLines: Boolean = true, |
| showAnnotationArgs: Array<String> = emptyArray(), |
| stubImportPackages: List<String>, |
| extraArguments: Array<String>, |
| extraDoclavaArguments: Array<String> = emptyArray(), |
| showUnannotated: Boolean, |
| project: File, |
| skipTestRoot: Boolean = false |
| ) { |
| // We have to run Doclava out of process because running it in process |
| // (with Doclava1 jars on the test classpath) only works once; it leaves |
| // around state such that the second test fails. Instead we invoke it |
| // separately on each test; slower but reliable. |
| |
| val doclavaArg = when (argument) { |
| ARG_API -> "-api" |
| ARG_REMOVED_API -> "-removedApi" |
| else -> if (argument.startsWith("--")) argument.substring(1) else argument |
| } |
| |
| val showAnnotationArgsDoclava1: Array<String> = if (showAnnotationArgs.isNotEmpty() || extraArguments.isNotEmpty()) { |
| val shown = mutableListOf<String>() |
| extraArguments.forEachIndexed { index, s -> |
| if (s == ARG_SHOW_ANNOTATION) { |
| shown += "-showAnnotation" |
| shown += extraArguments[index + 1] |
| } |
| } |
| showAnnotationArgs.forEach { s -> |
| shown += if (s == ARG_SHOW_ANNOTATION) { |
| "-showAnnotation" |
| } else { |
| s |
| } |
| } |
| shown.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| val hideAnnotationArgsDoclava1: Array<String> = if (extraArguments.isNotEmpty()) { |
| val hidden = mutableListOf<String>() |
| extraArguments.forEachIndexed { index, s -> |
| if (s == ARG_HIDE_ANNOTATION) { |
| hidden += "-hideAnnotation" |
| hidden += extraArguments[index + 1] |
| } |
| } |
| hidden.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| val showUnannotatedArgs = if (showUnannotated) { |
| arrayOf("-showUnannotated") |
| } else { |
| emptyArray() |
| } |
| |
| val docLava1 = findDoclava() |
| val jdkPath = findJdk() |
| |
| val hidePackageArgs = mutableListOf<String>() |
| options.hidePackages.forEach { |
| hidePackageArgs.add("-hidePackage") |
| hidePackageArgs.add(it) |
| } |
| |
| val stubImports = if (stubImportPackages.isNotEmpty()) { |
| arrayOf("-stubimportpackages", stubImportPackages.joinToString(separator = ":") { it }) |
| } else { |
| emptyArray() |
| } |
| |
| val args = arrayOf( |
| *sourceList, |
| "-stubpackages", |
| packages.joinToString(separator = ":") { it }, |
| *stubImports, |
| "-doclet", |
| "com.google.doclava.Doclava", |
| "-docletpath", |
| docLava1.path, |
| "-encoding", |
| "UTF-8", |
| "-source", |
| "1.8", |
| "-nodocs", |
| "-quiet", |
| "-sourcepath", |
| sourcePath, |
| "-classpath", |
| androidJar.path, |
| |
| *showAnnotationArgsDoclava1, |
| *hideAnnotationArgsDoclava1, |
| *showUnannotatedArgs, |
| *hidePackageArgs.toTypedArray(), |
| *extraDoclavaArguments, |
| |
| // -api, or // -stub, etc |
| doclavaArg, |
| output.path |
| ) |
| |
| val message = "\n${args.joinToString(separator = "\n") { "\"$it\"," }}" |
| println("Running doclava1 with the following args:\n$message") |
| |
| if (!runCommand( |
| "$jdkPath/bin/java", |
| arrayOf( |
| "-classpath", |
| "${docLava1.path}:$jdkPath/lib/tools.jar", |
| "com.google.doclava.Doclava", |
| *args |
| ) |
| ) |
| ) { |
| return |
| } |
| |
| // If there's a discrepancy between doclava1 and metalava, you can debug |
| // doclava; to do this, open the Doclava project, and take all the |
| // metalava arguments, drop the ones that don't apply and use the |
| // doclava-style names instead of metalava (e.g. -stubs instead of --stubs, |
| // -showAnnotation instead of --show-annotation, -sourcepath instead of |
| // --sourcepath, and so on. Finally, and most importantly, add these |
| // 4 arguments at the beginning: |
| // "-doclet", |
| // "com.google.doclava.Doclava", |
| // "-docletpath", |
| // "out/host/linux-x86/framework/jsilver.jar:out/host/linux-x86/framework/doclava.jar", |
| // ..and finally from your Main entry point take this array of strings |
| // and call Doclava.main(newArgs) |
| |
| val actualText = cleanupString(readFile(expected, stripBlankLines, trim), project, skipTestRoot) |
| assertEquals(stripComments(api, stripLineComments = false).trimIndent(), actualText) |
| } |
| |
| private fun findJdk(): String? { |
| val jdkPath = getJdkPath() |
| if (jdkPath == null) { |
| fail("JDK not found in the environment; make sure \$JAVA_HOME is set.") |
| } |
| return jdkPath |
| } |
| |
| private fun findDoclava(): File { |
| val docLava1 = File("testlibs/doclava-1.0.6-full-SNAPSHOT.jar") |
| if (!docLava1.isFile) { |
| /* |
| Not checked in (it's 22MB). |
| To generate the doclava1 jar, add this to external/doclava/build.gradle and run ./gradlew shadowJar: |
| |
| // shadow jar: Includes all dependencies |
| buildscript { |
| repositories { |
| jcenter() |
| } |
| dependencies { |
| classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.2' |
| } |
| } |
| apply plugin: 'com.github.johnrengelman.shadow' |
| shadowJar { |
| baseName = "doclava-$version-full-SNAPSHOT" |
| classifier = null |
| version = null |
| } |
| |
| and finally |
| $ cp ../../out/host/gradle/external/jdiff/build/libs/doclava-*-SNAPSHOT-full-SNAPSHOT.jar \ |
| testlibs/doclava-1.0.6-full-SNAPSHOT.jar |
| |
| */ |
| fail("Couldn't find $docLava1: Is the pwd set to the root of the metalava source code?") |
| } |
| return docLava1 |
| } |
| |
| private fun runCommand(executable: String, args: Array<String>): Boolean { |
| try { |
| val logger = StdLogger(StdLogger.Level.ERROR) |
| val processExecutor = DefaultProcessExecutor(logger) |
| val processInfo = ProcessInfoBuilder() |
| .setExecutable(executable) |
| .addArgs(args) |
| .createProcess() |
| |
| val processOutputHandler = LoggedProcessOutputHandler(logger) |
| val result = processExecutor.execute(processInfo, processOutputHandler) |
| |
| result.rethrowFailure().assertNormalExitValue() |
| } catch (e: ProcessException) { |
| fail("Failed to run $executable (${e.message}): not verifying this API on the old doclava engine") |
| return false |
| } |
| return true |
| } |
| |
| companion object { |
| const val API_LEVEL = 27 |
| |
| private val latestAndroidPlatform: String |
| get() = "android-$API_LEVEL" |
| |
| private val sdk: File |
| get() = File( |
| System.getenv("ANDROID_HOME") |
| ?: error("You must set \$ANDROID_HOME before running tests") |
| ) |
| |
| fun getAndroidJar(apiLevel: Int): File? { |
| val localFile = File("../../prebuilts/sdk/$apiLevel/public/android.jar") |
| if (localFile.exists()) { |
| return localFile |
| } else { |
| val androidJar = File("../../prebuilts/sdk/$apiLevel/android.jar") |
| if (androidJar.exists()) { |
| return androidJar |
| } |
| } |
| return null |
| } |
| |
| fun getPlatformFile(path: String): File { |
| return getAndroidJar(API_LEVEL) ?: run { |
| val file = FileUtils.join(sdk, SdkConstants.FD_PLATFORMS, latestAndroidPlatform, path) |
| if (!file.exists()) { |
| throw IllegalArgumentException( |
| "File \"$path\" not found in platform $latestAndroidPlatform" |
| ) |
| } |
| file |
| } |
| } |
| |
| fun java(@Language("JAVA") source: String): LintDetectorTest.TestFile { |
| return TestFiles.java(source.trimIndent()) |
| } |
| |
| fun kotlin(@Language("kotlin") source: String): LintDetectorTest.TestFile { |
| return TestFiles.kotlin(source.trimIndent()) |
| } |
| |
| fun kotlin(to: String, @Language("kotlin") source: String): LintDetectorTest.TestFile { |
| return TestFiles.kotlin(to, source.trimIndent()) |
| } |
| |
| private fun readFile(file: File, stripBlankLines: Boolean = false, trim: Boolean = false): String { |
| var apiLines: List<String> = Files.asCharSource(file, Charsets.UTF_8).readLines() |
| if (stripBlankLines) { |
| apiLines = apiLines.asSequence().filter { it.isNotBlank() }.toList() |
| } |
| var apiText = apiLines.joinToString(separator = "\n") { it } |
| if (trim) { |
| apiText = apiText.trim() |
| } |
| return apiText |
| } |
| } |
| } |
| |
| val intRangeAnnotationSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @Retention(SOURCE) |
| @Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE}) |
| public @interface IntRange { |
| long from() default Long.MIN_VALUE; |
| long to() default Long.MAX_VALUE; |
| } |
| """ |
| ).indented() |
| |
| val intDefAnnotationSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @Retention(SOURCE) |
| @Target({ANNOTATION_TYPE}) |
| public @interface IntDef { |
| int[] value() default {}; |
| boolean flag() default false; |
| } |
| """ |
| ).indented() |
| |
| val longDefAnnotationSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @Retention(SOURCE) |
| @Target({ANNOTATION_TYPE}) |
| public @interface LongDef { |
| long[] value() default {}; |
| boolean flag() default false; |
| } |
| """ |
| ).indented() |
| |
| val nonNullSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.Target; |
| |
| import static java.lang.annotation.ElementType.FIELD; |
| import static java.lang.annotation.ElementType.METHOD; |
| import static java.lang.annotation.ElementType.PARAMETER; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| /** |
| * Denotes that a parameter, field or method return value can never be null. |
| * @paramDoc This value must never be {@code null}. |
| * @returnDoc This value will never be {@code null}. |
| * @hide |
| */ |
| @SuppressWarnings({"WeakerAccess", "JavaDoc"}) |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface NonNull { |
| } |
| """ |
| ).indented() |
| |
| val libcoreNonNullSource: TestFile = DriverTest.java( |
| """ |
| package libcore.util; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| import java.lang.annotation.*; |
| @Documented |
| @Retention(SOURCE) |
| @Target({TYPE_USE}) |
| public @interface NonNull { |
| int from() default Integer.MIN_VALUE; |
| int to() default Integer.MAX_VALUE; |
| } |
| """ |
| ).indented() |
| |
| val libcoreNullableSource: TestFile = DriverTest.java( |
| """ |
| package libcore.util; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| import java.lang.annotation.*; |
| @Documented |
| @Retention(SOURCE) |
| @Target({TYPE_USE}) |
| public @interface Nullable { |
| int from() default Integer.MIN_VALUE; |
| int to() default Integer.MAX_VALUE; |
| } |
| """ |
| ).indented() |
| |
| val requiresPermissionSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @Retention(SOURCE) |
| @Target({ANNOTATION_TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER}) |
| public @interface RequiresPermission { |
| String value() default ""; |
| String[] allOf() default {}; |
| String[] anyOf() default {}; |
| boolean conditional() default false; |
| @Target({FIELD, METHOD, PARAMETER}) |
| @interface Read { |
| RequiresPermission value() default @RequiresPermission; |
| } |
| @Target({FIELD, METHOD, PARAMETER}) |
| @interface Write { |
| RequiresPermission value() default @RequiresPermission; |
| } |
| } |
| """ |
| ).indented() |
| |
| val requiresFeatureSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @Retention(SOURCE) |
| @Target({TYPE,FIELD,METHOD,CONSTRUCTOR}) |
| public @interface RequiresFeature { |
| String value(); |
| } |
| """ |
| ).indented() |
| |
| val requiresApiSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @Retention(SOURCE) |
| @Target({TYPE,FIELD,METHOD,CONSTRUCTOR}) |
| public @interface RequiresApi { |
| int value() default 1; |
| int api() default 1; |
| } |
| """ |
| ).indented() |
| |
| val sdkConstantSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.*; |
| @Target({ ElementType.FIELD }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SdkConstant { |
| enum SdkConstantType { |
| ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE |
| } |
| SdkConstantType value(); |
| } |
| """ |
| ).indented() |
| |
| val broadcastBehaviorSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.*; |
| /** @hide */ |
| @Target({ ElementType.FIELD }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface BroadcastBehavior { |
| boolean explicitOnly() default false; |
| boolean registeredOnly() default false; |
| boolean includeBackground() default false; |
| boolean protectedBroadcast() default false; |
| } |
| """ |
| ).indented() |
| |
| val nullableSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| /** |
| * Denotes that a parameter, field or method return value can be null. |
| * @paramDoc This value may be {@code null}. |
| * @returnDoc This value may be {@code null}. |
| * @hide |
| */ |
| @SuppressWarnings({"WeakerAccess", "JavaDoc"}) |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface Nullable { |
| } |
| """ |
| ).indented() |
| |
| val supportNonNullSource: TestFile = java( |
| """ |
| package android.support.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface NonNull { |
| } |
| """ |
| ).indented() |
| |
| val supportNullableSource: TestFile = java( |
| """ |
| package android.support.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface Nullable { |
| } |
| """ |
| ).indented() |
| |
| val androidxNonNullSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface NonNull { |
| } |
| """ |
| ).indented() |
| |
| val androidxNullableSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface Nullable { |
| } |
| """ |
| ).indented() |
| |
| val recentlyNonNullSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface RecentlyNonNull { |
| } |
| """ |
| ).indented() |
| |
| val recentlyNullableSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) |
| public @interface RecentlyNullable { |
| } |
| """ |
| ).indented() |
| |
| val supportParameterName: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD}) |
| public @interface ParameterName { |
| String value(); |
| } |
| """ |
| ).indented() |
| |
| val supportDefaultValue: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(SOURCE) |
| @Target({METHOD, PARAMETER, FIELD}) |
| public @interface DefaultValue { |
| String value(); |
| } |
| """ |
| ).indented() |
| |
| val uiThreadSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| /** |
| * Denotes that the annotated method or constructor should only be called on the |
| * UI thread. If the annotated element is a class, then all methods in the class |
| * should be called on the UI thread. |
| * @memberDoc This method must be called on the thread that originally created |
| * this UI element. This is typically the main thread of your app. |
| * @classDoc Methods in this class must be called on the thread that originally created |
| * this UI element, unless otherwise noted. This is typically the |
| * main thread of your app. * @hide |
| */ |
| @SuppressWarnings({"WeakerAccess", "JavaDoc"}) |
| @Retention(SOURCE) |
| @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) |
| public @interface UiThread { |
| } |
| """ |
| ).indented() |
| |
| val workerThreadSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| /** |
| * @memberDoc This method may take several seconds to complete, so it should |
| * only be called from a worker thread. |
| * @classDoc Methods in this class may take several seconds to complete, so it should |
| * only be called from a worker thread unless otherwise noted. |
| * @hide |
| */ |
| @SuppressWarnings({"WeakerAccess", "JavaDoc"}) |
| @Retention(SOURCE) |
| @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) |
| public @interface WorkerThread { |
| } |
| """ |
| ).indented() |
| |
| val suppressLintSource: TestFile = java( |
| """ |
| package android.annotation; |
| |
| import static java.lang.annotation.ElementType.*; |
| import java.lang.annotation.*; |
| @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) |
| @Retention(RetentionPolicy.CLASS) |
| public @interface SuppressLint { |
| String[] value(); |
| } |
| """ |
| ).indented() |
| |
| val systemServiceSource: TestFile = java( |
| """ |
| package android.annotation; |
| import static java.lang.annotation.ElementType.TYPE; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| import java.lang.annotation.*; |
| @Retention(SOURCE) |
| @Target(TYPE) |
| public @interface SystemService { |
| String value(); |
| } |
| """ |
| ).indented() |
| |
| val systemApiSource: TestFile = java( |
| """ |
| package android.annotation; |
| import static java.lang.annotation.ElementType.*; |
| import java.lang.annotation.*; |
| @Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SystemApi { |
| } |
| """ |
| ).indented() |
| |
| val testApiSource: TestFile = java( |
| """ |
| package android.annotation; |
| import static java.lang.annotation.ElementType.*; |
| import java.lang.annotation.*; |
| @Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface TestApi { |
| } |
| """ |
| ).indented() |
| |
| val widgetSource: TestFile = java( |
| """ |
| package android.annotation; |
| import java.lang.annotation.*; |
| @Target({ ElementType.TYPE }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface Widget { |
| } |
| """ |
| ).indented() |
| |
| val restrictToSource: TestFile = java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.*; |
| @SuppressWarnings("WeakerAccess") |
| @Retention(CLASS) |
| @Target({ANNOTATION_TYPE, TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE}) |
| public @interface RestrictTo { |
| Scope[] value(); |
| enum Scope { |
| LIBRARY, |
| LIBRARY_GROUP, |
| /** @deprecated */ |
| @Deprecated |
| GROUP_ID, |
| TESTS, |
| SUBCLASSES, |
| } |
| } |
| """ |
| ).indented() |