1 /* <lambda>null2 * Copyright 2025 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 androidx.appfunctions.compiler.testings 18 19 import androidx.room.compiler.processing.util.DiagnosticMessage 20 import androidx.room.compiler.processing.util.Resource 21 import androidx.room.compiler.processing.util.Source 22 import androidx.room.compiler.processing.util.compiler.TestCompilationArguments 23 import androidx.room.compiler.processing.util.compiler.TestCompilationResult 24 import androidx.room.compiler.processing.util.compiler.compile 25 import com.google.common.truth.Truth.assertWithMessage 26 import com.google.devtools.ksp.processing.SymbolProcessorProvider 27 import java.io.File 28 import java.nio.file.Files 29 import java.nio.file.Path 30 import javax.tools.Diagnostic 31 import kotlin.io.path.Path 32 import kotlin.io.path.createDirectories 33 import kotlin.io.path.createFile 34 import kotlin.io.path.deleteIfExists 35 import kotlin.io.path.writeText 36 37 /** A helper to test compilation. */ 38 class CompilationTestHelper( 39 /** The root directory containing the source test files. */ 40 private val testFileSrcDir: File, 41 /** The root directory containing the source golden files. */ 42 private val goldenFileSrcDir: File, 43 /** A list of proxy source files to be compiled with the test sources. */ 44 private val proxySourceFileNames: List<String>, 45 /** A list of [com.google.devtools.ksp.processing.SymbolProcessorProvider] under test. */ 46 private val symbolProcessorProviders: List<SymbolProcessorProvider>, 47 ) { 48 49 init { 50 check(testFileSrcDir.exists()) { 51 "Test file source directory [${testFileSrcDir.path}] does not exist" 52 } 53 check(testFileSrcDir.isDirectory) { "[$testFileSrcDir] is not a directory." } 54 55 check(goldenFileSrcDir.exists()) { 56 "Golden file source directory [${goldenFileSrcDir.path}] does not exist" 57 } 58 check(goldenFileSrcDir.isDirectory) { "[$goldenFileSrcDir] is not a directory." } 59 } 60 61 private val outputDir: Path by lazy { 62 requireNotNull(System.getProperty("test_output_dir")) { 63 "test_output_dir not set for diff test." 64 } 65 .let { Path(it) } 66 } 67 68 /** Compiles all [sourceFileNames] with additional [processorOptions]. */ 69 fun compileAll( 70 sourceFileNames: List<String>, 71 processorOptions: Map<String, String> = emptyMap<String, String>(), 72 ): CompilationReport { 73 val sources = 74 sourceFileNames.map { sourceFileName -> 75 val sourceFile = getTestSourceFile(sourceFileName) 76 Source.Companion.kotlin( 77 ensureKotlinFileNameFormat(sourceFileName), 78 sourceFile.readText() 79 ) 80 } + 81 proxySourceFileNames.map { proxySourceFileName -> 82 val proxySourceFile = getTestSourceFile(proxySourceFileName) 83 Source.Companion.kotlin( 84 ensureKotlinFileNameFormat(proxySourceFile.name), 85 proxySourceFile.readText() 86 ) 87 } 88 89 val workingDir = 90 Files.createTempDirectory("compile").toFile().also { file -> file.deleteOnExit() } 91 val result = 92 compile( 93 workingDir, 94 TestCompilationArguments( 95 sources = sources, 96 symbolProcessorProviders = symbolProcessorProviders, 97 processorOptions = processorOptions, 98 ) 99 ) 100 101 return CompilationReport.create(result, outputDir) 102 } 103 104 /** 105 * Asserts that the compilation succeeds and contains a generated file (either source or 106 * resource) with the given name, whose content matches the golden file. 107 */ 108 private fun assertSuccessWithGeneratedContent( 109 report: CompilationReport, 110 expectGeneratedFileName: String, 111 goldenFileName: String, 112 generatedFileContent: String? 113 ) { 114 assertWithMessage( 115 """ 116 Compile failed with error: 117 ${report.printDiagnostics(Diagnostic.Kind.ERROR)} 118 119 Generated content: 120 $generatedFileContent 121 """ 122 .trimIndent() 123 ) 124 .that(report.isSuccess) 125 .isTrue() 126 127 val goldenFile = getGoldenFile(goldenFileName) 128 val updateGoldenFiles = System.getProperty("update_golden_files")?.toBoolean() == true 129 assertWithMessage( 130 "Generated file [$expectGeneratedFileName] does not exist or had multiple matches" 131 ) 132 .that(generatedFileContent) 133 .isNotNull() 134 if (updateGoldenFiles) { 135 println("Updating golden file: ${goldenFile.path}") 136 goldenFile.writeText(checkNotNull(generatedFileContent)) 137 } else { 138 assertWithMessage( 139 """ 140 Content of generated file [$expectGeneratedFileName] does not match 141 the content of golden file [${goldenFile.path}]. 142 143 To update the golden file, 144 run: 145 ./gradlew appfunctions:appfunctions-compiler:test -Dupdate_golden_files=true 146 """ 147 .trimIndent() 148 ) 149 .that(generatedFileContent) 150 .isEqualTo(goldenFile.readText()) 151 } 152 } 153 154 /** 155 * Asserts that the compilation succeeds and contains [expectGeneratedSourceFileName] in 156 * generated sources that is identical to the content of [goldenFileName]. 157 */ 158 fun assertSuccessWithSourceContent( 159 report: CompilationReport, 160 expectGeneratedSourceFileName: String, 161 goldenFileName: String, 162 ) { 163 assertSuccessWithGeneratedContent( 164 report, 165 expectGeneratedSourceFileName, 166 goldenFileName, 167 report.generatedSourceFiles 168 .singleOrNull { sourceFile -> 169 sourceFile.source.relativePath.contains(expectGeneratedSourceFileName) 170 } 171 ?.source 172 ?.contents 173 ) 174 } 175 176 /** 177 * Asserts that the compilation succeeds and contains [expectGeneratedResourceFileName] in 178 * generated resources that is identical to the content of [goldenFileName]. 179 */ 180 fun assertSuccessWithResourceContent( 181 report: CompilationReport, 182 expectGeneratedResourceFileName: String, 183 goldenFileName: String, 184 ) { 185 assertSuccessWithGeneratedContent( 186 report, 187 expectGeneratedResourceFileName, 188 goldenFileName, 189 report.generatedResourceFiles 190 .singleOrNull { resourceFile -> 191 resourceFile.resource.relativePath.contains(expectGeneratedResourceFileName) 192 } 193 ?.resource 194 ?.getContents() 195 ) 196 } 197 198 fun assertErrorWithMessage(report: CompilationReport, expectedErrorMessage: String) { 199 assertWithMessage("Compile succeed").that(report.isSuccess).isFalse() 200 201 val errorDiagnostics = report.diagnostics[Diagnostic.Kind.ERROR] ?: emptyList() 202 var foundError = false 203 for (errorDiagnostic in errorDiagnostics) { 204 if (errorDiagnostic.msg.contains(expectedErrorMessage)) { 205 foundError = true 206 break 207 } 208 } 209 assertWithMessage( 210 """ 211 Unable to find the expected error message [$expectedErrorMessage] from the 212 diagnostics results: 213 214 ${report.printDiagnostics(Diagnostic.Kind.ERROR)} 215 """ 216 .trimIndent() 217 ) 218 .that(foundError) 219 .isTrue() 220 } 221 222 private fun ensureKotlinFileNameFormat(sourceFileName: String): String { 223 val nameParts = sourceFileName.split(".") 224 require(nameParts.last().lowercase() == "kt") { 225 "Source file $sourceFileName is not a Kotlin file" 226 } 227 val fileNameWithoutExtension = 228 nameParts.joinToString(separator = ".", limit = nameParts.size - 1) 229 return "${fileNameWithoutExtension}.kt" 230 } 231 232 private fun getTestSourceFile(fileName: String): File { 233 return File( 234 testFileSrcDir, 235 /** child= */ 236 fileName 237 ) 238 .also { file -> check(file.exists()) { "Source file [${file.path}] does not exist" } } 239 } 240 241 private fun getGoldenFile(fileName: String): File { 242 return File( 243 goldenFileSrcDir, 244 /** child= */ 245 fileName 246 ) 247 .also { file -> check(file.exists()) { "Golden file [${file.path}] does not exist" } } 248 } 249 250 private fun String.sanitizeFilePath(): String { 251 return this.replace("$", "\\$") 252 } 253 254 /** The compilation report. */ 255 data class CompilationReport( 256 /** Indicates whether the compilation succeed or not. */ 257 val isSuccess: Boolean, 258 /** A list of generated source files. */ 259 val generatedSourceFiles: List<GeneratedSourceFile>, 260 /** A map of diagnostics results. */ 261 val diagnostics: Map<Diagnostic.Kind, List<DiagnosticMessage>>, 262 /** A list of generated source files. */ 263 val generatedResourceFiles: List<GeneratedResourceFile>, 264 ) { 265 /** Print the diagnostics result of type [kind]. */ 266 fun printDiagnostics(kind: Diagnostic.Kind): String { 267 val errorDiagnostics = diagnostics[kind] ?: return "No ${kind.name} diagnostic message" 268 return buildString { 269 append("${kind.name} diagnostic messages:\n\n") 270 for (diagnostic in errorDiagnostics) { 271 append("$diagnostic\n\n") 272 } 273 } 274 } 275 276 companion object { 277 internal fun create(result: TestCompilationResult, outputDir: Path): CompilationReport { 278 return CompilationReport( 279 isSuccess = result.success, 280 generatedSourceFiles = 281 result.generatedSources.map { source -> 282 GeneratedSourceFile.create(source, outputDir) 283 }, 284 diagnostics = result.diagnostics, 285 generatedResourceFiles = 286 result.generatedResources.map { resource -> 287 GeneratedResourceFile.create(resource, outputDir) 288 } 289 ) 290 } 291 } 292 } 293 294 /** A wrapper class contains [source] with its file path. */ 295 data class GeneratedSourceFile(val source: Source, val sourceFilePath: Path) { 296 companion object { 297 internal fun create(source: Source, outputDir: Path): GeneratedSourceFile { 298 val filePath = 299 outputDir.resolve(source.relativePath).apply { 300 parent?.createDirectories() 301 deleteIfExists() 302 createFile() 303 writeText(source.contents) 304 } 305 return GeneratedSourceFile(source, filePath) 306 } 307 } 308 } 309 310 /** A wrapper class contains [Resource] with its file path. */ 311 data class GeneratedResourceFile(val resource: Resource, val resourceFilePath: Path) { 312 companion object { 313 internal fun create(resource: Resource, outputDir: Path): GeneratedResourceFile { 314 val filePath = 315 outputDir.resolve(resource.relativePath).apply { 316 parent?.createDirectories() 317 deleteIfExists() 318 createFile() 319 writeText(resource.getContents()) 320 } 321 return GeneratedResourceFile(resource, filePath) 322 } 323 } 324 } 325 326 companion object { 327 private fun Resource.getContents(): String = 328 openInputStream().bufferedReader().use { it.readText() } 329 } 330 } 331