| /* |
| * 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_TXT |
| import com.android.SdkConstants.DOT_XML |
| 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.LintCliClient |
| import com.android.tools.lint.UastEnvironment |
| import com.android.tools.lint.checks.ApiLookup |
| import com.android.tools.lint.checks.infrastructure.ClassName |
| import com.android.tools.lint.checks.infrastructure.TestFile |
| import com.android.tools.lint.checks.infrastructure.TestFiles.java |
| import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin |
| import com.android.tools.lint.checks.infrastructure.stripComments |
| import com.android.tools.lint.client.api.LintClient |
| import com.android.tools.metalava.model.FileFormat |
| import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS |
| import com.android.tools.metalava.model.text.ApiClassResolution |
| import com.android.tools.metalava.model.text.ApiFile |
| import com.android.tools.metalava.reporter.Severity |
| import com.android.tools.metalava.testing.TemporaryFolderOwner |
| import com.android.tools.metalava.testing.findKotlinStdlibPaths |
| import com.android.tools.metalava.testing.getAndroidJar |
| import com.android.tools.metalava.xml.parseDocument |
| import com.android.utils.SdkUtils |
| import com.android.utils.StdLogger |
| import com.google.common.io.ByteStreams |
| import com.google.common.io.Closeables |
| import com.google.common.io.Files |
| import com.intellij.openapi.util.Disposer |
| import java.io.ByteArrayOutputStream |
| import java.io.File |
| import java.io.FileNotFoundException |
| import java.io.PrintStream |
| import java.io.PrintWriter |
| import java.io.StringWriter |
| import java.net.URL |
| import kotlin.text.Charsets.UTF_8 |
| 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.ErrorCollector |
| import org.junit.rules.TemporaryFolder |
| |
| const val CHECK_JDIFF = false |
| |
| abstract class DriverTest : TemporaryFolderOwner { |
| @get:Rule override val temporaryFolder = TemporaryFolder() |
| |
| @get:Rule val errorCollector = ErrorCollector() |
| |
| @Before |
| fun setup() { |
| System.setProperty(ENV_VAR_METALAVA_TESTS_RUNNING, SdkConstants.VALUE_TRUE) |
| Disposer.setDebugMode(true) |
| } |
| |
| // Makes a note to fail the test, but still allows the test to complete before failing |
| protected fun addError(error: String) { |
| errorCollector.addError(Throwable(error)) |
| } |
| |
| protected fun getApiFile(): File { |
| return File(temporaryFolder.root.path, "public-api.txt") |
| } |
| |
| protected fun runDriver(vararg args: String, expectedFail: String = ""): String { |
| resetTicker() |
| |
| // Capture the actual input and output from System.out/err and compare it |
| // to the output printed through the official writer; they should be the same, |
| // otherwise we have stray println's littered in the code! |
| val previousOut = System.out |
| val previousErr = System.err |
| try { |
| val output = TeeWriter(previousOut) |
| System.setOut(PrintStream(output)) |
| val error = TeeWriter(previousErr) |
| System.setErr(PrintStream(error)) |
| |
| val sw = StringWriter() |
| val writer = PrintWriter(sw) |
| |
| Disposer.setDebugMode(true) |
| |
| if (run(arrayOf(*args), writer, writer)) { |
| assertTrue( |
| "Test expected to fail but didn't. Expected failure: $expectedFail", |
| expectedFail.isEmpty() |
| ) |
| } else { |
| val actualFail = cleanupString(sw.toString(), null) |
| if ( |
| cleanupString(expectedFail, null).replace(".", "").trim() != |
| actualFail.replace(".", "").trim() |
| ) { |
| val reportedCompatError = |
| actualFail.startsWith( |
| "Aborting: Found compatibility problems checking the " |
| ) |
| if ( |
| expectedFail == "Aborting: Found compatibility problems" && |
| reportedCompatError |
| ) { |
| // 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 { |
| if (reportedCompatError) { |
| // if a compatibility error was unexpectedly reported, then mark that as |
| // an error but keep going so we can see the actual compatibility error |
| if (expectedFail.trimIndent() != actualFail) { |
| addError( |
| "ComparisonFailure: expected failure $expectedFail, actual $actualFail" |
| ) |
| } |
| } else { |
| // no compatibility error; check for other errors now, and |
| // if one is found, fail right away |
| assertEquals(expectedFail.trimIndent(), actualFail) |
| } |
| } |
| } |
| } |
| |
| val stdout = output.toString(UTF_8.name()) |
| if (stdout.isNotEmpty()) { |
| addError("Unexpected write to stdout:\n $stdout") |
| } |
| val stderr = error.toString(UTF_8.name()) |
| if (stderr.isNotEmpty()) { |
| addError("Unexpected write to stderr:\n $stderr") |
| } |
| |
| val printedOutput = sw.toString() |
| if (printedOutput.isNotEmpty() && printedOutput.trim().isEmpty()) { |
| fail("Printed newlines with nothing else") |
| } |
| |
| UastEnvironment.checkApplicationEnvironmentDisposed() |
| Disposer.assertIsEmpty(true) |
| |
| return printedOutput |
| } finally { |
| System.setOut(previousOut) |
| System.setErr(previousErr) |
| } |
| } |
| |
| // This is here so we can keep a record of what was printed, to make sure we |
| // don't have any unexpected printlns in the source that are left behind after |
| // debugging and pollute the production output |
| class TeeWriter(private val otherStream: PrintStream) : ByteArrayOutputStream() { |
| override fun write(b: ByteArray?, off: Int, len: Int) { |
| otherStream.write(b, off, len) |
| super.write(b, off, len) |
| } |
| |
| override fun write(b: ByteArray?) { |
| otherStream.write(b) |
| super.write(b) |
| } |
| |
| override fun write(b: Int) { |
| otherStream.write(b) |
| super.write(b) |
| } |
| } |
| |
| 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") |
| } |
| |
| private fun <T> buildOptionalArgs(option: T?, converter: (T) -> Array<String>): Array<String> { |
| return if (option != null) { |
| converter(option) |
| } else { |
| emptyArray() |
| } |
| } |
| |
| /** File conversion tasks */ |
| data class ConvertData( |
| val fromApi: String, |
| val outputFile: String, |
| val baseApi: String? = null, |
| val strip: Boolean = true, |
| ) |
| |
| protected fun check( |
| /** 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 DEX API (corresponds to --dex-api) */ |
| dexApi: String? = null, |
| /** The removed API (corresponds to --removed-api) */ |
| removedApi: String? = null, |
| /** The overloaded method order, defaults to signature. */ |
| overloadedMethodOrder: Options.OverloadedMethodOrder? = |
| Options.OverloadedMethodOrder.SIGNATURE, |
| /** The subtract api signature content (corresponds to --subtract-api) */ |
| @Language("TEXT") subtractApi: String? = null, |
| /** Expected stubs (corresponds to --stubs) */ |
| stubFiles: Array<TestFile> = emptyArray(), |
| /** Stub source file list generated */ |
| stubsSourceList: String? = null, |
| /** Doc Stub source file list generated */ |
| docStubsSourceList: 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, |
| /** Signature file format */ |
| format: FileFormat = FileFormat.latest, |
| /** 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, |
| /** All expected issues to be generated when analyzing these sources */ |
| expectedIssues: String? = "", |
| /** Expected [Severity.ERROR] issues to be generated when analyzing these sources */ |
| errorSeverityExpectedIssues: String? = null, |
| 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, |
| /** Optional API signature files content to load **instead** of Java/Kotlin source files */ |
| @Language("TEXT") signatureSources: Array<String> = emptyArray(), |
| apiClassResolution: ApiClassResolution = ApiClassResolution.API, |
| /** |
| * An optional API signature file content to load **instead** of Java/Kotlin source files. |
| * This is added to [signatureSources]. This argument exists for backward compatibility. |
| */ |
| @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 last released API's compatibility with */ |
| @Language("TEXT") checkCompatibilityApiReleased: 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 use as the base API codebase during compat checks */ |
| @Language("TEXT") checkCompatibilityBaseApi: String? = null, |
| @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(), |
| /** "Show for stub purposes" API annotation ([ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION]) */ |
| showForStubPurposesAnnotations: Array<String> = emptyArray(), |
| /** Hide annotations (--hide-annotation arguments) */ |
| hideAnnotations: Array<String> = emptyArray(), |
| /** Hide meta-annotations (--hide-meta-annotation arguments) */ |
| hideMetaAnnotations: Array<String> = emptyArray(), |
| /** No compat check meta-annotations (--no-compat-check-meta-annotation arguments) */ |
| suppressCompatibilityMetaAnnotations: 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 = format.useKotlinStyleNulls(), |
| /** Expected output (stdout and stderr combined). If null, don't check. */ |
| expectedOutput: String? = null, |
| /** Expected fail message and state, if any */ |
| expectedFail: String? = 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, |
| /** |
| * Extract annotations and check that the given packages contain the given extracted XML |
| * files |
| */ |
| extractAnnotations: Map<String, String>? = null, |
| /** |
| * Creates the nullability annotations validator, and check that the report has the given |
| * lines (does not define files to be validated) |
| */ |
| validateNullability: Set<String>? = null, |
| /** Enable nullability validation for the listed classes */ |
| validateNullabilityFromList: String? = null, |
| /** 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<ConvertData> = emptyList(), |
| /** Hook for performing additional initialization of the project directory */ |
| projectSetup: ((File) -> Unit)? = null, |
| /** Content of the baseline file to use, if any */ |
| baseline: String? = null, |
| /** |
| * If non-null, we expect the baseline file to be updated to this. [baseline] must also be |
| * set. |
| */ |
| updateBaseline: String? = null, |
| /** Merge instead of replacing the baseline */ |
| mergeBaseline: String? = null, |
| |
| /** [ARG_BASELINE_API_LINT] */ |
| baselineApiLint: String? = null, |
| /** [ARG_UPDATE_BASELINE_API_LINT] */ |
| updateBaselineApiLint: String? = null, |
| |
| /** [ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED] */ |
| baselineCheckCompatibilityReleased: String? = null, |
| /** [ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED] */ |
| updateBaselineCheckCompatibilityReleased: String? = null, |
| |
| /** [ARG_ERROR_MESSAGE_API_LINT] */ |
| errorMessageApiLint: String? = null, |
| /** [ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED] */ |
| errorMessageCheckCompatibilityReleased: String? = null, |
| |
| /** |
| * If non null, enable API lint. If non-blank, a codebase where only new APIs not in the |
| * codebase are linted. |
| */ |
| @Language("TEXT") apiLint: String? = null, |
| /** The source files to pass to the analyzer */ |
| sourceFiles: Array<TestFile> = emptyArray(), |
| /** [ARG_REPEAT_ERRORS_MAX] */ |
| repeatErrorsMax: Int = 0 |
| ) { |
| // 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() |
| } |
| |
| // Ensure that lint infrastructure (for UAST) knows it's dealing with a test |
| LintCliClient(LintClient.CLIENT_UNIT_TESTS) |
| |
| defaultConfiguration.reset() |
| |
| val actualExpectedFail = |
| when { |
| expectedFail != null -> expectedFail |
| (checkCompatibilityApiReleased != null || |
| checkCompatibilityRemovedApiReleased != null) && |
| expectedIssues != null && |
| expectedIssues.trim().isNotEmpty() -> { |
| "Aborting: Found compatibility problems" |
| } |
| else -> "" |
| } |
| |
| // Unit test which checks that a signature file is as expected |
| val androidJar = getAndroidJar() |
| |
| val project = createProject(sourceFiles) |
| |
| val sourcePathDir = File(project, "src") |
| if (!sourcePathDir.isDirectory) { |
| sourcePathDir.mkdirs() |
| } |
| |
| var sourcePath = sourcePathDir.path |
| |
| // Make it easy to configure a source path with more than one source root: src and src2 |
| if (sourceFiles.any { it.targetPath.startsWith("src2") }) { |
| sourcePath = sourcePath + File.pathSeparator + sourcePath + "2" |
| } |
| |
| val apiClassResolutionArgs = |
| arrayOf(ARG_API_CLASS_RESOLUTION, apiClassResolution.optionValue) |
| |
| val sourceList = |
| if (signatureSources.isNotEmpty() || signatureSource != null) { |
| sourcePathDir.mkdirs() |
| |
| // if signatureSource is set, add it to signatureSources. |
| val sources = signatureSources.toMutableList() |
| signatureSource?.let { sources.add(it) } |
| |
| var num = 0 |
| val args = mutableListOf<String>() |
| sources.forEach { file -> |
| val signatureFile = |
| File(project, "load-api${ if (++num == 1) "" else num.toString() }.txt") |
| signatureFile.writeText(file.trimIndent()) |
| args.add(signatureFile.path) |
| } |
| if (!includeStrippedSuperclassWarnings) { |
| args.add(ARG_HIDE) |
| args.add("HiddenSuperclass") // Suppress warning #111 |
| } |
| args.toTypedArray() |
| } 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 allReportedIssues = StringBuilder() |
| val errorSeverityReportedIssues = StringBuilder() |
| DefaultReporter.rootFolder = project |
| DefaultReporter.reportPrinter = { message, severity -> |
| val cleanedUpMessage = cleanupString(message, project).trim() |
| if (severity == Severity.ERROR) { |
| errorSeverityReportedIssues.append(cleanedUpMessage).append('\n') |
| } |
| allReportedIssues.append(cleanedUpMessage).append('\n') |
| } |
| |
| val mergeAnnotationsArgs = |
| if (mergeXmlAnnotations != null) { |
| val merged = File(project, "merged-annotations.xml") |
| merged.writeText(mergeXmlAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val signatureAnnotationsArgs = |
| if (mergeSignatureAnnotations != null) { |
| val merged = File(project, "merged-annotations.txt") |
| merged.writeText(mergeSignatureAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val javaStubAnnotationsArgs = |
| if (mergeJavaStubAnnotations != null) { |
| // We need to place the qualifier class into its proper package location |
| // to make the parsing machinery happy |
| val cls = ClassName(mergeJavaStubAnnotations) |
| val pkg = cls.packageName |
| val relative = pkg?.replace('.', File.separatorChar) ?: "." |
| val merged = File(project, "qualifier/$relative/${cls.className}.java") |
| merged.parentFile.mkdirs() |
| merged.writeText(mergeJavaStubAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val inclusionAnnotationsArgs = |
| if (mergeInclusionAnnotations != null) { |
| val cls = ClassName(mergeInclusionAnnotations) |
| val pkg = cls.packageName |
| val relative = pkg?.replace('.', File.separatorChar) ?: "." |
| val merged = File(project, "inclusion/$relative/${cls.className}.java") |
| merged.parentFile?.mkdirs() |
| merged.writeText(mergeInclusionAnnotations.trimIndent()) |
| arrayOf(ARG_MERGE_INCLUSION_ANNOTATIONS, merged.path) |
| } else { |
| emptyArray() |
| } |
| |
| val apiLintArgs = |
| if (apiLint != null) { |
| if (apiLint.isBlank()) { |
| arrayOf(ARG_API_LINT) |
| } else { |
| val file = File(project, "prev-api-lint.txt") |
| file.writeText(apiLint.trimIndent()) |
| arrayOf(ARG_API_LINT, file.path) |
| } |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityApiReleasedFile = |
| if (checkCompatibilityApiReleased != null) { |
| val jar = File(checkCompatibilityApiReleased) |
| if (jar.isFile) { |
| jar |
| } else { |
| val file = File(project, "released-api.txt") |
| file.writeText(checkCompatibilityApiReleased.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") |
| file.writeText(checkCompatibilityRemovedApiReleased.trimIndent()) |
| file |
| } |
| } else { |
| null |
| } |
| |
| val checkCompatibilityBaseApiFile = |
| if (checkCompatibilityBaseApi != null) { |
| val maybeFile = File(checkCompatibilityBaseApi) |
| if (maybeFile.isFile) { |
| maybeFile |
| } else { |
| val file = File(project, "compatibility-base-api.txt") |
| file.writeText(checkCompatibilityBaseApi.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") |
| file.writeText(migrateNullsApi.trimIndent()) |
| file |
| } |
| } else { |
| null |
| } |
| |
| val manifestFileArgs = |
| if (manifest != null) { |
| val file = File(project, "manifest.xml") |
| file.writeText(manifest.trimIndent()) |
| arrayOf(ARG_MANIFEST, file.path) |
| } else { |
| emptyArray() |
| } |
| |
| val migrateNullsArguments = |
| if (migrateNullsApiFile != null) { |
| arrayOf(ARG_MIGRATE_NULLNESS, migrateNullsApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityApiReleasedArguments = |
| if (checkCompatibilityApiReleasedFile != null) { |
| arrayOf( |
| ARG_CHECK_COMPATIBILITY_API_RELEASED, |
| checkCompatibilityApiReleasedFile.path |
| ) |
| } else { |
| emptyArray() |
| } |
| |
| val checkCompatibilityBaseApiArguments = |
| if (checkCompatibilityBaseApiFile != null) { |
| arrayOf(ARG_CHECK_COMPATIBILITY_BASE_API, checkCompatibilityBaseApiFile.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() |
| } |
| |
| 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 showForStubPurposesAnnotationArguments = |
| if (showForStubPurposesAnnotations.isNotEmpty()) { |
| val args = mutableListOf<String>() |
| for (annotation in showForStubPurposesAnnotations) { |
| args.add(ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION) |
| args.add(annotation) |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| val hideMetaAnnotationArguments = |
| if (hideMetaAnnotations.isNotEmpty()) { |
| val args = mutableListOf<String>() |
| for (annotation in hideMetaAnnotations) { |
| args.add(ARG_HIDE_META_ANNOTATION) |
| args.add(annotation) |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| val suppressCompatMetaAnnotationArguments = |
| if (suppressCompatibilityMetaAnnotations.isNotEmpty()) { |
| val args = mutableListOf<String>() |
| for (annotation in suppressCompatibilityMetaAnnotations) { |
| args.add(ARG_SUPPRESS_COMPATIBILITY_META_ANNOTATION) |
| args.add(annotation) |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| val showUnannotatedArgs = |
| if (showUnannotated) { |
| arrayOf(ARG_SHOW_UNANNOTATED) |
| } else { |
| emptyArray() |
| } |
| |
| var removedApiFile: File? = null |
| val removedArgs = |
| if (removedApi != null) { |
| removedApiFile = temporaryFolder.newFile("removed.txt") |
| arrayOf(ARG_REMOVED_API, removedApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| // Always pass apiArgs and generate API text file in runDriver |
| var apiFile: File = newFile("public-api.txt") |
| val apiArgs = arrayOf(ARG_API, apiFile.path) |
| |
| val overloadedMethodArgs = |
| if (overloadedMethodOrder == null) { |
| emptyArray() |
| } else { |
| arrayOf(ARG_API_OVERLOADED_METHOD_ORDER, overloadedMethodOrder.name.lowercase()) |
| } |
| |
| 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 dexApiFile: File? = null |
| val dexApiArgs = |
| if (dexApi != null) { |
| dexApiFile = temporaryFolder.newFile("public-dex.txt") |
| arrayOf(ARG_DEX_API, dexApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val subtractApiFile: File? |
| val subtractApiArgs = |
| if (subtractApi != null) { |
| subtractApiFile = temporaryFolder.newFile("subtract-api.txt") |
| subtractApiFile.writeText(subtractApi.trimIndent()) |
| arrayOf(ARG_SUBTRACT_API, subtractApiFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val convertFiles = mutableListOf<ConvertFile>() |
| val convertArgs = |
| if (convertToJDiff.isNotEmpty()) { |
| val args = mutableListOf<String>() |
| var index = 1 |
| for (convert in convertToJDiff) { |
| val signature = convert.fromApi |
| val base = convert.baseApi |
| val convertSig = temporaryFolder.newFile("convert-signatures$index.txt") |
| convertSig.writeText(signature.trimIndent(), UTF_8) |
| val extension = DOT_XML |
| val output = temporaryFolder.newFile("convert-output$index$extension") |
| val baseFile = |
| if (base != null) { |
| val baseFile = |
| temporaryFolder.newFile("convert-signatures$index-base.txt") |
| baseFile.writeText(base.trimIndent(), UTF_8) |
| baseFile |
| } else { |
| null |
| } |
| convertFiles += ConvertFile(convertSig, output, baseFile, strip = true) |
| index++ |
| |
| if (baseFile != null) { |
| args += |
| when { |
| convert.strip -> "-new_api" |
| else -> ARG_CONVERT_NEW_TO_JDIFF |
| } |
| args += baseFile.path |
| } else { |
| args += |
| when { |
| convert.strip -> "-convert2xml" |
| else -> ARG_CONVERT_TO_JDIFF |
| } |
| } |
| args += convertSig.path |
| args += output.path |
| } |
| args.toTypedArray() |
| } else { |
| emptyArray() |
| } |
| |
| var stubsDir: File? = null |
| val stubsArgs = |
| if (stubFiles.isNotEmpty()) { |
| stubsDir = 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() |
| } |
| |
| var docStubsSourceListFile: File? = null |
| val docStubsSourceListArgs = |
| if (docStubsSourceList != null) { |
| docStubsSourceListFile = temporaryFolder.newFile("droiddoc-doc-src-list") |
| arrayOf(ARG_DOC_STUBS_SOURCE_LIST, docStubsSourceListFile.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") |
| applyApiLevelsXmlFile?.writeText(applyApiLevelsXml.trimIndent()) |
| arrayOf(ARG_APPLY_API_LEVELS, applyApiLevelsXmlFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| fun buildBaselineArgs( |
| argBaseline: String, |
| argUpdateBaseline: String, |
| argMergeBaseline: String, |
| filename: String, |
| baselineContent: String?, |
| updateContent: String?, |
| merge: Boolean |
| ): Pair<Array<String>, File?> { |
| if (baselineContent != null) { |
| val baselineFile = temporaryFolder.newFile(filename) |
| baselineFile?.writeText(baselineContent.trimIndent()) |
| if (!(updateContent != null || merge)) { |
| return Pair(arrayOf(argBaseline, baselineFile.path), baselineFile) |
| } else { |
| return Pair( |
| arrayOf( |
| argBaseline, |
| baselineFile.path, |
| if (mergeBaseline != null) argMergeBaseline else argUpdateBaseline, |
| baselineFile.path |
| ), |
| baselineFile |
| ) |
| } |
| } else { |
| return Pair(emptyArray(), null) |
| } |
| } |
| |
| val (baselineArgs, baselineFile) = |
| buildBaselineArgs( |
| ARG_BASELINE, |
| ARG_UPDATE_BASELINE, |
| ARG_MERGE_BASELINE, |
| "baseline.txt", |
| baseline, |
| updateBaseline, |
| mergeBaseline != null |
| ) |
| val (baselineApiLintArgs, baselineApiLintFile) = |
| buildBaselineArgs( |
| ARG_BASELINE_API_LINT, |
| ARG_UPDATE_BASELINE_API_LINT, |
| "", |
| "baseline-api-lint.txt", |
| baselineApiLint, |
| updateBaselineApiLint, |
| false |
| ) |
| val (baselineCheckCompatibilityReleasedArgs, baselineCheckCompatibilityReleasedFile) = |
| buildBaselineArgs( |
| ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED, |
| ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED, |
| "", |
| "baseline-check-released.txt", |
| baselineCheckCompatibilityReleased, |
| updateBaselineCheckCompatibilityReleased, |
| false |
| ) |
| |
| 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 kotlinPathArgs = findKotlinStdlibPathArgs(sourceList) |
| |
| 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 extractedAnnotationsZip: File? |
| val extractAnnotationsArgs = |
| if (extractAnnotations != null) { |
| extractedAnnotationsZip = temporaryFolder.newFile("extracted-annotations.zip") |
| arrayOf(ARG_EXTRACT_ANNOTATIONS, extractedAnnotationsZip.path) |
| } else { |
| extractedAnnotationsZip = null |
| emptyArray() |
| } |
| |
| val validateNullabilityTxt: File? |
| val validateNullabilityArgs = |
| if (validateNullability != null) { |
| validateNullabilityTxt = temporaryFolder.newFile("validate-nullability.txt") |
| arrayOf( |
| ARG_NULLABILITY_WARNINGS_TXT, |
| validateNullabilityTxt.path, |
| ARG_NULLABILITY_ERRORS_NON_FATAL // for testing, report on errors instead of |
| // throwing |
| ) |
| } else { |
| validateNullabilityTxt = null |
| emptyArray() |
| } |
| val validateNullabilityFromListFile: File? |
| val validateNullabilityFromListArgs = |
| if (validateNullabilityFromList != null) { |
| validateNullabilityFromListFile = |
| temporaryFolder.newFile("validate-nullability-classes.txt") |
| validateNullabilityFromListFile.writeText(validateNullabilityFromList) |
| arrayOf(ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullabilityFromListFile.path) |
| } else { |
| emptyArray() |
| } |
| |
| val errorMessageApiLintArgs = |
| buildOptionalArgs(errorMessageApiLint) { arrayOf(ARG_ERROR_MESSAGE_API_LINT, it) } |
| val errorMessageCheckCompatibilityReleasedArgs = |
| buildOptionalArgs(errorMessageCheckCompatibilityReleased) { |
| arrayOf(ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED, it) |
| } |
| |
| val repeatErrorsMaxArgs = |
| if (repeatErrorsMax > 0) { |
| arrayOf(ARG_REPEAT_ERRORS_MAX, repeatErrorsMax.toString()) |
| } else { |
| emptyArray() |
| } |
| |
| // Run optional additional setup steps on the project directory |
| projectSetup?.invoke(project) |
| |
| val actualOutput = |
| runDriver( |
| ARG_NO_COLOR, |
| |
| // 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", |
| newFolder("temp").path, |
| |
| // 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, |
| *apiArgs, |
| *overloadedMethodArgs, |
| *apiXmlArgs, |
| *dexApiArgs, |
| *subtractApiArgs, |
| *stubsArgs, |
| *stubsSourceListArgs, |
| *docStubsSourceListArgs, |
| "$ARG_OUTPUT_KOTLIN_NULLS=${if (outputKotlinStyleNulls) "yes" else "no"}", |
| "$ARG_INCLUDE_SIG_VERSION=${if (includeSignatureVersion) "yes" else "no"}", |
| *quiet, |
| *mergeAnnotationsArgs, |
| *signatureAnnotationsArgs, |
| *javaStubAnnotationsArgs, |
| *inclusionAnnotationsArgs, |
| *migrateNullsArguments, |
| *checkCompatibilityApiReleasedArguments, |
| *checkCompatibilityBaseApiArguments, |
| *checkCompatibilityRemovedReleasedArguments, |
| *proguardKeepArguments, |
| *manifestFileArgs, |
| *convertArgs, |
| *applyApiLevelsXmlArgs, |
| *baselineArgs, |
| *baselineApiLintArgs, |
| *baselineCheckCompatibilityReleasedArgs, |
| *showAnnotationArguments, |
| *hideAnnotationArguments, |
| *hideMetaAnnotationArguments, |
| *suppressCompatMetaAnnotationArguments, |
| *showForStubPurposesAnnotationArguments, |
| *showUnannotatedArgs, |
| *apiLintArgs, |
| *sdkFilesArgs, |
| *importedPackageArgs.toTypedArray(), |
| *skipEmitPackagesArgs.toTypedArray(), |
| *extractAnnotationsArgs, |
| *validateNullabilityArgs, |
| *validateNullabilityFromListArgs, |
| format.outputFlag(), |
| *apiClassResolutionArgs, |
| *sourceList, |
| *extraArguments, |
| *errorMessageApiLintArgs, |
| *errorMessageCheckCompatibilityReleasedArgs, |
| *repeatErrorsMaxArgs, |
| expectedFail = actualExpectedFail |
| ) |
| |
| if (expectedIssues != null || allReportedIssues.toString() != "") { |
| assertEquals( |
| expectedIssues?.trimIndent()?.trim() ?: "", |
| cleanupString(allReportedIssues.toString(), project) |
| ) |
| } |
| if (errorSeverityExpectedIssues != null) { |
| assertEquals( |
| errorSeverityExpectedIssues.trimIndent().trim(), |
| cleanupString(errorSeverityReportedIssues.toString(), project) |
| ) |
| } |
| |
| if (expectedOutput != null) { |
| assertEquals(expectedOutput.trimIndent().trim(), actualOutput.trim()) |
| } |
| |
| if (api != null) { |
| assertTrue( |
| "${apiFile.path} does not exist even though --api was used", |
| apiFile.exists() |
| ) |
| val actualText = readFile(apiFile, stripBlankLines, trim) |
| assertEquals(prepareExpectedApi(api, format), actualText) |
| // Make sure we can read back the files we write |
| ApiFile.parseApi(apiFile, options.annotationManager) |
| } |
| |
| 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, DOT_XML, stripLineComments = false).trimIndent(), |
| actualText |
| ) |
| // Make sure we can read back the files we write |
| parseDocument(apiXmlFile.readText(UTF_8), false) |
| } |
| |
| fun checkBaseline( |
| arg: String, |
| baselineContent: String?, |
| updateBaselineContent: String?, |
| mergeBaselineContent: String?, |
| file: File? |
| ) { |
| if (file == null) { |
| return |
| } |
| assertTrue("${file.path} does not exist even though $arg was used", file.exists()) |
| val actualText = readFile(file, stripBlankLines, trim) |
| |
| // Compare against: |
| // If "merged baseline" is set, use it. |
| // If "update baseline" is set, use it. |
| // Otherwise, the original baseline. |
| val sourceFile = mergeBaselineContent ?: updateBaselineContent ?: baselineContent ?: "" |
| assertEquals( |
| stripComments(sourceFile, DOT_XML, stripLineComments = false).trimIndent(), |
| actualText |
| ) |
| } |
| checkBaseline(ARG_BASELINE, baseline, updateBaseline, mergeBaseline, baselineFile) |
| checkBaseline( |
| ARG_BASELINE_API_LINT, |
| baselineApiLint, |
| updateBaselineApiLint, |
| null, |
| baselineApiLintFile |
| ) |
| checkBaseline( |
| ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED, |
| baselineCheckCompatibilityReleased, |
| updateBaselineCheckCompatibilityReleased, |
| null, |
| baselineCheckCompatibilityReleasedFile |
| ) |
| |
| if (convertFiles.isNotEmpty()) { |
| for (i in convertToJDiff.indices) { |
| val expected = convertToJDiff[i].outputFile |
| val converted = convertFiles[i].outputFile |
| assertTrue( |
| "${converted.path} does not exist even though $ARG_CONVERT_TO_JDIFF was used", |
| converted.exists() |
| ) |
| val actualText = readFile(converted, stripBlankLines, trim) |
| if (actualText.contains("<api")) { |
| parseDocument(actualText, false) |
| } |
| assertEquals( |
| stripComments(expected, DOT_XML, stripLineComments = false).trimIndent(), |
| actualText |
| ) |
| // Make sure we can read back the files we write |
| } |
| } |
| |
| 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, DOT_TXT, stripLineComments = false).trimIndent(), |
| actualText |
| ) |
| } |
| |
| 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(prepareExpectedApi(removedApi, format), actualText) |
| // Make sure we can read back the files we write |
| ApiFile.parseApi(removedApiFile, options.annotationManager) |
| } |
| |
| 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, DOT_TXT, 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 (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 (validateNullabilityTxt != null) { |
| assertTrue( |
| "Using $ARG_NULLABILITY_WARNINGS_TXT but $validateNullabilityTxt was not created", |
| validateNullabilityTxt.isFile |
| ) |
| val actualReport = |
| Files.asCharSource(validateNullabilityTxt, UTF_8) |
| .readLines() |
| .map(String::trim) |
| .toSet() |
| assertEquals(validateNullability, actualReport) |
| } |
| |
| if (stubFiles.isNotEmpty()) { |
| for (expected in stubFiles) { |
| val actual = File(stubsDir!!, expected.targetRelativePath) |
| if (!actual.exists()) { |
| val existing = |
| stubsDir |
| .walkTopDown() |
| .filter { it.isFile } |
| .map { it.path } |
| .joinToString("\n ") |
| throw FileNotFoundException( |
| "Could not find a generated stub for ${expected.targetRelativePath}. " + |
| "Found these files: \n $existing" |
| ) |
| } |
| val actualContents = readFile(actual, stripBlankLines, trim) |
| val stubSource = if (sourceFiles.isEmpty()) "text" else "source" |
| val message = |
| "Generated from-$stubSource stub contents does not match expected contents" |
| assertEquals(message, expected.contents, actualContents) |
| } |
| } |
| |
| if (stubsSourceList != null && stubsSourceListFile != null) { |
| assertTrue( |
| "${stubsSourceListFile.path} does not exist even though --write-stubs-source-list was used", |
| stubsSourceListFile.exists() |
| ) |
| val actualText = |
| cleanupString(readFile(stubsSourceListFile, stripBlankLines, trim), project) |
| // To make golden files look better put one entry per line instead of a single |
| // space separated line |
| .replace(' ', '\n') |
| assertEquals( |
| stripComments(stubsSourceList, DOT_TXT, stripLineComments = false).trimIndent(), |
| actualText |
| ) |
| } |
| |
| if (docStubsSourceList != null && docStubsSourceListFile != null) { |
| assertTrue( |
| "${docStubsSourceListFile.path} does not exist even though --write-stubs-source-list was used", |
| docStubsSourceListFile.exists() |
| ) |
| val actualText = |
| cleanupString(readFile(docStubsSourceListFile, stripBlankLines, trim), project) |
| // To make golden files look better put one entry per line instead of a single |
| // space separated line |
| .replace(' ', '\n') |
| assertEquals( |
| stripComments(docStubsSourceList, DOT_TXT, stripLineComments = false).trimIndent(), |
| actualText |
| ) |
| } |
| |
| if (checkCompilation && stubsDir != null) { |
| val generated = |
| gatherSources(reporter, 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(reporter, 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 (CHECK_JDIFF && apiXmlFile != null && convertToJDiff.isNotEmpty()) { |
| // TODO: Parse the XML file with jdiff too |
| } |
| } |
| |
| /** 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, 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 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 |
| } |
| |
| /** Strip comments, trim indent, and add a signature format version header if one is missing */ |
| private fun prepareExpectedApi(expectedApi: String, format: FileFormat): String { |
| val header = format.header() |
| |
| return stripComments(expectedApi, DOT_TXT, stripLineComments = false) |
| .trimIndent() |
| .let { |
| if (header != null && !it.startsWith("// Signature format:")) header + it else it |
| } |
| .trim() |
| } |
| |
| companion object { |
| @JvmStatic |
| protected fun readFile( |
| file: File, |
| stripBlankLines: Boolean = false, |
| trim: Boolean = false |
| ): String { |
| var apiLines: List<String> = Files.asCharSource(file, 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 |
| } |
| } |
| } |
| |
| private fun FileFormat.outputFlag(): String { |
| return if (isSignatureFormat()) { |
| "$ARG_FORMAT=v${signatureFormatAsInt()}" |
| } else { |
| "" |
| } |
| } |
| |
| private fun FileFormat.signatureFormatAsInt(): Int { |
| return when (this) { |
| FileFormat.V1 -> 1 |
| FileFormat.V2 -> 2 |
| FileFormat.V3 -> 3 |
| FileFormat.V4 -> 4 |
| FileFormat.BASELINE, |
| FileFormat.JDIFF, |
| FileFormat.SINCE_XML, |
| FileFormat.UNKNOWN -> error("this method is only allowed on signature formats, was $this") |
| } |
| } |
| |
| /** Returns the paths returned by [findKotlinStdlibPaths] as metalava args expected by Options. */ |
| fun findKotlinStdlibPathArgs(sources: Array<String>): Array<String> { |
| val kotlinPaths = findKotlinStdlibPaths(sources) |
| |
| return if (kotlinPaths.isEmpty()) emptyArray() |
| else |
| arrayOf( |
| ARG_CLASS_PATH, |
| kotlinPaths.joinToString(separator = File.pathSeparator) { it.path } |
| ) |
| } |
| |
| 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() |
| |
| @Suppress("ConstantConditionIf") |
| 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${if (SUPPORT_TYPE_USE_ANNOTATIONS) ", TYPE_USE" else ""}}) |
| public @interface NonNull { |
| } |
| """ |
| ) |
| .indented() |
| |
| val libcoreNonNullSource: TestFile = |
| 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 { |
| } |
| """ |
| ) |
| .indented() |
| |
| val libcoreNullFromTypeParamSource: TestFile = |
| 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 NullFromTypeParam { |
| } |
| """ |
| ) |
| .indented() |
| |
| val libcoreNullableSource: TestFile = |
| 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 { |
| } |
| """ |
| ) |
| .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() |
| |
| @Suppress("ConstantConditionIf") |
| 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${if (SUPPORT_TYPE_USE_ANNOTATIONS) ", TYPE_USE" else ""}}) |
| 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, LOCAL_VARIABLE, TYPE_USE, ANNOTATION_TYPE, PACKAGE}) |
| 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, LOCAL_VARIABLE, TYPE_USE, ANNOTATION_TYPE, PACKAGE}) |
| 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 androidxIntRangeSource: TestFile = |
| java( |
| """ |
| package androidx.annotation; |
| import java.lang.annotation.*; |
| import static java.lang.annotation.ElementType.*; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| @Retention(CLASS) |
| @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 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 = |
| kotlin( |
| """ |
| package androidx.annotation |
| |
| import androidx.annotation.RestrictTo.Scope |
| import java.lang.annotation.ElementType.* |
| |
| @MustBeDocumented |
| @kotlin.annotation.Retention(AnnotationRetention.BINARY) |
| @Target( |
| AnnotationTarget.ANNOTATION_CLASS, |
| AnnotationTarget.CLASS, |
| AnnotationTarget.FUNCTION, |
| AnnotationTarget.PROPERTY_GETTER, |
| AnnotationTarget.PROPERTY_SETTER, |
| AnnotationTarget.CONSTRUCTOR, |
| AnnotationTarget.FIELD, |
| AnnotationTarget.FILE |
| ) |
| // Needed due to Kotlin's lack of PACKAGE annotation target |
| // https://youtrack.jetbrains.com/issue/KT-45921 |
| @Suppress("DEPRECATED_JAVA_ANNOTATION") |
| @java.lang.annotation.Target(ANNOTATION_TYPE, TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE) |
| public annotation class RestrictTo(vararg val value: Scope) { |
| public enum class Scope { |
| LIBRARY, |
| LIBRARY_GROUP, |
| LIBRARY_GROUP_PREFIX, |
| @Deprecated("Use LIBRARY_GROUP_PREFIX instead.") |
| GROUP_ID, |
| TESTS, |
| SUBCLASSES, |
| } |
| } |
| """ |
| ) |
| .indented() |
| |
| val visibleForTestingSource: TestFile = |
| java( |
| """ |
| package androidx.annotation; |
| import static java.lang.annotation.RetentionPolicy.CLASS; |
| import java.lang.annotation.Retention; |
| @Retention(CLASS) |
| @SuppressWarnings("WeakerAccess") |
| public @interface VisibleForTesting { |
| int otherwise() default PRIVATE; |
| int PRIVATE = 2; |
| int PACKAGE_PRIVATE = 3; |
| int PROTECTED = 4; |
| int NONE = 5; |
| } |
| """ |
| ) |
| .indented() |
| |
| val columnSource: TestFile = |
| java( |
| """ |
| package android.provider; |
| |
| import static java.lang.annotation.ElementType.FIELD; |
| import static java.lang.annotation.RetentionPolicy.RUNTIME; |
| |
| import android.content.ContentProvider; |
| import android.content.ContentValues; |
| import android.database.Cursor; |
| |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.Target; |
| |
| @Documented |
| @Retention(RUNTIME) |
| @Target({FIELD}) |
| public @interface Column { |
| int value(); |
| boolean readOnly() default false; |
| } |
| """ |
| ) |
| .indented() |
| |
| val publishedApiSource: TestFile = |
| kotlin( |
| """ |
| /** |
| * When applied to a class or a member with internal visibility allows to use it from public inline functions and |
| * makes it effectively public. |
| * |
| * Public inline functions cannot use non-public API, since if they are inlined, those non-public API references |
| * would violate access restrictions at a call site (https://kotlinlang.org/docs/reference/inline-functions.html#public-inline-restrictions). |
| * |
| * To overcome this restriction an `internal` declaration can be annotated with the `@PublishedApi` annotation: |
| * - this allows to call that declaration from public inline functions; |
| * - the declaration becomes effectively public, and this should be considered with respect to binary compatibility maintaining. |
| */ |
| @Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) |
| @Retention(AnnotationRetention.BINARY) |
| @MustBeDocumented |
| @SinceKotlin("1.1") |
| public annotation class PublishedApi |
| """ |
| ) |
| .indented() |
| |
| val deprecatedForSdkSource: TestFile = |
| java( |
| """ |
| package android.annotation; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| import java.lang.annotation.Retention; |
| /** @hide */ |
| @Retention(SOURCE) |
| @SuppressWarnings("WeakerAccess") |
| public @interface DeprecatedForSdk { |
| String value(); |
| Class<?>[] allowIn() default {}; |
| } |
| """ |
| ) |
| .indented() |