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