1 /*
<lambda>null2  * Copyright (C) 2023 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.testing
18 
19 import java.io.File
20 import java.io.InputStreamReader
21 import java.io.LineNumberReader
22 import java.util.TreeMap
23 import java.util.TreeSet
24 
25 /** Encapsulates information read from a test baseline file. */
26 interface BaselineFile {
27     /** Check to see whether the specified [className] and [testName] are expected to fail. */
28     fun isExpectedFailure(className: String, testName: String): Boolean
29 
30     companion object {
31         /**
32          * Read the source baseline file from the containing project's directory.
33          *
34          * @param projectDir the project directory from which this baseline will be loaded.
35          * @param resourcePath the resource path to the baseline file.
36          */
37         fun forProject(projectDir: File, resourcePath: String): MutableBaselineFile {
38             // Load it from the project's test resources directory.
39             val baselineFile = projectDir.resolve("src/test/resources").resolve(resourcePath)
40             return forFile(baselineFile)
41         }
42 
43         /**
44          * Read the source baseline file from the file.
45          *
46          * @param baselineFile the baseline [File] from which this baseline will be loaded.
47          */
48         fun forFile(baselineFile: File): MutableBaselineFile {
49             val baseline = MutableBaselineFile(baselineFile = baselineFile)
50             if (baselineFile.exists()) {
51                 baselineFile.reader().use { baseline.read(it, baselineFile.path) }
52             }
53             return baseline
54         }
55 
56         /**
57          * Get the baseline file from the [Thread.contextClassLoader].
58          *
59          * @param resourcePath the resource path to the baseline file.
60          */
61         fun fromResource(resourcePath: String): BaselineFile {
62             val baseline = MutableBaselineFile()
63             val contextClassLoader = Thread.currentThread().contextClassLoader
64             val resource = contextClassLoader.getResource(resourcePath)
65             resource?.openStream()?.reader()?.use { baseline.read(it, resource.toExternalForm()) }
66             return baseline
67         }
68     }
69 }
70 
71 /**
72  * Mutable representation of a baseline file.
73  *
74  * Used by the update command line tool to update the baseline based on the information found in the
75  * test reports.
76  */
77 class MutableBaselineFile
78 internal constructor(
79     private val baselineFile: File? = null,
80     private val expectedFailures: MutableMap<String, MutableSet<String>> = TreeMap(),
81 ) : BaselineFile {
82 
83     /**
84      * Read the baseline file from the reader.
85      *
86      * @param location the location, (either a file path or url), of the baseline being read.
87      */
readnull88     internal fun read(streamReader: InputStreamReader, location: String) {
89         val reader = LineNumberReader(streamReader)
90         var currentClassName: String? = null
91         do {
92             val line = reader.readLine() ?: break
93             when {
94                 line.isEmpty() -> currentClassName = null
95                 line.startsWith("  ") -> {
96                     val testName = line.substring(2).trimEnd()
97                     currentClassName
98                         ?: throw IllegalStateException(
99                             "$location:${reader.lineNumber}: test name found but no preceding class name was found"
100                         )
101                     addExpectedFailure(currentClassName, testName)
102                 }
103                 else -> currentClassName = line.trimEnd()
104             }
105         } while (true)
106     }
107 
writenull108     fun write() {
109         baselineFile ?: throw IllegalStateException("Cannot write baseline read from resources")
110 
111         // If there are no expected failures then there is no point having a baseline file so
112         // delete it if necessary.
113         if (expectedFailures.isEmpty()) {
114             if (baselineFile.exists()) {
115                 baselineFile.delete()
116             }
117             return
118         }
119 
120         // Write the file.
121         baselineFile.parentFile.mkdirs()
122         baselineFile.printWriter().use { writer ->
123             var separator = ""
124             expectedFailures.forEach { (className, testNames) ->
125                 if (testNames.isNotEmpty()) {
126                     writer.print(separator)
127                     separator = "\n"
128 
129                     writer.println(className)
130                     testNames.forEach { testName -> writer.println("  $testName") }
131                 }
132             }
133         }
134     }
135 
isExpectedFailurenull136     override fun isExpectedFailure(className: String, testName: String): Boolean {
137         return expectedFailures[className]?.contains(testName) ?: false
138     }
139 
140     /** Add an expected failure to the baseline. */
addExpectedFailurenull141     fun addExpectedFailure(className: String, testName: String) {
142         val classFailures = expectedFailures.computeIfAbsent(className) { TreeSet() }
143         classFailures.add(testName)
144     }
145 
146     /** Remove an expected failure from the baseline. */
removeExpectedFailurenull147     fun removeExpectedFailure(className: String, testName: String) {
148         val classFailures = expectedFailures[className] ?: return
149         classFailures.remove(testName)
150         if (classFailures.isEmpty()) {
151             expectedFailures.remove(className)
152         }
153     }
154 }
155