• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
20 import com.android.SdkConstants.DOT_JAVA
21 import com.android.SdkConstants.DOT_KT
22 import com.android.ide.common.process.DefaultProcessExecutor
23 import com.android.ide.common.process.LoggedProcessOutputHandler
24 import com.android.ide.common.process.ProcessException
25 import com.android.ide.common.process.ProcessInfoBuilder
26 import com.android.tools.lint.checks.ApiLookup
27 import com.android.tools.lint.checks.infrastructure.ClassName
28 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
29 import com.android.tools.lint.checks.infrastructure.TestFile
30 import com.android.tools.lint.checks.infrastructure.TestFiles
31 import com.android.tools.lint.checks.infrastructure.TestFiles.java
32 import com.android.tools.lint.checks.infrastructure.stripComments
33 import com.android.tools.metalava.doclava1.ApiFile
34 import com.android.tools.metalava.doclava1.Errors
35 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
36 import com.android.tools.metalava.model.parseDocument
37 import com.android.utils.FileUtils
38 import com.android.utils.SdkUtils
39 import com.android.utils.StdLogger
40 import com.google.common.io.ByteStreams
41 import com.google.common.io.Closeables
42 import com.google.common.io.Files
43 import com.intellij.openapi.util.Disposer
44 import org.intellij.lang.annotations.Language
45 import org.junit.Assert.assertEquals
46 import org.junit.Assert.assertNotNull
47 import org.junit.Assert.assertTrue
48 import org.junit.Assert.fail
49 import org.junit.Before
50 import org.junit.Rule
51 import org.junit.rules.TemporaryFolder
52 import java.io.ByteArrayOutputStream
53 import java.io.File
54 import java.io.FileNotFoundException
55 import java.io.PrintStream
56 import java.io.PrintWriter
57 import java.io.StringWriter
58 import java.net.URL
59 import kotlin.text.Charsets.UTF_8
60 
61 const val CHECK_OLD_DOCLAVA_TOO = false
62 const val CHECK_JDIFF = false
63 const val CHECK_STUB_COMPILATION = false
64 
65 abstract class DriverTest {
66     @get:Rule
67     var temporaryFolder = TemporaryFolder()
68 
69     @Before
70     fun setup() {
71         System.setProperty(ENV_VAR_METALAVA_TESTS_RUNNING, SdkConstants.VALUE_TRUE)
72     }
73 
74     protected fun createProject(vararg files: TestFile): File {
75         val dir = temporaryFolder.newFolder("project")
76 
77         files
78             .map { it.createFile(dir) }
79             .forEach { assertNotNull(it) }
80 
81         return dir
82     }
83 
84     protected fun runDriver(vararg args: String, expectedFail: String = ""): String {
85         resetTicker()
86 
87         // Capture the actual input and output from System.out/err and compare it
88         // to the output printed through the official writer; they should be the same,
89         // otherwise we have stray println's littered in the code!
90         val previousOut = System.out
91         val previousErr = System.err
92         try {
93             val output = OutputForbiddenWriter("stdout")
94             System.setOut(PrintStream(output))
95             val error = OutputForbiddenWriter("stderr")
96             System.setErr(PrintStream(error))
97 
98             val sw = StringWriter()
99             val writer = PrintWriter(sw)
100 
101             Disposer.setDebugMode(true)
102 
103             if (!com.android.tools.metalava.run(arrayOf(*args), writer, writer)) {
104                 val actualFail = cleanupString(sw.toString(), null)
105                 if (cleanupString(expectedFail, null).replace(".", "").trim() !=
106                     actualFail.replace(".", "").trim()
107                 ) {
108                     if (expectedFail == "Aborting: Found compatibility problems with --check-compatibility" &&
109                         actualFail.startsWith("Aborting: Found compatibility problems checking the ")
110                     ) {
111                         // Special case for compat checks; we don't want to force each one of them
112                         // to pass in the right string (which may vary based on whether writing out
113                         // the signature was passed at the same time
114                         // ignore
115                     } else {
116                         assertEquals(expectedFail, actualFail)
117                         fail(actualFail)
118                     }
119                 }
120             }
121 
122             val stdout = output.toString(UTF_8.name())
123             assertTrue(stdout, stdout.isEmpty())
124 
125             val stderr = error.toString(UTF_8.name())
126             assertTrue(stderr, stderr.isEmpty())
127 
128             val printedOutput = sw.toString()
129             if (printedOutput.isNotEmpty() && printedOutput.trim().isEmpty()) {
130                 fail("Printed newlines with nothing else")
131             }
132 
133             Disposer.assertIsEmpty(true)
134 
135             return printedOutput
136         } finally {
137             System.setOut(previousOut)
138             System.setErr(previousErr)
139         }
140     }
141 
142     // This is here to make sure we don't have any unexpected random println's
143     // in the source that are left behind after debugging and ends up polluting
144     // the production output
145     class OutputForbiddenWriter(private val stream: String) : ByteArrayOutputStream() {
146         override fun write(b: ByteArray?, off: Int, len: Int) {
147             fail("Unexpected write directly to $stream")
148             super.write(b, off, len)
149         }
150 
151         override fun write(b: ByteArray?) {
152             fail("Unexpected write directly to $stream")
153             super.write(b)
154         }
155 
156         override fun write(b: Int) {
157             fail("Unexpected write directly to $stream")
158             super.write(b)
159         }
160     }
161 
162     private fun findKotlinStdlibPath(): List<String> {
163         val classPath: String = System.getProperty("java.class.path")
164         val paths = mutableListOf<String>()
165         for (path in classPath.split(':')) {
166             val file = File(path)
167             val name = file.name
168             if (name.startsWith("kotlin-stdlib") ||
169                 name.startsWith("kotlin-reflect") ||
170                 name.startsWith("kotlin-script-runtime")
171             ) {
172                 paths.add(file.path)
173             }
174         }
175         if (paths.isEmpty()) {
176             error("Did not find kotlin-stdlib-jre8 in $PROGRAM_NAME classpath: $classPath")
177         }
178         return paths
179     }
180 
181     protected fun getJdkPath(): String? {
182         val javaHome = System.getProperty("java.home")
183         if (javaHome != null) {
184             var javaHomeFile = File(javaHome)
185             if (File(javaHomeFile, "bin${File.separator}javac").exists()) {
186                 return javaHome
187             } else if (javaHomeFile.name == "jre") {
188                 javaHomeFile = javaHomeFile.parentFile
189                 if (javaHomeFile != null && File(javaHomeFile, "bin${File.separator}javac").exists()) {
190                     return javaHomeFile.path
191                 }
192             }
193         }
194         return System.getenv("JAVA_HOME")
195     }
196 
197     /** File conversion tasks */
198     data class ConvertData(
199         val fromApi: String,
200         val outputFile: String,
201         val baseApi: String? = null,
202         val strip: Boolean = true,
203         val format: FileFormat = FileFormat.JDIFF
204     )
205 
206     protected fun check(
207         /** Any jars to add to the class path */
208         classpath: Array<TestFile>? = null,
209         /** The API signature content (corresponds to --api) */
210         @Language("TEXT")
211         api: String? = null,
212         /** The API signature content (corresponds to --api-xml) */
213         @Language("XML")
214         apiXml: String? = null,
215         /** The exact API signature content (corresponds to --exact-api) */
216         exactApi: String? = null,
217         /** The removed API (corresponds to --removed-api) */
218         removedApi: String? = null,
219         /** The removed dex API (corresponds to --removed-dex-api) */
220         removedDexApi: String? = null,
221         /** The private API (corresponds to --private-api) */
222         privateApi: String? = null,
223         /** The private DEX API (corresponds to --private-dex-api) */
224         privateDexApi: String? = null,
225         /** The DEX API (corresponds to --dex-api) */
226         dexApi: String? = null,
227         /** The DEX mapping API (corresponds to --dex-api-mapping) */
228         dexApiMapping: String? = null,
229         /** The subtract api signature content (corresponds to --subtract-api) */
230         @Language("TEXT")
231         subtractApi: String? = null,
232         /** Expected stubs (corresponds to --stubs) */
233         @Language("JAVA") stubs: Array<String> = emptyArray(),
234         /** Stub source file list generated */
235         stubsSourceList: String? = null,
236         /** Whether the stubs should be written as documentation stubs instead of plain stubs. Decides
237          * whether the stubs include @doconly elements, uses rewritten/migration annotations, etc */
238         docStubs: Boolean = false,
239         /** Signature file format */
240         format: FileFormat? = null,
241         /** Whether to run in doclava1 compat mode */
242         compatibilityMode: Boolean = format == null || format == FileFormat.V1,
243         /** Whether to trim the output (leading/trailing whitespace removal) */
244         trim: Boolean = true,
245         /** Whether to remove blank lines in the output (the signature file usually contains a lot of these) */
246         stripBlankLines: Boolean = true,
247         /** Warnings expected to be generated when analyzing these sources */
248         warnings: String? = "",
249         /** Whether to run doclava1 on the test output and assert that the output is identical */
250         checkDoclava1: Boolean = compatibilityMode && (format == null || format == FileFormat.V1),
251         checkCompilation: Boolean = false,
252         /** Annotations to merge in (in .xml format) */
253         @Language("XML") mergeXmlAnnotations: String? = null,
254         /** Annotations to merge in (in .txt/.signature format) */
255         @Language("TEXT") mergeSignatureAnnotations: String? = null,
256         /** Qualifier annotations to merge in (in Java stub format) */
257         @Language("JAVA") mergeJavaStubAnnotations: String? = null,
258         /** Inclusion annotations to merge in (in Java stub format) */
259         @Language("JAVA") mergeInclusionAnnotations: String? = null,
260         /** An optional API signature file content to load **instead** of Java/Kotlin source files */
261         @Language("TEXT") signatureSource: String? = null,
262         /** An optional API jar file content to load **instead** of Java/Kotlin source files */
263         apiJar: File? = null,
264         /** An optional API signature to check the current API's compatibility with */
265         @Language("TEXT") checkCompatibilityApi: String? = null,
266         /** An optional API signature to check the last released API's compatibility with */
267         @Language("TEXT") checkCompatibilityApiReleased: String? = null,
268         /** An optional API signature to check the current removed API's compatibility with */
269         @Language("TEXT") checkCompatibilityRemovedApiCurrent: String? = null,
270         /** An optional API signature to check the last released removed API's compatibility with */
271         @Language("TEXT") checkCompatibilityRemovedApiReleased: String? = null,
272         /** An optional API signature to compute nullness migration status from */
273         allowCompatibleDifferences: Boolean = true,
274         @Language("TEXT") migrateNullsApi: String? = null,
275         /** An optional Proguard keep file to generate */
276         @Language("Proguard") proguard: String? = null,
277         /** Show annotations (--show-annotation arguments) */
278         showAnnotations: Array<String> = emptyArray(),
279         /** Hide annotations (--hideAnnotation arguments) */
280         hideAnnotations: Array<String> = emptyArray(),
281         /** If using [showAnnotations], whether to include unannotated */
282         showUnannotated: Boolean = false,
283         /** Additional arguments to supply */
284         extraArguments: Array<String> = emptyArray(),
285         /** Whether we should emit Kotlin-style null signatures */
286         outputKotlinStyleNulls: Boolean = format != null && format.useKotlinStyleNulls(),
287         /** Whether we should interpret API files being read as having Kotlin-style nullness types */
288         inputKotlinStyleNulls: Boolean = false,
289         /** Whether we should omit java.lang. etc from signature files */
290         omitCommonPackages: Boolean = !compatibilityMode,
291         /** Expected output (stdout and stderr combined). If null, don't check. */
292         expectedOutput: String? = null,
293         /** Expected fail message and state, if any */
294         expectedFail: String? = null,
295         /** List of extra jar files to record annotation coverage from */
296         coverageJars: Array<TestFile>? = null,
297         /** Optional manifest to load and associate with the codebase */
298         @Language("XML")
299         manifest: String? = null,
300         /** Packages to pre-import (these will therefore NOT be included in emitted stubs, signature files etc */
301         importedPackages: List<String> = emptyList(),
302         /** Packages to skip emitting signatures/stubs for even if public (typically used for unit tests
303          * referencing to classpath classes that aren't part of the definitions and shouldn't be part of the
304          * test output; e.g. a test may reference java.lang.Enum but we don't want to start reporting all the
305          * public APIs in the java.lang package just because it's indirectly referenced via the "enum" superclass
306          */
307         skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"),
308         /** Whether we should include --showAnnotations=android.annotation.SystemApi */
309         includeSystemApiAnnotations: Boolean = false,
310         /** Whether we should warn about super classes that are stripped because they are hidden */
311         includeStrippedSuperclassWarnings: Boolean = false,
312         /** Apply level to XML */
313         applyApiLevelsXml: String? = null,
314         /** Corresponds to SDK constants file broadcast_actions.txt */
315         sdk_broadcast_actions: String? = null,
316         /** Corresponds to SDK constants file activity_actions.txt */
317         sdk_activity_actions: String? = null,
318         /** Corresponds to SDK constants file service_actions.txt */
319         sdk_service_actions: String? = null,
320         /** Corresponds to SDK constants file categories.txt */
321         sdk_categories: String? = null,
322         /** Corresponds to SDK constants file features.txt */
323         sdk_features: String? = null,
324         /** Corresponds to SDK constants file widgets.txt */
325         sdk_widgets: String? = null,
326         /** Map from artifact id to artifact descriptor */
327         artifacts: Map<String, String>? = null,
328         /** Extract annotations and check that the given packages contain the given extracted XML files */
329         extractAnnotations: Map<String, String>? = null,
330         /** Creates the nullability annotations validator, and check that the report has the given lines (does not define files to be validated) */
331         validateNullability: Set<String>? = null,
332         /** Enable nullability validation for the listed classes */
333         validateNullabilityFromList: String? = null,
334         /**
335          * Whether to include source retention annotations in the stubs (in that case they do not
336          * go into the extracted annotations zip file)
337          */
338         includeSourceRetentionAnnotations: Boolean = true,
339         /**
340          * Whether to include the signature version in signatures
341          */
342         includeSignatureVersion: Boolean = false,
343         /**
344          * List of signature files to convert to JDiff XML and the
345          * expected XML output.
346          */
347         convertToJDiff: List<ConvertData> = emptyList(),
348         /**
349          * Hook for performing additional initialization of the project
350          * directory
351          */
352         projectSetup: ((File) -> Unit)? = null,
353         /** Baseline file to use, if any */
354         baseline: String? = null,
355         /** Whether to create the baseline if it does not exist. Requires [baseline] to be set. */
356         updateBaseline: Boolean = false,
357         /** Merge instead of replacing the baseline */
358         mergeBaseline: String? = null,
359         /**
360          * If non null, enable API lint. If non-blank, a codebase where only new APIs not in the codebase
361          * are linted.
362          */
363         @Language("TEXT") apiLint: String? = null,
364         /** The source files to pass to the analyzer */
365         vararg sourceFiles: TestFile
366     ) {
367         // Ensure different API clients don't interfere with each other
368         try {
369             val method = ApiLookup::class.java.getDeclaredMethod("dispose")
370             method.isAccessible = true
371             method.invoke(null)
372         } catch (ignore: Throwable) {
373             ignore.printStackTrace()
374         }
375 
376         if (compatibilityMode && mergeXmlAnnotations != null) {
377             fail(
378                 "Can't specify both compatibilityMode and mergeXmlAnnotations: there were no " +
379                     "annotations output in doclava1"
380             )
381         }
382         if (compatibilityMode && mergeSignatureAnnotations != null) {
383             fail(
384                 "Can't specify both compatibilityMode and mergeSignatureAnnotations: there were no " +
385                     "annotations output in doclava1"
386             )
387         }
388         if (compatibilityMode && mergeJavaStubAnnotations != null) {
389             fail(
390                 "Can't specify both compatibilityMode and mergeJavaStubAnnotations: there were no " +
391                     "annotations output in doclava1"
392             )
393         }
394         if (compatibilityMode && mergeInclusionAnnotations != null) {
395             fail(
396                 "Can't specify both compatibilityMode and mergeInclusionAnnotations"
397             )
398         }
399         Errors.resetLevels()
400 
401         @Suppress("NAME_SHADOWING")
402         val expectedFail = expectedFail ?: if (checkCompatibilityApi != null ||
403             checkCompatibilityApiReleased != null ||
404             checkCompatibilityRemovedApiCurrent != null ||
405             checkCompatibilityRemovedApiReleased != null
406         ) {
407             "Aborting: Found compatibility problems with --check-compatibility"
408         } else {
409             ""
410         }
411 
412         // Unit test which checks that a signature file is as expected
413         val androidJar = getPlatformFile("android.jar")
414 
415         val project = createProject(*sourceFiles)
416 
417         val packages = sourceFiles.asSequence().map { findPackage(it.getContents()!!) }.filterNotNull().toSet()
418 
419         val sourcePathDir = File(project, "src")
420         if (!sourcePathDir.isDirectory) {
421             sourcePathDir.mkdirs()
422         }
423 
424         var sourcePath = sourcePathDir.path
425 
426         // Make it easy to configure a source path with more than one source root: src and src2
427         if (sourceFiles.any { it.targetPath.startsWith("src2") }) {
428             sourcePath = sourcePath + File.pathSeparator + sourcePath + "2"
429         }
430 
431         val sourceList =
432             if (signatureSource != null) {
433                 sourcePathDir.mkdirs()
434                 assert(sourceFiles.isEmpty()) { "Shouldn't combine sources with signature file loads" }
435                 val signatureFile = File(project, "load-api.txt")
436                 signatureFile.writeText(signatureSource.trimIndent())
437                 if (includeStrippedSuperclassWarnings) {
438                     arrayOf(signatureFile.path)
439                 } else {
440                     arrayOf(
441                         signatureFile.path,
442                         ARG_HIDE,
443                         "HiddenSuperclass"
444                     ) // Suppress warning #111
445                 }
446             } else if (apiJar != null) {
447                 sourcePathDir.mkdirs()
448                 assert(sourceFiles.isEmpty()) { "Shouldn't combine sources with API jar file loads" }
449                 arrayOf(apiJar.path)
450             } else {
451                 sourceFiles.asSequence().map { File(project, it.targetPath).path }.toList().toTypedArray()
452             }
453 
454         val classpathArgs: Array<String> = if (classpath != null) {
455             val classpathString = classpath
456                 .map { it.createFile(project) }
457                 .map { it.path }
458                 .joinToString(separator = File.pathSeparator) { it }
459 
460             arrayOf(ARG_CLASS_PATH, classpathString)
461         } else {
462             emptyArray()
463         }
464 
465         val reportedWarnings = StringBuilder()
466         reporter = object : Reporter(project) {
467             override fun print(message: String) {
468                 reportedWarnings.append(cleanupString(message, project).trim()).append('\n')
469             }
470         }
471 
472         val mergeAnnotationsArgs = if (mergeXmlAnnotations != null) {
473             val merged = File(project, "merged-annotations.xml")
474             merged.writeText(mergeXmlAnnotations.trimIndent())
475             arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path)
476         } else {
477             emptyArray()
478         }
479 
480         val signatureAnnotationsArgs = if (mergeSignatureAnnotations != null) {
481             val merged = File(project, "merged-annotations.txt")
482             merged.writeText(mergeSignatureAnnotations.trimIndent())
483             arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path)
484         } else {
485             emptyArray()
486         }
487 
488         val javaStubAnnotationsArgs = if (mergeJavaStubAnnotations != null) {
489             // We need to place the qualifier class into its proper package location
490             // to make the parsing machinery happy
491             val cls = ClassName(mergeJavaStubAnnotations)
492             val pkg = cls.packageName
493             val relative = pkg?.replace('.', File.separatorChar) ?: "."
494             val merged = File(project, "qualifier/$relative/${cls.className}.java")
495             merged.parentFile.mkdirs()
496             merged.writeText(mergeJavaStubAnnotations.trimIndent())
497             arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path)
498         } else {
499             emptyArray()
500         }
501 
502         val inclusionAnnotationsArgs = if (mergeInclusionAnnotations != null) {
503             val cls = ClassName(mergeInclusionAnnotations)
504             val pkg = cls.packageName
505             val relative = pkg?.replace('.', File.separatorChar) ?: "."
506             val merged = File(project, "inclusion/$relative/${cls.className}.java")
507             merged.parentFile?.mkdirs()
508             merged.writeText(mergeInclusionAnnotations.trimIndent())
509             arrayOf(ARG_MERGE_INCLUSION_ANNOTATIONS, merged.path)
510         } else {
511             emptyArray()
512         }
513 
514         val apiLintArgs = if (apiLint != null) {
515             if (apiLint.isBlank()) {
516                 arrayOf(ARG_API_LINT)
517             } else {
518                 val file = File(project, "prev-api-lint.txt")
519                 file.writeText(apiLint.trimIndent())
520                 arrayOf(ARG_API_LINT, file.path)
521             }
522         } else {
523             emptyArray<String>()
524         }
525 
526         val checkCompatibilityApiFile = if (checkCompatibilityApi != null) {
527             val jar = File(checkCompatibilityApi)
528             if (jar.isFile) {
529                 jar
530             } else {
531                 val file = File(project, "current-api.txt")
532                 file.writeText(checkCompatibilityApi.trimIndent())
533                 file
534             }
535         } else {
536             null
537         }
538 
539         val checkCompatibilityApiReleasedFile = if (checkCompatibilityApiReleased != null) {
540             val jar = File(checkCompatibilityApiReleased)
541             if (jar.isFile) {
542                 jar
543             } else {
544                 val file = File(project, "released-api.txt")
545                 file.writeText(checkCompatibilityApiReleased.trimIndent())
546                 file
547             }
548         } else {
549             null
550         }
551 
552         val checkCompatibilityRemovedApiCurrentFile = if (checkCompatibilityRemovedApiCurrent != null) {
553             val jar = File(checkCompatibilityRemovedApiCurrent)
554             if (jar.isFile) {
555                 jar
556             } else {
557                 val file = File(project, "removed-current-api.txt")
558                 file.writeText(checkCompatibilityRemovedApiCurrent.trimIndent())
559                 file
560             }
561         } else {
562             null
563         }
564 
565         val checkCompatibilityRemovedApiReleasedFile = if (checkCompatibilityRemovedApiReleased != null) {
566             val jar = File(checkCompatibilityRemovedApiReleased)
567             if (jar.isFile) {
568                 jar
569             } else {
570                 val file = File(project, "removed-released-api.txt")
571                 file.writeText(checkCompatibilityRemovedApiReleased.trimIndent())
572                 file
573             }
574         } else {
575             null
576         }
577 
578         val migrateNullsApiFile = if (migrateNullsApi != null) {
579             val jar = File(migrateNullsApi)
580             if (jar.isFile) {
581                 jar
582             } else {
583                 val file = File(project, "stable-api.txt")
584                 file.writeText(migrateNullsApi.trimIndent())
585                 file
586             }
587         } else {
588             null
589         }
590 
591         val manifestFileArgs = if (manifest != null) {
592             val file = File(project, "manifest.xml")
593             file.writeText(manifest.trimIndent())
594             arrayOf(ARG_MANIFEST, file.path)
595         } else {
596             emptyArray()
597         }
598 
599         val migrateNullsArguments = if (migrateNullsApiFile != null) {
600             arrayOf(ARG_MIGRATE_NULLNESS, migrateNullsApiFile.path)
601         } else {
602             emptyArray()
603         }
604 
605         val checkCompatibilityArguments = if (checkCompatibilityApiFile != null) {
606             val extra: Array<String> = if (allowCompatibleDifferences) {
607                 arrayOf(ARG_ALLOW_COMPATIBLE_DIFFERENCES)
608             } else {
609                 emptyArray()
610             }
611             arrayOf(ARG_CHECK_COMPATIBILITY_API_CURRENT, checkCompatibilityApiFile.path, *extra)
612         } else {
613             emptyArray()
614         }
615 
616         val checkCompatibilityApiReleasedArguments = if (checkCompatibilityApiReleasedFile != null) {
617             arrayOf(ARG_CHECK_COMPATIBILITY_API_RELEASED, checkCompatibilityApiReleasedFile.path)
618         } else {
619             emptyArray()
620         }
621 
622         val checkCompatibilityRemovedCurrentArguments = if (checkCompatibilityRemovedApiCurrentFile != null) {
623             val extra: Array<String> = if (allowCompatibleDifferences) {
624                 arrayOf(ARG_ALLOW_COMPATIBLE_DIFFERENCES)
625             } else {
626                 emptyArray()
627             }
628             arrayOf(ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT, checkCompatibilityRemovedApiCurrentFile.path, *extra)
629         } else {
630             emptyArray()
631         }
632 
633         val checkCompatibilityRemovedReleasedArguments = if (checkCompatibilityRemovedApiReleasedFile != null) {
634             arrayOf(ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED, checkCompatibilityRemovedApiReleasedFile.path)
635         } else {
636             emptyArray()
637         }
638 
639         val quiet = if (expectedOutput != null && !extraArguments.contains(ARG_VERBOSE)) {
640             // If comparing output, avoid noisy output such as the banner etc
641             arrayOf(ARG_QUIET)
642         } else {
643             emptyArray()
644         }
645 
646         val coverageStats = if (coverageJars != null && coverageJars.isNotEmpty()) {
647             val sb = StringBuilder()
648             val root = File(project, "coverageJars")
649             root.mkdirs()
650             for (jar in coverageJars) {
651                 if (sb.isNotEmpty()) {
652                     sb.append(File.pathSeparator)
653                 }
654                 val file = jar.createFile(root)
655                 sb.append(file.path)
656             }
657             arrayOf(ARG_ANNOTATION_COVERAGE_OF, sb.toString())
658         } else {
659             emptyArray()
660         }
661 
662         var proguardFile: File? = null
663         val proguardKeepArguments = if (proguard != null) {
664             proguardFile = File(project, "proguard.cfg")
665             arrayOf(ARG_PROGUARD, proguardFile.path)
666         } else {
667             emptyArray()
668         }
669 
670         val showAnnotationArguments = if (showAnnotations.isNotEmpty() || includeSystemApiAnnotations) {
671             val args = mutableListOf<String>()
672             for (annotation in showAnnotations) {
673                 args.add(ARG_SHOW_ANNOTATION)
674                 args.add(annotation)
675             }
676             if (includeSystemApiAnnotations && !args.contains("android.annotation.SystemApi")) {
677                 args.add(ARG_SHOW_ANNOTATION)
678                 args.add("android.annotation.SystemApi")
679             }
680             if (includeSystemApiAnnotations && !args.contains("android.annotation.TestApi")) {
681                 args.add(ARG_SHOW_ANNOTATION)
682                 args.add("android.annotation.TestApi")
683             }
684             args.toTypedArray()
685         } else {
686             emptyArray()
687         }
688 
689         val hideAnnotationArguments = if (hideAnnotations.isNotEmpty()) {
690             val args = mutableListOf<String>()
691             for (annotation in hideAnnotations) {
692                 args.add(ARG_HIDE_ANNOTATION)
693                 args.add(annotation)
694             }
695             args.toTypedArray()
696         } else {
697             emptyArray()
698         }
699 
700         val showUnannotatedArgs =
701             if (showUnannotated) {
702                 arrayOf(ARG_SHOW_UNANNOTATED)
703             } else {
704                 emptyArray()
705             }
706 
707         val includeSourceRetentionAnnotationArgs =
708             if (includeSourceRetentionAnnotations) {
709                 arrayOf(ARG_INCLUDE_SOURCE_RETENTION)
710             } else {
711                 emptyArray()
712             }
713 
714         var removedApiFile: File? = null
715         val removedArgs = if (removedApi != null) {
716             removedApiFile = temporaryFolder.newFile("removed.txt")
717             arrayOf(ARG_REMOVED_API, removedApiFile.path)
718         } else {
719             emptyArray()
720         }
721 
722         var removedDexApiFile: File? = null
723         val removedDexArgs = if (removedDexApi != null) {
724             removedDexApiFile = temporaryFolder.newFile("removed-dex.txt")
725             arrayOf(ARG_REMOVED_DEX_API, removedDexApiFile.path)
726         } else {
727             emptyArray()
728         }
729 
730         var apiFile: File? = null
731         val apiArgs = if (api != null) {
732             apiFile = temporaryFolder.newFile("public-api.txt")
733             arrayOf(ARG_API, apiFile.path)
734         } else {
735             emptyArray()
736         }
737 
738         var exactApiFile: File? = null
739         val exactApiArgs = if (exactApi != null) {
740             exactApiFile = temporaryFolder.newFile("exact-api.txt")
741             arrayOf(ARG_EXACT_API, exactApiFile.path)
742         } else {
743             emptyArray()
744         }
745 
746         var apiXmlFile: File? = null
747         val apiXmlArgs = if (apiXml != null) {
748             apiXmlFile = temporaryFolder.newFile("public-api-xml.txt")
749             arrayOf(ARG_XML_API, apiXmlFile.path)
750         } else {
751             emptyArray()
752         }
753 
754         var privateApiFile: File? = null
755         val privateApiArgs = if (privateApi != null) {
756             privateApiFile = temporaryFolder.newFile("private.txt")
757             arrayOf(ARG_PRIVATE_API, privateApiFile.path)
758         } else {
759             emptyArray()
760         }
761 
762         var dexApiFile: File? = null
763         val dexApiArgs = if (dexApi != null) {
764             dexApiFile = temporaryFolder.newFile("public-dex.txt")
765             arrayOf(ARG_DEX_API, dexApiFile.path)
766         } else {
767             emptyArray()
768         }
769 
770         var dexApiMappingFile: File? = null
771         val dexApiMappingArgs = if (dexApiMapping != null) {
772             dexApiMappingFile = temporaryFolder.newFile("api-mapping.txt")
773             arrayOf(ARG_DEX_API_MAPPING, dexApiMappingFile.path)
774         } else {
775             emptyArray()
776         }
777 
778         var privateDexApiFile: File? = null
779         val privateDexApiArgs = if (privateDexApi != null) {
780             privateDexApiFile = temporaryFolder.newFile("private-dex.txt")
781             arrayOf(ARG_PRIVATE_DEX_API, privateDexApiFile.path)
782         } else {
783             emptyArray()
784         }
785 
786         var subtractApiFile: File? = null
787         val subtractApiArgs = if (subtractApi != null) {
788             subtractApiFile = temporaryFolder.newFile("subtract-api.txt")
789             subtractApiFile.writeText(subtractApi.trimIndent())
790             arrayOf(ARG_SUBTRACT_API, subtractApiFile.path)
791         } else {
792             emptyArray()
793         }
794 
795         val convertFiles = mutableListOf<Options.ConvertFile>()
796         val convertArgs = if (convertToJDiff.isNotEmpty()) {
797             val args = mutableListOf<String>()
798             var index = 1
799             for (convert in convertToJDiff) {
800                 val signature = convert.fromApi
801                 val base = convert.baseApi
802                 val convertSig = temporaryFolder.newFile("convert-signatures$index.txt")
803                 convertSig.writeText(signature.trimIndent(), UTF_8)
804                 val extension = convert.format.preferredExtension()
805                 val output = temporaryFolder.newFile("convert-output$index$extension")
806                 val baseFile = if (base != null) {
807                     val baseFile = temporaryFolder.newFile("convert-signatures$index-base.txt")
808                     baseFile.writeText(base.trimIndent(), UTF_8)
809                     baseFile
810                 } else {
811                     null
812                 }
813                 convertFiles += Options.ConvertFile(convertSig, output, baseFile,
814                     strip = true, outputFormat = convert.format)
815                 index++
816 
817                 if (baseFile != null) {
818                     args +=
819                         when {
820                             convert.format == FileFormat.V1 -> ARG_CONVERT_NEW_TO_V1
821                             convert.format == FileFormat.V2 -> ARG_CONVERT_NEW_TO_V2
822                             convert.strip -> "-new_api"
823                             else -> ARG_CONVERT_NEW_TO_JDIFF
824                         }
825                     args += baseFile.path
826                 } else {
827                     args +=
828                         when {
829                             convert.format == FileFormat.V1 -> ARG_CONVERT_TO_V1
830                             convert.format == FileFormat.V2 -> ARG_CONVERT_TO_V2
831                             convert.strip -> "-convert2xml"
832                             else -> ARG_CONVERT_TO_JDIFF
833                         }
834                 }
835                 args += convertSig.path
836                 args += output.path
837             }
838             args.toTypedArray()
839         } else {
840             emptyArray()
841         }
842 
843         var stubsDir: File? = null
844         val stubsArgs = if (stubs.isNotEmpty()) {
845             stubsDir = temporaryFolder.newFolder("stubs")
846             if (docStubs) {
847                 arrayOf(ARG_DOC_STUBS, stubsDir.path)
848             } else {
849                 arrayOf(ARG_STUBS, stubsDir.path)
850             }
851         } else {
852             emptyArray()
853         }
854 
855         var stubsSourceListFile: File? = null
856         val stubsSourceListArgs = if (stubsSourceList != null) {
857             stubsSourceListFile = temporaryFolder.newFile("droiddoc-src-list")
858             arrayOf(ARG_STUBS_SOURCE_LIST, stubsSourceListFile.path)
859         } else {
860             emptyArray()
861         }
862 
863         val applyApiLevelsXmlFile: File?
864         val applyApiLevelsXmlArgs = if (applyApiLevelsXml != null) {
865             ApiLookup::class.java.getDeclaredMethod("dispose").apply { isAccessible = true }.invoke(null)
866             applyApiLevelsXmlFile = temporaryFolder.newFile("api-versions.xml")
867             applyApiLevelsXmlFile?.writeText(applyApiLevelsXml.trimIndent())
868             arrayOf(ARG_APPLY_API_LEVELS, applyApiLevelsXmlFile.path)
869         } else {
870             emptyArray()
871         }
872 
873         var baselineFile: File? = null
874         val baselineArgs = if (baseline != null) {
875             baselineFile = temporaryFolder.newFile("baseline.txt")
876             baselineFile?.writeText(baseline.trimIndent())
877             if (!(updateBaseline || mergeBaseline != null)) {
878                 arrayOf(ARG_BASELINE, baselineFile.path)
879             } else {
880                 arrayOf(ARG_BASELINE,
881                     baselineFile.path,
882                     if (mergeBaseline != null) ARG_MERGE_BASELINE else ARG_UPDATE_BASELINE,
883                     baselineFile.path)
884             }
885         } else {
886             emptyArray()
887         }
888 
889         val importedPackageArgs = mutableListOf<String>()
890         importedPackages.forEach {
891             importedPackageArgs.add("--stub-import-packages")
892             importedPackageArgs.add(it)
893         }
894 
895         val skipEmitPackagesArgs = mutableListOf<String>()
896         skipEmitPackages.forEach {
897             skipEmitPackagesArgs.add("--skip-emit-packages")
898             skipEmitPackagesArgs.add(it)
899         }
900 
901         val kotlinPath = findKotlinStdlibPath()
902         val kotlinPathArgs =
903             if (kotlinPath.isNotEmpty() &&
904                 sourceList.asSequence().any { it.endsWith(DOT_KT) }
905             ) {
906                 arrayOf(ARG_CLASS_PATH, kotlinPath.joinToString(separator = File.pathSeparator) { it })
907             } else {
908                 emptyArray()
909             }
910 
911         val sdkFilesDir: File?
912         val sdkFilesArgs: Array<String>
913         if (sdk_broadcast_actions != null ||
914             sdk_activity_actions != null ||
915             sdk_service_actions != null ||
916             sdk_categories != null ||
917             sdk_features != null ||
918             sdk_widgets != null
919         ) {
920             val dir = File(project, "sdk-files")
921             sdkFilesArgs = arrayOf(ARG_SDK_VALUES, dir.path)
922             sdkFilesDir = dir
923         } else {
924             sdkFilesArgs = emptyArray()
925             sdkFilesDir = null
926         }
927 
928         val artifactArgs = if (artifacts != null) {
929             val args = mutableListOf<String>()
930             var index = 1
931             for ((artifactId, signatures) in artifacts) {
932                 val signatureFile = temporaryFolder.newFile("signature-file-$index.txt")
933                 signatureFile.writeText(signatures.trimIndent())
934                 index++
935 
936                 args.add(ARG_REGISTER_ARTIFACT)
937                 args.add(signatureFile.path)
938                 args.add(artifactId)
939             }
940             args.toTypedArray()
941         } else {
942             emptyArray()
943         }
944 
945         val extractedAnnotationsZip: File?
946         val extractAnnotationsArgs = if (extractAnnotations != null) {
947             extractedAnnotationsZip = temporaryFolder.newFile("extracted-annotations.zip")
948             arrayOf(ARG_EXTRACT_ANNOTATIONS, extractedAnnotationsZip.path)
949         } else {
950             extractedAnnotationsZip = null
951             emptyArray()
952         }
953 
954         val validateNullabilityTxt: File?
955         val validateNullabilityArgs = if (validateNullability != null) {
956             validateNullabilityTxt = temporaryFolder.newFile("validate-nullability.txt")
957             arrayOf(
958                 ARG_NULLABILITY_WARNINGS_TXT, validateNullabilityTxt.path,
959                 ARG_NULLABILITY_ERRORS_NON_FATAL // for testing, report on errors instead of throwing
960             )
961         } else {
962             validateNullabilityTxt = null
963             emptyArray()
964         }
965         val validateNullablityFromListFile: File?
966         val validateNullabilityFromListArgs = if (validateNullabilityFromList != null) {
967             validateNullablityFromListFile = temporaryFolder.newFile("validate-nullability-classes.txt")
968             validateNullablityFromListFile.writeText(validateNullabilityFromList)
969             arrayOf(
970                 ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullablityFromListFile.path
971             )
972         } else {
973             emptyArray()
974         }
975 
976         val signatureFormatArgs = if (format != null) {
977             arrayOf(format.outputFlag())
978         } else {
979             emptyArray()
980         }
981 
982         // Run optional additional setup steps on the project directory
983         projectSetup?.invoke(project)
984 
985         val actualOutput = runDriver(
986             ARG_NO_COLOR,
987             ARG_NO_BANNER,
988 
989             // Tell metalava where to store temp folder: place them under the
990             // test root folder such that we clean up the output strings referencing
991             // paths to the temp folder
992             "--temp-folder",
993             temporaryFolder.newFolder("temp").path,
994 
995             // For the tests we want to treat references to APIs like java.io.Closeable
996             // as a class that is part of the API surface, not as a hidden class as would
997             // be the case when analyzing a complete API surface
998             // ARG_UNHIDE_CLASSPATH_CLASSES,
999             ARG_ALLOW_REFERENCING_UNKNOWN_CLASSES,
1000 
1001             // Annotation generation temporarily turned off by default while integrating with
1002             // SDK builds; tests need these
1003             ARG_INCLUDE_ANNOTATIONS,
1004 
1005             ARG_SOURCE_PATH,
1006             sourcePath,
1007             ARG_CLASS_PATH,
1008             androidJar.path,
1009             *classpathArgs,
1010             *kotlinPathArgs,
1011             *removedArgs,
1012             *removedDexArgs,
1013             *apiArgs,
1014             *apiXmlArgs,
1015             *exactApiArgs,
1016             *privateApiArgs,
1017             *dexApiArgs,
1018             *privateDexApiArgs,
1019             *dexApiMappingArgs,
1020             *subtractApiArgs,
1021             *stubsArgs,
1022             *stubsSourceListArgs,
1023             "$ARG_COMPAT_OUTPUT=${if (compatibilityMode) "yes" else "no"}",
1024             "$ARG_OUTPUT_KOTLIN_NULLS=${if (outputKotlinStyleNulls) "yes" else "no"}",
1025             "$ARG_INPUT_KOTLIN_NULLS=${if (inputKotlinStyleNulls) "yes" else "no"}",
1026             "$ARG_OMIT_COMMON_PACKAGES=${if (omitCommonPackages) "yes" else "no"}",
1027             "$ARG_INCLUDE_SIG_VERSION=${if (includeSignatureVersion) "yes" else "no"}",
1028             *coverageStats,
1029             *quiet,
1030             *mergeAnnotationsArgs,
1031             *signatureAnnotationsArgs,
1032             *javaStubAnnotationsArgs,
1033             *inclusionAnnotationsArgs,
1034             *migrateNullsArguments,
1035             *checkCompatibilityArguments,
1036             *checkCompatibilityApiReleasedArguments,
1037             *checkCompatibilityRemovedCurrentArguments,
1038             *checkCompatibilityRemovedReleasedArguments,
1039             *proguardKeepArguments,
1040             *manifestFileArgs,
1041             *convertArgs,
1042             *applyApiLevelsXmlArgs,
1043             *baselineArgs,
1044             *showAnnotationArguments,
1045             *hideAnnotationArguments,
1046             *showUnannotatedArgs,
1047             *includeSourceRetentionAnnotationArgs,
1048             *apiLintArgs,
1049             *sdkFilesArgs,
1050             *importedPackageArgs.toTypedArray(),
1051             *skipEmitPackagesArgs.toTypedArray(),
1052             *artifactArgs,
1053             *extractAnnotationsArgs,
1054             *validateNullabilityArgs,
1055             *validateNullabilityFromListArgs,
1056             *signatureFormatArgs,
1057             *sourceList,
1058             *extraArguments,
1059             expectedFail = expectedFail
1060         )
1061 
1062         if (expectedOutput != null) {
1063             assertEquals(expectedOutput.trimIndent().trim(), actualOutput.trim())
1064         }
1065 
1066         if (api != null && apiFile != null) {
1067             assertTrue("${apiFile.path} does not exist even though --api was used", apiFile.exists())
1068             val actualText = readFile(apiFile, stripBlankLines, trim)
1069             assertEquals(stripComments(api, stripLineComments = false).trimIndent(), actualText)
1070             // Make sure we can read back the files we write
1071             ApiFile.parseApi(apiFile, options.outputKotlinStyleNulls)
1072         }
1073 
1074         if (apiXml != null && apiXmlFile != null) {
1075             assertTrue(
1076                 "${apiXmlFile.path} does not exist even though $ARG_XML_API was used",
1077                 apiXmlFile.exists()
1078             )
1079             val actualText = readFile(apiXmlFile, stripBlankLines, trim)
1080             assertEquals(stripComments(apiXml, stripLineComments = false).trimIndent(), actualText)
1081             // Make sure we can read back the files we write
1082             parseDocument(apiXmlFile.readText(UTF_8), false)
1083         }
1084 
1085         if (baseline != null && baselineFile != null) {
1086             assertTrue(
1087                 "${baselineFile.path} does not exist even though $ARG_BASELINE was used",
1088                 baselineFile.exists()
1089             )
1090             val actualText = readFile(baselineFile, stripBlankLines, trim)
1091             val sourceFile = mergeBaseline ?: baseline
1092             assertEquals(stripComments(sourceFile, stripLineComments = false).trimIndent(), actualText)
1093         }
1094 
1095         if (convertFiles.isNotEmpty()) {
1096             for (i in 0 until convertToJDiff.size) {
1097                 val expected = convertToJDiff[i].outputFile
1098                 val converted = convertFiles[i].outputFile
1099                 if (convertToJDiff[i].baseApi != null &&
1100                     compatibilityMode &&
1101                     actualOutput.contains("No API change detected, not generating diff")) {
1102                     continue
1103                 }
1104                 assertTrue(
1105                     "${converted.path} does not exist even though $ARG_CONVERT_TO_JDIFF was used",
1106                     converted.exists()
1107                 )
1108                 val actualText = readFile(converted, stripBlankLines, trim)
1109                 if (actualText.contains("<api")) {
1110                     parseDocument(actualText, false)
1111                 }
1112                 assertEquals(stripComments(expected, stripLineComments = false).trimIndent(), actualText)
1113                 // Make sure we can read back the files we write
1114             }
1115         }
1116 
1117         if (removedApi != null && removedApiFile != null) {
1118             assertTrue(
1119                 "${removedApiFile.path} does not exist even though --removed-api was used",
1120                 removedApiFile.exists()
1121             )
1122             val actualText = readFile(removedApiFile, stripBlankLines, trim)
1123             assertEquals(stripComments(removedApi, stripLineComments = false).trimIndent(), actualText)
1124             // Make sure we can read back the files we write
1125             ApiFile.parseApi(removedApiFile, options.outputKotlinStyleNulls)
1126         }
1127 
1128         if (removedDexApi != null && removedDexApiFile != null) {
1129             assertTrue(
1130                 "${removedDexApiFile.path} does not exist even though --removed-dex-api was used",
1131                 removedDexApiFile.exists()
1132             )
1133             val actualText = readFile(removedDexApiFile, stripBlankLines, trim)
1134             assertEquals(stripComments(removedDexApi, stripLineComments = false).trimIndent(), actualText)
1135         }
1136 
1137         if (exactApi != null && exactApiFile != null) {
1138             assertTrue(
1139                 "${exactApiFile.path} does not exist even though --exact-api was used",
1140                 exactApiFile.exists()
1141             )
1142             val actualText = readFile(exactApiFile, stripBlankLines, trim)
1143             assertEquals(stripComments(exactApi, stripLineComments = false).trimIndent(), actualText)
1144             // Make sure we can read back the files we write
1145             ApiFile.parseApi(exactApiFile, options.outputKotlinStyleNulls)
1146         }
1147 
1148         if (privateApi != null && privateApiFile != null) {
1149             assertTrue(
1150                 "${privateApiFile.path} does not exist even though --private-api was used",
1151                 privateApiFile.exists()
1152             )
1153             val actualText = readFile(privateApiFile, stripBlankLines, trim)
1154             assertEquals(stripComments(privateApi, stripLineComments = false).trimIndent(), actualText)
1155             // Make sure we can read back the files we write
1156             ApiFile.parseApi(privateApiFile, options.outputKotlinStyleNulls)
1157         }
1158 
1159         if (dexApi != null && dexApiFile != null) {
1160             assertTrue(
1161                 "${dexApiFile.path} does not exist even though --dex-api was used",
1162                 dexApiFile.exists()
1163             )
1164             val actualText = readFile(dexApiFile, stripBlankLines, trim)
1165             assertEquals(stripComments(dexApi, stripLineComments = false).trimIndent(), actualText)
1166         }
1167 
1168         if (privateDexApi != null && privateDexApiFile != null) {
1169             assertTrue(
1170                 "${privateDexApiFile.path} does not exist even though --private-dex-api was used",
1171                 privateDexApiFile.exists()
1172             )
1173             val actualText = readFile(privateDexApiFile, stripBlankLines, trim)
1174             assertEquals(stripComments(privateDexApi, stripLineComments = false).trimIndent(), actualText)
1175         }
1176 
1177         if (dexApiMapping != null && dexApiMappingFile != null) {
1178             assertTrue(
1179                 "${dexApiMappingFile.path} does not exist even though --dex-api-maping was used",
1180                 dexApiMappingFile.exists()
1181             )
1182             val actualText = readFile(dexApiMappingFile, stripBlankLines, trim)
1183             assertEquals(stripComments(dexApiMapping, stripLineComments = false).trimIndent(), actualText)
1184         }
1185 
1186         if (proguard != null && proguardFile != null) {
1187             val expectedProguard = readFile(proguardFile)
1188             assertTrue(
1189                 "${proguardFile.path} does not exist even though --proguard was used",
1190                 proguardFile.exists()
1191             )
1192             assertEquals(stripComments(proguard, stripLineComments = false).trimIndent(), expectedProguard.trim())
1193         }
1194 
1195         if (sdk_broadcast_actions != null) {
1196             val actual = readFile(File(sdkFilesDir, "broadcast_actions.txt"), stripBlankLines, trim)
1197             assertEquals(sdk_broadcast_actions.trimIndent().trim(), actual.trim())
1198         }
1199 
1200         if (sdk_activity_actions != null) {
1201             val actual = readFile(File(sdkFilesDir, "activity_actions.txt"), stripBlankLines, trim)
1202             assertEquals(sdk_activity_actions.trimIndent().trim(), actual.trim())
1203         }
1204 
1205         if (sdk_service_actions != null) {
1206             val actual = readFile(File(sdkFilesDir, "service_actions.txt"), stripBlankLines, trim)
1207             assertEquals(sdk_service_actions.trimIndent().trim(), actual.trim())
1208         }
1209 
1210         if (sdk_categories != null) {
1211             val actual = readFile(File(sdkFilesDir, "categories.txt"), stripBlankLines, trim)
1212             assertEquals(sdk_categories.trimIndent().trim(), actual.trim())
1213         }
1214 
1215         if (sdk_features != null) {
1216             val actual = readFile(File(sdkFilesDir, "features.txt"), stripBlankLines, trim)
1217             assertEquals(sdk_features.trimIndent().trim(), actual.trim())
1218         }
1219 
1220         if (sdk_widgets != null) {
1221             val actual = readFile(File(sdkFilesDir, "widgets.txt"), stripBlankLines, trim)
1222             assertEquals(sdk_widgets.trimIndent().trim(), actual.trim())
1223         }
1224 
1225         if (warnings != null) {
1226             assertEquals(
1227                 warnings.trimIndent().trim(),
1228                 cleanupString(reportedWarnings.toString(), project)
1229             )
1230         }
1231 
1232         if (extractAnnotations != null && extractedAnnotationsZip != null) {
1233             assertTrue(
1234                 "Using --extract-annotations but $extractedAnnotationsZip was not created",
1235                 extractedAnnotationsZip.isFile
1236             )
1237             for ((pkg, xml) in extractAnnotations) {
1238                 assertPackageXml(pkg, extractedAnnotationsZip, xml)
1239             }
1240         }
1241 
1242         if (validateNullabilityTxt != null) {
1243             assertTrue(
1244                 "Using $ARG_NULLABILITY_WARNINGS_TXT but $validateNullabilityTxt was not created",
1245                 validateNullabilityTxt.isFile
1246             )
1247             val actualReport =
1248                 Files.asCharSource(validateNullabilityTxt, UTF_8).readLines().map(String::trim).toSet()
1249             assertEquals(validateNullability, actualReport)
1250         }
1251 
1252         if (stubs.isNotEmpty() && stubsDir != null) {
1253             for (i in 0 until stubs.size) {
1254                 var stub = stubs[i].trimIndent()
1255 
1256                 var targetPath: String
1257                 var stubFile: File
1258                 if (stub.startsWith("[") && stub.contains("]")) {
1259                     val pathEnd = stub.indexOf("]\n")
1260                     targetPath = stub.substring(1, pathEnd)
1261                     stubFile = File(stubsDir, targetPath)
1262                     if (stubFile.isFile) {
1263                         stub = stub.substring(pathEnd + 2)
1264                     }
1265                 } else {
1266                     val sourceFile = sourceFiles[i]
1267                     targetPath = if (sourceFile.targetPath.endsWith(DOT_KT)) {
1268                         // Kotlin source stubs are rewritten as .java files for now
1269                         sourceFile.targetPath.substring(0, sourceFile.targetPath.length - 3) + DOT_JAVA
1270                     } else {
1271                         sourceFile.targetPath
1272                     }
1273                     stubFile = File(stubsDir, targetPath.substring("src/".length))
1274                 }
1275                 if (!stubFile.isFile) {
1276                     if (stub.startsWith("[") && stub.contains("]")) {
1277                         val pathEnd = stub.indexOf("]\n")
1278                         val path = stub.substring(1, pathEnd)
1279                         stubFile = File(stubsDir, path)
1280                         if (stubFile.isFile) {
1281                             stub = stub.substring(pathEnd + 2)
1282                         }
1283                     }
1284                     if (!stubFile.exists()) {
1285                         /* Example:
1286                             stubs = arrayOf(
1287                                 """
1288                                 [test/visible/package-info.java]
1289                                 <html>My package docs</html>
1290                                 package test.visible;
1291                                 """,
1292                                 ...
1293                            Here the stub will be read from $stubsDir/test/visible/package-info.java.
1294                          */
1295                         throw FileNotFoundException(
1296                             "Could not find generated stub for $targetPath; consider " +
1297                                 "setting target relative path in stub header as prefix surrounded by []"
1298                         )
1299                     }
1300                 }
1301                 val actualText = readFile(stubFile, stripBlankLines, trim)
1302                 assertEquals(stub, actualText)
1303             }
1304         }
1305 
1306         if (stubsSourceList != null && stubsSourceListFile != null) {
1307             assertTrue(
1308                 "${stubsSourceListFile.path} does not exist even though --write-stubs-source-list was used",
1309                 stubsSourceListFile.exists()
1310             )
1311             val actualText = cleanupString(readFile(stubsSourceListFile, stripBlankLines, trim), project)
1312                 // To make golden files look better put one entry per line instead of a single
1313                 // space separated line
1314                 .replace(' ', '\n')
1315             assertEquals(stripComments(stubsSourceList, stripLineComments = false).trimIndent(), actualText)
1316         }
1317 
1318         if (checkCompilation && stubsDir != null && CHECK_STUB_COMPILATION) {
1319             val generated = gatherSources(listOf(stubsDir)).asSequence().map { it.path }.toList().toTypedArray()
1320 
1321             // Also need to include on the compile path annotation classes referenced in the stubs
1322             val extraAnnotationsDir = File("stub-annotations/src/main/java")
1323             if (!extraAnnotationsDir.isDirectory) {
1324                 fail("Couldn't find $extraAnnotationsDir: Is the pwd set to the root of the metalava source code?")
1325                 fail("Couldn't find $extraAnnotationsDir: Is the pwd set to the root of an Android source tree?")
1326             }
1327             val extraAnnotations =
1328                 gatherSources(listOf(extraAnnotationsDir)).asSequence().map { it.path }.toList().toTypedArray()
1329 
1330             if (!runCommand(
1331                     "${getJdkPath()}/bin/javac", arrayOf(
1332                         "-d", project.path, *generated, *extraAnnotations
1333                     )
1334                 )
1335             ) {
1336                 fail("Couldn't compile stub file -- compilation problems")
1337                 return
1338             }
1339         }
1340 
1341         if (checkDoclava1 && !CHECK_OLD_DOCLAVA_TOO) {
1342             println(
1343                 "This test requested diffing with doclava1, but doclava1 testing was disabled with the " +
1344                     "DriverTest#CHECK_OLD_DOCLAVA_TOO = false"
1345             )
1346         }
1347 
1348         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
1349             api != null && apiFile != null
1350         ) {
1351             apiFile.delete()
1352             checkSignaturesWithDoclava(
1353                 api = api,
1354                 argument = "-api",
1355                 output = apiFile,
1356                 expected = apiFile,
1357                 sourceList = sourceList,
1358                 sourcePath = sourcePath,
1359                 packages = packages,
1360                 androidJar = androidJar,
1361                 trim = trim,
1362                 stripBlankLines = stripBlankLines,
1363                 showAnnotationArgs = showAnnotationArguments,
1364                 stubImportPackages = importedPackages,
1365                 showUnannotated = showUnannotated,
1366                 project = project,
1367                 extraArguments = extraArguments
1368             )
1369         }
1370 
1371         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && apiXml != null && apiXmlFile != null) {
1372             apiXmlFile.delete()
1373 
1374             // Either we write the signatureSource, or you must have specified an API report
1375             val signatureFile: File =
1376                 apiFile ?: if (signatureSource != null) {
1377                     val temp = temporaryFolder.newFile("jdiff-doclava-api.txt")
1378                     temp.writeText(signatureSource.trimIndent(), UTF_8)
1379                     temp
1380                 } else {
1381                     fail("When verifying XML files with doclava you must either specify signatureSource or api")
1382                     error("Unreachable")
1383                 }
1384 
1385             // Need to emit the codebase
1386             generateJDiffXmlWithDoclava1(signatureFile, apiXmlFile, null)
1387 
1388             val actualText = cleanupString(readFile(apiXmlFile, stripBlankLines, trim), project, true)
1389             assertEquals(stripComments(apiXml, stripLineComments = false).trimIndent(), actualText)
1390         }
1391 
1392         if (CHECK_JDIFF && apiXmlFile != null && convertToJDiff.isNotEmpty()) {
1393             // TODO: Parse the XML file with jdiff too
1394         }
1395 
1396         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && convertToJDiff.isNotEmpty()) {
1397             var index = 1
1398             for (convert in convertToJDiff) {
1399                 if (convert.format != FileFormat.JDIFF) {
1400                     continue
1401                 }
1402                 val signature = convert.fromApi
1403                 val expectedXml = convert.outputFile
1404                 val base = convert.baseApi
1405                 val strip = convert.strip
1406                 val convertSig = temporaryFolder.newFile("doclava-jdiff-signatures$index.txt")
1407                 convertSig.writeText(signature.trimIndent(), UTF_8)
1408                 val output = temporaryFolder.newFile("doclava-jdiff-output$index.xml")
1409                 val baseFile = if (base != null) {
1410                     val baseFile = temporaryFolder.newFile("doclava-jdiff-signatures$index-base.txt")
1411                     baseFile.writeText(base.trimIndent(), UTF_8)
1412                     baseFile
1413                 } else {
1414                     null
1415                 }
1416 
1417                 generateJDiffXmlWithDoclava1(convertSig, output, baseFile, strip = strip)
1418 
1419                 val actualText = cleanupString(readFile(output, stripBlankLines, trim), project, true)
1420                 assertEquals(stripComments(expectedXml, stripLineComments = false).trimIndent(), actualText)
1421                 index++
1422             }
1423         }
1424 
1425         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
1426             exactApi != null && exactApiFile != null
1427         ) {
1428             exactApiFile.delete()
1429             checkSignaturesWithDoclava(
1430                 api = exactApi,
1431                 argument = "-exactApi",
1432                 output = exactApiFile,
1433                 expected = exactApiFile,
1434                 sourceList = sourceList,
1435                 sourcePath = sourcePath,
1436                 packages = packages,
1437                 androidJar = androidJar,
1438                 trim = trim,
1439                 stripBlankLines = stripBlankLines,
1440                 showAnnotationArgs = showAnnotationArguments,
1441                 stubImportPackages = importedPackages,
1442                 showUnannotated = showUnannotated,
1443                 project = project,
1444                 extraArguments = extraArguments
1445             )
1446         }
1447 
1448         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
1449             removedApi != null && removedApiFile != null
1450         ) {
1451             removedApiFile.delete()
1452             checkSignaturesWithDoclava(
1453                 api = removedApi,
1454                 argument = "-removedApi",
1455                 output = removedApiFile,
1456                 expected = removedApiFile,
1457                 sourceList = sourceList,
1458                 sourcePath = sourcePath,
1459                 packages = packages,
1460                 androidJar = androidJar,
1461                 trim = trim,
1462                 stripBlankLines = stripBlankLines,
1463                 showAnnotationArgs = showAnnotationArguments,
1464                 stubImportPackages = importedPackages,
1465                 showUnannotated = showUnannotated,
1466                 project = project,
1467                 extraArguments = extraArguments
1468             )
1469         }
1470 
1471         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && stubsDir != null) {
1472             stubsDir.deleteRecursively()
1473             val firstFile = File(stubsDir, sourceFiles[0].targetPath.substring("src/".length))
1474             checkSignaturesWithDoclava(
1475                 api = stubs[0],
1476                 argument = "-stubs",
1477                 output = stubsDir,
1478                 expected = firstFile,
1479                 sourceList = sourceList,
1480                 sourcePath = sourcePath,
1481                 packages = packages,
1482                 androidJar = androidJar,
1483                 trim = trim,
1484                 stripBlankLines = stripBlankLines,
1485                 showAnnotationArgs = showAnnotationArguments,
1486                 stubImportPackages = importedPackages,
1487                 showUnannotated = showUnannotated,
1488                 project = project,
1489                 extraArguments = extraArguments
1490             )
1491         }
1492 
1493         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && proguard != null && proguardFile != null) {
1494             proguardFile.delete()
1495             checkSignaturesWithDoclava(
1496                 api = proguard,
1497                 argument = "-proguard",
1498                 output = proguardFile,
1499                 expected = proguardFile,
1500                 sourceList = sourceList,
1501                 sourcePath = sourcePath,
1502                 packages = packages,
1503                 androidJar = androidJar,
1504                 trim = trim,
1505                 stripBlankLines = stripBlankLines,
1506                 showAnnotationArgs = showAnnotationArguments,
1507                 stubImportPackages = importedPackages,
1508                 showUnannotated = showUnannotated,
1509                 project = project,
1510                 extraArguments = extraArguments
1511             )
1512         }
1513 
1514         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
1515             privateApi != null && privateApiFile != null
1516         ) {
1517             privateApiFile.delete()
1518             checkSignaturesWithDoclava(
1519                 api = privateApi,
1520                 argument = "-privateApi",
1521                 output = privateApiFile,
1522                 expected = privateApiFile,
1523                 sourceList = sourceList,
1524                 sourcePath = sourcePath,
1525                 packages = packages,
1526                 androidJar = androidJar,
1527                 trim = trim,
1528                 stripBlankLines = stripBlankLines,
1529                 showAnnotationArgs = showAnnotationArguments,
1530                 stubImportPackages = importedPackages,
1531                 // Workaround: -privateApi is a no-op if you don't also provide -api
1532                 extraDoclavaArguments = arrayOf("-api", File(privateApiFile.parentFile, "dummy-api.txt").path),
1533                 showUnannotated = showUnannotated,
1534                 project = project,
1535                 extraArguments = extraArguments
1536             )
1537         }
1538 
1539         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
1540             privateDexApi != null && privateDexApiFile != null
1541         ) {
1542             privateDexApiFile.delete()
1543             checkSignaturesWithDoclava(
1544                 api = privateDexApi,
1545                 argument = "-privateDexApi",
1546                 output = privateDexApiFile,
1547                 expected = privateDexApiFile,
1548                 sourceList = sourceList,
1549                 sourcePath = sourcePath,
1550                 packages = packages,
1551                 androidJar = androidJar,
1552                 trim = trim,
1553                 stripBlankLines = stripBlankLines,
1554                 showAnnotationArgs = showAnnotationArguments,
1555                 stubImportPackages = importedPackages,
1556                 // Workaround: -privateDexApi is a no-op if you don't also provide -api
1557                 extraDoclavaArguments = arrayOf("-api", File(privateDexApiFile.parentFile, "dummy-api.txt").path),
1558                 showUnannotated = showUnannotated,
1559                 project = project,
1560                 extraArguments = extraArguments
1561             )
1562         }
1563 
1564         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
1565             dexApi != null && dexApiFile != null
1566         ) {
1567             dexApiFile.delete()
1568             checkSignaturesWithDoclava(
1569                 api = dexApi,
1570                 argument = "-dexApi",
1571                 output = dexApiFile,
1572                 expected = dexApiFile,
1573                 sourceList = sourceList,
1574                 sourcePath = sourcePath,
1575                 packages = packages,
1576                 androidJar = androidJar,
1577                 trim = trim,
1578                 stripBlankLines = stripBlankLines,
1579                 showAnnotationArgs = showAnnotationArguments,
1580                 stubImportPackages = importedPackages,
1581                 // Workaround: -dexApi is a no-op if you don't also provide -api
1582                 extraDoclavaArguments = arrayOf("-api", File(dexApiFile.parentFile, "dummy-api.txt").path),
1583                 showUnannotated = showUnannotated,
1584                 project = project,
1585                 extraArguments = extraArguments
1586             )
1587         }
1588 
1589         if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
1590             dexApiMapping != null && dexApiMappingFile != null
1591         ) {
1592             dexApiMappingFile.delete()
1593             checkSignaturesWithDoclava(
1594                 api = dexApiMapping,
1595                 argument = "-apiMapping",
1596                 output = dexApiMappingFile,
1597                 expected = dexApiMappingFile,
1598                 sourceList = sourceList,
1599                 sourcePath = sourcePath,
1600                 packages = packages,
1601                 androidJar = androidJar,
1602                 trim = trim,
1603                 stripBlankLines = stripBlankLines,
1604                 showAnnotationArgs = showAnnotationArguments,
1605                 stubImportPackages = importedPackages,
1606                 // Workaround: -apiMapping is a no-op if you don't also provide -api
1607                 extraDoclavaArguments = arrayOf("-api", File(dexApiMappingFile.parentFile, "dummy-api.txt").path),
1608                 showUnannotated = showUnannotated,
1609                 project = project,
1610                 skipTestRoot = true,
1611                 extraArguments = extraArguments
1612             )
1613         }
1614     }
1615 
1616     /** Checks that the given zip annotations file contains the given XML package contents */
1617     private fun assertPackageXml(pkg: String, output: File, @Language("XML") expected: String) {
1618         assertNotNull(output)
1619         assertTrue(output.exists())
1620         val url = URL(
1621             "jar:" + SdkUtils.fileToUrlString(output) + "!/" + pkg.replace('.', '/') +
1622                 "/annotations.xml"
1623         )
1624         val stream = url.openStream()
1625         try {
1626             val bytes = ByteStreams.toByteArray(stream)
1627             assertNotNull(bytes)
1628             val xml = String(bytes, UTF_8).replace("\r\n", "\n")
1629             assertEquals(expected.trimIndent().trim(), xml.trimIndent().trim())
1630         } finally {
1631             Closeables.closeQuietly(stream)
1632         }
1633     }
1634 
1635     /** Hides path prefixes from /tmp folders used by the testing infrastructure */
1636     private fun cleanupString(string: String, project: File?, dropTestRoot: Boolean = false): String {
1637         var s = string
1638 
1639         if (project != null) {
1640             s = s.replace(project.path, "TESTROOT")
1641             s = s.replace(project.canonicalPath, "TESTROOT")
1642         }
1643 
1644         s = s.replace(temporaryFolder.root.path, "TESTROOT")
1645 
1646         val tmp = System.getProperty("java.io.tmpdir")
1647         if (tmp != null) {
1648             s = s.replace(tmp, "TEST")
1649         }
1650 
1651         s = s.trim()
1652 
1653         if (dropTestRoot) {
1654             s = s.replace("TESTROOT/", "")
1655         }
1656 
1657         return s
1658     }
1659 
1660     private fun generateJDiffXmlWithDoclava1(
1661         signatureFile: File,
1662         xmlOutput: File,
1663         baseFile: File?,
1664         strip: Boolean = false
1665     ) {
1666         val docLava1 = findDoclava()
1667 
1668         val args = if (baseFile != null) {
1669             arrayOf(
1670                 if (strip) "-new_api" else "-new_api_no_strip",
1671                 baseFile.path,
1672                 signatureFile.path,
1673                 xmlOutput.path
1674             )
1675         } else {
1676             arrayOf(
1677                 if (strip) "-convert2xml" else "-convert2xmlnostrip",
1678                 signatureFile.path,
1679                 xmlOutput.path
1680             )
1681         }
1682 
1683         val message = "\n${args.joinToString(separator = "\n") { "\"$it\"," }}"
1684         println("Running doclava1 with the following args:\n$message")
1685 
1686         println("My args:")
<lambda>null1687         args.forEach {
1688             println("\"$it\",")
1689         }
1690 
1691         val jdkPath = findJdk()
1692         if (!runCommand(
1693                 "$jdkPath/bin/java",
1694                 arrayOf(
1695                     "-classpath",
1696                     "${docLava1.path}:$jdkPath/lib/tools.jar",
1697                     "com.google.doclava.apicheck.ApiCheck",
1698                     *args
1699                 )
1700             )
1701         ) {
1702             return
1703         }
1704     }
1705 
checkSignaturesWithDoclavanull1706     private fun checkSignaturesWithDoclava(
1707         api: String,
1708         argument: String,
1709         output: File,
1710         expected: File = output,
1711         sourceList: Array<String>,
1712         sourcePath: String,
1713         packages: Set<String>,
1714         androidJar: File,
1715         trim: Boolean = true,
1716         stripBlankLines: Boolean = true,
1717         showAnnotationArgs: Array<String> = emptyArray(),
1718         stubImportPackages: List<String>,
1719         extraArguments: Array<String>,
1720         extraDoclavaArguments: Array<String> = emptyArray(),
1721         showUnannotated: Boolean,
1722         project: File,
1723         skipTestRoot: Boolean = false
1724     ) {
1725         // We have to run Doclava out of process because running it in process
1726         // (with Doclava1 jars on the test classpath) only works once; it leaves
1727         // around state such that the second test fails. Instead we invoke it
1728         // separately on each test; slower but reliable.
1729 
1730         val doclavaArg = when (argument) {
1731             ARG_API -> "-api"
1732             ARG_REMOVED_API -> "-removedApi"
1733             else -> if (argument.startsWith("--")) argument.substring(1) else argument
1734         }
1735 
1736         val showAnnotationArgsDoclava1: Array<String> =
1737             if (showAnnotationArgs.isNotEmpty() || extraArguments.isNotEmpty()) {
1738                 val shown = mutableListOf<String>()
1739                 extraArguments.forEachIndexed { index, s ->
1740                     if (s == ARG_SHOW_ANNOTATION) {
1741                         shown += "-showAnnotation"
1742                         shown += extraArguments[index + 1]
1743                     }
1744                 }
1745                 showAnnotationArgs.forEach { s ->
1746                     shown += if (s == ARG_SHOW_ANNOTATION) {
1747                         "-showAnnotation"
1748                     } else {
1749                         s
1750                     }
1751                 }
1752                 shown.toTypedArray()
1753             } else {
1754                 emptyArray()
1755             }
1756         val hideAnnotationArgsDoclava1: Array<String> = if (extraArguments.isNotEmpty()) {
1757             val hidden = mutableListOf<String>()
1758             extraArguments.forEachIndexed { index, s ->
1759                 if (s == ARG_HIDE_ANNOTATION) {
1760                     hidden += "-hideAnnotation"
1761                     hidden += extraArguments[index + 1]
1762                 }
1763             }
1764             hidden.toTypedArray()
1765         } else {
1766             emptyArray()
1767         }
1768         val showUnannotatedArgs = if (showUnannotated) {
1769             arrayOf("-showUnannotated")
1770         } else {
1771             emptyArray()
1772         }
1773 
1774         val docLava1 = findDoclava()
1775         val jdkPath = findJdk()
1776 
1777         val hidePackageArgs = mutableListOf<String>()
1778         options.hidePackages.forEach {
1779             hidePackageArgs.add("-hidePackage")
1780             hidePackageArgs.add(it)
1781         }
1782 
1783         val stubImports = if (stubImportPackages.isNotEmpty()) {
1784             arrayOf("-stubimportpackages", stubImportPackages.joinToString(separator = ":") { it })
1785         } else {
1786             emptyArray()
1787         }
1788 
1789         val args = arrayOf(
1790             *sourceList,
1791             "-stubpackages",
1792             packages.joinToString(separator = ":") { it },
1793             *stubImports,
1794             "-doclet",
1795             "com.google.doclava.Doclava",
1796             "-docletpath",
1797             docLava1.path,
1798             "-encoding",
1799             "UTF-8",
1800             "-source",
1801             "1.8",
1802             "-nodocs",
1803             "-quiet",
1804             "-sourcepath",
1805             sourcePath,
1806             "-classpath",
1807             androidJar.path,
1808 
1809             *showAnnotationArgsDoclava1,
1810             *hideAnnotationArgsDoclava1,
1811             *showUnannotatedArgs,
1812             *hidePackageArgs.toTypedArray(),
1813             *extraDoclavaArguments,
1814 
1815             // -api, or // -stub, etc
1816             doclavaArg,
1817             output.path
1818         )
1819 
1820         val message = "\n${args.joinToString(separator = "\n") { "\"$it\"," }}"
1821         println("Running doclava1 with the following args:\n$message")
1822 
1823         if (!runCommand(
1824                 "$jdkPath/bin/java",
1825                 arrayOf(
1826                     "-classpath",
1827                     "${docLava1.path}:$jdkPath/lib/tools.jar",
1828                     "com.google.doclava.Doclava",
1829                     *args
1830                 )
1831             )
1832         ) {
1833             return
1834         }
1835 
1836         // If there's a discrepancy between doclava1 and metalava, you can debug
1837         // doclava; to do this, open the Doclava project, and take all the
1838         // metalava arguments, drop the ones that don't apply and use the
1839         // doclava-style names instead of metalava (e.g. -stubs instead of --stubs,
1840         // -showAnnotation instead of --show-annotation, -sourcepath instead of
1841         // --sourcepath, and so on. Finally, and most importantly, add these
1842         // 4 arguments at the beginning:
1843         //   "-doclet",
1844         //   "com.google.doclava.Doclava",
1845         //   "-docletpath",
1846         //   "out/host/linux-x86/framework/jsilver.jar:out/host/linux-x86/framework/doclava.jar",
1847         // ..and finally from your Main entry point take this array of strings
1848         // and call Doclava.main(newArgs)
1849 
1850         val actualText = cleanupString(readFile(expected, stripBlankLines, trim), project, skipTestRoot)
1851         assertEquals(stripComments(api, stripLineComments = false).trimIndent(), actualText)
1852     }
1853 
findJdknull1854     protected fun findJdk(): String? {
1855         val jdkPath = getJdkPath()
1856         if (jdkPath == null) {
1857             fail("JDK not found in the environment; make sure \$JAVA_HOME is set.")
1858         }
1859         return jdkPath
1860     }
1861 
findDoclavanull1862     private fun findDoclava(): File {
1863         val docLava1 = File("testlibs/doclava-1.0.6-full-SNAPSHOT.jar")
1864         if (!docLava1.isFile) {
1865             /*
1866                 Not checked in (it's 22MB).
1867                 To generate the doclava1 jar, add this to external/doclava/build.gradle and run ./gradlew shadowJar:
1868 
1869                 // shadow jar: Includes all dependencies
1870                 buildscript {
1871                     repositories {
1872                         jcenter()
1873                     }
1874                     dependencies {
1875                         classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.2'
1876                     }
1877                 }
1878                 apply plugin: 'com.github.johnrengelman.shadow'
1879                 shadowJar {
1880                    baseName = "doclava-$version-full-SNAPSHOT"
1881                    classifier = null
1882                    version = null
1883                 }
1884 
1885                 and finally
1886                 $ cp ../../out/host/gradle/external/jdiff/build/libs/doclava-*-SNAPSHOT-full-SNAPSHOT.jar \
1887                      testlibs/doclava-1.0.6-full-SNAPSHOT.jar
1888 
1889              */
1890             fail("Couldn't find $docLava1: Is the pwd set to the root of the metalava source code?")
1891         }
1892         return docLava1
1893     }
1894 
1895     private fun runCommand(executable: String, args: Array<String>): Boolean {
1896         try {
1897             val logger = StdLogger(StdLogger.Level.ERROR)
1898             val processExecutor = DefaultProcessExecutor(logger)
1899             val processInfo = ProcessInfoBuilder()
1900                 .setExecutable(executable)
1901                 .addArgs(args)
1902                 .createProcess()
1903 
1904             val processOutputHandler = LoggedProcessOutputHandler(logger)
1905             val result = processExecutor.execute(processInfo, processOutputHandler)
1906 
1907             result.rethrowFailure().assertNormalExitValue()
1908         } catch (e: ProcessException) {
1909             fail("Failed to run $executable (${e.message}): not verifying this API on the old doclava engine")
1910             return false
1911         }
1912         return true
1913     }
1914 
1915     companion object {
1916         const val API_LEVEL = 27
1917 
1918         private val latestAndroidPlatform: String
1919             get() = "android-$API_LEVEL"
1920 
1921         private val sdk: File
1922             get() = File(
1923                 System.getenv("ANDROID_HOME")
1924                     ?: error("You must set \$ANDROID_HOME before running tests")
1925             )
1926 
1927         fun getAndroidJar(apiLevel: Int): File? {
1928             val localFile = File("../../prebuilts/sdk/$apiLevel/public/android.jar")
1929             if (localFile.exists()) {
1930                 return localFile
1931             } else {
1932                 val androidJar = File("../../prebuilts/sdk/$apiLevel/android.jar")
1933                 if (androidJar.exists()) {
1934                     return androidJar
1935                 }
1936             }
1937             return null
1938         }
1939 
1940         fun getPlatformFile(path: String): File {
1941             return getAndroidJar(API_LEVEL) ?: run {
1942                 val file = FileUtils.join(sdk, SdkConstants.FD_PLATFORMS, latestAndroidPlatform, path)
1943                 if (!file.exists()) {
1944                     throw IllegalArgumentException(
1945                         "File \"$path\" not found in platform $latestAndroidPlatform"
1946                     )
1947                 }
1948                 file
1949             }
1950         }
1951 
1952         fun java(to: String, @Language("JAVA") source: String): LintDetectorTest.TestFile {
1953             return TestFiles.java(to, source.trimIndent())
1954         }
1955 
1956         fun java(@Language("JAVA") source: String): LintDetectorTest.TestFile {
1957             return TestFiles.java(source.trimIndent())
1958         }
1959 
1960         fun kotlin(@Language("kotlin") source: String): LintDetectorTest.TestFile {
1961             return TestFiles.kotlin(source.trimIndent())
1962         }
1963 
1964         fun kotlin(to: String, @Language("kotlin") source: String): LintDetectorTest.TestFile {
1965             return TestFiles.kotlin(to, source.trimIndent())
1966         }
1967 
1968         private fun readFile(file: File, stripBlankLines: Boolean = false, trim: Boolean = false): String {
1969             var apiLines: List<String> = Files.asCharSource(file, UTF_8).readLines()
1970             if (stripBlankLines) {
1971                 apiLines = apiLines.asSequence().filter { it.isNotBlank() }.toList()
1972             }
1973             var apiText = apiLines.joinToString(separator = "\n") { it }
1974             if (trim) {
1975                 apiText = apiText.trim()
1976             }
1977             return apiText
1978         }
1979     }
1980 }
1981 
1982 val intRangeAnnotationSource: TestFile = java(
1983     """
1984         package android.annotation;
1985         import java.lang.annotation.*;
1986         import static java.lang.annotation.ElementType.*;
1987         import static java.lang.annotation.RetentionPolicy.SOURCE;
1988         @Retention(SOURCE)
1989         @Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE})
1990         public @interface IntRange {
1991             long from() default Long.MIN_VALUE;
1992             long to() default Long.MAX_VALUE;
1993         }
1994         """
1995 ).indented()
1996 
1997 val intDefAnnotationSource: TestFile = java(
1998     """
1999     package android.annotation;
2000     import java.lang.annotation.Retention;
2001     import java.lang.annotation.RetentionPolicy;
2002     import java.lang.annotation.Target;
2003     import static java.lang.annotation.ElementType.*;
2004     import static java.lang.annotation.RetentionPolicy.SOURCE;
2005     @Retention(SOURCE)
2006     @Target({ANNOTATION_TYPE})
2007     public @interface IntDef {
2008         int[] value() default {};
2009         boolean flag() default false;
2010     }
2011     """
2012 ).indented()
2013 
2014 val longDefAnnotationSource: TestFile = java(
2015     """
2016     package android.annotation;
2017     import java.lang.annotation.Retention;
2018     import java.lang.annotation.RetentionPolicy;
2019     import java.lang.annotation.Target;
2020     import static java.lang.annotation.ElementType.*;
2021     import static java.lang.annotation.RetentionPolicy.SOURCE;
2022     @Retention(SOURCE)
2023     @Target({ANNOTATION_TYPE})
2024     public @interface LongDef {
2025         long[] value() default {};
2026         boolean flag() default false;
2027     }
2028     """
2029 ).indented()
2030 
2031 @Suppress("ConstantConditionIf")
2032 val nonNullSource: TestFile = java(
2033     """
2034     package android.annotation;
2035     import java.lang.annotation.Retention;
2036     import java.lang.annotation.Target;
2037 
2038     import static java.lang.annotation.ElementType.FIELD;
2039     import static java.lang.annotation.ElementType.METHOD;
2040     import static java.lang.annotation.ElementType.PARAMETER;
2041     import static java.lang.annotation.RetentionPolicy.SOURCE;
2042     /**
2043      * Denotes that a parameter, field or method return value can never be null.
2044      * @paramDoc This value must never be {@code null}.
2045      * @returnDoc This value will never be {@code null}.
2046      * @hide
2047      */
2048     @SuppressWarnings({"WeakerAccess", "JavaDoc"})
2049     @Retention(SOURCE)
2050     @Target({METHOD, PARAMETER, FIELD${if (SUPPORT_TYPE_USE_ANNOTATIONS) ", TYPE_USE" else ""}})
2051     public @interface NonNull {
2052     }
2053     """
2054 ).indented()
2055 
2056 val libcoreNonNullSource: TestFile = java(
2057     """
2058     package libcore.util;
2059     import static java.lang.annotation.ElementType.*;
2060     import static java.lang.annotation.RetentionPolicy.SOURCE;
2061     import java.lang.annotation.*;
2062     @Documented
2063     @Retention(SOURCE)
2064     @Target({TYPE_USE})
2065     public @interface NonNull {
2066        int from() default Integer.MIN_VALUE;
2067        int to() default Integer.MAX_VALUE;
2068     }
2069     """
2070 ).indented()
2071 
2072 val libcoreNullableSource: TestFile = java(
2073     """
2074     package libcore.util;
2075     import static java.lang.annotation.ElementType.*;
2076     import static java.lang.annotation.RetentionPolicy.SOURCE;
2077     import java.lang.annotation.*;
2078     @Documented
2079     @Retention(SOURCE)
2080     @Target({TYPE_USE})
2081     public @interface Nullable {
2082        int from() default Integer.MIN_VALUE;
2083        int to() default Integer.MAX_VALUE;
2084     }
2085     """
2086 ).indented()
2087 
2088 val requiresPermissionSource: TestFile = java(
2089     """
2090     package android.annotation;
2091     import java.lang.annotation.*;
2092     import static java.lang.annotation.ElementType.*;
2093     import static java.lang.annotation.RetentionPolicy.SOURCE;
2094     @Retention(SOURCE)
2095     @Target({ANNOTATION_TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER})
2096     public @interface RequiresPermission {
2097         String value() default "";
2098         String[] allOf() default {};
2099         String[] anyOf() default {};
2100         boolean conditional() default false;
2101         @Target({FIELD, METHOD, PARAMETER})
2102         @interface Read {
2103             RequiresPermission value() default @RequiresPermission;
2104         }
2105         @Target({FIELD, METHOD, PARAMETER})
2106         @interface Write {
2107             RequiresPermission value() default @RequiresPermission;
2108         }
2109     }
2110     """
2111 ).indented()
2112 
2113 val requiresFeatureSource: TestFile = java(
2114     """
2115     package android.annotation;
2116     import java.lang.annotation.*;
2117     import static java.lang.annotation.ElementType.*;
2118     import static java.lang.annotation.RetentionPolicy.SOURCE;
2119     @Retention(SOURCE)
2120     @Target({TYPE,FIELD,METHOD,CONSTRUCTOR})
2121     public @interface RequiresFeature {
2122         String value();
2123     }
2124     """
2125 ).indented()
2126 
2127 val requiresApiSource: TestFile = java(
2128     """
2129     package androidx.annotation;
2130     import java.lang.annotation.*;
2131     import static java.lang.annotation.ElementType.*;
2132     import static java.lang.annotation.RetentionPolicy.SOURCE;
2133     @Retention(SOURCE)
2134     @Target({TYPE,FIELD,METHOD,CONSTRUCTOR})
2135     public @interface RequiresApi {
2136         int value() default 1;
2137         int api() default 1;
2138     }
2139     """
2140 ).indented()
2141 
2142 val sdkConstantSource: TestFile = java(
2143     """
2144     package android.annotation;
2145     import java.lang.annotation.*;
2146     @Target({ ElementType.FIELD })
2147     @Retention(RetentionPolicy.SOURCE)
2148     public @interface SdkConstant {
2149         enum SdkConstantType {
2150             ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE
2151         }
2152         SdkConstantType value();
2153     }
2154     """
2155 ).indented()
2156 
2157 val broadcastBehaviorSource: TestFile = java(
2158     """
2159     package android.annotation;
2160     import java.lang.annotation.*;
2161     /** @hide */
2162     @Target({ ElementType.FIELD })
2163     @Retention(RetentionPolicy.SOURCE)
2164     public @interface BroadcastBehavior {
2165         boolean explicitOnly() default false;
2166         boolean registeredOnly() default false;
2167         boolean includeBackground() default false;
2168         boolean protectedBroadcast() default false;
2169     }
2170     """
2171 ).indented()
2172 
2173 @Suppress("ConstantConditionIf")
2174 val nullableSource: TestFile = java(
2175     """
2176     package android.annotation;
2177     import java.lang.annotation.*;
2178     import static java.lang.annotation.ElementType.*;
2179     import static java.lang.annotation.RetentionPolicy.SOURCE;
2180     /**
2181      * Denotes that a parameter, field or method return value can be null.
2182      * @paramDoc This value may be {@code null}.
2183      * @returnDoc This value may be {@code null}.
2184      * @hide
2185      */
2186     @SuppressWarnings({"WeakerAccess", "JavaDoc"})
2187     @Retention(SOURCE)
2188     @Target({METHOD, PARAMETER, FIELD${if (SUPPORT_TYPE_USE_ANNOTATIONS) ", TYPE_USE" else ""}})
2189     public @interface Nullable {
2190     }
2191     """
2192 ).indented()
2193 
2194 val supportNonNullSource: TestFile = java(
2195     """
2196     package android.support.annotation;
2197     import java.lang.annotation.*;
2198     import static java.lang.annotation.ElementType.*;
2199     import static java.lang.annotation.RetentionPolicy.SOURCE;
2200     @SuppressWarnings("WeakerAccess")
2201     @Retention(SOURCE)
2202     @Target({METHOD, PARAMETER, FIELD, TYPE_USE, TYPE_PARAMETER})
2203     public @interface NonNull {
2204     }
2205     """
2206 ).indented()
2207 
2208 val supportNullableSource: TestFile = java(
2209     """
2210     package android.support.annotation;
2211     import java.lang.annotation.*;
2212     import static java.lang.annotation.ElementType.*;
2213     import static java.lang.annotation.RetentionPolicy.SOURCE;
2214     @SuppressWarnings("WeakerAccess")
2215     @Retention(SOURCE)
2216     @Target({METHOD, PARAMETER, FIELD, TYPE_USE, TYPE_PARAMETER})
2217     public @interface Nullable {
2218     }
2219     """
2220 ).indented()
2221 
2222 val androidxNonNullSource: TestFile = java(
2223     """
2224     package androidx.annotation;
2225     import java.lang.annotation.*;
2226     import static java.lang.annotation.ElementType.*;
2227     import static java.lang.annotation.RetentionPolicy.SOURCE;
2228     @SuppressWarnings("WeakerAccess")
2229     @Retention(SOURCE)
2230     @Target({METHOD, PARAMETER, FIELD, TYPE_USE})
2231     public @interface NonNull {
2232     }
2233     """
2234 ).indented()
2235 
2236 val androidxNullableSource: TestFile = java(
2237     """
2238     package androidx.annotation;
2239     import java.lang.annotation.*;
2240     import static java.lang.annotation.ElementType.*;
2241     import static java.lang.annotation.RetentionPolicy.SOURCE;
2242     @SuppressWarnings("WeakerAccess")
2243     @Retention(SOURCE)
2244     @Target({METHOD, PARAMETER, FIELD, TYPE_USE})
2245     public @interface Nullable {
2246     }
2247     """
2248 ).indented()
2249 
2250 val recentlyNonNullSource: TestFile = java(
2251     """
2252     package androidx.annotation;
2253     import java.lang.annotation.*;
2254     import static java.lang.annotation.ElementType.*;
2255     import static java.lang.annotation.RetentionPolicy.SOURCE;
2256     @SuppressWarnings("WeakerAccess")
2257     @Retention(SOURCE)
2258     @Target({METHOD, PARAMETER, FIELD, TYPE_USE})
2259     public @interface RecentlyNonNull {
2260     }
2261     """
2262 ).indented()
2263 
2264 val recentlyNullableSource: TestFile = java(
2265     """
2266     package androidx.annotation;
2267     import java.lang.annotation.*;
2268     import static java.lang.annotation.ElementType.*;
2269     import static java.lang.annotation.RetentionPolicy.SOURCE;
2270     @SuppressWarnings("WeakerAccess")
2271     @Retention(SOURCE)
2272     @Target({METHOD, PARAMETER, FIELD, TYPE_USE})
2273     public @interface RecentlyNullable {
2274     }
2275     """
2276 ).indented()
2277 
2278 val supportParameterName: TestFile = java(
2279     """
2280     package androidx.annotation;
2281     import java.lang.annotation.*;
2282     import static java.lang.annotation.ElementType.*;
2283     import static java.lang.annotation.RetentionPolicy.SOURCE;
2284     @SuppressWarnings("WeakerAccess")
2285     @Retention(SOURCE)
2286     @Target({METHOD, PARAMETER, FIELD})
2287     public @interface ParameterName {
2288         String value();
2289     }
2290     """
2291 ).indented()
2292 
2293 val supportDefaultValue: TestFile = java(
2294     """
2295     package androidx.annotation;
2296     import java.lang.annotation.*;
2297     import static java.lang.annotation.ElementType.*;
2298     import static java.lang.annotation.RetentionPolicy.SOURCE;
2299     @SuppressWarnings("WeakerAccess")
2300     @Retention(SOURCE)
2301     @Target({METHOD, PARAMETER, FIELD})
2302     public @interface DefaultValue {
2303         String value();
2304     }
2305     """
2306 ).indented()
2307 
2308 val uiThreadSource: TestFile = java(
2309     """
2310     package androidx.annotation;
2311     import java.lang.annotation.*;
2312     import static java.lang.annotation.ElementType.*;
2313     import static java.lang.annotation.RetentionPolicy.SOURCE;
2314     /**
2315      * Denotes that the annotated method or constructor should only be called on the
2316      * UI thread. If the annotated element is a class, then all methods in the class
2317      * should be called on the UI thread.
2318      * @memberDoc This method must be called on the thread that originally created
2319      *            this UI element. This is typically the main thread of your app.
2320      * @classDoc Methods in this class must be called on the thread that originally created
2321      *            this UI element, unless otherwise noted. This is typically the
2322      *            main thread of your app. * @hide
2323      */
2324     @SuppressWarnings({"WeakerAccess", "JavaDoc"})
2325     @Retention(SOURCE)
2326     @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
2327     public @interface UiThread {
2328     }
2329     """
2330 ).indented()
2331 
2332 val workerThreadSource: TestFile = java(
2333     """
2334     package androidx.annotation;
2335     import java.lang.annotation.*;
2336     import static java.lang.annotation.ElementType.*;
2337     import static java.lang.annotation.RetentionPolicy.SOURCE;
2338     /**
2339      * @memberDoc This method may take several seconds to complete, so it should
2340      *            only be called from a worker thread.
2341      * @classDoc Methods in this class may take several seconds to complete, so it should
2342      *            only be called from a worker thread unless otherwise noted.
2343      * @hide
2344      */
2345     @SuppressWarnings({"WeakerAccess", "JavaDoc"})
2346     @Retention(SOURCE)
2347     @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
2348     public @interface WorkerThread {
2349     }
2350     """
2351 ).indented()
2352 
2353 val suppressLintSource: TestFile = java(
2354     """
2355     package android.annotation;
2356 
2357     import static java.lang.annotation.ElementType.*;
2358     import java.lang.annotation.*;
2359     @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
2360     @Retention(RetentionPolicy.CLASS)
2361     public @interface SuppressLint {
2362         String[] value();
2363     }
2364     """
2365 ).indented()
2366 
2367 val systemServiceSource: TestFile = java(
2368     """
2369     package android.annotation;
2370     import static java.lang.annotation.ElementType.TYPE;
2371     import static java.lang.annotation.RetentionPolicy.SOURCE;
2372     import java.lang.annotation.*;
2373     @Retention(SOURCE)
2374     @Target(TYPE)
2375     public @interface SystemService {
2376         String value();
2377     }
2378     """
2379 ).indented()
2380 
2381 val systemApiSource: TestFile = java(
2382     """
2383     package android.annotation;
2384     import static java.lang.annotation.ElementType.*;
2385     import java.lang.annotation.*;
2386     @Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE})
2387     @Retention(RetentionPolicy.SOURCE)
2388     public @interface SystemApi {
2389     }
2390     """
2391 ).indented()
2392 
2393 val testApiSource: TestFile = java(
2394     """
2395     package android.annotation;
2396     import static java.lang.annotation.ElementType.*;
2397     import java.lang.annotation.*;
2398     @Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE})
2399     @Retention(RetentionPolicy.SOURCE)
2400     public @interface TestApi {
2401     }
2402     """
2403 ).indented()
2404 
2405 val widgetSource: TestFile = java(
2406     """
2407     package android.annotation;
2408     import java.lang.annotation.*;
2409     @Target({ ElementType.TYPE })
2410     @Retention(RetentionPolicy.SOURCE)
2411     public @interface Widget {
2412     }
2413     """
2414 ).indented()
2415 
2416 val restrictToSource: TestFile = java(
2417     """
2418     package androidx.annotation;
2419     import java.lang.annotation.*;
2420     import static java.lang.annotation.ElementType.*;
2421     import static java.lang.annotation.RetentionPolicy.*;
2422     @SuppressWarnings("WeakerAccess")
2423     @Retention(CLASS)
2424     @Target({ANNOTATION_TYPE, TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
2425     public @interface RestrictTo {
2426         Scope[] value();
2427         enum Scope {
2428             LIBRARY,
2429             LIBRARY_GROUP,
2430             /** @deprecated */
2431             @Deprecated
2432             GROUP_ID,
2433             TESTS,
2434             SUBCLASSES,
2435         }
2436     }
2437     """
2438 ).indented()
2439 
2440 val visibleForTestingSource: TestFile = java(
2441     """
2442     package androidx.annotation;
2443     import static java.lang.annotation.RetentionPolicy.CLASS;
2444     import java.lang.annotation.Retention;
2445     @Retention(CLASS)
2446     @SuppressWarnings("WeakerAccess")
2447     public @interface VisibleForTesting {
2448         int otherwise() default PRIVATE;
2449         int PRIVATE = 2;
2450         int PACKAGE_PRIVATE = 3;
2451         int PROTECTED = 4;
2452         int NONE = 5;
2453     }
2454     """
2455 ).indented()
2456