jps: support multiplatform incremental compilation for jvm and js

- support common modules metadata compilation under flag (it is not required since all common source roots are included transitively for now)
- introduce expect actual tracker in jps: move implementation from gradle to build-common
- support js incremental compilation: move implementation from gradle to build-common

Original commit: 0eee2729cd
This commit is contained in:
Sergey Rostov
2018-04-03 13:00:06 +03:00
parent a64a2f694f
commit 5f818789cf
72 changed files with 5649 additions and 731 deletions
@@ -40,29 +40,34 @@ import org.jetbrains.jps.cmdline.ProjectDescriptor
import org.jetbrains.jps.incremental.*
import org.jetbrains.jps.incremental.messages.BuildMessage
import org.jetbrains.jps.model.JpsModuleRootModificationUtil
import org.jetbrains.jps.model.java.JpsJavaDependencyScope
import org.jetbrains.jps.model.java.JpsJavaExtensionService
import org.jetbrains.jps.util.JpsPathUtil
import org.jetbrains.kotlin.cli.common.arguments.K2MetadataCompilerArguments
import org.jetbrains.kotlin.config.IncrementalCompilation
import org.jetbrains.kotlin.incremental.CacheVersion
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.incremental.isJavaFile
import org.jetbrains.kotlin.incremental.testingUtils.*
import org.jetbrains.kotlin.jps.build.dependeciestxt.DependenciesTxt
import org.jetbrains.kotlin.jps.build.dependeciestxt.DependenciesTxtBuilder
import org.jetbrains.kotlin.jps.incremental.getKotlinCache
import org.jetbrains.kotlin.jps.incremental.withLookupStorage
import org.jetbrains.kotlin.jps.model.JpsKotlinFacetModuleExtension
import org.jetbrains.kotlin.jps.platforms.kotlinBuildTargets
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.utils.Printer
import org.jetbrains.kotlin.utils.keysToMap
import java.io.*
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.util.*
import java.util.concurrent.Future
import kotlin.reflect.jvm.javaField
abstract class AbstractIncrementalJpsTest(
private val allowNoFilesWithSuffixInTestData: Boolean = false,
private val checkDumpsCaseInsensitively: Boolean = false,
private val allowNoBuildLogFileInTestData: Boolean = false
private val allowNoFilesWithSuffixInTestData: Boolean = false,
private val checkDumpsCaseInsensitively: Boolean = false,
private val allowNoBuildLogFileInTestData: Boolean = false
) : BaseKotlinJpsBuildTestCase() {
companion object {
private val COMPILATION_FAILED = "COMPILATION FAILED"
@@ -79,6 +84,7 @@ abstract class AbstractIncrementalJpsTest(
// is used to compare lookup dumps in a human readable way (lookup symbols are hashed in an actual lookup storage)
protected lateinit var lookupsDuringTest: MutableSet<LookupSymbol>
private var isICEnabledBackup: Boolean = false
private var isJSICEnabledBackup: Boolean = false
protected var mapWorkingToOriginalFile: MutableMap<File, File> = hashMapOf()
@@ -115,7 +121,10 @@ abstract class AbstractIncrementalJpsTest(
super.setUp()
lookupsDuringTest = hashSetOf()
isICEnabledBackup = IncrementalCompilation.isEnabled()
isJSICEnabledBackup = IncrementalCompilation.isEnabledForJs()
IncrementalCompilation.setIsEnabled(true)
IncrementalCompilation.setIsEnabledForJs(true)
if (DEBUG_LOGGING_ENABLED) {
enableDebugLogging()
@@ -129,6 +138,7 @@ abstract class AbstractIncrementalJpsTest(
(AbstractIncrementalJpsTest::systemPropertiesBackup).javaField!![this] = null
lookupsDuringTest.clear()
IncrementalCompilation.setIsEnabled(isICEnabledBackup)
IncrementalCompilation.setIsEnabledForJs(isJSICEnabledBackup)
super.tearDown()
}
@@ -138,7 +148,7 @@ abstract class AbstractIncrementalJpsTest(
private val mockConstantSearch: Callbacks.ConstantAffectionResolver?
get() = MockJavaConstantSearch(workDir)
private fun build(scope: CompileScopeTestBuilder = CompileScopeTestBuilder.make().allModules()): MakeResult {
private fun build(scope: CompileScopeTestBuilder = CompileScopeTestBuilder.make().allModules(), name: String? = null): MakeResult {
val workDirPath = FileUtil.toSystemIndependentName(workDir.absolutePath)
val logger = MyLogger(workDirPath)
@@ -148,48 +158,67 @@ abstract class AbstractIncrementalJpsTest(
projectDescriptor.project.setTestingContext(TestingContext(lookupTracker, logger))
try {
val builder = IncProjectBuilder(projectDescriptor, BuilderRegistry.getInstance(), myBuildParams, CanceledStatus.NULL, mockConstantSearch, true)
val builder = IncProjectBuilder(
projectDescriptor,
BuilderRegistry.getInstance(),
myBuildParams,
CanceledStatus.NULL,
mockConstantSearch,
true
)
val buildResult = BuildResult()
builder.addMessageHandler(buildResult)
builder.build(scope.build(), false)
val finalScope = scope.build()
builder.build(finalScope, false)
lookupTracker.lookups.mapTo(lookupsDuringTest) { LookupSymbol(it.name, it.scopeFqName) }
// for getting kotlin platform only
val dummyCompileContext = CompileContextImpl.createContextForTests(finalScope, projectDescriptor)
if (!buildResult.isSuccessful) {
val errorMessages =
buildResult
.getMessages(BuildMessage.Kind.ERROR)
.map { it.messageText }
.map { it.replace("^.+:\\d+:\\s+".toRegex(), "").trim() }
.joinToString("\n")
return MakeResult(logger.log + "$COMPILATION_FAILED\n" + errorMessages + "\n", true, null)
buildResult
.getMessages(BuildMessage.Kind.ERROR)
.map { it.messageText }
.map { it.replace("^.+:\\d+:\\s+".toRegex(), "").trim() }
.joinToString("\n")
return MakeResult(
logger.log + "$COMPILATION_FAILED\n" + errorMessages + "\n",
true,
null,
name
)
} else {
return MakeResult(
logger.log,
false,
createMappingsDump(projectDescriptor, dummyCompileContext),
name
)
}
else {
return MakeResult(logger.log, false, createMappingsDump(projectDescriptor))
}
}
finally {
} finally {
projectDescriptor.dataManager.flush(false)
projectDescriptor.release()
}
}
private fun initialMake(): MakeResult {
val makeResult = build()
protected fun initialMake(): MakeResult {
val makeResult = build(name = "initial")
val initBuildLogFile = File(testDataDir, "init-build.log")
if (initBuildLogFile.exists()) {
UsefulTestCase.assertSameLinesWithFile(initBuildLogFile.absolutePath, makeResult.log)
}
else {
} else {
assertFalse("Initial make failed:\n$makeResult", makeResult.makeFailed)
}
return makeResult
}
private fun make(): MakeResult {
return build()
private fun make(name: String?): MakeResult {
return build(name = name)
}
private fun rebuild(): MakeResult {
@@ -205,21 +234,21 @@ abstract class AbstractIncrementalJpsTest(
}
val rebuildResult = rebuild()
assertEquals("Rebuild failed: ${rebuildResult.makeFailed}, last make failed: ${makeOverallResult.makeFailed}. Rebuild result: $rebuildResult",
rebuildResult.makeFailed, makeOverallResult.makeFailed)
assertEquals(
"Rebuild failed: ${rebuildResult.makeFailed}, last make failed: ${makeOverallResult.makeFailed}. Rebuild result: $rebuildResult",
rebuildResult.makeFailed, makeOverallResult.makeFailed
)
if (!outAfterMake.exists()) {
assertFalse(outDir.exists())
}
else {
assertEqualDirectories(outDir, outAfterMake, makeOverallResult.makeFailed)
} else {
assertEqualDirectories(outAfterMake, outDir, makeOverallResult.makeFailed)
}
if (!makeOverallResult.makeFailed) {
if (checkDumpsCaseInsensitively && rebuildResult.mappingsDump?.toLowerCase() == makeOverallResult.mappingsDump?.toLowerCase()) {
// do nothing
}
else {
} else {
TestCase.assertEquals(rebuildResult.mappingsDump, makeOverallResult.mappingsDump)
}
}
@@ -233,30 +262,27 @@ abstract class AbstractIncrementalJpsTest(
rebuildAndCheckOutput(makeOverallResult)
}
private fun readModuleDependencies(): Map<String, List<DependencyDescriptor>>? {
val dependenciesTxt = File(testDataDir, "dependencies.txt")
if (!dependenciesTxt.exists()) return null
open protected val dependenciesTxtFile get() = File(testDataDir, "dependencies.txt")
val result = HashMap<String, List<DependencyDescriptor>>()
for (line in dependenciesTxt.readLines()) {
val split = line.split("->")
val module = split[0]
val dependencies = if (split.size > 1) split[1] else ""
val dependencyList = dependencies.split(",").filterNot { it.isEmpty() }
result[module] = dependencyList.map(::parseDependency)
}
private fun readModuleDependencies(): DependenciesTxt? {
val dependenciesTxtFile = dependenciesTxtFile
if (!dependenciesTxtFile.exists()) return null
return result
return DependenciesTxtBuilder().readFile(dependenciesTxtFile)
}
protected open fun createBuildLog(incrementalMakeResults: List<AbstractIncrementalJpsTest.MakeResult>): String =
buildString {
incrementalMakeResults.forEachIndexed { i, makeResult ->
if (i > 0) append("\n")
buildString {
incrementalMakeResults.forEachIndexed { i, makeResult ->
if (i > 0) append("\n")
if (makeResult.name != null) {
append("================ Step #${i + 1} ${makeResult.name} =================\n\n")
} else {
append("================ Step #${i + 1} =================\n\n")
append(makeResult.log)
}
append(makeResult.log)
}
}
protected open fun doTest(testDataPath: String) {
testDataDir = File(testDataPath)
@@ -272,8 +298,7 @@ abstract class AbstractIncrementalJpsTest(
if (buildLogFile != null && buildLogFile.exists()) {
UsefulTestCase.assertSameLinesWithFile(buildLogFile.absolutePath, logs)
}
else if (!allowNoBuildLogFileInTestData) {
} else if (!allowNoBuildLogFileInTestData) {
throw IllegalStateException("No build log file in $testDataDir")
}
@@ -282,17 +307,38 @@ abstract class AbstractIncrementalJpsTest(
clearCachesRebuildAndCheckOutput(lastMakeResult)
}
private fun createMappingsDump(project: ProjectDescriptor) =
createKotlinIncrementalCacheDump(project) + "\n\n\n" +
createLookupCacheDump(project) + "\n\n\n" +
createCommonMappingsDump(project) + "\n\n\n" +
createJavaMappingsDump(project)
protected open fun doInitialMakeTest(testDataPath: String) {
testDataDir = File(testDataPath)
workDir = FileUtilRt.createTempDirectory(TEMP_DIRECTORY_TO_USE, "jps-build", null)
Disposer.register(testRootDisposable, Disposable { FileUtilRt.delete(workDir) })
private fun createKotlinIncrementalCacheDump(project: ProjectDescriptor): String {
configureModules()
val results = initialMake()
val buildLogFile = buildLogFinder.findBuildLog(testDataDir)
if (buildLogFile != null && buildLogFile.exists()) {
UsefulTestCase.assertSameLinesWithFile(buildLogFile.absolutePath, results.log)
} else throw IllegalStateException("No build log file in $testDataDir")
}
private fun createMappingsDump(
project: ProjectDescriptor,
dummyCompileContext: CompileContext
) =
createKotlinIncrementalCacheDump(project, dummyCompileContext) + "\n\n\n" +
createLookupCacheDump(project) + "\n\n\n" +
createCommonMappingsDump(project) + "\n\n\n" +
createJavaMappingsDump(project)
private fun createKotlinIncrementalCacheDump(
project: ProjectDescriptor,
dummyCompileContext: CompileContext
): String {
return buildString {
for (target in project.allModuleTargets.sortedBy { it.presentableName }) {
append("<target $target>\n")
append(project.dataManager.getKotlinCache(target).dump())
append(project.dataManager.getKotlinCache(dummyCompileContext.kotlinBuildTargets[target]!!).dump())
append("</target $target>\n\n\n")
}
}
@@ -350,23 +396,35 @@ abstract class AbstractIncrementalJpsTest(
return byteArrayOutputStream.toString()
}
protected data class MakeResult(val log: String, val makeFailed: Boolean, val mappingsDump: String?)
protected data class MakeResult(
val log: String,
val makeFailed: Boolean,
val mappingsDump: String?,
val name: String?
)
open val testDataSrc: File
get() = testDataDir
private fun performModificationsAndMake(moduleNames: Set<String>?): List<MakeResult> {
val results = arrayListOf<MakeResult>()
val modifications = getModificationsToPerform(testDataDir, moduleNames, allowNoFilesWithSuffixInTestData, TouchPolicy.TIMESTAMP)
val modifications = getModificationsToPerform(testDataSrc, moduleNames, allowNoFilesWithSuffixInTestData, TouchPolicy.TIMESTAMP)
for (step in modifications) {
val stepsTxt = File(testDataSrc, "steps.txt")
val modificationNames = if (stepsTxt.exists()) stepsTxt.readLines() else null
modifications.forEachIndexed { index, step ->
step.forEach { it.perform(workDir, mapWorkingToOriginalFile) }
performAdditionalModifications(step)
if (moduleNames == null) {
preProcessSources(File(workDir, "src"))
}
else {
} else {
moduleNames.forEach { preProcessSources(File(workDir, "$it/src")) }
}
results.add(make())
val name = modificationNames?.getOrNull(index)
val makeResult = make(name)
results.add(makeResult)
}
return results
}
@@ -374,54 +432,92 @@ abstract class AbstractIncrementalJpsTest(
protected open fun performAdditionalModifications(modifications: List<Modification>) {
}
// null means one module
private fun configureModules(): Set<String>? {
fun prepareModuleSources(moduleName: String?) {
val sourceDirName = moduleName?.let { "$it/src" } ?: "src"
val filePrefix = moduleName?.let { "${it}_" } ?: ""
val sourceDestinationDir = File(workDir, sourceDirName)
val sourcesMapping = copyTestSources(testDataDir, sourceDestinationDir, filePrefix)
mapWorkingToOriginalFile.putAll(sourcesMapping)
preProcessSources(sourceDestinationDir)
}
protected open fun generateModuleSources(dependenciesTxt: DependenciesTxt) {
JpsJavaExtensionService.getInstance().getOrCreateProjectExtension(myProject).outputUrl = JpsPathUtil.pathToUrl(getAbsolutePath("out"))
}
protected open fun prepareModuleSources(module: DependenciesTxt.Module? = null) {
if (module != null) {
prepareModuleSourcesByName("${module.name}/src", "${module.name}_")
} else {
prepareModuleSourcesByName("src", "")
}
}
protected fun prepareIndexedModuleSources(module: DependenciesTxt.Module) {
prepareModuleSourcesByName("${module.name}/src", "${module.indexedName}_")
}
private fun prepareModuleSourcesByName(sourceDirName: String, filePrefix: String) {
val sourceDestinationDir = File(workDir, sourceDirName)
val sourcesMapping = copyTestSources(testDataSrc, sourceDestinationDir, filePrefix)
mapWorkingToOriginalFile.putAll(sourcesMapping)
preProcessSources(sourceDestinationDir)
}
// null means one module
protected fun configureModules(): Set<String>? {
val outputUrl = JpsPathUtil.pathToUrl(getAbsolutePath("out"))
JpsJavaExtensionService.getInstance().getOrCreateProjectExtension(myProject).outputUrl = outputUrl
val jdk = addJdk("my jdk")
val moduleDependencies = readModuleDependencies()
val dependenciesTxt = readModuleDependencies()
mapWorkingToOriginalFile = hashMapOf()
val moduleNames: Set<String>?
if (moduleDependencies == null) {
if (dependenciesTxt == null) {
addModule("module", arrayOf(getAbsolutePath("src")), null, null, jdk)
prepareModuleSources(moduleName = null)
prepareModuleSources(module = null)
moduleNames = null
}
else {
val nameToModule = moduleDependencies.keys
.keysToMap { addModule(it, arrayOf(getAbsolutePath("$it/src")), null, null, jdk)!! }
} else {
dependenciesTxt.modules.forEach {
val module = addModule(
it.name,
arrayOf(getAbsolutePath("${it.name}/src")),
null,
null,
jdk
)!!
for ((moduleName, dependencies) in moduleDependencies) {
val module = nameToModule[moduleName]!!
val kotlinFacetSettings = it.kotlinFacetSettings
if (kotlinFacetSettings != null) {
val compilerArguments = kotlinFacetSettings.compilerArguments
if (compilerArguments is K2MetadataCompilerArguments) {
val out = getAbsolutePath("${it.name}/out")
File(out).mkdirs()
compilerArguments.destination = out
}
for (dependency in dependencies) {
JpsModuleRootModificationUtil.addDependency(module, nameToModule[dependency.name],
JpsJavaDependencyScope.COMPILE, dependency.exported)
module.container.setChild(
JpsKotlinFacetModuleExtension.KIND,
JpsKotlinFacetModuleExtension(kotlinFacetSettings)
)
}
it.jpsModule = module
}
for (module in nameToModule.values) {
prepareModuleSources(module.name)
dependenciesTxt.dependencies.forEach {
JpsModuleRootModificationUtil.addDependency(
it.from.jpsModule,
it.to.jpsModule,
it.scope,
it.exported
)
}
moduleNames = nameToModule.keys
generateModuleSources(dependenciesTxt)
dependenciesTxt.modules.forEach {
prepareModuleSources(it)
}
moduleNames = dependenciesTxt.modules.map { it.name }.toSet()
}
AbstractKotlinJpsBuildTestCase.addKotlinStdlibDependency(myProject)
AbstractKotlinJpsBuildTestCase.addKotlinTestDependency(myProject)
return moduleNames
}
protected open fun preProcessSources(srcDir: File) {
}
@@ -530,10 +626,3 @@ private class MockJavaConstantSearch(private val workDir: File) : Callbacks.Cons
internal val ProjectDescriptor.allModuleTargets: Collection<ModuleBuildTarget>
get() = buildTargetIndex.allTargets.filterIsInstance<ModuleBuildTarget>()
private class DependencyDescriptor(val name: String, val exported: Boolean)
private fun parseDependency(dependency: String): DependencyDescriptor =
DependencyDescriptor(dependency.removeSuffix(EXPORTED_SUFFIX), dependency.endsWith(EXPORTED_SUFFIX))
private val EXPORTED_SUFFIX = "[exported]"
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.jps.build
import org.jetbrains.kotlin.jps.build.dependeciestxt.DependenciesTxt
import org.jetbrains.kotlin.jps.build.dependeciestxt.MppJpsIncTestsGenerator
import java.io.File
abstract class AbstractMultiplatformJpsTest : AbstractIncrementalJpsTest() {
override val dependenciesTxtFile: File
get() = File(testDataDir.parent, "dependencies.txt").also {
check(it.exists()) {
"`dependencies.txt` should be in parent dir. " +
"See jps-plugin/testData/incremental/multiplatform/multiModule/README.md for details"
}
}
override val testDataSrc: File
get() = File(workDir, "generatedTestDataSources")
override fun generateModuleSources(dependenciesTxt: DependenciesTxt) {
testDataSrc.mkdirs()
val testCaseName = testDataDir.name
val generator = MppJpsIncTestsGenerator(dependenciesTxt) { testDataSrc }
val testCase = generator.testCases.find { it.name == testCaseName } ?: error("Unsupported test case name: $testCaseName")
testCase.generate()
}
override fun prepareModuleSources(module: DependenciesTxt.Module?) {
prepareIndexedModuleSources(module!!)
}
}
@@ -0,0 +1,358 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.jps.build;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class MultiplatformJpsTestGenerated extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInMultiplatformMultiModule() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simple")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Simple extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInSimple() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simple"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
@TestMetadata("editingCExpectActual")
public void testEditingCExpectActual() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingCExpectActual/");
}
@TestMetadata("editingCKotlin")
public void testEditingCKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingCKotlin/");
}
@TestMetadata("editingPJsKotlin")
public void testEditingPJsKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJsKotlin/");
}
@TestMetadata("editingPJvmJava")
public void testEditingPJvmJava() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJvmJava/");
}
@TestMetadata("editingPJvmKotlin")
public void testEditingPJvmKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJvmKotlin/");
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingCExpectActual")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingCExpectActual extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingCExpectActual() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingCExpectActual"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingCKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingCKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingCKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingCKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJsKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingPJsKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingPJsKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJsKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJvmJava")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingPJvmJava extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingPJvmJava() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJvmJava"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJvmKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingPJvmKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingPJvmKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simple/editingPJvmKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class SimpleJsJvmProjectWithTests extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInSimpleJsJvmProjectWithTests() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
@TestMetadata("editingCMainExpectActual")
public void testEditingCMainExpectActual() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests/editingCMainExpectActual/");
}
@TestMetadata("editingCTestsExpectActual")
public void testEditingCTestsExpectActual() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests/editingCTestsExpectActual/");
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests/editingCMainExpectActual")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingCMainExpectActual extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingCMainExpectActual() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests/editingCMainExpectActual"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests/editingCTestsExpectActual")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingCTestsExpectActual extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingCTestsExpectActual() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/simpleJsJvmProjectWithTests/editingCTestsExpectActual"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Ultimate extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInUltimate() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
@TestMetadata("editingACommonExpectActual")
public void testEditingACommonExpectActual() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingACommonExpectActual/");
}
@TestMetadata("editingAJsClientKotlin")
public void testEditingAJsClientKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJsClientKotlin/");
}
@TestMetadata("editingAJvmClientJava")
public void testEditingAJvmClientJava() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJvmClientJava/");
}
@TestMetadata("editingAJvmClientKotlin")
public void testEditingAJvmClientKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJvmClientKotlin/");
}
@TestMetadata("editingBCommonExpectActual")
public void testEditingBCommonExpectActual() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingBCommonExpectActual/");
}
@TestMetadata("editingRJsKotlin")
public void testEditingRJsKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRJsKotlin/");
}
@TestMetadata("editingRJvmKotlin")
public void testEditingRJvmKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRJvmKotlin/");
}
@TestMetadata("editingRaJsKotlin")
public void testEditingRaJsKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRaJsKotlin/");
}
@TestMetadata("editingRaJvmKotlin")
public void testEditingRaJvmKotlin() throws Exception {
runTest("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRaJvmKotlin/");
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingACommonExpectActual")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingACommonExpectActual extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingACommonExpectActual() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingACommonExpectActual"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJsClientKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingAJsClientKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingAJsClientKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJsClientKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJvmClientJava")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingAJvmClientJava extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingAJvmClientJava() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJvmClientJava"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJvmClientKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingAJvmClientKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingAJvmClientKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingAJvmClientKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingBCommonExpectActual")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingBCommonExpectActual extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingBCommonExpectActual() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingBCommonExpectActual"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRJsKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingRJsKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingRJsKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRJsKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRJvmKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingRJvmKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingRJvmKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRJvmKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRaJsKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingRaJsKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingRaJsKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRaJsKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
@TestMetadata("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRaJvmKotlin")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class EditingRaJvmKotlin extends AbstractMultiplatformJpsTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInEditingRaJvmKotlin() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("jps-plugin/testData/incremental/multiplatform/multiModule/ultimate/editingRaJvmKotlin"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, true);
}
}
}
}
@@ -0,0 +1,267 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.jps.build.dependeciestxt
import org.jetbrains.jps.model.java.JpsJavaDependencyScope
import org.jetbrains.jps.model.module.JpsModule
import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.K2MetadataCompilerArguments
import org.jetbrains.kotlin.config.CompilerSettings
import org.jetbrains.kotlin.config.KotlinFacetSettings
import org.jetbrains.kotlin.config.TargetPlatformKind
import java.io.File
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
/**
* Dependencies description file.
* See [README.md] for more details.
*/
data class DependenciesTxt(
val file: File,
val fileName: String,
val modules: List<Module>,
val dependencies: List<Dependency>
) {
override fun toString() = fileName
data class Module(val name: String) {
var index: Int = -1
val indexedName
get() = "${index / 10}${index % 10}_$name"
/**
* Facet should not be created for old tests
*/
var kotlinFacetSettings: KotlinFacetSettings? = null
lateinit var jpsModule: JpsModule
val dependencies = mutableListOf<Dependency>()
val usages = mutableListOf<Dependency>()
val isCommonModule
get() = kotlinFacetSettings?.targetPlatformKind == TargetPlatformKind.Common
val isJvmModule
get() = kotlinFacetSettings?.targetPlatformKind is TargetPlatformKind.Jvm
val expectedBy
get() = dependencies.filter { it.expectedBy }
@Flag
var edit: Boolean = false
@Flag
var editJvm: Boolean = false
@Flag
var editExpectActual: Boolean = false
companion object {
val flags: Map<String, KMutableProperty1<Module, Boolean>> = Module::class.memberProperties
.filter { it.findAnnotation<Flag>() != null }
.filterIsInstance<KMutableProperty1<Module, Boolean>>()
.associateBy { it.name }
}
}
annotation class Flag
data class Dependency(
val from: Module,
val to: Module,
val scope: JpsJavaDependencyScope,
val expectedBy: Boolean,
val exported: Boolean
) {
val effectivelyExported
get() = expectedBy || exported
init {
from.dependencies.add(this)
to.usages.add(this)
}
}
}
class DependenciesTxtBuilder {
val modules = mutableMapOf<String, ModuleRef>()
private val dependencies = mutableListOf<DependencyBuilder>()
/**
* Reference to module which can be defined later
*/
class ModuleRef(name: String) {
var defined: Boolean = false
var actual: DependenciesTxt.Module = DependenciesTxt.Module(name)
override fun toString() = actual.name
fun build(index: Int): DependenciesTxt.Module {
val result = actual
result.index = index
val kotlinFacetSettings = result.kotlinFacetSettings
if (kotlinFacetSettings != null) {
kotlinFacetSettings.implementedModuleNames =
result.dependencies.filter { it.expectedBy }.map { it.to.name }
}
return result
}
}
/**
* Temporary object for resolving references to modules.
*/
data class DependencyBuilder(
val from: ModuleRef,
val to: ModuleRef,
val scope: JpsJavaDependencyScope,
val expectedBy: Boolean,
val exported: Boolean
) {
fun build(): DependenciesTxt.Dependency {
if (expectedBy) check(to.actual.isCommonModule) { "$this: ${to.actual} is not common module" }
return DependenciesTxt.Dependency(from.actual, to.actual, scope, expectedBy, exported)
}
}
fun readFile(file: File, fileTitle: String = file.toString()): DependenciesTxt {
file.forEachLine { line ->
parseDeclaration(line)
}
// module.build() requires built dependencies
val dependencies = dependencies.map { it.build() }
return DependenciesTxt(
file,
fileTitle,
modules.values.mapIndexed { index, moduleRef -> moduleRef.build(index) },
dependencies
)
}
private fun parseDeclaration(line: String) = doParseDeclaration(removeComments(line))
private fun removeComments(line: String) = line.split("//", limit = 2)[0].trim()
private fun doParseDeclaration(line: String) {
when {
line.isEmpty() -> Unit // skip empty lines
line.contains("->") -> {
val (from, rest) = line.split("->", limit = 2)
if (rest.isBlank()) {
// `name -> ` - module
newModule(ValueWithFlags(from))
} else {
val (to, flags) = parseValueWithFlags(rest.trim())
newDependency(from.trim(), to.trim(), flags) // `from -> to [flag1, flag2, ...]` - dependency
}
}
else -> newModule(parseValueWithFlags(line)) // `name [flag1, flag2, ...]` - module
}
}
/**
* `value [flag1, flag2, ...]`
*/
private fun parseValueWithFlags(str: String): ValueWithFlags {
val parts = str.split("[", limit = 2)
return if (parts.size > 1) {
val (value, flags) = parts
ValueWithFlags(
value = value.trim(),
flags = flags.trim()
.removeSuffix("]")
.split(",")
.map { it.trim() }
.filter { it.isNotEmpty() }
.toSet()
)
} else ValueWithFlags(str)
}
data class ValueWithFlags(val value: String, val flags: Set<String> = setOf())
private fun moduleRef(name: String) =
modules.getOrPut(name) { ModuleRef(name) }
private fun newModule(def: ValueWithFlags): DependenciesTxt.Module {
val name = def.value.trim()
val module = DependenciesTxt.Module(name)
val kotlinFacetSettings = KotlinFacetSettings()
module.kotlinFacetSettings = kotlinFacetSettings
kotlinFacetSettings.useProjectSettings = false
kotlinFacetSettings.compilerSettings = CompilerSettings().also {
it.additionalArguments = "-version -Xmulti-platform"
}
val moduleRef = moduleRef(name)
check(!moduleRef.defined) { "Module `$name` already defined" }
moduleRef.defined = true
moduleRef.actual = module
def.flags.forEach { flag ->
when (flag) {
"common" -> kotlinFacetSettings.compilerArguments = K2MetadataCompilerArguments()
"jvm" -> kotlinFacetSettings.compilerArguments = K2JVMCompilerArguments()
"js" -> kotlinFacetSettings.compilerArguments = K2JSCompilerArguments()
else -> {
val flagProperty = DependenciesTxt.Module.flags[flag]
if (flagProperty != null) flagProperty.set(module, true)
else error("Unknown module flag `$flag`")
}
}
}
return module
}
private fun newDependency(from: String, to: String, flags: Set<String>): DependencyBuilder? {
if (to.isEmpty()) {
// `x -> ` should just create undefined module `x`
moduleRef(from)
check(flags.isEmpty()) {
"`name -> [flag1, flag2, ...]` - not allowed due to the ambiguity of belonging to modules/dependencies. " +
"Please use `x [attrs...]` syntax for module attributes."
}
return null
} else {
var exported = false
var scope = JpsJavaDependencyScope.COMPILE
var expectedBy = false
flags.forEach { flag ->
when (flag) {
"exported" -> exported = true
"compile" -> scope = JpsJavaDependencyScope.COMPILE
"test" -> scope = JpsJavaDependencyScope.TEST
"runtime" -> scope = JpsJavaDependencyScope.RUNTIME
"provided" -> scope = JpsJavaDependencyScope.PROVIDED
"expectedBy" -> expectedBy = true
else -> error("Unknown dependency flag `$flag`")
}
}
return DependencyBuilder(
from = moduleRef(from),
to = moduleRef(to),
scope = scope,
expectedBy = expectedBy,
exported = exported
).also {
dependencies.add(it)
}
}
}
}
@@ -0,0 +1,432 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.jps.build.dependeciestxt
import java.io.File
/**
* Utility for generating common/platform module stub contents based on it's dependencies.
*/
fun actualizeMppJpsIncTestCaseDirs(rootDir: String, dir: String) {
File("$rootDir/$dir").listFiles { it: File -> it.isDirectory }.forEach { dirFile ->
val dependenciesTxtFile = File(dirFile, "dependencies.txt")
if (dependenciesTxtFile.exists()) {
val fileTitle = "$dir/${dirFile.name}/dependencies.txt"
val dependenciesTxt = DependenciesTxtBuilder().readFile(dependenciesTxtFile, fileTitle)
MppJpsIncTestsGenerator(dependenciesTxt) { File(dirFile, it.name) }
.actualizeTestCasesDirs(dirFile)
}
}
return
}
class MppJpsIncTestsGenerator(val txt: DependenciesTxt, val testCaseDirProvider: (TestCase) -> File) {
val DependenciesTxt.Module.capitalName get() = name.capitalize()
val testCases: List<TestCase>
init {
val testCases = mutableListOf<TestCase>()
txt.modules.forEach {
if (it.edit)
testCases.add(EditingTestCase(it, changeJavaClass = false))
if (it.editJvm && it.isJvmModule)
testCases.add(EditingTestCase(it, changeJavaClass = true))
if (it.editExpectActual && it.isCommonModule)
testCases.add(EditingExpectActualTestCase(it))
}
this.testCases = testCases
}
fun actualizeTestCasesDirs(rootDir: File) {
val requiredDirs = mutableSetOf<File>()
testCases.forEach {
val dir = it.dir
check(requiredDirs.add(dir)) { "TestCase dir clash $dir" }
if (!dir.exists()) {
File(dir, "build.log").setFileContent("")
}
}
rootDir.listFiles().forEach {
if (it.isDirectory && it !in requiredDirs) {
it.deleteRecursively()
}
}
}
/**
* Set required content for [this] [File].
*/
fun File.setFileContent(content: String) {
check(!exists()) {
"File `$this` already exists," +
"\n\n============= contents ============\n" +
readText() +
"\n===================================\n" +
"\n============ new content ==========\n" +
content +
"\n===================================\n"
}
parentFile.mkdirs()
writeText(content)
}
data class ModuleContentSettings(
val module: DependenciesTxt.Module,
val serviceNameSuffix: String = "",
val generateActualDeclarationsFor: List<DependenciesTxt.Module> = module.expectedBy.map { it.to },
val generatePlatformDependent: Boolean = true,
var generateKtFile: Boolean = true,
var generateJavaFile: Boolean = true
)
inner class EditingTestCase(val module: DependenciesTxt.Module, val changeJavaClass: Boolean) : TestCase() {
override val name: String =
if (changeJavaClass) "editing${module.capitalName}Java"
else "editing${module.capitalName}Kotlin"
override val dir: File = testCaseDirProvider(this)
override fun generate() {
generateBaseContent()
// create new file with service implementation
// don't create expect/actual functions (generatePlatformDependent = false)
module.contentsSettings = ModuleContentSettings(
module,
serviceNameSuffix = "New",
generatePlatformDependent = false,
generateKtFile = !changeJavaClass,
generateJavaFile = changeJavaClass
)
when {
module.isCommonModule -> {
step("create new service") {
generateCommonFile(
module,
fileNameSuffix = ".new.$step"
)
}
step("edit new service") {
generateCommonFile(
module,
fileNameSuffix = ".touch.$step"
)
}
step("delete new service") {
serviceKtFile(
module,
fileNameSuffix = ".delete.$step"
).setFileContent("")
}
}
else -> {
step("create new service") {
// generateKtFile event if changeJavaClass requested (for test calling java from kotlin)
val prevModuleContentsSettings = module.contentsSettings
module.contentsSettings = module.contentsSettings.copy(generateKtFile = true)
generatePlatformFile(
module,
fileNameSuffix = ".new.$step"
)
module.contentsSettings = prevModuleContentsSettings
}
step("edit new service") {
generatePlatformFile(
module,
fileNameSuffix = ".touch.$step"
)
}
step("delete new service") {
if (changeJavaClass) serviceJavaFile(module, fileNameSuffix = ".delete.$step").setFileContent("")
// kotlin file also created for testing java class
serviceKtFile(module, fileNameSuffix = ".delete.$step").setFileContent("")
}
}
}
generateStepsTxt()
}
}
inner class EditingExpectActualTestCase(val commonModule: DependenciesTxt.Module) : TestCase() {
override val name: String = "editing${commonModule.capitalName}ExpectActual"
override val dir: File = testCaseDirProvider(this)
override fun generate() {
generateBaseContent()
check(commonModule.isCommonModule)
val implModules = commonModule.usages.filter { it.expectedBy }.map { it.from }
commonModule.contentsSettings = ModuleContentSettings(commonModule, serviceNameSuffix = "New")
implModules.forEach { implModule ->
implModule.contentsSettings = ModuleContentSettings(
implModule,
serviceNameSuffix = "New",
generateActualDeclarationsFor = listOf(commonModule)
)
}
step("create new service in ${commonModule.name}") {
generateCommonFile(commonModule, fileNameSuffix = ".new.$step")
}
implModules.forEach { implModule ->
step("create new service in ${implModule.name}") {
generatePlatformFile(implModule, fileNameSuffix = ".new.$step")
}
}
step("change new service in ${commonModule.name}") {
generateCommonFile(commonModule, fileNameSuffix = ".touch.$step")
}
implModules.forEach { implModule ->
if (implModule.isJvmModule) {
implModule.contentsSettings.generateKtFile = false
implModule.contentsSettings.generateJavaFile = true
step("change new service in ${implModule.name}: java") {
generatePlatformFile(implModule, fileNameSuffix = ".touch.$step")
}
implModule.contentsSettings.generateKtFile = true
implModule.contentsSettings.generateJavaFile = false
step("change new service in ${implModule.name}: kotlin") {
generatePlatformFile(implModule, fileNameSuffix = ".touch.$step")
}
} else {
step("change new service in ${implModule.name}") {
generatePlatformFile(implModule, fileNameSuffix = ".touch.$step")
}
}
}
implModules.forEach { implModule ->
step("delete new service in ${implModule.name}") {
serviceKtFile(implModule, fileNameSuffix = ".delete.$step").setFileContent("")
}
}
step("delete new service in ${commonModule.name}") {
serviceKtFile(commonModule, fileNameSuffix = ".delete.$step").setFileContent("")
}
generateStepsTxt()
}
}
abstract inner class TestCase() {
abstract val name: String
abstract fun generate()
abstract val dir: File
private val modules = mutableMapOf<DependenciesTxt.Module, ModuleContentSettings>()
var step = 1
val steps = mutableListOf<String>()
protected inline fun step(name: String, body: () -> Unit) {
body()
steps.add(name)
step++
}
var DependenciesTxt.Module.contentsSettings: ModuleContentSettings
get() = modules.getOrPut(this) { ModuleContentSettings(this) }
set(value) {
modules[this] = value
}
protected fun generateStepsTxt() {
File(dir, "steps.txt").setFileContent(steps.joinToString("\n"))
}
fun generateBaseContent() {
dir.mkdir()
txt.modules.forEach {
generateModuleContents(it)
}
}
private fun generateModuleContents(module: DependenciesTxt.Module) {
when {
module.isCommonModule -> {
// common module
generateCommonFile(module)
}
module.expectedBy.isEmpty() -> {
// regular module
generatePlatformFile(module)
}
else -> {
// common module platform implementation
generatePlatformFile(module)
}
}
}
private val DependenciesTxt.Module.serviceName
get() = "$capitalName${contentsSettings.serviceNameSuffix}"
private val DependenciesTxt.Module.javaClassName
get() = "${serviceName}JavaClass"
protected fun serviceKtFile(module: DependenciesTxt.Module, fileNameSuffix: String = ""): File {
val suffix =
if (module.isCommonModule) "${module.serviceName}Header"
else "${module.name.capitalize()}${module.contentsSettings.serviceNameSuffix}Impl"
return File(dir, "${module.indexedName}_service$suffix.kt$fileNameSuffix")
}
fun serviceJavaFile(module: DependenciesTxt.Module, fileNameSuffix: String = ""): File {
return File(dir, "${module.indexedName}_${module.javaClassName}.java$fileNameSuffix")
}
private val DependenciesTxt.Module.platformDependentFunName: String
get() {
check(isCommonModule)
return "${name}_platformDependent$serviceName"
}
private val DependenciesTxt.Module.platformIndependentFunName: String
get() {
check(isCommonModule)
return "${name}_platformIndependent$serviceName"
}
private val DependenciesTxt.Module.platformOnlyFunName: String
get() {
// platformOnly fun names already unique, so no module name prefix required
return "${name}_platformOnly${contentsSettings.serviceNameSuffix}"
}
protected fun generateCommonFile(
module: DependenciesTxt.Module,
fileNameSuffix: String = ""
) {
val settings = module.contentsSettings
serviceKtFile(module, fileNameSuffix).setFileContent(buildString {
if (settings.generatePlatformDependent)
appendln("expect fun ${module.platformDependentFunName}(): String")
appendln("fun ${module.platformIndependentFunName}() = \"common$fileNameSuffix\"")
appendTestFun(module, settings)
})
}
protected fun generatePlatformFile(
module: DependenciesTxt.Module,
fileNameSuffix: String = ""
) {
val isJvm = module.isJvmModule
val settings = module.contentsSettings
val javaClassName = module.javaClassName
if (settings.generateKtFile) {
serviceKtFile(module, fileNameSuffix).setFileContent(buildString {
if (settings.generatePlatformDependent) {
for (expectedBy in settings.generateActualDeclarationsFor) {
appendln(
"actual fun ${expectedBy.platformDependentFunName}(): String" +
" = \"${module.name}$fileNameSuffix\""
)
}
}
appendln(
"fun ${module.platformOnlyFunName}()" +
" = \"${module.name}$fileNameSuffix\""
)
appendTestFun(module, settings)
})
}
if (isJvm && settings.generateJavaFile) {
serviceJavaFile(module, fileNameSuffix).setFileContent(
"""
|public class $javaClassName {
| public String doStuff() {
| return "${module.name}$fileNameSuffix";
| }
|}
""".trimMargin()
)
}
}
// call all functions declared in this module and all of its dependencies recursively
private fun StringBuilder.appendTestFun(
module: DependenciesTxt.Module,
settings: ModuleContentSettings
) {
appendln()
appendln("fun Test${module.serviceName}() {")
val thisAndDependencies = mutableSetOf(module)
module.collectDependenciesRecursivelyTo(thisAndDependencies)
thisAndDependencies.forEach { thisOrDependent ->
if (thisOrDependent.isCommonModule) {
appendln(" ${thisOrDependent.platformIndependentFunName}()")
if (settings.generatePlatformDependent) {
appendln(" ${thisOrDependent.platformDependentFunName}()")
}
} else {
// platform module
appendln(" ${thisOrDependent.platformOnlyFunName}()")
if (thisOrDependent.isJvmModule && thisOrDependent.contentsSettings.generateJavaFile) {
appendln(" ${thisOrDependent.javaClassName}().doStuff()")
}
}
}
appendln("}")
}
private fun DependenciesTxt.Module.collectDependenciesRecursivelyTo(
collection: MutableCollection<DependenciesTxt.Module>,
exportedOnly: Boolean = false
) {
dependencies.forEach {
if (!exportedOnly || it.effectivelyExported) {
val dependentModule = it.to
collection.add(dependentModule)
dependentModule.collectDependenciesRecursivelyTo(collection, exportedOnly = true)
}
}
}
override fun toString() = name
}
}
@@ -0,0 +1,36 @@
Useful micro-language for concise description of project structure.
Mostly like the [DOT language](https://www.graphviz.org/doc/info/attrs.html).
## Example
```
c [common]
p1 [jvm]
p2 [jvm]
p1 -> c [expectedBy]
p2 -> c [expectedBy]
```
## Format
File contains declarations of modules and dependencies:
- Module: `module_name [flag1, key1=value1, ...]`
- Dependency: `source_module_name -> target_module_name [flag1, key1=value1, ...]`
Referring to undefined module is allowed (`jvm` module will be created at this case).
This modules can be defined after reference. Several declarations for same module is not allowed.
Supported module flags:
- `common`
- `jvm` (default)
- `js`
- `edit`, `editJvm`, `editExcpetActual` - see jps-plugin/testData/incremental/multiplatform/multiModule/README.md
Supported dependency flags:
- `compile` (default)
- `test`
- `runtime`
- `provided`
- `expectedBy`
- `exproted`