diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/AbstractCompileWithDependenciesTracking.kt b/libraries/tools/kotlin-gradle-plugin-api/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/AbstractCompileWithDependenciesTracking.kt new file mode 100644 index 00000000000..c511f59845b --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-api/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/AbstractCompileWithDependenciesTracking.kt @@ -0,0 +1,9 @@ +package org.jetbrains.kotlin.gradle.plugin + +import org.gradle.api.Task +import org.gradle.api.tasks.compile.AbstractCompile + +abstract class AbstractCompileWithDependenciesTracking : AbstractCompile() { + open fun isDependentTaskOutOfDate(task: Task): Boolean = false +} + diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt index 2d62fc792a9..ef432f149dc 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt @@ -83,7 +83,7 @@ abstract class BaseGradleIT { open inner class Project(val projectName: String, val wrapperVersion: String = "1.4", val minLogLevel: LogLevel = LogLevel.DEBUG) { open val resourcesRoot = File(resourcesRootFile, "testProject/$projectName") - val projectDir = File(workingDir, projectName) + val projectDir = File(workingDir.canonicalFile, projectName) open fun setupWorkingDir() { copyRecursively(this.resourcesRoot, workingDir) @@ -91,7 +91,14 @@ abstract class BaseGradleIT { } } - class CompiledProject(val project: Project, val output: String, val resultCode: Int) + class CompiledProject(val project: Project, val output: String, val resultCode: Int) { + companion object { + val kotlinSourcesListRegex = Regex("\\[KOTLIN\\] compile iteration: ([^\\r\\n]*)") + val javaSourcesListRegex = Regex("\\[DEBUG\\] \\[[^\\]]*JavaCompiler\\] Compiler arguments: ([^\\r\\n]*)") + } + val compiledKotlinSources : Iterable by lazy { kotlinSourcesListRegex.findAll(output).asIterable().flatMap { it.groups[1]!!.value.split(", ").map { File(project.projectDir, it).canonicalFile } } } + val compiledJavaSources : Iterable by lazy { javaSourcesListRegex.findAll(output).asIterable().flatMap { it.groups[1]!!.value.split(" ").filter { it.endsWith(".java", ignoreCase = true) }.map { File(it).canonicalFile } } } + } fun Project.build(vararg tasks: String, options: BuildOptions = defaultBuildOptions(), check: CompiledProject.() -> Unit) { val cmd = createBuildCommand(tasks, options) @@ -173,6 +180,35 @@ abstract class BaseGradleIT { return this } + private fun Iterable.projectRelativePaths(project: Project): Iterable { +// val projectDir = File(workingDir.canonicalFile, project.projectName) + return map { it.canonicalFile.toRelativeString(project.projectDir) } + } + + fun CompiledProject.assertSameFiles(expected: Iterable, actual: Iterable, messagePrefix: String = ""): CompiledProject { + val expectedSet = expected.toSortedSet() + val actualSet = actual.toSortedSet() + assertTrue(actualSet == expectedSet, messagePrefix + "expected files: ${expectedSet.joinToString()}\n actual files: ${actualSet.joinToString()}") + return this + } + + fun CompiledProject.assertContainFiles(expected: Iterable, actual: Iterable, messagePrefix: String = ""): CompiledProject { + val expectedSet = expected.toSortedSet() + assertTrue(expectedSet.containsAll(actual.toList()), messagePrefix + "expected files: ${expectedSet.joinToString()}\n actual files: ${actual.toSortedSet().joinToString()}") + return this + } + + fun CompiledProject.assertCompiledKotlinSources(sources: Iterable): CompiledProject = assertSameFiles(sources, compiledKotlinSources.projectRelativePaths(this.project), "Compiled Kotlin files differ:\n ") + fun CompiledProject.assertCompiledKotlinSources(vararg sources: String): CompiledProject = assertCompiledKotlinSources(sources.asIterable()) + + fun CompiledProject.assertCompiledKotlinSourcesContain(vararg sources: String): CompiledProject = assertContainFiles(sources.asIterable(), compiledKotlinSources.projectRelativePaths(this.project), "Compiled Kotlin files differ:\n ") + + fun CompiledProject.assertCompiledJavaSources(sources: Iterable): CompiledProject = assertSameFiles(sources, compiledJavaSources.projectRelativePaths(this.project), "Compiled Java files differ:\n ") + fun CompiledProject.assertCompiledJavaSources(vararg sources: String): CompiledProject = assertCompiledJavaSources(sources.asIterable()) + + fun CompiledProject.assertCompiledJavaSourcesContain(vararg sources: String): CompiledProject = assertContainFiles(sources.asIterable(), compiledJavaSources.projectRelativePaths(this.project), "Compiled Java files differ:\n ") + + private fun Project.createBuildCommand(params: Array, options: BuildOptions): List = createGradleCommand(createGradleTailParameters(options, params)) diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseIncrementalGradleIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseIncrementalGradleIT.kt new file mode 100644 index 00000000000..e745bfa178a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseIncrementalGradleIT.kt @@ -0,0 +1,216 @@ +package org.jetbrains.kotlin.gradle + +import com.google.common.io.Files +import org.gradle.api.logging.LogLevel +import org.junit.Assume +import java.io.File +import java.util.* +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + + +abstract class BaseIncrementalGradleIT : BaseGradleIT() { + + open inner class IncrementalTestProject(name: String, wrapperVersion: String = "2.4", minLogLevel: LogLevel = LogLevel.DEBUG) : Project(name, wrapperVersion, minLogLevel) { + var modificationStage: Int = 1 + } + + inner class JpsTestProject(val resourcesBase: File, val relPath: String, wrapperVersion: String = "1.6", minLogLevel: LogLevel = LogLevel.DEBUG) : IncrementalTestProject(File(relPath).name, wrapperVersion, minLogLevel) { + override val resourcesRoot = File(resourcesBase, relPath) + + override fun setupWorkingDir() { + val srcDir = File(projectDir, "src") + srcDir.mkdirs() + resourcesRoot.walk() + .filter { it.isFile && (it.name.endsWith(".kt") || it.name.endsWith(".java")) } + .forEach { Files.copy(it, File(srcDir, it.name)) } + copyDirRecursively(File(resourcesRootFile, "GradleWrapper-$wrapperVersion"), projectDir) + File(projectDir, "build.gradle").writeText(""" +buildscript { + repositories { + maven { + url 'file://' + pathToKotlinPlugin + } + } + dependencies { + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.1-SNAPSHOT' + } +} + +apply plugin: "kotlin" + +sourceSets { + main { + kotlin { + srcDir 'src' + } + java { + srcDir 'src' + } + } +} + +repositories { + maven { + url 'file://' + pathToKotlinPlugin + } +} + """) + } + } + + fun IncrementalTestProject.modify(runStage: Int? = null) { + // TODO: multimodule support + val projectSrcDir = File(File(workingDir, projectName), "src") + assertTrue(projectSrcDir.exists()) + val actualStage = runStage ?: modificationStage + + println("<--- Modify stage: ${runStage?.toString() ?: "single"}") + + fun resource2project(f: File) = File(projectSrcDir, f.toRelativeString(resourcesRoot)) + + resourcesRoot.walk().filter { it.isFile }.forEach { + val nameParts = it.name.split(".") + if (nameParts.size > 2) { + val (fileStage, hasStage) = nameParts.last().toIntOr(0) + if (!hasStage || fileStage == actualStage) { + val orig = File(resource2project(it.parentFile), nameParts.dropLast(if (hasStage) 2 else 1).joinToString(".")) + when (if (hasStage) nameParts[nameParts.size - 2] else nameParts.last()) { + "touch" -> { + assert(orig.exists()) + orig.setLastModified(Date().time) + println("<--- Modify: touch $orig") + } + "new" -> { + it.copyTo(orig, overwrite = true) + orig.setLastModified(Date().time) + println("<--- Modify: new $orig from $it") + } + "delete" -> { + assert(orig.exists()) + orig.delete() + println("<--- Modify: delete $orig") + } + } + } + } + } + + modificationStage = actualStage + 1 + } + + class StageResults(val stage: Int, val compiledKotlinFiles: HashSet = hashSetOf(), val compiledJavaFiles: HashSet = hashSetOf(), var compileSucceeded: Boolean = true) + + fun parseTestBuildLog(file: File): List { + class StagedLines(val stage: Int, val line: String) + + return file.readLines() + .map { if (it.startsWith("========== Step")) "" else it } + .fold(arrayListOf()) { slines, line -> + val (curStage, prevWasBlank) = slines.lastOrNull()?.let{ Pair(it.stage, it.line.isBlank()) } ?: Pair(0, false) + slines.add(StagedLines(curStage + if (line.isBlank() && prevWasBlank) 1 else 0, line)) + slines + } + .fold(arrayListOf()) { res, sline -> + // for lazy creation of the node + fun curStageResults(): StageResults { + if (res.isEmpty() || sline.stage > res.last().stage) { + res.add(StageResults(sline.stage)) + } + return res.last() + } + + when { + sline.line.endsWith(".java", ignoreCase = true) -> curStageResults().compiledJavaFiles.add(sline.line) + sline.line.endsWith(".kt", ignoreCase = true) -> curStageResults().compiledKotlinFiles.add(sline.line) + sline.line.equals("COMPILATION FAILED", ignoreCase = true) -> curStageResults().compileSucceeded = false + } + res + } + } + + + fun IncrementalTestProject.performAndAssertBuildStages(options: BuildOptions = defaultBuildOptions()) { + + val checkKnown = testIsKnownJpsTestProject(resourcesRoot) + Assume.assumeTrue(checkKnown.second ?: "", checkKnown.first) + + build("build", options = options) { + assertSuccessful() + assertReportExists() + } + + val buildLogFile = resourcesRoot.listFiles { f: File -> f.name.endsWith("build.log") }?.sortedBy { it.length() }?.firstOrNull() + assertNotNull(buildLogFile, "*build.log file not found" ) + + val buildLog = parseTestBuildLog(buildLogFile!!) + assertTrue(buildLog.any()) + + println("<--- Build log size: ${buildLog.size}") + buildLog.forEach { + println("<--- Build log stage: ${if (it.compileSucceeded) "succeeded" else "failed"}: kotlin: ${it.compiledKotlinFiles} java: ${it.compiledJavaFiles}") + } + + if (buildLog.size == 1) { + modify() + buildAndAssertStageResults(buildLog.first()) + } + else { + buildLog.forEachIndexed { stage, stageResults -> + modify(stage + 1) + buildAndAssertStageResults(stageResults) + } + } + } + + fun IncrementalTestProject.buildAndAssertStageResults(expected: StageResults, options: BuildOptions = defaultBuildOptions()) { + build("build", options = options) { + if (expected.compileSucceeded) { + assertSuccessful() + assertCompiledJavaSources(expected.compiledJavaFiles) + assertCompiledKotlinSources(expected.compiledKotlinFiles) + } + else { + assertFailed() + } + } + } +} + + +private val knownExtensions = arrayListOf("kt", "java") +private val knownModifyExtensions = arrayListOf("new", "touch", "delete") + +private fun String.toIntOr(defaultVal: Int): Pair { + try { + return Pair(toInt(), true) + } + catch (e: NumberFormatException) { + return Pair(defaultVal, false) + } +} + +fun isJpsTestProject(projectRoot: File): Boolean = projectRoot.listFiles { f: File -> f.name.endsWith("build.log") }?.any() ?: false + +fun testIsKnownJpsTestProject(projectRoot: File): Pair { + var hasKnownSources = false + projectRoot.walk().filter { it.isFile }.forEach { + if (it.name.equals("dependencies.txt", ignoreCase = true)) + return@testIsKnownJpsTestProject Pair(false, "multimodule tests are not supported yet") + val nameParts = it.name.split(".") + if (nameParts.size > 1) { + val (fileStage, hasStage) = nameParts.last().toIntOr(0) + val modifyExt = nameParts[nameParts.size - (if (hasStage) 2 else 1)] + val ext = nameParts[nameParts.size - (if (hasStage) 3 else 2)] + if (modifyExt in knownModifyExtensions && ext !in knownExtensions) + return@testIsKnownJpsTestProject Pair(false, "unknown staged file ${it.name}") + } + if (!hasKnownSources && it.extension in knownExtensions) { + hasKnownSources = true + } + } + return if (hasKnownSources) Pair(true, null) + else Pair(false, "no known sources found") +} + + diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIncrementalFromJpsIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIncrementalFromJpsIT.kt new file mode 100644 index 00000000000..60091621a37 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIncrementalFromJpsIT.kt @@ -0,0 +1,98 @@ +package org.jetbrains.kotlin.gradle + +import org.junit.Test +import java.io.File + +class KotlinGradleIncrementalFromJpsIT(): BaseIncrementalGradleIT() { + + companion object { + val jpsResourcesPath = File("../../../jps-plugin/testData/incremental") + } + + override fun defaultBuildOptions(): BuildOptions = BuildOptions(withDaemon = true) + + @Test + fun testClassSignatureChanged() { + val project = JpsTestProject(jpsResourcesPath, "pureKotlin/classSignatureChanged", "2.4") + + project.build("build") { + assertSuccessful() + assertReportExists() + assertCompiledKotlinSources("src/class.kt", "src/usage.kt") + } + + project.modify() + + project.build("build") { + assertSuccessful() + assertCompiledKotlinSources("src/class.kt", "src/usage.kt") + } + } + + @Test + fun testKotlinInJavaFunRenamed() { + val project = JpsTestProject(jpsResourcesPath, "withJava/kotlinUsedInJava/funRenamed", "2.4") + + project.build("build") { + assertSuccessful() + assertReportExists() + assertCompiledKotlinSources("src/fun.kt") + assertCompiledJavaSources("src/WillBeUnresolved.java") + } + + project.modify() + + project.build("build") { + assertCompiledKotlinSources("src/fun.kt") + assertFailed() + } + } + + @Test + fun testJavaInKotlinChangeSignature() { + val project = JpsTestProject(jpsResourcesPath, "withJava/javaUsedInKotlin/changeSignature", "2.4") + + project.build("build") { + assertSuccessful() + assertReportExists() + assertCompiledKotlinSources("src/usage.kt") + assertCompiledJavaSources("src/JavaClass.java") + } + + project.modify() + + project.build("build") { + assertSuccessful() + assertCompiledKotlinSources("src/usage.kt") + assertCompiledJavaSources("src/JavaClass.java") + } + } + + @Test + fun testClassSignatureChangedAuto() { + val project = JpsTestProject(jpsResourcesPath, "pureKotlin/classSignatureChanged", "2.4") + + project.performAndAssertBuildStages() + } + + @Test + fun testAnnotationListChanged() { + val project = JpsTestProject(jpsResourcesPath, "classHierarchyAffected/annotationListChanged", "2.4") + + project.performAndAssertBuildStages() + } + + @Test + fun testClassBecameFinal() { + val project = JpsTestProject(jpsResourcesPath, "classHierarchyAffected/classBecameFinal", "2.4") + + project.performAndAssertBuildStages() + } + + @Test + fun testSupertypesListChanged() { + val project = JpsTestProject(jpsResourcesPath, "classHierarchyAffected/supertypesListChanged", "2.4") + + project.performAndAssertBuildStages() + } +}