K2: Support binary library dependencies between test modules

The test infrastructure for analysis supports binary module tests, but
the binary build does not use another binary module as a dependency when
it passes the class path. As a result, each binary module build does not
work when they have dependency on each other.

This commit fixes the issue by
1. Topological sort in the order of dependency graph for test modules.
2. Pass module paths as extra class paths when they have dependency on
   each other.

^KT-64994
This commit is contained in:
Jaebaek Seo
2024-02-08 15:28:42 -08:00
committed by Space Cloud
parent 407448d8e3
commit 512efb9649
17 changed files with 164 additions and 16 deletions
@@ -0,0 +1,54 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.analysis.api.fir.test.cases.generated.cases.components.compilerFacility;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.analysis.api.fir.test.configurators.AnalysisApiFirTestConfiguratorFactory;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.FrontendKind;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.compilerFacility.AbstractMultiModuleCompilerFacilityTest;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("analysis/analysis-api/testData/components/compilerFacility/compilationMultiBinaryModule")
@TestDataPath("$PROJECT_ROOT")
public class FirIdeNormalAnalysisLibraryBinaryModuleMultiModuleCompilerFacilityTestGenerated extends AbstractMultiModuleCompilerFacilityTest {
@NotNull
@Override
public AnalysisApiTestConfigurator getConfigurator() {
return AnalysisApiFirTestConfiguratorFactory.INSTANCE.createConfigurator(
new AnalysisApiTestConfiguratorFactoryData(
FrontendKind.Fir,
TestModuleKind.LibraryBinary,
AnalysisSessionMode.Normal,
AnalysisApiMode.Ide
)
);
}
@Test
public void testAllFilesPresentInCompilationMultiBinaryModule() {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/compilerFacility/compilationMultiBinaryModule"), Pattern.compile("^(.+)\\.(kt)$"), null, true);
}
@Test
@TestMetadata("dependencyBetweenBinaryLibraries.kt")
public void testDependencyBetweenBinaryLibraries() {
runTest("analysis/analysis-api/testData/components/compilerFacility/compilationMultiBinaryModule/dependencyBetweenBinaryLibraries.kt");
}
}
@@ -0,0 +1,6 @@
MODULE_FRAGMENT
FILE fqName:<root> fileName:main.kt
FUN name:test visibility:public modality:FINAL <> () returnType:kotlin.Int
BLOCK_BODY
RETURN type=kotlin.Nothing from='public final fun test (): kotlin.Int declared in <root>'
CALL 'public final fun bar (): kotlin.Int declared in p2' type=kotlin.Int origin=null
@@ -0,0 +1,16 @@
// MODULE: lib
// FILE: p3/foo.kt
package p3;
fun foo() = 3
// MODULE: lib2(lib)
// FILE: p2/bar.kt
package p2;
fun bar() = 4 + p3.foo()
// MODULE: main(lib, lib2)
// MODULE_KIND: Source
// FILE: main.kt
import p2.bar
fun test() = bar()
@@ -0,0 +1,4 @@
public final class MainKt {
// source: 'main.kt'
public final static method test(): int
}
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.test.directives.model.singleOrZeroValue
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.sourceFileProvider
import java.nio.file.Path
object KtCodeFragmentModuleFactory : KtModuleFactory {
override fun createModule(
@@ -30,6 +31,7 @@ object KtCodeFragmentModuleFactory : KtModuleFactory {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>
): KtModuleWithFiles {
requireNotNull(contextModule) { "Code fragment requires a context module" }
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.analysis.test.framework.services.libraries.compiledL
import org.jetbrains.kotlin.analysis.test.framework.services.libraries.testModuleDecompiler
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import java.nio.file.Path
/**
* @see org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind.LibraryBinary
@@ -23,8 +24,9 @@ object KtLibraryBinaryModuleFactory : KtModuleFactory {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>,
): KtModuleWithFiles {
val library = testServices.compiledLibraryProvider.compileToLibrary(testModule).artifact
val library = testServices.compiledLibraryProvider.compileToLibrary(testModule, dependencyPaths).artifact
val decompiledFiles = testServices.testModuleDecompiler.getAllPsiFilesFromLibrary(library, project)
return KtModuleWithFiles(
@@ -27,6 +27,7 @@ object KtLibrarySourceModuleFactory : KtModuleFactory {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>,
): KtModuleWithFiles {
Assume.assumeFalse("Compilation of multi-platform libraries is not supported", testModule.targetPlatform.isMultiPlatform())
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.analysis.test.framework.test.configurators.moduleKin
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestService
import org.jetbrains.kotlin.test.services.TestServices
import java.nio.file.Path
fun interface KtModuleFactory : TestService {
/**
@@ -26,6 +27,7 @@ fun interface KtModuleFactory : TestService {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>,
): KtModuleWithFiles
}
@@ -10,6 +10,7 @@ import org.jetbrains.kotlin.analysis.api.standalone.base.project.structure.KtMod
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import java.nio.file.Path
/**
* @see org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind.ScriptSource
@@ -20,6 +21,7 @@ object KtScriptModuleFactory : KtModuleFactory {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>,
): KtModuleWithFiles {
val ktFile = TestModuleStructureFactory.createSourcePsiFiles(testModule, testServices, project).single() as KtFile
val module = KtScriptModuleImpl(
@@ -10,6 +10,7 @@ import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.api.standalone.base.project.structure.KtModuleWithFiles
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import java.nio.file.Path
/**
* @see org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind.Source
@@ -20,6 +21,7 @@ object KtSourceModuleFactory : KtModuleFactory {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>,
): KtModuleWithFiles {
val psiFiles = TestModuleStructureFactory.createSourcePsiFiles(testModule, testServices, project)
@@ -42,6 +42,31 @@ private typealias LibraryCache = MutableMap<Set<Path>, KtBinaryModule>
private typealias ModulesByName = Map<String, KtModuleWithFiles>
/**
* A function to run the topological sort (or post-order sort) for [TestModule]s based on the dependency graph.
* This function guarantees:
* - For [TestModule] A and B, where A has dependency on B, A will never appears earlier than B in the result list.
*/
private fun sortInDependencyPostOrder(testModules: List<TestModule>): List<TestModule> {
val namesToModules = buildMap { testModules.forEach { put(it.name, it) } }
val notVisited = testModules.toMutableSet()
val sortedModules = mutableListOf<TestModule>()
fun dfsWalk(module: TestModule) {
notVisited.remove(module)
for (dependency in module.regularDependencies) {
val dependencyAsModule = namesToModules[dependency.moduleName] ?: error("Module ${dependency.moduleName} is missing")
if (dependencyAsModule in notVisited) dfsWalk(dependencyAsModule)
}
sortedModules.add(module)
}
while (notVisited.isNotEmpty()) {
dfsWalk(notVisited.first())
}
return sortedModules
}
object TestModuleStructureFactory {
fun createProjectStructureByTestStructure(
moduleStructure: TestModuleStructure,
@@ -64,25 +89,38 @@ object TestModuleStructureFactory {
return KtModuleProjectStructure(modules, libraryCache.values)
}
/**
* A function to create [KtModuleWithFiles] for the given [moduleStructure]. This function guarantees:
* - For [TestModule] A and B, where A has dependency on B,
* - B will always be created earlier than A.
* - The class path of B will be given to the creation of A's module.
*
* In particular, it handles unresolved symbol issues caused by building binary libraries.
*/
private fun createModules(
moduleStructure: TestModuleStructure,
testServices: TestServices,
project: Project
project: Project,
): List<KtModuleWithFiles> {
val moduleCount = moduleStructure.modules.size
val modulesSortedByDependencies = sortInDependencyPostOrder(moduleStructure.modules)
val moduleCount = modulesSortedByDependencies.size
val moduleNamesToPaths = mutableMapOf<String, Collection<Path>>()
val existingModules = HashMap<String, KtModuleWithFiles>(moduleCount)
val result = ArrayList<KtModuleWithFiles>(moduleCount)
for (testModule in moduleStructure.modules) {
for (testModule in modulesSortedByDependencies) {
val contextModuleName = testModule.directives.singleOrZeroValue(AnalysisApiTestDirectives.CONTEXT_MODULE)
val contextModule = contextModuleName?.let(existingModules::getValue)
val dependencies = testModule.regularDependencies.mapNotNull { moduleNamesToPaths[it.moduleName] }.flatten()
val moduleWithFiles = testServices
.getKtModuleFactoryForTestModule(testModule)
.createModule(testModule, contextModule, testServices, project)
.createModule(testModule, contextModule, testServices, project, dependencies)
existingModules[testModule.name] = moduleWithFiles
result.add(moduleWithFiles)
val libraryModule = moduleWithFiles.ktModule as? KtLibraryModuleImpl ?: continue
moduleNamesToPaths[testModule.name] = libraryModule.getBinaryRoots()
}
return result
@@ -30,19 +30,25 @@ import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import kotlin.io.path.div
import kotlin.io.path.outputStream
import kotlin.io.path.pathString
abstract class CliTestModuleCompiler : TestModuleCompiler() {
internal abstract val compilerKind: CompilerExecutor.CompilerKind
protected abstract fun buildPlatformCompilerOptions(module: TestModule, testServices: TestServices): List<String>
override fun compile(tmpDir: Path, module: TestModule, testServices: TestServices): Path = CompilerExecutor.compileLibrary(
override fun compile(
tmpDir: Path,
module: TestModule,
testServices: TestServices,
dependencyPaths: Collection<Path>,
): Path = CompilerExecutor.compileLibrary(
compilerKind,
tmpDir,
buildCompilerOptions(module, testServices),
compilationErrorExpected = Directives.COMPILATION_ERRORS in module.directives,
libraryName = module.name,
extraClasspath = buildExtraClasspath(module, testServices),
extraClasspath = buildExtraClasspath(module, testServices) + dependencyPaths.map { it.pathString },
)
override fun compileTestModuleToLibrarySources(module: TestModule, testServices: TestServices): Path {
@@ -145,8 +151,8 @@ class DispatchingTestModuleCompiler : TestModuleCompiler() {
CompilerExecutor.CompilerKind.JS to JsKlibTestModuleCompiler(),
)
override fun compile(tmpDir: Path, module: TestModule, testServices: TestServices): Path {
return getCompiler(module).compileTestModuleToLibrary(module, testServices)
override fun compile(tmpDir: Path, module: TestModule, testServices: TestServices, dependencyPaths: Collection<Path>): Path {
return getCompiler(module).compileTestModuleToLibrary(module, testServices, dependencyPaths)
}
override fun compileTestModuleToLibrarySources(module: TestModule, testServices: TestServices): Path {
@@ -13,11 +13,11 @@ import java.nio.file.Path
class CompiledLibraryProvider(private val testServices: TestServices) : TestService {
private val libraries = mutableMapOf<String, CompiledLibrary>()
fun compileToLibrary(module: TestModule): CompiledLibrary {
fun compileToLibrary(module: TestModule, dependencyPaths: Collection<Path> = emptyList()): CompiledLibrary {
if (module.name in libraries) {
error("Library for module ${module.name} is already compiled")
}
val libraryJar = testServices.testModuleCompiler.compileTestModuleToLibrary(module, testServices)
val libraryJar = testServices.testModuleCompiler.compileTestModuleToLibrary(module, testServices, dependencyPaths)
val librarySourcesJar = testServices.testModuleCompiler.compileTestModuleToLibrarySources(module, testServices)
return CompiledLibrary(libraryJar, librarySourcesJar).also { libraries[module.name] = it }
@@ -18,7 +18,7 @@ import kotlin.io.path.div
import kotlin.io.path.writeText
abstract class TestModuleCompiler : TestService {
fun compileTestModuleToLibrary(module: TestModule, testServices: TestServices): Path {
fun compileTestModuleToLibrary(module: TestModule, testServices: TestServices, dependencyPaths: Collection<Path> = emptyList()): Path {
val tmpDir = KtTestUtil.tmpDir("testSourcesToCompile").toPath()
for (testFile in module.files) {
val text = testServices.sourceFileProvider.getContentOfSourceFile(testFile)
@@ -28,10 +28,15 @@ abstract class TestModuleCompiler : TestService {
val tmpSourceFile = filePath.createFile()
tmpSourceFile.writeText(text)
}
return compile(tmpDir, module, testServices)
return compile(tmpDir, module, testServices, dependencyPaths)
}
abstract fun compile(tmpDir: Path, module: TestModule, testServices: TestServices): Path
abstract fun compile(
tmpDir: Path,
module: TestModule,
testServices: TestServices,
dependencyPaths: Collection<Path> = emptyList(),
): Path
abstract fun compileTestModuleToLibrarySources(module: TestModule, testServices: TestServices): Path?
@@ -26,6 +26,7 @@ import org.jetbrains.kotlin.test.getAnalyzerServices
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestModuleStructure
import org.jetbrains.kotlin.test.services.TestServices
import java.nio.file.Path
object AnalysisApiFirOutOfContentRootTestConfigurator : AnalysisApiFirSourceLikeTestConfigurator(false) {
override val testPrefix: String
@@ -68,6 +69,7 @@ private object KtOutOfContentRootModuleFactory : KtModuleFactory {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>,
): KtModuleWithFiles {
val psiFiles = TestModuleStructureFactory.createSourcePsiFiles(testModule, testServices, project)
val platform = testModule.targetPlatform
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import java.nio.file.Path
object AnalysisApiFirStdlibSourceTestConfigurator : AnalysisApiFirSourceLikeTestConfigurator(false) {
override fun configureTest(builder: TestConfigurationBuilder, disposable: Disposable) {
@@ -30,6 +31,7 @@ private object KtStdlibSourceModuleFactory : KtModuleFactory {
contextModule: KtModuleWithFiles?,
testServices: TestServices,
project: Project,
dependencyPaths: Collection<Path>,
): KtModuleWithFiles {
val libraryJar = ForTestCompileRuntime.runtimeJarForTests().toPath()
val librarySourcesJar = ForTestCompileRuntime.runtimeSourcesJarForTests().toPath()
@@ -100,18 +100,22 @@ internal fun AnalysisApiTestGroup.generateAnalysisApiTests() {
component(
"compilerFacility",
filter = testModuleKindIs(TestModuleKind.Source, TestModuleKind.LibrarySource)
filter = testModuleKindIs(TestModuleKind.Source, TestModuleKind.LibrarySource, TestModuleKind.LibraryBinary)
and frontendIs(FrontendKind.Fir)
and analysisSessionModeIs(AnalysisSessionMode.Normal)
and analysisApiModeIs(AnalysisApiMode.Ide)
) {
test(AbstractCompilerFacilityTest::class) {
test(AbstractCompilerFacilityTest::class, filter = testModuleKindIs(TestModuleKind.Source, TestModuleKind.LibrarySource)) {
model("compilation", pattern = TestGeneratorUtil.KT)
}
test(AbstractMultiModuleCompilerFacilityTest::class, filter = testModuleKindIs(TestModuleKind.Source)) {
model("compilationMultiModule", pattern = TestGeneratorUtil.KT)
}
test(AbstractMultiModuleCompilerFacilityTest::class, filter = testModuleKindIs(TestModuleKind.LibraryBinary)) {
model("compilationMultiBinaryModule", pattern = TestGeneratorUtil.KT)
}
}
group(filter = testModuleKindIs(TestModuleKind.Source, TestModuleKind.ScriptSource)) {