xref: /aosp_15_r20/external/ksp/test-utils/src/main/kotlin/com/google/devtools/ksp/testutils/AbstractKSPTest.kt (revision af87fb4bb8e3042070d2a054e912924f599b22b7)
1 /*
<lambda>null2  * Copyright 2022 Google LLC
3  * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.google.devtools.ksp.testutils
19 
20 import com.google.devtools.ksp.processor.AbstractTestProcessor
21 import com.intellij.openapi.Disposable
22 import com.intellij.openapi.project.Project
23 import com.intellij.openapi.util.Disposer
24 import com.intellij.testFramework.TestDataFile
25 import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
26 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
27 import org.jetbrains.kotlin.cli.jvm.config.addJavaSourceRoot
28 import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
29 import org.jetbrains.kotlin.codegen.GenerationUtils
30 import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
31 import org.jetbrains.kotlin.psi.KtFile
32 import org.jetbrains.kotlin.test.ExecutionListenerBasedDisposableProvider
33 import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
34 import org.jetbrains.kotlin.test.builders.testConfiguration
35 import org.jetbrains.kotlin.test.compileJavaFiles
36 import org.jetbrains.kotlin.test.directives.ConfigurationDirectives
37 import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives
38 import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives
39 import org.jetbrains.kotlin.test.model.DependencyKind
40 import org.jetbrains.kotlin.test.model.FrontendKind
41 import org.jetbrains.kotlin.test.model.ResultingArtifact
42 import org.jetbrains.kotlin.test.model.TestModule
43 import org.jetbrains.kotlin.test.runners.AbstractKotlinCompilerTest
44 import org.jetbrains.kotlin.test.services.*
45 import org.jetbrains.kotlin.test.services.configuration.CommonEnvironmentConfigurator
46 import org.jetbrains.kotlin.test.services.configuration.JvmEnvironmentConfigurator
47 import org.jetbrains.kotlin.test.services.impl.TemporaryDirectoryManagerImpl
48 import org.jetbrains.kotlin.test.util.KtTestUtil
49 import org.junit.jupiter.api.AfterEach
50 import org.junit.jupiter.api.Assertions
51 import org.junit.jupiter.api.BeforeEach
52 import org.junit.jupiter.api.TestInfo
53 import java.io.File
54 
55 abstract class DisposableTest {
56     private var _disposable: Disposable? = null
57     protected val disposable: Disposable get() = _disposable!!
58 
59     @BeforeEach
60     private fun initDisposable(testInfo: TestInfo) {
61         _disposable = Disposer.newDisposable("disposable for ${testInfo.displayName}")
62     }
63 
64     @AfterEach
65     private fun disposeDisposable() {
66         _disposable?.let { Disposer.dispose(it) }
67         _disposable = null
68     }
69 }
70 
71 abstract class AbstractKSPTest(frontend: FrontendKind<*>) : DisposableTest() {
72     companion object {
73         val TEST_PROCESSOR = "// TEST PROCESSOR:"
74         val EXPECTED_RESULTS = "// EXPECTED:"
75     }
76 
77     val kspTestRoot = KtTestUtil.tmpDir("test")
rootDirForModulenull78     fun rootDirForModule(name: String) = File(kspTestRoot, name)
79     fun outDirForModule(name: String) = File(rootDirForModule(name), "out")
80     fun javaDirForModule(name: String) = File(rootDirForModule(name), "javaSrc")
81     val TestModule.testRoot: File
82         get() = rootDirForModule(name)
83     val TestModule.outDir: File
84         get() = outDirForModule(name)
85     val TestModule.javaDir: File
86         get() = javaDirForModule(name)
87 
88     protected lateinit var testInfo: KotlinTestInfo
89         private set
90 
91     @BeforeEach
92     fun initTestInfo(testInfo: TestInfo) {
93         this.testInfo = KotlinTestInfo(
94             className = testInfo.testClass.orElseGet(null)?.name ?: "_undefined_",
95             methodName = testInfo.testMethod.orElseGet(null)?.name ?: "_testUndefined_",
96             tags = testInfo.tags
97         )
98     }
99 
configureTestnull100     open fun configureTest(builder: TestConfigurationBuilder) = Unit
101 
102     abstract fun runTest(
103         testServices: TestServices,
104         mainModule: TestModule,
105         libModules: List<TestModule>,
106         testProcessor: AbstractTestProcessor,
107     ): List<String>
108 
109     private val configure: TestConfigurationBuilder.() -> Unit = {
110         globalDefaults {
111             this@globalDefaults.frontend = frontend
112             targetPlatform = JvmPlatforms.defaultJvmPlatform
113             dependencyKind = DependencyKind.Source
114         }
115         useConfigurators(
116             ::CommonEnvironmentConfigurator,
117             ::JvmEnvironmentConfigurator,
118         )
119         assertions = JUnit5Assertions
120         useAdditionalService<TemporaryDirectoryManager>(::TemporaryDirectoryManagerImpl)
121         useAdditionalService<ApplicationDisposableProvider> { ExecutionListenerBasedDisposableProvider() }
122         useAdditionalService<KotlinStandardLibrariesPathProvider> { StandardLibrariesPathProviderForKotlinProject }
123 
124         useDirectives(*AbstractKotlinCompilerTest.defaultDirectiveContainers.toTypedArray())
125         useDirectives(JvmEnvironmentConfigurationDirectives)
126 
127         defaultDirectives {
128             +JvmEnvironmentConfigurationDirectives.FULL_JDK
129             // SourceFileProviderImpl doesn't group files by module. Let's load them manually.
130             +JvmEnvironmentConfigurationDirectives.SKIP_JAVA_SOURCES
131             +ConfigurationDirectives.WITH_STDLIB
132             +LanguageSettingsDirectives.ALLOW_KOTLIN_PACKAGE
133         }
134 
135         configureTest(this)
136 
137         startingArtifactFactory = { ResultingArtifact.Source() }
138         this.testInfo = this@AbstractKSPTest.testInfo
139     }
140 
loadKtFilesnull141     fun TestModule.loadKtFiles(project: Project): List<KtFile> {
142         return files.filter { it.isKtFile }.map {
143             KtTestUtil.createFile(it.name, it.originalContent, project)
144         }
145     }
146 
writeJavaFilesnull147     fun TestModule.writeJavaFiles(): List<File> {
148         javaDir.mkdirs()
149         val files = javaFiles.map { it to File(javaDir, it.relativePath) }
150         files.forEach { (testFile, file) ->
151             file.parentFile.mkdirs()
152             file.writeText(testFile.originalContent)
153         }
154         return files.map { it.second }
155     }
156 
157     // No, this is far from complete. It only works for our test cases.
158     //
159     // No, neither CompiledLibraryProvider nor LibraryEnvironmentConfigurator can be used. They rely on
160     // dist/kotlinc/lib/*
161     //
162     // No, sourceFileProvider doesn't group files by module unfortunately. Let's do it by ourselves.
compileModulenull163     open fun compileModule(module: TestModule, testServices: TestServices) {
164         val javaFiles = module.writeJavaFiles()
165         val compilerConfiguration = testServices.compilerConfigurationProvider.getCompilerConfiguration(module)
166         val dependencies = module.allDependencies.map { outDirForModule(it.moduleName) }
167         compilerConfiguration.addJvmClasspathRoots(dependencies)
168         compilerConfiguration.addJavaSourceRoot(module.javaDir)
169 
170         // TODO: other platforms
171         val kotlinCoreEnvironment = KotlinCoreEnvironment.createForTests(
172             disposable,
173             compilerConfiguration,
174             EnvironmentConfigFiles.JVM_CONFIG_FILES
175         )
176         val ktFiles = module.loadKtFiles(kotlinCoreEnvironment.project)
177         GenerationUtils.compileFilesTo(ktFiles, kotlinCoreEnvironment, module.outDir)
178 
179         if (module.javaFiles.isEmpty())
180             return
181 
182         val classpath = (dependencies + KtTestUtil.getAnnotationsJar() + module.outDir)
183             .joinToString(File.pathSeparator) { it.absolutePath }
184         val options = listOf(
185             "-classpath", classpath,
186             "-d", module.outDir.path
187         )
188         compileJavaFiles(javaFiles, options, assertions = JUnit5Assertions)
189     }
190 
runTestnull191     fun runTest(@TestDataFile path: String) {
192         val testConfiguration = testConfiguration(path, configure)
193         Disposer.register(disposable, testConfiguration.rootDisposable)
194         val testServices = testConfiguration.testServices
195         val moduleStructure = testConfiguration.moduleStructureExtractor.splitTestDataByModules(
196             path,
197             testConfiguration.directives,
198         )
199         val mainModule = moduleStructure.modules.last()
200         val libModules = moduleStructure.modules.dropLast(1)
201 
202         for (lib in libModules) {
203             compileModule(lib, testServices)
204         }
205         val compilerConfigurationMain = testServices.compilerConfigurationProvider.getCompilerConfiguration(mainModule)
206         compilerConfigurationMain.addJvmClasspathRoots(libModules.map { it.outDir })
207 
208         val contents = mainModule.files.first().originalFile.readLines()
209 
210         val testProcessorName = contents
211             .filter { it.startsWith(TEST_PROCESSOR) }
212             .single()
213             .substringAfter(TEST_PROCESSOR)
214             .trim()
215         val testProcessor: AbstractTestProcessor =
216             Class.forName("com.google.devtools.ksp.processor.$testProcessorName")
217                 .getDeclaredConstructor().newInstance() as AbstractTestProcessor
218 
219         val expectedResults = contents
220             .dropWhile { !it.startsWith(EXPECTED_RESULTS) }
221             .drop(1)
222             .takeWhile { !it.startsWith("// END") }
223             .map { it.substring(3).trim() }
224 
225         val results = runTest(testServices, mainModule, libModules, testProcessor)
226         Assertions.assertEquals(expectedResults.joinToString("\n"), results.joinToString("\n"))
227     }
228 }
229