gradle-plugin: Add tests for incremental build

This commit is contained in:
Ilya Matveev
2017-07-17 17:06:01 +07:00
committed by ilmat192
parent 7fbb1fcfc7
commit 5dcbfeb4cf
4 changed files with 510 additions and 135 deletions
@@ -66,7 +66,6 @@ test {
dependsOn ':dist'
//testLogging.showStandardStreams = true
systemProperty("konan.home", rootProject.file('dist'))
systemProperty("konan.root", rootProject.projectDir)
}
jar {
@@ -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<String>) {
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.
}
@@ -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
}
@@ -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<File> srcFiles = []
List<String> compilationTasks = []
String downloadTask = ":downloadKonanCompiler"
List<String> getBuildingTasks() { return compilationTasks }
List<String> 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<String>) {
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<File> defFiles = []
List<String> interopTasks = []
protected KonanInteropProject(TemporaryFolder projectDir) { super(projectDir) }
List<String> 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
}
}