Skip to content

Commit

Permalink
Support multi module tests (#137)
Browse files Browse the repository at this point in the history
Add infra for running multi module tests
  • Loading branch information
yigit authored Nov 3, 2020
1 parent 3bcf10c commit c844f2a
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import org.jetbrains.kotlin.load.java.structure.impl.JavaTypeImpl
import org.jetbrains.kotlin.load.java.structure.impl.JavaTypeParameterImpl
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.calls.inference.components.NewTypeSubstitutor
Expand Down Expand Up @@ -99,7 +100,7 @@ class ResolverImpl(

private val typeMapper = KotlinTypeMapper(
BindingContext.EMPTY, ClassBuilderMode.LIGHT_CLASSES,
JvmProtoBufUtil.DEFAULT_MODULE_NAME,
module.name.getNonSpecialIdentifier(),
KotlinTypeMapper.LANGUAGE_VERSION_SETTINGS_DEFAULT// TODO use proper LanguageVersionSettings
)

Expand Down Expand Up @@ -629,3 +630,26 @@ private fun TypeSubstitutor.toNewSubstitutor() = composeWith(
private fun KotlinType.createTypeSubstitutor(): NewTypeSubstitutor {
return SubstitutionUtils.buildDeepSubstitutor(this).toNewSubstitutor()
}

/**
* Extracts the identifier from a module Name.
*
* One caveat here is that kotlin passes a special name into the plugin which cannot be used as an identifier.
* On the other hand, to construct the correct TypeMapper, we need a non-special name.
* This function extracts the non-special name from a given name if it is special.
*
* @see: https://github.com/JetBrains/kotlin/blob/master/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/TopDownAnalyzerFacadeForJVM.kt#L305
*/
private fun Name.getNonSpecialIdentifier() :String {
// the analyzer might pass down a special name which will break type mapper name computations.
// If it is a special name, we turn it back to an id
if (!isSpecial || asString().isBlank()) {
return asString()
}
// special names starts with a `<` and usually end with `>`
return if (asString().last() == '>') {
asString().substring(1, asString().length - 1)
} else {
asString().substring(1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2020 Google LLC
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.devtools.ksp.processor

import com.google.devtools.ksp.getClassDeclarationByName
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.impl.BaseVisitor
import com.google.devtools.ksp.symbol.*

class MultiModuleTestProcessor : AbstractTestProcessor() {
private val results = mutableListOf<String>()
override fun toResult(): List<String> {
return results
}

override fun process(resolver: Resolver) {
val target = resolver.getClassDeclarationByName("TestTarget")
val classes = mutableSetOf<KSClassDeclaration>()
val classCollector = object : BaseVisitor() {
override fun visitClassDeclaration(type: KSClassDeclaration, data: Unit) {
if (classes.add(type)) {
super.visitClassDeclaration(type, data)
}
}

override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: Unit) {
(property.type.resolve().declaration as? KSClassDeclaration)?.accept(this, Unit)
}
}
target?.accept(classCollector, Unit)
results.addAll(classes.map { it.toSignature() }.sorted())
}

private fun KSClassDeclaration.toSignature(): String {
val id = qualifiedName?.asString() ?: "no-qual-name:($this)"
return "$id[${origin.name}]"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
package com.google.devtools.ksp.test;

import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.test.*;
import org.junit.runner.RunWith;

import java.io.File;
import java.util.List;
import java.util.regex.Pattern;

@SuppressWarnings("all")
Expand Down Expand Up @@ -131,6 +132,11 @@ public void testMakeNullable() throws Exception {
runTest("testData/api/makeNullable.kt");
}

@TestMetadata(("multipleModules.kt"))
public void testMultipleModules() throws Exception {
runTest("testData/api/multipleModules.kt");
}

@TestMetadata("platformDeclaration.kt")
public void testPlatformDeclaration() throws Exception {
runTest("testData/api/platformDeclaration.kt");
Expand Down Expand Up @@ -185,4 +191,19 @@ public void testValidateTypes() throws Exception {
public void testVisibilities() throws Exception {
runTest("testData/api/visibilities.kt");
}

@Override
protected @NotNull List<KspTestFile> createTestFilesFromFile(@NotNull File file, @NotNull String expectedText) {
return TestFiles.createTestFiles(file.getName(), expectedText, new TestFiles.TestFileFactory<TestModule, KspTestFile>() {
@Override
public KspTestFile createFile(@Nullable TestModule module, @NotNull String fileName, @NotNull String text, @NotNull Directives directives) {
return new KspTestFile(fileName, text, directives, module);
}

@Override
public TestModule createModule(@NotNull String name, @NotNull List<String> dependencies, @NotNull List<String> friends) {
return new TestModule(name, dependencies, friends);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,71 @@

package com.google.devtools.ksp.test

import com.google.devtools.ksp.KotlinSymbolProcessingExtension
import com.google.devtools.ksp.KspOptions
import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger
import com.google.devtools.ksp.processor.AbstractTestProcessor
import junit.framework.TestCase
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
import org.jetbrains.kotlin.codegen.CodegenTestCase
import org.jetbrains.kotlin.codegen.GenerationUtils
import com.google.devtools.ksp.KotlinSymbolProcessingExtension
import com.google.devtools.ksp.KspOptions
import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger
import com.google.devtools.ksp.processor.AbstractTestProcessor
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
import org.jetbrains.kotlin.test.ConfigurationKind
import org.jetbrains.kotlin.test.*
import java.io.File
import org.jetbrains.kotlin.test.TestJdkKind

abstract class AbstractKotlinKSPTest : CodegenTestCase() {
abstract class AbstractKotlinKSPTest : KotlinBaseTest<AbstractKotlinKSPTest.KspTestFile>() {
companion object {
const val TEST_PROCESSOR = "// TEST PROCESSOR:"
val EXPECTED_RESULTS = "// EXPECTED:"
}
private val testTmpDir by lazy {
KotlinTestUtils.tmpDir("test")
}

override fun doMultiFileTest(wholeFile: File, files: List<TestFile>) {
val javaFiles = listOfNotNull(writeJavaFiles(files))
createEnvironmentWithMockJdkAndIdeaAnnotations(ConfigurationKind.NO_KOTLIN_REFLECT, emptyList(), TestJdkKind.FULL_JDK_9, *(javaFiles.toTypedArray()))
override fun doMultiFileTest(wholeFile: File, files: List<KspTestFile>) {
// get the main module where KSP tests will be run. If there are modules declared in the test file, it will be
// the module of the last file (modules are required to be declared in the compilation order).
// If there is no module declared in the test input, we'll create one that will contain all test files
val mainModule: TestModule = files.findLast {
it.testModule != null
}?.testModule ?: TestModule("main", emptyList(), emptyList())

// group each test file with its module
val filesByModule = groupFilesByModule(mainModule, files)

// now compile each sub module, we'll compile the main module last.
filesByModule.forEach { (module, files) ->
if (module !== mainModule) {
compileModule(
module = module,
testFiles = files,
dependencies = module.dependencies.map {
it.outDir
},
testProcessor = null)
}
}
// modules are compiled, now compile the test project with KSP
val testProcessorName = wholeFile
.readLines()
.filter { it.startsWith(TEST_PROCESSOR) }
.single()
.substringAfter(TEST_PROCESSOR)
.trim()
val testProcessor = Class.forName("com.google.devtools.ksp.processor.$testProcessorName").newInstance() as AbstractTestProcessor
val logger = MessageCollectorBasedKSPLogger(PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false))
val analysisExtension =
KotlinSymbolProcessingExtension(KspOptions.Builder().apply {
javaSourceRoots.addAll(javaFiles.map { File(it.parent) }.distinct())
classOutputDir = File("/tmp/kspTest/classes/main")
javaOutputDir = File("/tmp/kspTest/src/main/java")
kotlinOutputDir = File("/tmp/kspTest/src/main/kotlin")
resourceOutputDir = File("/tmp/kspTest/src/main/resources")
}.build(), logger, testProcessor)
val project = myEnvironment.project
AnalysisHandlerExtension.registerExtension(project, analysisExtension)
loadMultiFiles(files)
GenerationUtils.compileFiles(myFiles.psiFiles, myEnvironment, classBuilderFactory)

compileModule(
module = mainModule,
testFiles = filesByModule[mainModule]!!,
dependencies = mainModule.dependencies.map { it.outDir },
testProcessor = testProcessor
)

val result = testProcessor.toResult()
val expectedResults = wholeFile
.readLines()
Expand All @@ -70,4 +92,101 @@ abstract class AbstractKotlinKSPTest : CodegenTestCase() {
.map { it.substring(3).trim() }
TestCase.assertEquals(expectedResults.joinToString("\n"), result.joinToString("\n"))
}
}

private fun compileModule(
module: TestModule,
testFiles: List<KspTestFile>,
dependencies: List<File>,
testProcessor: AbstractTestProcessor?) {
val moduleRoot = module.rootDir
module.writeJavaFiles(testFiles)
val configuration = createConfiguration(
ConfigurationKind.NO_KOTLIN_REFLECT,
TestJdkKind.FULL_JDK_9,
listOf(KotlinTestUtils.getAnnotationsJar()) + dependencies,
listOf(module.javaSrcDir),
emptyList()
)
configuration.put(CommonConfigurationKeys.MODULE_NAME, module.name)

val environment = KotlinCoreEnvironment.createForTests(
testRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
)
val moduleFiles = CodegenTestCase.loadMultiFiles(testFiles, environment.project)
val outDir = module.outDir.also {
it.mkdirs()
}
if (testProcessor != null) {
val logger = MessageCollectorBasedKSPLogger(
PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false)
)
val analysisExtension =
KotlinSymbolProcessingExtension(KspOptions.Builder().apply {
javaSourceRoots.add(module.javaSrcDir)
classOutputDir = File(moduleRoot,"kspTest/classes/main")
javaOutputDir = File(moduleRoot,"kspTest/src/main/java")
kotlinOutputDir = File(moduleRoot,"kspTest/src/main/kotlin")
resourceOutputDir = File(moduleRoot,"kspTest/src/main/resources")
}.build(), logger, testProcessor)
val project = environment.project
AnalysisHandlerExtension.registerExtension(project, analysisExtension)
GenerationUtils.compileFiles(moduleFiles.psiFiles, environment, ClassBuilderFactories.TEST)
} else {
GenerationUtils.compileFilesTo(moduleFiles.psiFiles, environment, outDir)
}
}

/**
* Groups each file by module. For files that do not have an associated module, they get added to the [mainModule].
*/
private fun groupFilesByModule(
mainModule: TestModule,
testFiles: List<KspTestFile>
) : LinkedHashMap<TestModule, MutableList<KspTestFile>> {
val result = LinkedHashMap<TestModule, MutableList<KspTestFile>>()
testFiles.forEach { testFile ->
result.getOrPut(testFile.testModule ?: mainModule) {
mutableListOf()
}.add(testFile)
}
return result
}

/**
* Write the java files in the given list into the java source directory of the TestModule.
*/
private fun TestModule.writeJavaFiles(testFiles : List<KspTestFile>) {
val targetDir = javaSrcDir
targetDir.mkdirs()
testFiles.filter {
it.name.endsWith(".java")
}.map { testFile ->
File(targetDir, testFile.name).also {
it.parentFile.mkdirs()
}.also {
it.writeText(
testFile.content, Charsets.UTF_8
)
}
}
}

private val TestModule.rootDir:File
get() = File(testTmpDir, name)

private val TestModule.javaSrcDir:File
get() = File(rootDir, "javaSrc")

private val TestModule.outDir:File
get() = File(rootDir, "out")

/**
* TestFile class for KSP where we can also keep a reference to the [TestModule]
*/
class KspTestFile(
name: String,
content: String,
directives: Directives,
var testModule: TestModule?
) : KotlinBaseTest.TestFile(name, content, directives)
}
39 changes: 39 additions & 0 deletions compiler-plugin/testData/api/multipleModules.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// WITH_RUNTIME
// TEST PROCESSOR: MultiModuleTestProcessor
// EXPECTED:
// ClassInMainModule[KOTLIN]
// ClassInModule1[CLASS]
// ClassInModule2[CLASS]
// JavaClassInMainModule[JAVA]
// JavaClassInModule1[CLASS]
// JavaClassInModule2[CLASS]
// TestTarget[KOTLIN]
// END
// MODULE: module1
// FILE: ClassInModule1.kt
class ClassInModule1 {
val javaClassInModule1: JavaClassInModule1 = TODO()
}
// FILE: JavaClassInModule1.java
public class JavaClassInModule1 {}
// MODULE: module2(module1)
// FILE: ClassInModule2.kt
class ClassInModule2 {
val javaClassInModule2: JavaClassInModule2 = TODO()
val classInModule1: ClassInModule1 = TODO()
}
// FILE: JavaClassInModule2.java
public class JavaClassInModule2 {}
// MODULE: main(module1, module2)
// FILE: main.kt
class TestTarget {
val field: ClassInMainModule = TODO()
}
// FILE: ClassInMainModule.kt
class ClassInMainModule {
val field: ClassInModule2 = TODO()
val javaClassInMainModule : JavaClassInMainModule = TODO()
}
// FILE: JavaClassInMainModule.java
class JavaClassInMainModule {
}

0 comments on commit c844f2a

Please sign in to comment.