1 /*
<lambda>null2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.tools.metalava
18
19 import com.android.SdkConstants.DOT_TXT
20 import com.android.tools.lint.LintCliClient
21 import com.android.tools.lint.UastEnvironment
22 import com.android.tools.lint.checks.ApiLookup
23 import com.android.tools.lint.checks.infrastructure.ClassName
24 import com.android.tools.lint.checks.infrastructure.TestFile
25 import com.android.tools.lint.checks.infrastructure.TestFiles.java
26 import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
27 import com.android.tools.lint.checks.infrastructure.stripComments
28 import com.android.tools.lint.client.api.LintClient
29 import com.android.tools.metalava.cli.common.ARG_HIDE
30 import com.android.tools.metalava.cli.common.ARG_NO_COLOR
31 import com.android.tools.metalava.cli.common.ARG_QUIET
32 import com.android.tools.metalava.cli.common.ARG_REPEAT_ERRORS_MAX
33 import com.android.tools.metalava.cli.common.ARG_SOURCE_PATH
34 import com.android.tools.metalava.cli.common.ARG_VERBOSE
35 import com.android.tools.metalava.cli.common.CheckerContext
36 import com.android.tools.metalava.cli.common.CheckerFunction
37 import com.android.tools.metalava.cli.common.ExecutionEnvironment
38 import com.android.tools.metalava.cli.common.TestEnvironment
39 import com.android.tools.metalava.cli.compatibility.ARG_API_COMPAT_ANNOTATION
40 import com.android.tools.metalava.cli.compatibility.ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED
41 import com.android.tools.metalava.cli.compatibility.ARG_CHECK_COMPATIBILITY_API_RELEASED
42 import com.android.tools.metalava.cli.compatibility.ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED
43 import com.android.tools.metalava.cli.compatibility.ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED
44 import com.android.tools.metalava.cli.compatibility.ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED
45 import com.android.tools.metalava.cli.lint.ARG_API_LINT
46 import com.android.tools.metalava.cli.lint.ARG_API_LINT_PREVIOUS_API
47 import com.android.tools.metalava.cli.lint.ARG_BASELINE_API_LINT
48 import com.android.tools.metalava.cli.lint.ARG_ERROR_MESSAGE_API_LINT
49 import com.android.tools.metalava.cli.lint.ARG_UPDATE_BASELINE_API_LINT
50 import com.android.tools.metalava.cli.signature.ARG_FORMAT
51 import com.android.tools.metalava.model.ANDROIDX_ANNOTATION_PACKAGE
52 import com.android.tools.metalava.model.ANDROID_ANNOTATION_PACKAGE
53 import com.android.tools.metalava.model.Assertions
54 import com.android.tools.metalava.model.StripJavaLangPrefix
55 import com.android.tools.metalava.model.provider.Capability
56 import com.android.tools.metalava.model.psi.PsiModelOptions
57 import com.android.tools.metalava.model.source.SourceModelProvider
58 import com.android.tools.metalava.model.source.SourceSet
59 import com.android.tools.metalava.model.source.utils.DOT_KT
60 import com.android.tools.metalava.model.testing.CodebaseCreatorConfig
61 import com.android.tools.metalava.model.testing.CodebaseCreatorConfigAware
62 import com.android.tools.metalava.model.text.ApiClassResolution
63 import com.android.tools.metalava.model.text.ApiFile
64 import com.android.tools.metalava.model.text.FileFormat
65 import com.android.tools.metalava.model.text.SignatureFile
66 import com.android.tools.metalava.model.text.assertSignatureFilesMatch
67 import com.android.tools.metalava.model.text.prepareSignatureFileForTest
68 import com.android.tools.metalava.reporter.ReporterEnvironment
69 import com.android.tools.metalava.reporter.Severity
70 import com.android.tools.metalava.testing.JavacHelper
71 import com.android.tools.metalava.testing.KnownJarFiles
72 import com.android.tools.metalava.testing.KnownSourceFiles
73 import com.android.tools.metalava.testing.TemporaryFolderOwner
74 import com.android.tools.metalava.testing.findKotlinStdlibPaths
75 import com.android.tools.metalava.testing.getAndroidJar
76 import com.android.utils.SdkUtils
77 import com.google.common.io.Closeables
78 import com.intellij.openapi.util.Disposer
79 import java.io.ByteArrayOutputStream
80 import java.io.File
81 import java.io.FileNotFoundException
82 import java.io.PrintStream
83 import java.io.PrintWriter
84 import java.io.StringWriter
85 import java.net.URI
86 import kotlin.text.Charsets.UTF_8
87 import org.intellij.lang.annotations.Language
88 import org.junit.Assert.assertEquals
89 import org.junit.Assert.assertNotNull
90 import org.junit.Assert.assertTrue
91 import org.junit.Assert.fail
92 import org.junit.Before
93 import org.junit.Rule
94 import org.junit.rules.ErrorCollector
95 import org.junit.rules.TemporaryFolder
96 import org.junit.runner.RunWith
97
98 @RunWith(DriverTestRunner::class)
99 abstract class DriverTest :
100 CodebaseCreatorConfigAware<SourceModelProvider>, TemporaryFolderOwner, Assertions {
101 @get:Rule override val temporaryFolder = TemporaryFolder()
102
103 @get:Rule val errorCollector = ErrorCollector()
104
105 /** The [CodebaseCreatorConfig] under which this test will be run. */
106 final override lateinit var codebaseCreatorConfig: CodebaseCreatorConfig<SourceModelProvider>
107
108 /**
109 * The setting of [PsiModelOptions.useK2Uast]. Is computed lazily as it depends on
110 * [codebaseCreatorConfig] which is set after object initialization.
111 */
112 protected val isK2 by
113 lazy(LazyThreadSafetyMode.NONE) {
114 codebaseCreatorConfig.modelOptions[PsiModelOptions.useK2Uast]
115 }
116
117 @Before
118 fun setup() {
119 Disposer.setDebugMode(true)
120 }
121
122 // Makes a note to fail the test, but still allows the test to complete before failing
123 protected fun addError(error: String) {
124 errorCollector.addError(Throwable(error))
125 }
126
127 protected fun getApiFile(): File {
128 return File(temporaryFolder.root.path, "public-api.txt")
129 }
130
131 private fun runDriver(
132 // The SameParameterValue check reports that this is passed the same value because the first
133 // value that is passed is always the same but this is a varargs parameter so other values
134 // that are passed matter, and they are not the same.
135 args: Array<String>,
136 expectedFail: String,
137 reporterEnvironment: ReporterEnvironment,
138 testEnvironment: TestEnvironment,
139 ): String {
140 // Capture the actual input and output from System.out/err and compare it to the output
141 // printed through the official writer; they should be the same, otherwise we have stray
142 // print calls littered in the code!
143 val previousOut = System.out
144 val previousErr = System.err
145 try {
146 val output = TeeWriter(previousOut)
147 System.setOut(PrintStream(output))
148 val error = TeeWriter(previousErr)
149 System.setErr(PrintStream(error))
150
151 val sw = StringWriter()
152 val writer = PrintWriter(sw)
153
154 Disposer.setDebugMode(true)
155
156 val executionEnvironment =
157 ExecutionEnvironment(
158 stdout = writer,
159 stderr = writer,
160 reporterEnvironment = reporterEnvironment,
161 testEnvironment = testEnvironment,
162 )
163 val exitCode = run(executionEnvironment, args)
164 if (exitCode == 0) {
165 assertTrue(
166 "Test expected to fail but didn't. Expected failure: $expectedFail",
167 expectedFail.isEmpty()
168 )
169 } else {
170 val actualFail = cleanupString(sw.toString(), null)
171 if (
172 cleanupString(expectedFail, null).replace(".", "").trim() !=
173 actualFail.replace(".", "").trim()
174 ) {
175 val reportedCompatError =
176 actualFail.startsWith(
177 "Aborting: Found compatibility problems checking the "
178 )
179 if (
180 expectedFail == "Aborting: Found compatibility problems" &&
181 reportedCompatError
182 ) {
183 // Special case for compat checks; we don't want to force each one of them
184 // to pass in the right string (which may vary based on whether writing out
185 // the signature was passed at the same time
186 // ignore
187 } else {
188 if (reportedCompatError) {
189 // if a compatibility error was unexpectedly reported, then mark that as
190 // an error but keep going, so we can see the actual compatibility error
191 if (expectedFail.trimIndent() != actualFail) {
192 addError(
193 "ComparisonFailure: expected failure $expectedFail, actual $actualFail"
194 )
195 }
196 } else {
197 // no compatibility error; check for other errors now, and
198 // if one is found, fail right away
199 assertEquals(
200 "expectedFail does not match actual failures",
201 expectedFail.trimIndent(),
202 actualFail
203 )
204 }
205 }
206 }
207 }
208
209 val stdout = output.toString(UTF_8.name())
210 if (stdout.isNotEmpty()) {
211 addError("Unexpected write to stdout:\n $stdout")
212 }
213 val stderr = error.toString(UTF_8.name())
214 if (stderr.isNotEmpty()) {
215 addError("Unexpected write to stderr:\n $stderr")
216 }
217
218 val printedOutput = sw.toString()
219 if (printedOutput.isNotEmpty() && printedOutput.trim().isEmpty()) {
220 fail("Printed newlines with nothing else")
221 }
222
223 UastEnvironment.checkApplicationEnvironmentDisposed()
224 Disposer.assertIsEmpty(true)
225
226 return printedOutput
227 } finally {
228 System.setOut(previousOut)
229 System.setErr(previousErr)
230 }
231 }
232
233 // This is here, so we can keep a record of what was printed, to make sure we don't have any
234 // unexpected print calls in the source that are left behind after debugging and pollute the
235 // production output
236 class TeeWriter(private val otherStream: PrintStream) : ByteArrayOutputStream() {
237 override fun write(b: ByteArray, off: Int, len: Int) {
238 otherStream.write(b, off, len)
239 super.write(b, off, len)
240 }
241
242 override fun write(b: ByteArray) {
243 otherStream.write(b)
244 super.write(b)
245 }
246
247 override fun write(b: Int) {
248 otherStream.write(b)
249 super.write(b)
250 }
251 }
252
253 private fun <T> buildOptionalArgs(option: T?, converter: (T) -> Array<String>): Array<String> {
254 return if (option != null) {
255 converter(option)
256 } else {
257 emptyArray()
258 }
259 }
260
261 /** Test information related to a baseline file. */
262 data class BaselineTestInfo(
263 /**
264 * The contents of the input baseline.
265 *
266 * If this is `null` then no baseline testing is performed.
267 */
268 val inputContents: String? = null,
269
270 /** The contents of the expected updated baseline. */
271 val expectedOutputContents: String? = null,
272
273 /** Indicates whether testing of the baseline should suppress reporting of issues or not. */
274 val silentUpdate: Boolean = true,
275 ) {
276 init {
277 if (inputContents == null && expectedOutputContents != null) {
278 error("`inputContents` must be non-null as `expectedOutputContents` is non-null")
279 }
280 }
281 }
282
283 /** Represents a check that can be performed on a baseline file. */
284 @Suppress("ArrayInDataClass")
285 private data class BaselineCheck(
286 /** The option for the input baseline, used in test failure messages. */
287 val baselineOption: String,
288
289 /** The args to pass to metalava. */
290 val args: Array<String>,
291
292 /**
293 * The input/output file.
294 *
295 * If this is `null` then no check is performed.
296 */
297 val file: File?,
298
299 /** The expected contents of [file]. */
300 val expectedFileContents: String,
301 ) {
302 /** Apply the baseline check. */
303 fun apply() {
304 file ?: return
305
306 assertTrue(
307 "${file.path} does not exist even though $baselineOption was used",
308 file.exists()
309 )
310
311 val actualText = readFile(file)
312 assertEquals(
313 stripComments(
314 expectedFileContents.trimIndent(),
315 DOT_TXT,
316 stripLineComments = false
317 ),
318 actualText
319 )
320 }
321 }
322
323 private fun buildBaselineCheck(
324 baselineOption: String,
325 updateBaselineOption: String,
326 filename: String,
327 info: BaselineTestInfo,
328 ): BaselineCheck {
329 return info.inputContents?.let { inputContents ->
330 val baselineFile = temporaryFolder.newFile(filename)
331 baselineFile?.writeText(inputContents.trimIndent())
332 val args = arrayOf(baselineOption, baselineFile.path)
333
334 info.expectedOutputContents?.let { expectedOutputContents ->
335 // If silent update is request then use the same baseline file for update as for the
336 // input, otherwise create a separate update file.
337 val updateFile =
338 if (info.silentUpdate) baselineFile
339 else temporaryFolder.newFile("update-$filename")
340
341 // As expected output contents are provided add extra arguments to output the
342 // baseline and then compare the baseline file against the expected output. Use the
343 // update baseline option in any error messages.
344 BaselineCheck(
345 updateBaselineOption,
346 args + arrayOf(updateBaselineOption, updateFile.path),
347 updateFile,
348 expectedOutputContents,
349 )
350 }
351 ?:
352 // As no expected output is provided then compare the baseline file against the
353 // supplied input contents to make sure that they have not changed. Use the
354 // basic baseline option in any error messages.
355 BaselineCheck(
356 baselineOption,
357 args,
358 baselineFile,
359 inputContents,
360 )
361 }
362 ?: BaselineCheck("", emptyArray(), null, "")
363 }
364
365 @Suppress("DEPRECATION")
366 protected fun check(
367 configFiles: Array<TestFile> = emptyArray(),
368 /** Any jars to add to the class path */
369 classpath: Array<TestFile>? = null,
370 /** The API signature content (corresponds to --api) */
371 @Language("TEXT") api: String? = null,
372 /** The removed API (corresponds to --removed-api) */
373 removedApi: String? = null,
374 /** The subtract api signature content (corresponds to --subtract-api) */
375 @Language("TEXT") subtractApi: String? = null,
376 /** Expected stubs (corresponds to --stubs) */
377 stubFiles: Array<TestFile> = emptyArray(),
378 /** Expected paths of stub files created */
379 stubPaths: Array<String>? = null,
380 /**
381 * Whether the stubs should be written as documentation stubs instead of plain stubs.
382 * Decides whether the stubs include @doconly elements, uses rewritten/migration
383 * annotations, etc
384 */
385 docStubs: Boolean = false,
386 /** Signature file format */
387 format: FileFormat = FileFormat.LATEST,
388 /** All expected issues to be generated when analyzing these sources */
389 expectedIssues: String? = "",
390 /** Expected [Severity.ERROR] issues to be generated when analyzing these sources */
391 errorSeverityExpectedIssues: String? = null,
392 checkCompilation: Boolean = false,
393 /** Annotations to merge in (in .xml format) */
394 @Language("XML") mergeXmlAnnotations: String? = null,
395 /** Annotations to merge in (in .txt/.signature format) */
396 @Language("TEXT") mergeSignatureAnnotations: String? = null,
397 /** Qualifier annotations to merge in (in Java stub format) */
398 @Language("JAVA") mergeJavaStubAnnotations: String? = null,
399 /** Inclusion annotations to merge in (in Java stub format) */
400 mergeInclusionAnnotations: Array<TestFile> = emptyArray(),
401 /** Optional API signature files content to load **instead** of Java/Kotlin source files */
402 @Language("TEXT") signatureSources: Array<String> = emptyArray(),
403 apiClassResolution: ApiClassResolution = ApiClassResolution.API,
404 /**
405 * An optional API signature file content to load **instead** of Java/Kotlin source files.
406 * This is added to [signatureSources]. This argument exists for backward compatibility.
407 */
408 @Language("TEXT") signatureSource: String? = null,
409 /** An optional API jar file content to load **instead** of Java/Kotlin source files */
410 apiJar: File? = null,
411 /**
412 * An optional API signature to check the last released API's compatibility with.
413 *
414 * This can either be the name of a file or the contents of the signature file. In the
415 * latter case the contents are adjusted to make sure it is a valid signature file with a
416 * valid header and written to a file.
417 */
418 @Language("TEXT") checkCompatibilityApiReleased: String? = null,
419 /**
420 * Allow specifying multiple instances of [checkCompatibilityApiReleased].
421 *
422 * In order from narrowest to widest API.
423 */
424 checkCompatibilityApiReleasedList: List<String> = emptyList(),
425 /**
426 * An optional API signature to check the last released removed API's compatibility with.
427 *
428 * See [checkCompatibilityApiReleased].
429 */
430 @Language("TEXT") checkCompatibilityRemovedApiReleased: String? = null,
431 /**
432 * Allow specifying multiple instances of [checkCompatibilityRemovedApiReleased].
433 *
434 * In order from narrowest to widest API.
435 */
436 checkCompatibilityRemovedApiReleasedList: List<String> = emptyList(),
437 @Language("TEXT") migrateNullsApi: String? = null,
438 migrateNullsApiList: List<String> = listOfNotNull(migrateNullsApi),
439 /** An optional Proguard keep file to generate */
440 @Language("Proguard") proguard: String? = null,
441 /** Show annotations (--show-annotation arguments) */
442 showAnnotations: Array<String> = emptyArray(),
443 /** "Show for stub purposes" API annotation ([ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION]) */
444 showForStubPurposesAnnotations: Array<String> = emptyArray(),
445 /** Hide annotations (--hide-annotation arguments) */
446 hideAnnotations: Array<String> = emptyArray(),
447 /** API Compatibility important annotations (--api-compat-annotation) */
448 apiCompatAnnotations: Set<String> = emptySet(),
449 /** No compat check meta-annotations (--no-compat-check-meta-annotation arguments) */
450 suppressCompatibilityMetaAnnotations: Array<String> = emptyArray(),
451 /** If using [showAnnotations], whether to include unannotated */
452 showUnannotated: Boolean = false,
453 /** Additional arguments to supply */
454 extraArguments: Array<out String> = emptyArray(),
455 /** Expected output (stdout and stderr combined). If null, don't check. */
456 expectedOutput: String? = null,
457 /** Expected fail message and state, if any */
458 expectedFail: String? = null,
459 /** Optional manifest to load and associate with the codebase */
460 @Language("XML") manifest: String? = null,
461 /**
462 * Packages to pre-import (these will therefore NOT be included in emitted stubs, signature
463 * files etc
464 */
465 importedPackages: List<String> = emptyList(),
466 /** See [TestEnvironment.skipEmitPackages], defaults to [DEFAULT_SKIP_EMIT_PACKAGES]. */
467 skipEmitPackages: List<String>? = null,
468 /** Whether we should include --showAnnotations=android.annotation.SystemApi */
469 includeSystemApiAnnotations: Boolean = false,
470 /** Whether we should warn about super classes that are stripped because they are hidden */
471 includeStrippedSuperclassWarnings: Boolean = false,
472 /**
473 * Apply level to XML.
474 *
475 * This can either be the name of a file or the contents of the XML file. In the latter case
476 * the contents are trimmed and written to a file.
477 */
478 applyApiLevelsXml: String? = null,
479 /** Corresponds to SDK constants file broadcast_actions.txt */
480 sdkBroadcastActions: String? = null,
481 /** Corresponds to SDK constants file activity_actions.txt */
482 sdkActivityActions: String? = null,
483 /** Corresponds to SDK constants file service_actions.txt */
484 sdkServiceActions: String? = null,
485 /** Corresponds to SDK constants file categories.txt */
486 sdkCategories: String? = null,
487 /** Corresponds to SDK constants file features.txt */
488 sdkFeatures: String? = null,
489 /** Corresponds to SDK constants file widgets.txt */
490 sdkWidgets: String? = null,
491 /**
492 * Extract annotations and check that the given packages contain the given extracted XML
493 * files
494 */
495 extractAnnotations: Map<String, String>? = null,
496 /**
497 * Creates the nullability annotations validator, and check that the report has the given
498 * lines (does not define files to be validated)
499 */
500 validateNullability: Set<String>? = null,
501 /** Enable nullability validation for the listed classes */
502 validateNullabilityFromList: String? = null,
503 /** Hook for performing additional initialization of the project directory */
504 projectSetup: ((File) -> Unit)? = null,
505 /** [ARG_BASELINE] and [ARG_UPDATE_BASELINE] */
506 baselineTestInfo: BaselineTestInfo = BaselineTestInfo(),
507 /** [ARG_BASELINE_API_LINT] and [ARG_UPDATE_BASELINE_API_LINT] */
508 baselineApiLintTestInfo: BaselineTestInfo = BaselineTestInfo(),
509 /**
510 * [ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED] and
511 * [ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED]
512 */
513 baselineCheckCompatibilityReleasedTestInfo: BaselineTestInfo = BaselineTestInfo(),
514
515 /** [ARG_ERROR_MESSAGE_API_LINT] */
516 errorMessageApiLint: String? = null,
517 /** [ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED] */
518 errorMessageCheckCompatibilityReleased: String? = null,
519
520 /**
521 * If non-null, enable API lint. If non-blank, a codebase where only new APIs not in the
522 * codebase are linted.
523 */
524 @Language("TEXT") apiLint: String? = null,
525 /** The source files to pass to the analyzer */
526 sourceFiles: Array<TestFile> = emptyArray(),
527 /** Lint project description */
528 projectDescription: TestFile? = null,
529 /** [ARG_REPEAT_ERRORS_MAX] */
530 repeatErrorsMax: Int = 0,
531 /**
532 * Called on a [CheckerContext] after the analysis phase in the metalava main command.
533 *
534 * This allows testing of the internal state of the metalava main command. Ideally, tests
535 * should not use this as it makes the tests more fragile and can increase the cost of
536 * refactoring. However, it is often the only way to verify the effects of changes that
537 * start to add a new feature but which does not yet have any effect on the output.
538 */
539 postAnalysisChecker: CheckerFunction? = null,
540 ) {
541 // Ensure different API clients don't interfere with each other
542 try {
543 val method = ApiLookup::class.java.getDeclaredMethod("dispose")
544 method.isAccessible = true
545 method.invoke(null)
546 } catch (ignore: Throwable) {
547 ignore.printStackTrace()
548 }
549
550 // Ensure that lint infrastructure (for UAST) knows it's dealing with a test
551 LintCliClient(LintClient.CLIENT_UNIT_TESTS)
552
553 // Verify that a test that provided kotlin code is only being run against a provider that
554 // supports kotlin code.
555 val anyKotlin = sourceFiles.any { it.targetPath.endsWith(DOT_KT) }
556 if (anyKotlin) {
557 if (Capability.KOTLIN !in codebaseCreatorConfig.creator.capabilities) {
558 error(
559 "Provider ${codebaseCreatorConfig.providerName} does not support Kotlin; please add `@RequiresCapabilities(Capability.KOTLIN)` to the test"
560 )
561 }
562 if (format.version < FileFormat.Version.V4) {
563 error("Kotlin test must use FileFormat.V4 or higher")
564 }
565 }
566
567 val releasedApiCheck =
568 CompatibilityCheckRequest.create(
569 optionName = ARG_CHECK_COMPATIBILITY_API_RELEASED,
570 fileOrSignatureContents = checkCompatibilityApiReleased,
571 fileOrSignatureContentsList = checkCompatibilityApiReleasedList,
572 newBasename = "released-api.txt",
573 )
574 val releasedRemovedApiCheck =
575 CompatibilityCheckRequest.create(
576 optionName = ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED,
577 fileOrSignatureContents = checkCompatibilityRemovedApiReleased,
578 fileOrSignatureContentsList = checkCompatibilityRemovedApiReleasedList,
579 newBasename = "removed-released-api.txt",
580 )
581
582 val actualExpectedFail =
583 when {
584 expectedFail != null -> expectedFail
585 (releasedApiCheck.required() || releasedRemovedApiCheck.required()) &&
586 expectedIssues?.contains(": error:") == true -> {
587 "Aborting: Found compatibility problems"
588 }
589 else -> ""
590 }
591
592 // Unit test which checks that a signature file is as expected
593 val androidJar = getAndroidJar()
594
595 val project = createProject(sourceFiles)
596
597 val sourcePathDir = File(project, "src")
598 if (!sourcePathDir.isDirectory) {
599 sourcePathDir.mkdirs()
600 }
601
602 var sourcePath = sourcePathDir.path
603
604 // Make it easy to configure a source path with more than one source root: src and src2
605 if (sourceFiles.any { it.targetPath.startsWith("src2") }) {
606 sourcePath = sourcePath + File.pathSeparator + sourcePath + "2"
607 }
608
609 fun pathUnderProject(path: String): String = File(project, path).path
610
611 val projectDescriptionFile = projectDescription?.createFile(project)
612
613 val apiClassResolutionArgs =
614 arrayOf(ARG_API_CLASS_RESOLUTION, apiClassResolution.optionValue)
615
616 val sourceList =
617 if (signatureSources.isNotEmpty() || signatureSource != null) {
618 sourcePathDir.mkdirs()
619
620 // if signatureSource is set, add it to signatureSources.
621 val sources = signatureSources.toMutableList()
622 signatureSource?.let { sources.add(it) }
623
624 var num = 0
625 val args = mutableListOf<String>()
626 sources.forEach { file ->
627 val signatureFile =
628 File(project, "load-api${ if (++num == 1) "" else num.toString() }.txt")
629 signatureFile.writeSignatureText(file)
630 args.add(signatureFile.path)
631 }
632 if (!includeStrippedSuperclassWarnings) {
633 args.add(ARG_HIDE)
634 args.add("HiddenSuperclass") // Suppress warning #111
635 }
636 args.toTypedArray()
637 } else if (apiJar != null) {
638 sourcePathDir.mkdirs()
639 assert(sourceFiles.isEmpty()) {
640 "Shouldn't combine sources with API jar file loads"
641 }
642 arrayOf(apiJar.path)
643 } else {
644 sourceFiles
645 .asSequence()
646 .map { pathUnderProject(it.targetPath) }
647 .toList()
648 .toTypedArray()
649 }
650
651 val classpathArgs: Array<String> =
652 if (classpath != null) {
653 val classpathString =
654 classpath
655 .map { it.createFile(project) }
656 .map { it.path }
657 .joinToString(separator = File.pathSeparator) { it }
658
659 arrayOf(ARG_CLASS_PATH, classpathString)
660 } else {
661 emptyArray()
662 }
663
664 val allReportedIssues = StringBuilder()
665 val errorSeverityReportedIssues = StringBuilder()
666 val reporterEnvironment =
667 object : ReporterEnvironment {
668 override val rootFolder = project
669
670 override fun printReport(message: String, severity: Severity) {
671 val cleanedUpMessage = cleanupString(message, rootFolder).trim()
672 if (severity == Severity.ERROR) {
673 errorSeverityReportedIssues.append(cleanedUpMessage).append('\n')
674 }
675 allReportedIssues.append(cleanedUpMessage).append('\n')
676 }
677 }
678
679 val configFileArgs =
680 configFiles
681 .flatMap { listOf(ARG_CONFIG_FILE, it.indented().createFile(project).path) }
682 .toTypedArray()
683
684 val mergeAnnotationsArgs =
685 if (mergeXmlAnnotations != null) {
686 val merged = File(project, "merged-annotations.xml")
687 merged.writeText(mergeXmlAnnotations.trimIndent())
688 arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path)
689 } else {
690 emptyArray()
691 }
692
693 val signatureAnnotationsArgs =
694 if (mergeSignatureAnnotations != null) {
695 val merged = File(project, "merged-annotations.txt")
696 merged.writeText(mergeSignatureAnnotations.trimIndent())
697 arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path)
698 } else {
699 emptyArray()
700 }
701
702 val javaStubAnnotationsArgs =
703 if (mergeJavaStubAnnotations != null) {
704 // We need to place the qualifier class into its proper package location
705 // to make the parsing machinery happy
706 val cls = ClassName(mergeJavaStubAnnotations)
707 val pkg = cls.packageName
708 val relative = pkg?.replace('.', File.separatorChar) ?: "."
709 val merged = File(project, "qualifier/$relative/${cls.className}.java")
710 merged.parentFile.mkdirs()
711 merged.writeText(mergeJavaStubAnnotations.trimIndent())
712 arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path)
713 } else {
714 emptyArray()
715 }
716
717 val inclusionAnnotationsArgs =
718 if (mergeInclusionAnnotations.isNotEmpty()) {
719 // Create each file in their own directory.
720 mergeInclusionAnnotations
721 .flatMapIndexed { i, testFile ->
722 val suffix = if (i == 0) "" else i.toString()
723 val targetDir = File(project, "inclusion$suffix")
724 targetDir.mkdirs()
725 testFile.createFile(targetDir)
726 listOf(ARG_MERGE_INCLUSION_ANNOTATIONS, targetDir.path)
727 }
728 .toTypedArray()
729 } else {
730 emptyArray()
731 }
732
733 val apiLintArgs =
734 if (apiLint != null) {
735 if (apiLint.isBlank()) {
736 arrayOf(ARG_API_LINT)
737 } else {
738 val file = File(project, "prev-api-lint.txt")
739 file.writeSignatureText(apiLint)
740 arrayOf(ARG_API_LINT, ARG_API_LINT_PREVIOUS_API, file.path)
741 }
742 } else {
743 emptyArray()
744 }
745
746 val manifestFileArgs =
747 if (manifest != null) {
748 val file = File(project, "manifest.xml")
749 file.writeText(manifest.trimIndent())
750 arrayOf(ARG_MANIFEST, file.path)
751 } else {
752 emptyArray()
753 }
754
755 val migrateNullsArguments =
756 migrateNullsApiList.contentOrPathListToArgsArray(
757 project,
758 "stable-api.txt",
759 ARG_MIGRATE_NULLNESS
760 )
761
762 val apiCompatAnnotationArguments =
763 if (apiCompatAnnotations.isNotEmpty()) {
764 val args = mutableListOf<String>()
765 for (annotation in apiCompatAnnotations) {
766 args.add(ARG_API_COMPAT_ANNOTATION)
767 args.add(annotation)
768 }
769 args.toTypedArray()
770 } else {
771 emptyArray()
772 }
773
774 val quiet =
775 if (expectedOutput != null && !extraArguments.contains(ARG_VERBOSE)) {
776 // If comparing output, avoid noisy output such as the banner etc
777 arrayOf(ARG_QUIET)
778 } else {
779 emptyArray()
780 }
781
782 var proguardFile: File? = null
783 val proguardKeepArguments =
784 if (proguard != null) {
785 proguardFile = File(project, "proguard.cfg")
786 arrayOf(ARG_PROGUARD, proguardFile.path)
787 } else {
788 emptyArray()
789 }
790
791 val showAnnotationArguments =
792 if (showAnnotations.isNotEmpty() || includeSystemApiAnnotations) {
793 val args = mutableListOf<String>()
794 for (annotation in showAnnotations) {
795 args.add(ARG_SHOW_ANNOTATION)
796 args.add(annotation)
797 }
798 if (includeSystemApiAnnotations && !args.contains("android.annotation.SystemApi")) {
799 args.add(ARG_SHOW_ANNOTATION)
800 args.add("android.annotation.SystemApi")
801 }
802 if (includeSystemApiAnnotations && !args.contains("android.annotation.TestApi")) {
803 args.add(ARG_SHOW_ANNOTATION)
804 args.add("android.annotation.TestApi")
805 }
806 args.toTypedArray()
807 } else {
808 emptyArray()
809 }
810
811 val hideAnnotationArguments =
812 if (hideAnnotations.isNotEmpty()) {
813 val args = mutableListOf<String>()
814 for (annotation in hideAnnotations) {
815 args.add(ARG_HIDE_ANNOTATION)
816 args.add(annotation)
817 }
818 args.toTypedArray()
819 } else {
820 emptyArray()
821 }
822
823 val showForStubPurposesAnnotationArguments =
824 if (showForStubPurposesAnnotations.isNotEmpty()) {
825 val args = mutableListOf<String>()
826 for (annotation in showForStubPurposesAnnotations) {
827 args.add(ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION)
828 args.add(annotation)
829 }
830 args.toTypedArray()
831 } else {
832 emptyArray()
833 }
834
835 val suppressCompatMetaAnnotationArguments =
836 if (suppressCompatibilityMetaAnnotations.isNotEmpty()) {
837 val args = mutableListOf<String>()
838 for (annotation in suppressCompatibilityMetaAnnotations) {
839 args.add(ARG_SUPPRESS_COMPATIBILITY_META_ANNOTATION)
840 args.add(annotation)
841 }
842 args.toTypedArray()
843 } else {
844 emptyArray()
845 }
846
847 val showUnannotatedArgs =
848 if (showUnannotated) {
849 arrayOf(ARG_SHOW_UNANNOTATED)
850 } else {
851 emptyArray()
852 }
853
854 var removedApiFile: File? = null
855 val removedArgs =
856 if (removedApi != null) {
857 removedApiFile = temporaryFolder.newFile("removed.txt")
858 arrayOf(ARG_REMOVED_API, removedApiFile.path)
859 } else {
860 emptyArray()
861 }
862
863 // Always pass apiArgs and generate API text file in runDriver
864 val apiFile: File = getOrCreateFile("public-api.txt")
865 val apiArgs = arrayOf(ARG_API, apiFile.path)
866
867 val subtractApiFile: File?
868 val subtractApiArgs =
869 if (subtractApi != null) {
870 subtractApiFile = temporaryFolder.newFile("subtract-api.txt")
871 subtractApiFile.writeSignatureText(subtractApi)
872 arrayOf(ARG_SUBTRACT_API, subtractApiFile.path)
873 } else {
874 emptyArray()
875 }
876
877 var stubsDir: File? = null
878 val stubsArgs =
879 if (stubFiles.isNotEmpty() || stubPaths != null) {
880 stubsDir = getOrCreateFolder("stubs")
881 if (docStubs) {
882 arrayOf(ARG_DOC_STUBS, stubsDir.path)
883 } else {
884 arrayOf(ARG_STUBS, stubsDir.path)
885 }
886 } else {
887 emptyArray()
888 }
889
890 val applyApiLevelsXmlArgs =
891 if (applyApiLevelsXml != null) {
892 ApiLookup::class
893 .java
894 .getDeclaredMethod("dispose")
895 .apply { isAccessible = true }
896 .invoke(null)
897 val applyApiLevelsXmlFile =
898 useExistingFileOrCreateNewFile(project, applyApiLevelsXml, "api-versions.xml") {
899 it.trimIndent()
900 }
901 arrayOf(ARG_APPLY_API_LEVELS, applyApiLevelsXmlFile.path)
902 } else {
903 emptyArray()
904 }
905
906 val baselineCheck =
907 buildBaselineCheck(
908 ARG_BASELINE,
909 ARG_UPDATE_BASELINE,
910 "baseline.txt",
911 baselineTestInfo,
912 )
913 val baselineApiLintCheck =
914 buildBaselineCheck(
915 ARG_BASELINE_API_LINT,
916 ARG_UPDATE_BASELINE_API_LINT,
917 "baseline-api-lint.txt",
918 baselineApiLintTestInfo,
919 )
920 val baselineCheckCompatibilityReleasedCheck =
921 buildBaselineCheck(
922 ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED,
923 ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED,
924 "baseline-check-released.txt",
925 baselineCheckCompatibilityReleasedTestInfo,
926 )
927
928 val importedPackageArgs = mutableListOf<String>()
929 importedPackages.forEach {
930 importedPackageArgs.add("--stub-import-packages")
931 importedPackageArgs.add(it)
932 }
933
934 val kotlinPathArgs = findKotlinStdlibPathArgs(sourceList)
935
936 val sdkFilesDir: File?
937 val sdkFilesArgs: Array<String>
938 if (
939 sdkBroadcastActions != null ||
940 sdkActivityActions != null ||
941 sdkServiceActions != null ||
942 sdkCategories != null ||
943 sdkFeatures != null ||
944 sdkWidgets != null
945 ) {
946 val dir = File(project, "sdk-files")
947 sdkFilesArgs = arrayOf(ARG_SDK_VALUES, dir.path)
948 sdkFilesDir = dir
949 } else {
950 sdkFilesArgs = emptyArray()
951 sdkFilesDir = null
952 }
953
954 val extractedAnnotationsZip: File?
955 val extractAnnotationsArgs =
956 if (extractAnnotations != null) {
957 extractedAnnotationsZip = temporaryFolder.newFile("extracted-annotations.zip")
958 arrayOf(ARG_EXTRACT_ANNOTATIONS, extractedAnnotationsZip.path)
959 } else {
960 extractedAnnotationsZip = null
961 emptyArray()
962 }
963
964 val validateNullabilityTxt: File?
965 val validateNullabilityArgs =
966 if (validateNullability != null) {
967 validateNullabilityTxt = temporaryFolder.newFile("validate-nullability.txt")
968 arrayOf(
969 ARG_NULLABILITY_WARNINGS_TXT,
970 validateNullabilityTxt.path,
971 ARG_NULLABILITY_ERRORS_NON_FATAL // for testing, report on errors instead of
972 // throwing
973 )
974 } else {
975 validateNullabilityTxt = null
976 emptyArray()
977 }
978 val validateNullabilityFromListFile: File?
979 val validateNullabilityFromListArgs =
980 if (validateNullabilityFromList != null) {
981 validateNullabilityFromListFile =
982 temporaryFolder.newFile("validate-nullability-classes.txt")
983 validateNullabilityFromListFile.writeText(validateNullabilityFromList)
984 arrayOf(ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullabilityFromListFile.path)
985 } else {
986 emptyArray()
987 }
988
989 val errorMessageApiLintArgs =
990 buildOptionalArgs(errorMessageApiLint) { arrayOf(ARG_ERROR_MESSAGE_API_LINT, it) }
991 val errorMessageCheckCompatibilityReleasedArgs =
992 buildOptionalArgs(errorMessageCheckCompatibilityReleased) {
993 arrayOf(ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED, it)
994 }
995
996 val repeatErrorsMaxArgs =
997 if (repeatErrorsMax > 0) {
998 arrayOf(ARG_REPEAT_ERRORS_MAX, repeatErrorsMax.toString())
999 } else {
1000 emptyArray()
1001 }
1002
1003 // Run optional additional setup steps on the project directory
1004 projectSetup?.invoke(project)
1005
1006 // Make sure that the options is initialized. Just in case access was disallowed by another
1007 // test.
1008 options = Options()
1009
1010 val args =
1011 arrayOf(
1012 ARG_NO_COLOR,
1013
1014 // Tell metalava where to store temp folder: place them under the
1015 // test root folder such that we clean up the output strings referencing
1016 // paths to the temp folder
1017 "--temp-folder",
1018 getOrCreateFolder("temp").path,
1019
1020 // Annotation generation temporarily turned off by default while integrating with
1021 // SDK builds; tests need these
1022 ARG_INCLUDE_ANNOTATIONS,
1023 ARG_SOURCE_PATH,
1024 sourcePath,
1025 *sourceList,
1026 *configFileArgs,
1027 *removedArgs,
1028 *apiArgs,
1029 *subtractApiArgs,
1030 *stubsArgs,
1031 *quiet,
1032 *mergeAnnotationsArgs,
1033 *signatureAnnotationsArgs,
1034 *javaStubAnnotationsArgs,
1035 *inclusionAnnotationsArgs,
1036 *migrateNullsArguments,
1037 *releasedApiCheck.arguments(project),
1038 *releasedRemovedApiCheck.arguments(project),
1039 *proguardKeepArguments,
1040 *manifestFileArgs,
1041 *applyApiLevelsXmlArgs,
1042 *baselineCheck.args,
1043 *baselineApiLintCheck.args,
1044 *baselineCheckCompatibilityReleasedCheck.args,
1045 *apiCompatAnnotationArguments,
1046 *showAnnotationArguments,
1047 *hideAnnotationArguments,
1048 *suppressCompatMetaAnnotationArguments,
1049 *showForStubPurposesAnnotationArguments,
1050 *showUnannotatedArgs,
1051 *sdkFilesArgs,
1052 *importedPackageArgs.toTypedArray(),
1053 *extractAnnotationsArgs,
1054 *validateNullabilityArgs,
1055 *validateNullabilityFromListArgs,
1056 format.outputFlags(),
1057 *apiClassResolutionArgs,
1058 *extraArguments,
1059 *errorMessageApiLintArgs,
1060 *errorMessageCheckCompatibilityReleasedArgs,
1061 *repeatErrorsMaxArgs,
1062 // Must always be last as this can consume a following argument, breaking the test.
1063 *apiLintArgs,
1064 ) +
1065 buildList {
1066 if (projectDescriptionFile != null) {
1067 // Classpath isn't needed when it is specified through the project xml
1068 add(ARG_PROJECT)
1069 add(projectDescriptionFile.absolutePath)
1070 // When project description is provided,
1071 // skip listing (common) sources
1072 } else {
1073 add(ARG_CLASS_PATH)
1074 add(androidJar.path)
1075 addAll(classpathArgs)
1076 addAll(kotlinPathArgs)
1077 }
1078 }
1079 .toTypedArray()
1080
1081 val testEnvironment =
1082 TestEnvironment(
1083 skipEmitPackages = skipEmitPackages ?: DEFAULT_SKIP_EMIT_PACKAGES,
1084 sourceModelProvider = codebaseCreatorConfig.creator,
1085 modelOptions = codebaseCreatorConfig.modelOptions,
1086 postAnalysisChecker = postAnalysisChecker,
1087 )
1088
1089 val actualOutput =
1090 runDriver(
1091 args = args,
1092 expectedFail = actualExpectedFail,
1093 reporterEnvironment = reporterEnvironment,
1094 testEnvironment = testEnvironment,
1095 )
1096
1097 if (expectedIssues != null || allReportedIssues.toString() != "") {
1098 assertEquals(
1099 "expectedIssues does not match actual issues reported",
1100 expectedIssues?.trimIndent()?.trim() ?: "",
1101 allReportedIssues.toString().trim(),
1102 )
1103 }
1104 if (errorSeverityExpectedIssues != null) {
1105 assertEquals(
1106 "errorSeverityExpectedIssues does not match actual issues reported",
1107 errorSeverityExpectedIssues.trimIndent().trim(),
1108 errorSeverityReportedIssues.toString().trim(),
1109 )
1110 }
1111
1112 if (expectedOutput != null) {
1113 assertEquals(
1114 "expectedOutput does not match actual output",
1115 expectedOutput.trimIndent().trim(),
1116 actualOutput.trim()
1117 )
1118 }
1119
1120 if (api != null) {
1121 assertTrue(
1122 "${apiFile.path} does not exist even though --api was used",
1123 apiFile.exists()
1124 )
1125 assertSignatureFilesMatch(api, apiFile.readText(), expectedFormat = format)
1126 // Make sure we can read back the files we write
1127 ApiFile.parseApi(SignatureFile.fromFiles(apiFile), options.codebaseConfig)
1128 }
1129
1130 baselineCheck.apply()
1131 baselineApiLintCheck.apply()
1132 baselineCheckCompatibilityReleasedCheck.apply()
1133
1134 if (removedApi != null && removedApiFile != null) {
1135 assertTrue(
1136 "${removedApiFile.path} does not exist even though --removed-api was used",
1137 removedApiFile.exists()
1138 )
1139 assertSignatureFilesMatch(
1140 removedApi,
1141 removedApiFile.readText(),
1142 expectedFormat = format
1143 )
1144 // Make sure we can read back the files we write
1145 ApiFile.parseApi(SignatureFile.fromFiles(removedApiFile), options.codebaseConfig)
1146 }
1147
1148 if (proguard != null && proguardFile != null) {
1149 assertTrue(
1150 "${proguardFile.path} does not exist even though --proguard was used",
1151 proguardFile.exists()
1152 )
1153 val expectedProguard = readFile(proguardFile)
1154 assertEquals(
1155 stripComments(proguard, DOT_TXT, stripLineComments = false).trimIndent(),
1156 expectedProguard
1157 )
1158 }
1159
1160 if (sdkBroadcastActions != null) {
1161 val actual = readFile(File(sdkFilesDir, "broadcast_actions.txt"))
1162 assertEquals(sdkBroadcastActions.trimIndent().trim(), actual.trim())
1163 }
1164
1165 if (sdkActivityActions != null) {
1166 val actual = readFile(File(sdkFilesDir, "activity_actions.txt"))
1167 assertEquals(sdkActivityActions.trimIndent().trim(), actual.trim())
1168 }
1169
1170 if (sdkServiceActions != null) {
1171 val actual = readFile(File(sdkFilesDir, "service_actions.txt"))
1172 assertEquals(sdkServiceActions.trimIndent().trim(), actual.trim())
1173 }
1174
1175 if (sdkCategories != null) {
1176 val actual = readFile(File(sdkFilesDir, "categories.txt"))
1177 assertEquals(sdkCategories.trimIndent().trim(), actual.trim())
1178 }
1179
1180 if (sdkFeatures != null) {
1181 val actual = readFile(File(sdkFilesDir, "features.txt"))
1182 assertEquals(sdkFeatures.trimIndent().trim(), actual.trim())
1183 }
1184
1185 if (sdkWidgets != null) {
1186 val actual = readFile(File(sdkFilesDir, "widgets.txt"))
1187 assertEquals(sdkWidgets.trimIndent().trim(), actual.trim())
1188 }
1189
1190 if (extractAnnotations != null && extractedAnnotationsZip != null) {
1191 assertTrue(
1192 "Using --extract-annotations but $extractedAnnotationsZip was not created",
1193 extractedAnnotationsZip.isFile
1194 )
1195 for ((pkg, xml) in extractAnnotations) {
1196 assertPackageXml(pkg, extractedAnnotationsZip, xml)
1197 }
1198 }
1199
1200 if (validateNullabilityTxt != null) {
1201 assertTrue(
1202 "Using $ARG_NULLABILITY_WARNINGS_TXT but $validateNullabilityTxt was not created",
1203 validateNullabilityTxt.isFile
1204 )
1205 val actualReport = validateNullabilityTxt.readLines().map(String::trim).toSet()
1206 assertEquals(validateNullability, actualReport)
1207 }
1208
1209 val stubsCreated =
1210 stubsDir
1211 ?.walkTopDown()
1212 ?.filter { it.isFile }
1213 ?.map { it.relativeTo(stubsDir).path }
1214 ?.sorted()
1215 ?.joinToString("\n")
1216
1217 if (stubPaths != null) {
1218 assertEquals("stub paths", stubPaths.joinToString("\n"), stubsCreated)
1219 }
1220
1221 if (stubFiles.isNotEmpty()) {
1222 for (expected in stubFiles) {
1223 val actual = File(stubsDir!!, expected.targetRelativePath)
1224 if (!actual.exists()) {
1225 throw FileNotFoundException(
1226 "Could not find a generated stub for ${expected.targetRelativePath}. " +
1227 "Found these files: \n${stubsCreated!!.prependIndent(" ")}"
1228 )
1229 }
1230 val actualContents = readFile(actual)
1231 val stubSource = if (sourceFiles.isEmpty()) "text" else "source"
1232 val message =
1233 "Generated from-$stubSource stub contents does not match expected contents"
1234 assertEquals(message, expected.contents, actualContents)
1235 }
1236 }
1237
1238 if (checkCompilation && stubsDir != null) {
1239 val generated =
1240 SourceSet.createFromSourcePath(options.reporter, listOf(stubsDir)).sources
1241
1242 // Compile the stubs, throwing an exception if it fails.
1243 JavacHelper.compile(
1244 outputDirectory = project,
1245 sources = generated,
1246 classPath = listOf(KnownJarFiles.stubAnnotationsJar),
1247 )
1248 }
1249 }
1250
1251 /** Encapsulates information needed to request a compatibility check. */
1252 private class CompatibilityCheckRequest
1253 private constructor(
1254 private val optionName: String,
1255 private val fileOrSignatureContentsList: List<String>,
1256 private val newBasename: String,
1257 ) {
1258 companion object {
1259 fun create(
1260 optionName: String,
1261 fileOrSignatureContents: String?,
1262 fileOrSignatureContentsList: List<String>,
1263 newBasename: String,
1264 ): CompatibilityCheckRequest =
1265 CompatibilityCheckRequest(
1266 optionName = optionName,
1267 fileOrSignatureContentsList =
1268 listOfNotNull(fileOrSignatureContents) + fileOrSignatureContentsList,
1269 newBasename = newBasename,
1270 )
1271 }
1272
1273 /** Indicates whether the compatibility check is required. */
1274 fun required(): Boolean = fileOrSignatureContentsList.isNotEmpty()
1275
1276 /** The arguments to pass to Metalava. */
1277 fun arguments(project: File): Array<out String> {
1278 return fileOrSignatureContentsList.contentOrPathListToArgsArray(
1279 project,
1280 newBasename,
1281 optionName
1282 )
1283 }
1284 }
1285
1286 /** Checks that the given zip annotations file contains the given XML package contents */
1287 private fun assertPackageXml(pkg: String, output: File, @Language("XML") expected: String) {
1288 assertNotNull(output)
1289 assertTrue(output.exists())
1290 val url =
1291 URI(
1292 "jar:" +
1293 SdkUtils.fileToUrlString(output) +
1294 "!/" +
1295 pkg.replace('.', '/') +
1296 "/annotations.xml"
1297 )
1298 .toURL()
1299 val stream = url.openStream()
1300 try {
1301 val bytes = stream.readBytes()
1302 assertNotNull(bytes)
1303 val xml = String(bytes, UTF_8).replace("\r\n", "\n")
1304 assertEquals(expected.trimIndent().trim(), xml.trimIndent().trim())
1305 } finally {
1306 Closeables.closeQuietly(stream)
1307 }
1308 }
1309
1310 companion object {
1311 @JvmStatic
1312 protected fun readFile(file: File): String {
1313 var apiLines: List<String> = file.readLines()
1314 apiLines = apiLines.filter { it.isNotBlank() }
1315 return apiLines.joinToString(separator = "\n") { it }.trim()
1316 }
1317
1318 /**
1319 * Get a signature API [File] from either a file path or its contents.
1320 *
1321 * @param project the directory in which to create a new file.
1322 * @param fileOrFileContents either a path to an existing file or the contents of the
1323 * signature file. If the latter the contents will be trimmed, updated to add a
1324 * [FileFormat.V2] header if needed and written to a new file created within [project].
1325 * @param newBasename the basename of a new file created.
1326 */
1327 private fun useExistingSignatureFileOrCreateNewFile(
1328 project: File,
1329 fileOrFileContents: String,
1330 newBasename: String
1331 ) =
1332 useExistingFileOrCreateNewFile(project, fileOrFileContents, newBasename) {
1333 prepareSignatureFileForTest(it, FileFormat.V2)
1334 }
1335
1336 /**
1337 * Get an optional [File] from either a file path or its contents.
1338 *
1339 * @param project the directory in which to create a new file.
1340 * @param fileOrFileContents either a path to an existing file or the contents of the file.
1341 * If the latter the [transformer] will be applied to [fileOrFileContents] and written to
1342 * a new file created within [project].
1343 * @param newBasename the basename of a new file created.
1344 */
1345 private fun useExistingFileOrCreateNewFile(
1346 project: File,
1347 fileOrFileContents: String,
1348 newBasename: String,
1349 transformer: (String) -> String,
1350 ) =
1351 File(fileOrFileContents).let { maybeFile ->
1352 if (maybeFile.isFile) {
1353 maybeFile
1354 } else {
1355 val file = findNonExistentFile(project, newBasename)
1356 file.writeText(transformer(fileOrFileContents))
1357 file
1358 }
1359 }
1360
1361 /**
1362 * Converts the contents of the list, which may be either the name of a file or the contents
1363 * of a file into an array of arguments.
1364 *
1365 * This will use files supplied and create new files from the contents of the file and then
1366 * precede each file by the [optionName].
1367 *
1368 * @param project the project directory for the test.
1369 * @param baseName the base name of the files, including extension. Any created files will
1370 * have a unique name based on this name.
1371 * @param optionName the name of the option to use in the arguments.
1372 */
1373 private fun List<String>.contentOrPathListToArgsArray(
1374 project: File,
1375 baseName: String,
1376 optionName: String
1377 ): Array<String> {
1378 if (isEmpty()) return emptyArray()
1379
1380 val paths = map { useExistingSignatureFileOrCreateNewFile(project, it, baseName).path }
1381
1382 // For each path in the list generate an option with the path as the value.
1383 return paths.flatMap { listOf(optionName, it) }.toTypedArray()
1384 }
1385
1386 private fun findNonExistentFile(project: File, basename: String): File {
1387 // Split the basename into the name without any extension an optional extension.
1388 val index = basename.lastIndexOf('.')
1389 val (nameWithoutExtension, optionalExtension) =
1390 if (index == -1) {
1391 Pair(basename, "")
1392 } else {
1393 Pair(basename.substring(0, index), basename.substring(index))
1394 }
1395
1396 var count = 0
1397 do {
1398 val name =
1399 if (count == 0) basename else "$nameWithoutExtension-$count$optionalExtension"
1400 count += 1
1401
1402 val file = File(project, name)
1403 if (!file.isFile) return file
1404 } while (true)
1405 }
1406 }
1407 }
1408
FileFormatnull1409 private fun FileFormat.outputFlags(): String {
1410 return "$ARG_FORMAT=${specifier()}"
1411 }
1412
writeSignatureTextnull1413 private fun File.writeSignatureText(contents: String) {
1414 writeText(prepareSignatureFileForTest(contents, FileFormat.V2))
1415 }
1416
1417 /** Returns the paths returned by [findKotlinStdlibPaths] as metalava args expected by Options. */
findKotlinStdlibPathArgsnull1418 fun findKotlinStdlibPathArgs(sources: Array<String>): Array<String> {
1419 val kotlinPaths = findKotlinStdlibPaths(sources)
1420
1421 return if (kotlinPaths.isEmpty()) emptyArray()
1422 else
1423 arrayOf(
1424 ARG_CLASS_PATH,
1425 kotlinPaths.joinToString(separator = File.pathSeparator) { it.path }
1426 )
1427 }
1428
1429 val intRangeAnnotationSource: TestFile =
1430 java(
1431 """
1432 package android.annotation;
1433 import java.lang.annotation.*;
1434 import static java.lang.annotation.ElementType.*;
1435 import static java.lang.annotation.RetentionPolicy.SOURCE;
1436 @Retention(SOURCE)
1437 @Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE})
1438 public @interface IntRange {
1439 long from() default Long.MIN_VALUE;
1440 long to() default Long.MAX_VALUE;
1441 }
1442 """
1443 )
1444 .indented()
1445
1446 val intDefAnnotationSource: TestFile =
1447 java(
1448 """
1449 package android.annotation;
1450 import java.lang.annotation.Retention;
1451 import java.lang.annotation.RetentionPolicy;
1452 import java.lang.annotation.Target;
1453 import static java.lang.annotation.ElementType.*;
1454 import static java.lang.annotation.RetentionPolicy.SOURCE;
1455 @Retention(SOURCE)
1456 @Target({ANNOTATION_TYPE})
1457 public @interface IntDef {
1458 int[] value() default {};
1459 boolean flag() default false;
1460 }
1461 """
1462 )
1463 .indented()
1464
1465 val longDefAnnotationSource: TestFile =
1466 java(
1467 """
1468 package android.annotation;
1469 import java.lang.annotation.Retention;
1470 import java.lang.annotation.RetentionPolicy;
1471 import java.lang.annotation.Target;
1472 import static java.lang.annotation.ElementType.*;
1473 import static java.lang.annotation.RetentionPolicy.SOURCE;
1474 @Retention(SOURCE)
1475 @Target({ANNOTATION_TYPE})
1476 public @interface LongDef {
1477 long[] value() default {};
1478 boolean flag() default false;
1479 }
1480 """
1481 )
1482 .indented()
1483
1484 val nonNullSource = KnownSourceFiles.nonNullSource
1485 val nullableSource = KnownSourceFiles.nullableSource
1486 val libcoreNonNullSource = KnownSourceFiles.libcoreNonNullSource
1487 val libcoreNullableSource = KnownSourceFiles.libcoreNullableSource
1488
1489 val libcoreNullFromTypeParamSource: TestFile =
1490 java(
1491 """
1492 package libcore.util;
1493 import static java.lang.annotation.ElementType.*;
1494 import static java.lang.annotation.RetentionPolicy.SOURCE;
1495 import java.lang.annotation.*;
1496 @Documented
1497 @Retention(SOURCE)
1498 @Target({TYPE_USE})
1499 public @interface NullFromTypeParam {
1500 }
1501 """
1502 )
1503 .indented()
1504
1505 val requiresPermissionSource: TestFile =
1506 java(
1507 """
1508 package android.annotation;
1509 import java.lang.annotation.*;
1510 import static java.lang.annotation.ElementType.*;
1511 import static java.lang.annotation.RetentionPolicy.SOURCE;
1512 @Retention(SOURCE)
1513 @Target({ANNOTATION_TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER})
1514 public @interface RequiresPermission {
1515 String value() default "";
1516 String[] allOf() default {};
1517 String[] anyOf() default {};
1518 boolean conditional() default false;
1519 @Target({FIELD, METHOD, PARAMETER})
1520 @interface Read {
1521 RequiresPermission value() default @RequiresPermission;
1522 }
1523 @Target({FIELD, METHOD, PARAMETER})
1524 @interface Write {
1525 RequiresPermission value() default @RequiresPermission;
1526 }
1527 }
1528 """
1529 )
1530 .indented()
1531
1532 val requiresFeatureSource: TestFile =
1533 java(
1534 """
1535 package android.annotation;
1536 import java.lang.annotation.*;
1537 import static java.lang.annotation.ElementType.*;
1538 import static java.lang.annotation.RetentionPolicy.SOURCE;
1539 @Retention(SOURCE)
1540 @Target({TYPE,FIELD,METHOD,CONSTRUCTOR})
1541 public @interface RequiresFeature {
1542 String value();
1543 }
1544 """
1545 )
1546 .indented()
1547
1548 val requiresApiSource: TestFile =
1549 java(
1550 """
1551 package androidx.annotation;
1552 import java.lang.annotation.*;
1553 import static java.lang.annotation.ElementType.*;
1554 import static java.lang.annotation.RetentionPolicy.SOURCE;
1555 @Retention(SOURCE)
1556 @Target({TYPE,FIELD,METHOD,CONSTRUCTOR})
1557 public @interface RequiresApi {
1558 int value() default 1;
1559 int api() default 1;
1560 }
1561 """
1562 )
1563 .indented()
1564
restrictedForEnvironmentClassnull1565 private fun restrictedForEnvironmentClass(packageName: String): TestFile =
1566 java(
1567 """
1568 package $packageName;
1569 import java.lang.annotation.*;
1570 import static java.lang.annotation.ElementType.*;
1571 import static java.lang.annotation.RetentionPolicy.SOURCE;
1572 /** @hide */
1573 @Retention(SOURCE)
1574 @Target({TYPE})
1575 public @interface RestrictedForEnvironment {
1576 Environment[] environments();
1577 int from();
1578 enum Environment {
1579 SDK_SANDBOX {
1580 @Override
1581 public String toString() {
1582 return "SDK Runtime";
1583 }
1584 }
1585 }
1586 @Retention(RetentionPolicy.RUNTIME)
1587 @Target(TYPE)
1588 @interface Container {
1589 RestrictedForEnvironment[] value();
1590 }
1591 }
1592 """
1593 )
1594 .indented()
1595
1596 val androidXRestrictedForEnvironment = restrictedForEnvironmentClass(ANDROIDX_ANNOTATION_PACKAGE)
1597 val androidRestrictedForEnvironment = restrictedForEnvironmentClass(ANDROID_ANNOTATION_PACKAGE)
1598
1599 val sdkConstantSource: TestFile =
1600 java(
1601 """
1602 package android.annotation;
1603 import java.lang.annotation.*;
1604 @Target({ ElementType.FIELD })
1605 @Retention(RetentionPolicy.SOURCE)
1606 public @interface SdkConstant {
1607 enum SdkConstantType {
1608 ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE
1609 }
1610 SdkConstantType value();
1611 }
1612 """
1613 )
1614 .indented()
1615
1616 val broadcastBehaviorSource: TestFile =
1617 java(
1618 """
1619 package android.annotation;
1620 import java.lang.annotation.*;
1621 /** @hide */
1622 @Target({ ElementType.FIELD })
1623 @Retention(RetentionPolicy.SOURCE)
1624 public @interface BroadcastBehavior {
1625 boolean explicitOnly() default false;
1626 boolean registeredOnly() default false;
1627 boolean includeBackground() default false;
1628 boolean protectedBroadcast() default false;
1629 }
1630 """
1631 )
1632 .indented()
1633
1634 val androidxNonNullSource: TestFile =
1635 java(
1636 """
1637 package androidx.annotation;
1638 import java.lang.annotation.*;
1639 import static java.lang.annotation.ElementType.*;
1640 import static java.lang.annotation.RetentionPolicy.SOURCE;
1641 @SuppressWarnings("WeakerAccess")
1642 @Retention(SOURCE)
1643 @Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, TYPE_USE, ANNOTATION_TYPE, PACKAGE})
1644 public @interface NonNull {
1645 }
1646 """
1647 )
1648 .indented()
1649
1650 val androidxNullableSource: TestFile =
1651 java(
1652 """
1653 package androidx.annotation;
1654 import java.lang.annotation.*;
1655 import static java.lang.annotation.ElementType.*;
1656 import static java.lang.annotation.RetentionPolicy.SOURCE;
1657 @SuppressWarnings("WeakerAccess")
1658 @Retention(SOURCE)
1659 @Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, TYPE_USE, ANNOTATION_TYPE, PACKAGE})
1660 public @interface Nullable {
1661 }
1662 """
1663 )
1664 .indented()
1665
1666 val recentlyNonNullSource: TestFile =
1667 java(
1668 """
1669 package androidx.annotation;
1670 import java.lang.annotation.*;
1671 import static java.lang.annotation.ElementType.*;
1672 import static java.lang.annotation.RetentionPolicy.SOURCE;
1673 @SuppressWarnings("WeakerAccess")
1674 @Retention(SOURCE)
1675 @Target({METHOD, PARAMETER, FIELD, TYPE_USE})
1676 public @interface RecentlyNonNull {
1677 }
1678 """
1679 )
1680 .indented()
1681
1682 val recentlyNullableSource: TestFile =
1683 java(
1684 """
1685 package androidx.annotation;
1686 import java.lang.annotation.*;
1687 import static java.lang.annotation.ElementType.*;
1688 import static java.lang.annotation.RetentionPolicy.SOURCE;
1689 @SuppressWarnings("WeakerAccess")
1690 @Retention(SOURCE)
1691 @Target({METHOD, PARAMETER, FIELD, TYPE_USE})
1692 public @interface RecentlyNullable {
1693 }
1694 """
1695 )
1696 .indented()
1697
1698 val uiThreadSource: TestFile =
1699 java(
1700 """
1701 package androidx.annotation;
1702 import java.lang.annotation.*;
1703 import static java.lang.annotation.ElementType.*;
1704 import static java.lang.annotation.RetentionPolicy.SOURCE;
1705 /**
1706 * Denotes that the annotated method or constructor should only be called on the
1707 * UI thread. If the annotated element is a class, then all methods in the class
1708 * should be called on the UI thread.
1709 * @memberDoc This method must be called on the thread that originally created
1710 * this UI element. This is typically the main thread of your app.
1711 * @classDoc Methods in this class must be called on the thread that originally created
1712 * this UI element, unless otherwise noted. This is typically the
1713 * main thread of your app. * @hide
1714 */
1715 @SuppressWarnings({"WeakerAccess", "JavaDoc"})
1716 @Retention(SOURCE)
1717 @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
1718 public @interface UiThread {
1719 }
1720 """
1721 )
1722 .indented()
1723
1724 val workerThreadSource: TestFile =
1725 java(
1726 """
1727 package androidx.annotation;
1728 import java.lang.annotation.*;
1729 import static java.lang.annotation.ElementType.*;
1730 import static java.lang.annotation.RetentionPolicy.SOURCE;
1731 /**
1732 * @memberDoc This method may take several seconds to complete, so it should
1733 * only be called from a worker thread.
1734 * @classDoc Methods in this class may take several seconds to complete, so it should
1735 * only be called from a worker thread unless otherwise noted.
1736 * @hide
1737 */
1738 @SuppressWarnings({"WeakerAccess", "JavaDoc"})
1739 @Retention(SOURCE)
1740 @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
1741 public @interface WorkerThread {
1742 }
1743 """
1744 )
1745 .indented()
1746
1747 val suppressLintSource: TestFile =
1748 java(
1749 """
1750 package android.annotation;
1751
1752 import static java.lang.annotation.ElementType.*;
1753 import java.lang.annotation.*;
1754 @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
1755 @Retention(RetentionPolicy.CLASS)
1756 public @interface SuppressLint {
1757 String[] value();
1758 }
1759 """
1760 )
1761 .indented()
1762
1763 val systemServiceSource: TestFile =
1764 java(
1765 """
1766 package android.annotation;
1767 import static java.lang.annotation.ElementType.TYPE;
1768 import static java.lang.annotation.RetentionPolicy.SOURCE;
1769 import java.lang.annotation.*;
1770 @Retention(SOURCE)
1771 @Target(TYPE)
1772 public @interface SystemService {
1773 String value();
1774 }
1775 """
1776 )
1777 .indented()
1778
1779 val systemApiSource = KnownSourceFiles.systemApiSource
1780
1781 val testApiSource: TestFile =
1782 java(
1783 """
1784 package android.annotation;
1785 import static java.lang.annotation.ElementType.*;
1786 import java.lang.annotation.*;
1787 @Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE})
1788 @Retention(RetentionPolicy.SOURCE)
1789 public @interface TestApi {
1790 }
1791 """
1792 )
1793 .indented()
1794
1795 val widgetSource: TestFile =
1796 java(
1797 """
1798 package android.annotation;
1799 import java.lang.annotation.*;
1800 @Target({ ElementType.TYPE })
1801 @Retention(RetentionPolicy.SOURCE)
1802 public @interface Widget {
1803 }
1804 """
1805 )
1806 .indented()
1807
1808 val restrictToSource: TestFile =
1809 kotlin(
1810 """
1811 package androidx.annotation
1812
1813 import androidx.annotation.RestrictTo.Scope
1814 import java.lang.annotation.ElementType.*
1815
1816 @MustBeDocumented
1817 @Retention(AnnotationRetention.BINARY)
1818 @Target(
1819 AnnotationTarget.ANNOTATION_CLASS,
1820 AnnotationTarget.CLASS,
1821 AnnotationTarget.FUNCTION,
1822 AnnotationTarget.PROPERTY_GETTER,
1823 AnnotationTarget.PROPERTY_SETTER,
1824 AnnotationTarget.CONSTRUCTOR,
1825 AnnotationTarget.FIELD,
1826 AnnotationTarget.FILE
1827 )
1828 // Needed due to Kotlin's lack of PACKAGE annotation target
1829 // https://youtrack.jetbrains.com/issue/KT-45921
1830 @Suppress("DEPRECATED_JAVA_ANNOTATION")
1831 @java.lang.annotation.Target(ANNOTATION_TYPE, TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE)
1832 annotation class RestrictTo(vararg val value: Scope) {
1833 enum class Scope {
1834 LIBRARY,
1835 LIBRARY_GROUP,
1836 LIBRARY_GROUP_PREFIX,
1837 @Deprecated("Use LIBRARY_GROUP_PREFIX instead.")
1838 GROUP_ID,
1839 TESTS,
1840 SUBCLASSES,
1841 }
1842 }
1843 """
1844 )
1845 .indented()
1846
1847 val visibleForTestingSource: TestFile =
1848 java(
1849 """
1850 package androidx.annotation;
1851 import static java.lang.annotation.RetentionPolicy.CLASS;
1852 import java.lang.annotation.Retention;
1853 @Retention(CLASS)
1854 @SuppressWarnings("WeakerAccess")
1855 public @interface VisibleForTesting {
1856 int otherwise() default PRIVATE;
1857 int PRIVATE = 2;
1858 int PACKAGE_PRIVATE = 3;
1859 int PROTECTED = 4;
1860 int NONE = 5;
1861 }
1862 """
1863 )
1864 .indented()
1865
1866 val columnSource: TestFile =
1867 java(
1868 """
1869 package android.provider;
1870
1871 import static java.lang.annotation.ElementType.FIELD;
1872 import static java.lang.annotation.RetentionPolicy.RUNTIME;
1873
1874 import android.content.ContentProvider;
1875 import android.content.ContentValues;
1876 import android.database.Cursor;
1877
1878 import java.lang.annotation.Documented;
1879 import java.lang.annotation.Retention;
1880 import java.lang.annotation.Target;
1881
1882 @Documented
1883 @Retention(RUNTIME)
1884 @Target({FIELD})
1885 public @interface Column {
1886 int value();
1887 boolean readOnly() default false;
1888 }
1889 """
1890 )
1891 .indented()
1892
1893 val publishedApiSource: TestFile =
1894 kotlin(
1895 """
1896 /**
1897 * When applied to a class or a member with internal visibility allows to use it from public inline functions and
1898 * makes it effectively public.
1899 *
1900 * Public inline functions cannot use non-public API, since if they are inlined, those non-public API references
1901 * would violate access restrictions at a call site (https://kotlinlang.org/docs/reference/inline-functions.html#public-inline-restrictions).
1902 *
1903 * To overcome this restriction an `internal` declaration can be annotated with the `@PublishedApi` annotation:
1904 * - this allows to call that declaration from public inline functions;
1905 * - the declaration becomes effectively public, and this should be considered with respect to binary compatibility maintaining.
1906 */
1907 @Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
1908 @Retention(AnnotationRetention.BINARY)
1909 @MustBeDocumented
1910 @SinceKotlin("1.1")
1911 annotation class PublishedApi
1912 """
1913 )
1914 .indented()
1915
1916 val deprecatedForSdkSource: TestFile =
1917 java(
1918 """
1919 package android.annotation;
1920 import static java.lang.annotation.RetentionPolicy.SOURCE;
1921 import java.lang.annotation.Retention;
1922 /** @hide */
1923 @Retention(SOURCE)
1924 @SuppressWarnings("WeakerAccess")
1925 public @interface DeprecatedForSdk {
1926 String value();
1927 Class<?>[] allowIn() default {};
1928 }
1929 """
1930 )
1931 .indented()
1932
1933 val DEFAULT_SKIP_EMIT_PACKAGES =
1934 listOf(
1935 // Do not emit classes in a number of java packages. While tests will
1936 // use those classes they generally do not want to emit definitions of
1937 // those classes as part of a signature or stubs files.
1938 "java.lang",
1939 "java.util",
1940 "java.io",
1941 // Ditto for android.annotation and androidx.annotation classes.
1942 ANDROID_ANNOTATION_PACKAGE,
1943 ANDROIDX_ANNOTATION_PACKAGE,
1944 // Ditto for libcore.util.
1945 "libcore.util",
1946 )
1947
1948 /**
1949 * A special [FileFormat] used by tests that want to output type use annotations to signature files.
1950 */
1951 val TYPE_USE_FORMAT =
1952 FileFormat.V5.copy(
1953 kotlinNameTypeOrder = true,
1954 includeTypeUseAnnotations = true,
1955 kotlinStyleNulls = false,
1956 specifiedStripJavaLangPrefix = StripJavaLangPrefix.ALWAYS,
1957 )
1958