Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2021 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.room.compiler.processing.ksp |
| 18 | |
Brad Corso | 4707733 | 2021-07-30 22:36:34 +0000 | [diff] [blame] | 19 | import androidx.room.compiler.processing.XAnnotation |
| 20 | import androidx.room.compiler.processing.XAnnotationValue |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 21 | import androidx.room.compiler.processing.XElement |
| 22 | import androidx.room.compiler.processing.XMessager |
| 23 | import androidx.room.compiler.processing.addOriginatingElement |
| 24 | import androidx.room.compiler.processing.util.Source |
| 25 | import androidx.room.compiler.processing.util.runKspTest |
Daniel Santiago Rivera | 433ac98 | 2023-07-19 17:39:44 +0000 | [diff] [blame] | 26 | import com.google.common.truth.Truth.assertThat |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 27 | import com.google.devtools.ksp.processing.CodeGenerator |
| 28 | import com.google.devtools.ksp.processing.Dependencies |
Jim Sproch | e1b4e86 | 2021-10-06 00:48:22 -0700 | [diff] [blame] | 29 | import com.google.devtools.ksp.symbol.KSClassDeclaration |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 30 | import com.google.devtools.ksp.symbol.KSFile |
| 31 | import com.squareup.kotlinpoet.FileSpec |
| 32 | import com.squareup.kotlinpoet.FunSpec |
| 33 | import com.squareup.kotlinpoet.PropertySpec |
| 34 | import com.squareup.kotlinpoet.TypeSpec |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 35 | import java.io.File |
| 36 | import java.io.OutputStream |
| 37 | import javax.tools.Diagnostic |
Daniel Santiago Rivera | fb43532 | 2022-11-21 08:54:41 -0500 | [diff] [blame] | 38 | import kotlin.io.path.Path |
| 39 | import org.junit.Test |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 40 | |
| 41 | class KspFilerTest { |
| 42 | |
| 43 | @Test |
| 44 | fun originatingFileAddedForTopLevelFunction() { |
| 45 | runKspTest(sources = listOf(simpleKotlinClass)) { invocation -> |
| 46 | val sourceElement = invocation.processingEnv.requireTypeElement("foo.bar.Baz") |
| 47 | val fileWithTopLevelFun = FileSpec.builder("foo", "Bar.kt").apply { |
| 48 | addFunction(FunSpec.builder("baz").addOriginatingElement(sourceElement).build()) |
| 49 | }.build() |
| 50 | |
| 51 | val codeGenerator = DependencyTrackingCodeGenerator() |
| 52 | KspFiler(codeGenerator, TestMessager()).write(fileWithTopLevelFun) |
| 53 | codeGenerator.fileDependencies[fileWithTopLevelFun.name] |
| 54 | .containsExactlySimpleKotlinClass() |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | @Test |
| 59 | fun originatingFileAddedForTopLevelProperty() { |
| 60 | runKspTest(sources = listOf(simpleKotlinClass)) { invocation -> |
| 61 | val sourceElement = invocation.processingEnv.requireTypeElement("foo.bar.Baz") |
| 62 | val fileWithTopLevelProp = FileSpec.builder("foo", "Bar.kt").apply { |
| 63 | addProperty( |
| 64 | PropertySpec.builder("baz", String::class).apply { |
| 65 | initializer("%S", "") |
| 66 | addOriginatingElement(sourceElement) |
| 67 | }.build() |
| 68 | ) |
| 69 | }.build() |
| 70 | |
| 71 | val codeGenerator = DependencyTrackingCodeGenerator() |
| 72 | KspFiler(codeGenerator, TestMessager()).write(fileWithTopLevelProp) |
| 73 | codeGenerator.fileDependencies[fileWithTopLevelProp.name] |
| 74 | .containsExactlySimpleKotlinClass() |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | @Test |
| 79 | fun originatingFileAddedForTopLevelElement() { |
| 80 | runKspTest(sources = listOf(simpleKotlinClass)) { invocation -> |
| 81 | val sourceElement = invocation.processingEnv.requireTypeElement("foo.bar.Baz") |
| 82 | val fileWithType = FileSpec.builder("foo", "Bar.kt").apply { |
| 83 | addType( |
| 84 | TypeSpec.classBuilder("Bar").apply { |
| 85 | addOriginatingElement(sourceElement) |
| 86 | }.build() |
| 87 | ) |
| 88 | }.build() |
| 89 | |
| 90 | val codeGenerator = DependencyTrackingCodeGenerator() |
| 91 | KspFiler(codeGenerator, TestMessager()).write(fileWithType) |
| 92 | codeGenerator.fileDependencies[fileWithType.name] |
| 93 | .containsExactlySimpleKotlinClass() |
| 94 | } |
| 95 | } |
| 96 | |
Eli Hart | 4b910a5d | 2021-11-18 11:35:44 -0800 | [diff] [blame] | 97 | @Test |
| 98 | fun originatingClassAddedForClassPathAndFileType() { |
| 99 | runKspTest(sources = listOf(simpleKotlinClass)) { invocation -> |
| 100 | val sourceElement = invocation.processingEnv.requireTypeElement("foo.bar.Baz") |
| 101 | val classPathElement = invocation.processingEnv |
| 102 | .requireTypeElement("com.google.devtools.ksp.processing.SymbolProcessor") |
| 103 | |
| 104 | val fileWithType = FileSpec.builder("foo", "Bar.kt").apply { |
| 105 | addType( |
| 106 | TypeSpec.classBuilder("Bar").apply { |
| 107 | addOriginatingElement(sourceElement) |
| 108 | addOriginatingElement(classPathElement) |
| 109 | }.build() |
| 110 | ) |
| 111 | }.build() |
| 112 | |
| 113 | val codeGenerator = DependencyTrackingCodeGenerator() |
| 114 | KspFiler(codeGenerator, TestMessager()).write(fileWithType) |
| 115 | codeGenerator.fileDependencies[fileWithType.name] |
| 116 | .containsExactlySimpleKotlinClass() |
| 117 | val (file, classDeclarations) = codeGenerator.classDependencies.entries.single() |
| 118 | assertThat(file).isEqualTo("Bar.kt") |
| 119 | assertThat(classDeclarations.single()) |
| 120 | .isEqualTo((classPathElement as KspTypeElement).declaration) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | @Test |
| 125 | fun originatingClassAddedForClassPathType() { |
| 126 | runKspTest(sources = listOf()) { invocation -> |
| 127 | val classPathElement = invocation.processingEnv |
| 128 | .requireTypeElement("com.google.devtools.ksp.processing.SymbolProcessor") |
| 129 | |
| 130 | val fileWithType = FileSpec.builder("foo", "Bar.kt").apply { |
| 131 | addType( |
| 132 | TypeSpec.classBuilder("Bar").apply { |
| 133 | addOriginatingElement(classPathElement) |
| 134 | }.build() |
| 135 | ) |
| 136 | }.build() |
| 137 | |
| 138 | val codeGenerator = DependencyTrackingCodeGenerator() |
| 139 | KspFiler(codeGenerator, TestMessager()).write(fileWithType) |
| 140 | val (file, classDeclarations) = codeGenerator.classDependencies.entries.single() |
| 141 | assertThat(file).isEqualTo("Bar.kt") |
| 142 | assertThat(classDeclarations.single()) |
| 143 | .isEqualTo((classPathElement as KspTypeElement).declaration) |
| 144 | } |
| 145 | } |
| 146 | |
Daniel Santiago Rivera | fb43532 | 2022-11-21 08:54:41 -0500 | [diff] [blame] | 147 | @Test |
| 148 | fun writeResource() { |
| 149 | runKspTest( |
| 150 | sources = emptyList() |
| 151 | ) { invocation -> |
| 152 | invocation.processingEnv.filer.writeResource( |
| 153 | filePath = Path("test.log"), |
| 154 | originatingElements = emptyList() |
| 155 | ).bufferedWriter(Charsets.UTF_8).use { |
| 156 | it.write("Hello!") |
| 157 | } |
| 158 | invocation.processingEnv.filer.writeResource( |
| 159 | filePath = Path("META-INF/services/com.test.Foo"), |
| 160 | originatingElements = emptyList() |
| 161 | ).bufferedWriter(Charsets.UTF_8).use { |
| 162 | it.write("Not a real service...") |
| 163 | } |
| 164 | invocation.assertCompilationResult { |
| 165 | hasNoWarnings() |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 170 | private fun Dependencies?.containsExactlySimpleKotlinClass() { |
| 171 | assertThat(this).isNotNull() |
| 172 | val originatingFiles = this!!.originatingFiles.map { it.fileName } |
| 173 | assertThat(originatingFiles).containsExactly("Baz.kt") |
| 174 | } |
| 175 | |
| 176 | class TestMessager : XMessager() { |
Brad Corso | 4707733 | 2021-07-30 22:36:34 +0000 | [diff] [blame] | 177 | override fun onPrintMessage( |
| 178 | kind: Diagnostic.Kind, |
| 179 | msg: String, |
| 180 | element: XElement?, |
| 181 | annotation: XAnnotation?, |
| 182 | annotationValue: XAnnotationValue? |
| 183 | ) { |
| 184 | var errorMsg = "${kind.name} element: $element " + |
| 185 | "annotation: $annotation " + |
| 186 | "annotationValue: $annotationValue " + |
| 187 | "msg: $msg" |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 188 | if (kind == Diagnostic.Kind.ERROR) { |
Brad Corso | 4707733 | 2021-07-30 22:36:34 +0000 | [diff] [blame] | 189 | error(errorMsg) |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 190 | } else { |
Brad Corso | 4707733 | 2021-07-30 22:36:34 +0000 | [diff] [blame] | 191 | println(errorMsg) |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 192 | } |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | class DependencyTrackingCodeGenerator : CodeGenerator { |
| 197 | |
| 198 | val fileDependencies = mutableMapOf<String, Dependencies>() |
Eli Hart | 4b910a5d | 2021-11-18 11:35:44 -0800 | [diff] [blame] | 199 | val classDependencies = mutableMapOf<String, MutableSet<KSClassDeclaration>>() |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 200 | |
| 201 | override val generatedFile: Collection<File> |
| 202 | get() = emptyList() |
| 203 | |
| 204 | override fun associate( |
| 205 | sources: List<KSFile>, |
| 206 | packageName: String, |
| 207 | fileName: String, |
| 208 | extensionName: String |
| 209 | ) { |
| 210 | // no-op for the sake of dependency tracking. |
| 211 | } |
| 212 | |
Jim Sproch | e1b4e86 | 2021-10-06 00:48:22 -0700 | [diff] [blame] | 213 | override fun associateWithClasses( |
| 214 | classes: List<KSClassDeclaration>, |
| 215 | packageName: String, |
| 216 | fileName: String, |
| 217 | extensionName: String |
| 218 | ) { |
Eli Hart | 4b910a5d | 2021-11-18 11:35:44 -0800 | [diff] [blame] | 219 | classDependencies.getOrPut(fileName) { mutableSetOf() }.addAll(classes) |
Jim Sproch | e1b4e86 | 2021-10-06 00:48:22 -0700 | [diff] [blame] | 220 | } |
| 221 | |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 222 | override fun createNewFile( |
| 223 | dependencies: Dependencies, |
| 224 | packageName: String, |
| 225 | fileName: String, |
| 226 | extensionName: String |
| 227 | ): OutputStream { |
| 228 | fileDependencies[fileName] = dependencies |
| 229 | return OutputStream.nullOutputStream() |
| 230 | } |
Jim Sproch | d324f46 | 2022-11-09 01:42:05 -0800 | [diff] [blame] | 231 | |
| 232 | override fun associateByPath( |
| 233 | sources: List<KSFile>, |
| 234 | path: String, |
| 235 | extensionName: String |
| 236 | ) { |
| 237 | // no-op for the sake of dependency tracking. |
| 238 | } |
| 239 | |
| 240 | override fun createNewFileByPath( |
| 241 | dependencies: Dependencies, |
| 242 | path: String, |
| 243 | extensionName: String |
| 244 | ): OutputStream { |
| 245 | val fileName = path.split(File.separator).last() |
| 246 | fileDependencies[fileName] = dependencies |
| 247 | return OutputStream.nullOutputStream() |
| 248 | } |
Ben Schwab | 2d0f6bc | 2021-06-12 15:56:36 +0000 | [diff] [blame] | 249 | } |
| 250 | |
| 251 | companion object { |
| 252 | val simpleKotlinClass = Source.kotlin( |
| 253 | "Baz.kt", |
| 254 | """ |
| 255 | package foo.bar; |
| 256 | |
| 257 | class Baz |
| 258 | """.trimIndent() |
| 259 | ) |
| 260 | } |
| 261 | } |