diff --git a/tools/kotlin-native-gradle-plugin/build.gradle b/tools/kotlin-native-gradle-plugin/build.gradle index f8a649ca9c8..723e20526e5 100644 --- a/tools/kotlin-native-gradle-plugin/build.gradle +++ b/tools/kotlin-native-gradle-plugin/build.gradle @@ -66,7 +66,6 @@ test { dependsOn ':dist' //testLogging.showStandardStreams = true systemProperty("konan.home", rootProject.file('dist')) - systemProperty("konan.root", rootProject.projectDir) } jar { diff --git a/tools/kotlin-native-gradle-plugin/src/test/groovy/Specs.groovy b/tools/kotlin-native-gradle-plugin/src/test/groovy/Specs.groovy deleted file mode 100644 index 6b0ddde9a19..00000000000 --- a/tools/kotlin-native-gradle-plugin/src/test/groovy/Specs.groovy +++ /dev/null @@ -1,134 +0,0 @@ -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome -import org.junit.Rule -import org.junit.rules.TemporaryFolder -import spock.lang.Specification - -class KonanProjectGenerator { - static void generateBuildFile(File buildFile) { - buildFile.write(""" - plugins { id 'konan' } - - konanInterop { - stdio { } - } - - konanArtifacts { - main { useInterop 'stdio' } - } - """.stripIndent() - ) - } - - static void generateSrcFile(File srcFile) { - srcFile.write(""" - import kotlinx.cinterop.* - import stdio.* - - fun main(args: Array) { - printf("%d\\n", 42) - } - """.stripIndent() - ) - } - - static void generateDefFile(File defFile) { - defFile.write(""" - headers = stdio.h stdlib.h string.h - excludeDependentModules.osx = true - """.stripIndent() - ) - } - - static void generatePropertiesFile(File propertiesFile, String konanHome) { - propertiesFile.write("konan.home=$konanHome") - } -} - -class IncrementalSpecification extends Specification { - - @Rule - TemporaryFolder projectDir = new TemporaryFolder() - - File buildFile - File srcFile - File defFile - File propertiesFile - - String konanHome = System.getProperty("konan.home") ?: { throw new IllegalStateException("konan.home isn't specified") }() - String konanRoot = System.getProperty("konan.root") ?: { throw new IllegalStateException("konan.root isn't specified") }() - - def interopTasks = [":genStdioInteropStubs", ":compileStdioInteropStubs"].toSet() - def compilationTasks = [":compileKonanMain", ":compileKonan", ":build"].toSet() - - def buildingTasks = interopTasks + compilationTasks - def downloadTask = ":downloadKonanCompiler" - def konanTasks = buildingTasks + downloadTask - - def setup() { - buildFile = projectDir.newFile("build.gradle") - projectDir.newFolder("src", "main", "kotlin") - projectDir.newFolder("src", "main", "c_interop") - srcFile = projectDir.newFile("src/main/kotlin/main.kt") - defFile = projectDir.newFile("src/main/c_interop/stdio.def") - propertiesFile = projectDir.newFile("gradle.properties") - - KonanProjectGenerator.generateBuildFile(buildFile) - KonanProjectGenerator.generateDefFile(defFile) - KonanProjectGenerator.generateSrcFile(srcFile) - KonanProjectGenerator.generatePropertiesFile(propertiesFile, konanHome) - } - - def 'Compilation is up-to-date if there is no changes'() { - when: - def runner = GradleRunner.create() - .withProjectDir(projectDir.root) - .withArguments('build') - .withPluginClasspath() - def result = runner.build() - def upToDateResult = runner.build() - - then: - result.tasks.collect { it.path }.containsAll(buildingTasks) - result.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) - upToDateResult.taskPaths(TaskOutcome.UP_TO_DATE).containsAll(buildingTasks) - result.task(downloadTask).outcome == TaskOutcome.SUCCESS - upToDateResult.task(downloadTask).outcome == TaskOutcome.SUCCESS - } - - def 'Source change should cause recompilation'() { - when: - def runner = GradleRunner.create() - .withProjectDir(projectDir.root) - .withArguments('build') - .withPluginClasspath() - def result = runner.build() - srcFile.append("\n // Some change in the source file") - def secondResult = runner.build() - - then: - result.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) - secondResult.taskPaths(TaskOutcome.SUCCESS).containsAll(compilationTasks) - secondResult.taskPaths(TaskOutcome.UP_TO_DATE).containsAll(interopTasks) - } - - def 'Def-file change should cause recompilation'() { - when: - def runner = GradleRunner.create() - .withProjectDir(projectDir.root) - .withArguments('build') - .withPluginClasspath() - def result = runner.build() - defFile.append("\n # Some change in the def-file") - def secondResult = runner.build() - - then: - result.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) - secondResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) - } - - // TODO: Add checks for artifact/interop parameters. - -} - - diff --git a/tools/kotlin-native-gradle-plugin/src/test/groovy/org/jetbrains/kotlin/gradle/plugin/test/IncrementalSpecification.groovy b/tools/kotlin-native-gradle-plugin/src/test/groovy/org/jetbrains/kotlin/gradle/plugin/test/IncrementalSpecification.groovy new file mode 100644 index 00000000000..6a5f6897b8e --- /dev/null +++ b/tools/kotlin-native-gradle-plugin/src/test/groovy/org/jetbrains/kotlin/gradle/plugin/test/IncrementalSpecification.groovy @@ -0,0 +1,322 @@ +package org.jetbrains.kotlin.gradle.plugin.test + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.IgnoreIf +import spock.lang.Specification +import spock.lang.Unroll + +class IncrementalSpecification extends Specification { + + @Rule + TemporaryFolder tmpFolder = new TemporaryFolder() + + Tuple buildTwice(KonanInteropProject project, Closure change) { + def runner = project.createRunner().withArguments('build') + def firstResult = runner.build() + change(project) + def secondResult = runner.build() + return new Tuple(project, firstResult, secondResult) + } + + Tuple buildTwice(Closure change) { + return buildTwice(KonanInteropProject.create(tmpFolder), change) + } + + Tuple buildTwiceEmpty(Closure change) { + def project = KonanInteropProject.createEmpty(tmpFolder) + project.generateSrcFile("main.kt") + return buildTwice(project, change) + } + + Boolean noRecompilationHappened(KonanInteropProject project, BuildResult firstResult, BuildResult secondResult) { + return project.with { + firstResult.tasks.collect { it.path }.containsAll(buildingTasks) && + firstResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) && + secondResult.taskPaths(TaskOutcome.UP_TO_DATE).containsAll(buildingTasks) && + firstResult.task(downloadTask).outcome == TaskOutcome.SUCCESS && + secondResult.task(downloadTask).outcome == TaskOutcome.SUCCESS + } + } + + Boolean onlyRecompilationHappened(KonanInteropProject project, BuildResult firstResult, BuildResult secondResult) { + return project.with { + firstResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) && + secondResult.taskPaths(TaskOutcome.SUCCESS).containsAll(compilationTasks) && + secondResult.taskPaths(TaskOutcome.UP_TO_DATE).containsAll(interopTasks) + } + } + + Boolean recompilationAndInteropProcessingHappened(KonanInteropProject project, BuildResult firstResult, BuildResult secondResult) { + return project.with { + firstResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) && + secondResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) + } + } + + //region tests ===================================================================================================== + def 'Compilation is up-to-date if there is no changes'() { + when: + def results = buildTwice {} + + then: + noRecompilationHappened(*results) + } + + def 'Source change should cause only recompilation'() { + when: + def results = buildTwice { KonanInteropProject project -> + project.srcFiles[0].append("\n // Some change in the source file") + } + + then: + onlyRecompilationHappened(*results) + + } + + def 'Def-file change should cause recompilation and interop reprocessing'() { + when: + def results = buildTwice { KonanInteropProject project -> + project.defFiles[0].append("\n # Some change in the def-file") + } + + then: + recompilationAndInteropProcessingHappened(*results) + } + + def 'Compilation is up-to-date if there is no changes in empty project'() { + when: + def results = buildTwiceEmpty {} + + then: + noRecompilationHappened(*results) + } + + @Unroll("#parameter change for a compilation task should cause only recompilation") + def 'Parameter changes should cause only recompilaton'() { + when: + def results = buildTwiceEmpty { KonanInteropProject project -> + project.buildFile.append("konanArtifacts['main'].$appending") + } + + then: + onlyRecompilationHappened(*results) + + + where: + parameter | appending + "outputDir" | "outputDir 'build/new/outputDir'" + "produce" | "produce 'library'" + "enableOptimization" | "enableOptimization()" + "linkerOpts" | "linkerOpts '--help'" + "languageVersion" | "languageVersion '1.2'" + "apiVersion" | "apiVersion '1.0'" + "enableAssertions" | "enableAssertions()" + "enableDebug" | "enableDebug true" + "outputName" | "outputName 'foo'" + + + } + + def 'inputFiles change for a compilation task should cause only recompilation'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) { KonanInteropProject it -> + it.generateSrcFile('main.kt') + it.newFolder('src', 'foo', 'kotlin') + it.generateSrcFile('src/foo/kotlin/', 'bar.kt', """ + fun bar() { println("Hello!") } + """.stripIndent()) + } + def results = buildTwice(project) { KonanInteropProject it -> + it.buildFile.append('konanArtifacts[\'main\'].inputFiles project.fileTree(\'src/foo/kotlin\')') + } + + then: + onlyRecompilationHappened(*results) + } + + @Unroll("#parameter change for a compilation task should cause only recompilation") + def 'Library changes should cause only recompilaton'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) { KonanInteropProject it -> + it.generateSrcFile('main.kt') + it.newFolder('src', 'lib', 'kotlin') + it.generateSrcFile("src/lib/kotlin", "lib.kt", "fun bar() { println(\"Hello!\") }") + it.buildFile.append(""" + konanArtifacts { + lib { + inputFiles fileTree('src/lib/kotlin') + produce '$produce' + } + } + """.stripIndent()) + } + def results = buildTwice(project) { KonanInteropProject it -> + it.buildFile.append("konanArtifacts['main'].$parameter konanArtifacts[\'lib\'].compilationTask.artifactPath") + } + + then: + onlyRecompilationHappened(*results) + + where: + parameter | produce + "library" | "library" + "nativeLibrary" | "bitcode" + } + + def 'useInterop change for a compilation task should cause only recompilation'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) { KonanInteropProject it -> + it.generateSrcFile('main.kt') + it.buildFile.append("konanInterop { foo {} }\n") + } + def results = buildTwice(project) { KonanInteropProject it -> + it.buildFile.append('konanArtifacts[\'main\'].useInterop "foo"\n') + } + + then: + onlyRecompilationHappened(*results) + } + + def 'manifest parameter change for a compilation task should cause only recompilation'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) { KonanInteropProject it -> + it.generateSrcFile('main.kt') + } + def results = buildTwice(project) { KonanInteropProject it -> + def manifest = it.generateSrcFile('manifest', "#some manifest file") + it.buildFile.append("konanArtifacts['main'].manifest '${manifest.canonicalPath}'") + } + + then: + onlyRecompilationHappened(*results) + } + + def 'manifest file change should cause only recompilation'() { + when: + def manifest + def project = KonanInteropProject.createEmpty(tmpFolder) { KonanInteropProject it -> + it.generateSrcFile('main.kt') + manifest = it.generateSrcFile('manifest', "#some manifest file\n") + it.buildFile.append("konanArtifacts['main'].manifest '${manifest.canonicalPath}'") + } + def results = buildTwice(project) { KonanInteropProject it -> + manifest.append("#something else\n") + } + + then: + onlyRecompilationHappened(*results) + } + + @Unroll("#parameter change for an interop task should cause recompilation and interop reprocessing") + def 'Parameter change for an interop task should cause recompilation and interop reprocessing'() { + when: + def results = buildTwiceEmpty { KonanInteropProject project -> + project.buildFile.append("konanInterop['stdio'].$appending") + } + + then: + recompilationAndInteropProcessingHappened(*results) + + where: + parameter | appending + "pkg" | "pkg 'org.sample'" + "compilerOpts" | "compilerOpts '-g'" + "linkerOpts" | "linkerOpts '--help'" + "includeDirs" | "includeDirs 'src'" + } + + def 'defFile change for an interop task should cause recompilation and interop reprocessing'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) + project.generateSrcFile('main.kt') + def defFile = project.generateDefFile("foo.def", "#some content") + def results = buildTwice(project) { KonanInteropProject it -> + it.buildFile.append("konanInterop['stdio'].defFile file('${defFile.canonicalPath}')\n") + } + + then: + recompilationAndInteropProcessingHappened(*results) + } + + def 'header change for an interop task should cause recompilation and interop reprocessing'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) + project.generateSrcFile('main.kt') + def header = project.generateSrcFile('header.h', "#define CONST 1") + def results = buildTwice(project) { KonanInteropProject it -> + it.buildFile.append("konanInterop['stdio'].headers '${header.canonicalPath}'\n") + } + + then: + recompilationAndInteropProcessingHappened(*results) + } + + def 'link change for an interop task should cause recompilation and interop reprocessing'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) { KonanInteropProject it -> + it.generateSrcFile('main.kt') + it.newFolder('src', 'lib', 'kotlin') + it.buildFile.append(""" + konanArtifacts { + lib { + inputFiles fileTree('src/lib/kotlin') + produce 'bitcode' + } + } + """.stripIndent()) + } + def results = buildTwice(project) { KonanInteropProject it -> + it.buildFile.append(""" + konanInterop['stdio'].generateStubsTask.dependsOn(konanArtifacts['lib'].compilationTask) + konanInterop['stdio'].link files(konanArtifacts['lib'].compilationTask.artifactPath) + """.stripIndent()) + } + + then: + recompilationAndInteropProcessingHappened(*results) + } + + def 'konan version change should cause recompilation and interop reprocessing'() { + when: + def project = KonanInteropProject.createEmpty(tmpFolder) { KonanInteropProject it -> + it.generateSrcFile('main.kt') + it.propertiesFile.append("konan.version=0.3\n") + } + def results = buildTwice(project) { KonanInteropProject it -> + def newText = it.propertiesFile.text.replace('konan.version=0.3', 'konan.version=0.4') + it.propertiesFile.write(newText) + } + + then: + recompilationAndInteropProcessingHappened(*results) + } + + @IgnoreIf({ System.getProperty('os.name').contains('windows') }) + def 'target change should cause recompilation and interop reprocessing'() { + when: + def newTarget + if (System.getProperty('os.name').toLowerCase().contains('linux')) { + newTarget = "raspberrypi" + } else if (System.getProperty('os.name').toLowerCase().contains('mac')) { + newTarget = "iphone" + } else { + throw new IllegalStateException("Unknown host platform") + } + + def results = buildTwiceEmpty { KonanInteropProject project -> + project.buildFile.append("konanArtifacts['main'].target '$newTarget'\n") + project.buildFile.append("konanInterop['stdio'].target '$newTarget'\n") + } + + then: + recompilationAndInteropProcessingHappened(*results) + } + + + //endregion +} + + diff --git a/tools/kotlin-native-gradle-plugin/src/test/groovy/org/jetbrains/kotlin/gradle/plugin/test/KonanProject.groovy b/tools/kotlin-native-gradle-plugin/src/test/groovy/org/jetbrains/kotlin/gradle/plugin/test/KonanProject.groovy new file mode 100644 index 00000000000..366a8d8ddc4 --- /dev/null +++ b/tools/kotlin-native-gradle-plugin/src/test/groovy/org/jetbrains/kotlin/gradle/plugin/test/KonanProject.groovy @@ -0,0 +1,188 @@ +package org.jetbrains.kotlin.gradle.plugin.test + +import org.gradle.testkit.runner.GradleRunner +import org.junit.rules.TemporaryFolder + +class KonanProject { + + TemporaryFolder projectDir + + File getProjectDirRoot() { return projectDir.root } + + String konanHome = System.getProperty("konan.home") ?: { throw new IllegalStateException("konan.home isn't specified") }() + + File buildFile + File propertiesFile + + Set srcFiles = [] + + List compilationTasks = [] + String downloadTask = ":downloadKonanCompiler" + + List getBuildingTasks() { return compilationTasks } + List getKonanTasks() { return getBuildingTasks() + downloadTask } + + GradleRunner createRunner() { return GradleRunner.create().withProjectDir(projectDirRoot).withPluginClasspath() } + + protected KonanProject(TemporaryFolder projectDir) { + this.projectDir = projectDir + } + + void generateFolders() { + projectDir.newFolder("src", "main", "kotlin") + } + + File generateBuildFile(String content) { + buildFile = projectDir.newFile("build.gradle") + buildFile.write(content) + return buildFile + } + + File generateBuildFile() { + def result = generateBuildFile(""" + plugins { id 'konan' } + + konanArtifacts { + main { } + } + """.stripIndent() + ) + compilationTasks = [":compileKonanMain", ":compileKonan", ":build"] + return result + } + + File generateSrcFile(String directoryPath, String fileName, String content) { + def result = projectDir.newFile("$directoryPath/$fileName") + result.write(content) + srcFiles.add(result) + return result + } + + File generateSrcFile(String fileName, String content) { + return generateSrcFile("src/main/kotlin",fileName, content) + } + + File generateSrcFile(String fileName) { + return generateSrcFile(fileName, """ + fun main(args: Array) { + println(42) + } + """.stripIndent() + ) + } + + File generatePropertiesFile(String konanHome) { + propertiesFile = projectDir.newFile("gradle.properties") + propertiesFile.write("konan.home=$konanHome\n") + return propertiesFile + } + + File newFolder(String... path) { return projectDir.newFolder(path) } + + static KonanProject create(TemporaryFolder projectDir) { + return createEmpty(projectDir) { + it.generateSrcFile("main.kt") + } + } + + static KonanProject create(TemporaryFolder projectDir, Closure config) { + def result = create(projectDir) + config(result) + return result + } + + static KonanProject createEmpty(TemporaryFolder projectDir) { + def result = new KonanProject(projectDir) + result.with { + generateFolders() + generateBuildFile() + generatePropertiesFile(konanHome) + } + return result + } + + static KonanProject createEmpty(TemporaryFolder projectDir, Closure config) { + def result = createEmpty(projectDir) + config(result) + return result + } + +} + +class KonanInteropProject extends KonanProject { + + Set defFiles = [] + + List interopTasks = [] + + protected KonanInteropProject(TemporaryFolder projectDir) { super(projectDir) } + + List getBuildingTasks() { return interopTasks + compilationTasks } + + void generateFolders() { + super.generateFolders() + projectDir.newFolder("src", "main", "c_interop") + } + + File generateBuildFile() { + def result = generateBuildFile(""" + plugins { id 'konan' } + + konanInterop { + stdio { } + } + + konanArtifacts { + main { useInterop 'stdio' } + } + """.stripIndent() + ) + interopTasks = [":genStdioInteropStubs", ":compileStdioInteropStubs"] + compilationTasks = [":compileKonanMain", ":compileKonan", ":build"] + return result + } + + File generateDefFile(String fileName, String content) { + def result = projectDir.newFile("src/main/c_interop/$fileName") + result.write(content) + defFiles.add(result) + return result + } + + File generateDefFile(String fileName) { + return generateDefFile(fileName, """ + headers = stdio.h stdlib.h string.h + excludeDependentModules.osx = true + """.stripIndent() + ) + } + + static KonanInteropProject create(TemporaryFolder projectDir) { + return createEmpty(projectDir) { + it.generateSrcFile("main.kt") + it.generateDefFile("stdio.def") + } + } + + static KonanInteropProject create(TemporaryFolder projectDir, Closure config) { + def result = create(projectDir) + config(result) + return result + } + + static KonanInteropProject createEmpty(TemporaryFolder projectDir) { + def result = new KonanInteropProject(projectDir) + result.with { + generateFolders() + generateBuildFile() + generatePropertiesFile(konanHome) + } + return result + } + + static KonanInteropProject createEmpty(TemporaryFolder projectDir, Closure config) { + def result = createEmpty(projectDir) + config(result) + return result + } +} \ No newline at end of file