blob: 6aa1002f402531b09bb16396482464c9fe07491c [file] [log] [blame]
/*
* 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.gradle
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin
import org.jetbrains.kotlin.gradle.plugin.sources.METADATA_CONFIGURATION_NAME_SUFFIX
import org.jetbrains.kotlin.gradle.plugin.sources.SourceSetConsistencyChecks
import org.jetbrains.kotlin.gradle.util.checkBytecodeContains
import org.jetbrains.kotlin.gradle.util.isWindows
import org.jetbrains.kotlin.gradle.util.modify
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.HostManager
import org.junit.Assert
import org.junit.Test
import java.util.jar.JarFile
import java.util.zip.ZipFile
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class NewMultiplatformIT : BaseGradleIT() {
val gradleVersion = GradleVersionRequired.AtLeast("4.8")
val nativeHostTargetName = when {
HostManager.hostIsMingw -> "mingw64"
HostManager.hostIsLinux -> "linux64"
HostManager.hostIsMac -> "macos64"
else -> error("Unknown host")
}
private fun Project.targetClassesDir(targetName: String, sourceSetName: String = "main") =
classesDir(sourceSet = "$targetName/$sourceSetName")
@Test
fun testLibAndApp() {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
val appProject = Project("sample-app", gradleVersion, "new-mpp-lib-and-app")
val oldStyleAppProject = Project("sample-old-style-app", gradleVersion, "new-mpp-lib-and-app")
val compileTasksNames =
listOf("Jvm6", "NodeJs", "Metadata", "Wasm32", nativeHostTargetName.capitalize()).map { ":compileKotlin$it" }
with(libProject) {
build("publish") {
assertSuccessful()
assertTasksExecuted(*compileTasksNames.toTypedArray(), ":jvm6Jar", ":nodeJsJar", ":metadataJar")
val groupDir = projectDir.resolve("repo/com/example")
val jvmJarName = "sample-lib-jvm6/1.0/sample-lib-jvm6-1.0.jar"
val jsJarName = "sample-lib-nodejs/1.0/sample-lib-nodejs-1.0.jar"
val metadataJarName = "sample-lib-metadata/1.0/sample-lib-metadata-1.0.jar"
val wasmKlibName = "sample-lib-wasm32/1.0/sample-lib-wasm32-1.0.klib"
val nativeKlibName = "sample-lib-$nativeHostTargetName/1.0/sample-lib-$nativeHostTargetName-1.0.klib"
listOf(jvmJarName, jsJarName, metadataJarName, "sample-lib/1.0/sample-lib-1.0.module").forEach {
Assert.assertTrue("$it should exist", groupDir.resolve(it).exists())
}
listOf(jvmJarName, jsJarName, metadataJarName, wasmKlibName, nativeKlibName).forEach {
val pom = groupDir.resolve(it.replaceAfterLast('.', "pom"))
Assert.assertTrue(
"$pom should contain a name section.",
pom.readText().contains("<name>Sample MPP library</name>")
)
Assert.assertFalse(
"$pom should not contain standard K/N libraries as dependencies.",
pom.readText().contains("<groupId>Kotlin/Native</groupId>")
)
}
val jvmJarEntries = ZipFile(groupDir.resolve(jvmJarName)).entries().asSequence().map { it.name }.toSet()
Assert.assertTrue("com/example/lib/CommonKt.class" in jvmJarEntries)
Assert.assertTrue("com/example/lib/MainKt.class" in jvmJarEntries)
val jsJar = ZipFile(groupDir.resolve(jsJarName))
val compiledJs = jsJar.getInputStream(jsJar.getEntry("sample-lib.js")).reader().readText()
Assert.assertTrue("function id(" in compiledJs)
Assert.assertTrue("function idUsage(" in compiledJs)
Assert.assertTrue("function expectedFun(" in compiledJs)
Assert.assertTrue("function main(" in compiledJs)
val metadataJarEntries = ZipFile(groupDir.resolve(metadataJarName)).entries().asSequence().map { it.name }.toSet()
Assert.assertTrue("com/example/lib/CommonKt.kotlin_metadata" in metadataJarEntries)
Assert.assertTrue(groupDir.resolve(wasmKlibName).exists())
Assert.assertTrue(groupDir.resolve(nativeKlibName).exists())
}
}
val libLocalRepoUri = libProject.projectDir.resolve("repo").toURI()
with(appProject) {
setupWorkingDir()
gradleBuildScript().appendText("\nrepositories { maven { url '$libLocalRepoUri' } }")
fun CompiledProject.checkAppBuild() {
assertSuccessful()
assertTasksExecuted(*compileTasksNames.toTypedArray())
projectDir.resolve(targetClassesDir("jvm6")).run {
Assert.assertTrue(resolve("com/example/app/AKt.class").exists())
Assert.assertTrue(resolve("com/example/app/UseBothIdsKt.class").exists())
}
projectDir.resolve(targetClassesDir("jvm8")).run {
Assert.assertTrue(resolve("com/example/app/AKt.class").exists())
Assert.assertTrue(resolve("com/example/app/UseBothIdsKt.class").exists())
Assert.assertTrue(resolve("com/example/app/Jdk8ApiUsageKt.class").exists())
}
projectDir.resolve(targetClassesDir("metadata")).run {
Assert.assertTrue(resolve("com/example/app/AKt.kotlin_metadata").exists())
}
projectDir.resolve(targetClassesDir("nodeJs")).resolve("sample-app.js").readText().run {
Assert.assertTrue(contains("console.info"))
Assert.assertTrue(contains("function nodeJsMain("))
}
projectDir.resolve(targetClassesDir("wasm32")).run {
Assert.assertTrue(resolve("sample-app.klib").exists())
}
assertFileExists("build/bin/wasm32/main/debug/executable/sample_app.wasm.js")
assertFileExists("build/bin/wasm32/main/debug/executable/sample_app.wasm")
assertFileExists("build/bin/wasm32/main/release/executable/sample_app.wasm.js")
assertFileExists("build/bin/wasm32/main/release/executable/sample_app.wasm")
val nativeExeName = if (isWindows) "sample-app.exe" else "sample-app.kexe"
assertFileExists("build/bin/$nativeHostTargetName/main/release/executable/$nativeExeName")
assertFileExists("build/bin/$nativeHostTargetName/main/debug/executable/$nativeExeName")
}
build("assemble", "resolveRuntimeDependencies") {
checkAppBuild()
assertTasksExecuted(":resolveRuntimeDependencies") // KT-26301
}
// Now run again with a project dependency instead of a module one:
libProject.projectDir.copyRecursively(projectDir.resolve(libProject.projectDir.name))
projectDir.resolve("settings.gradle").appendText("\ninclude '${libProject.projectDir.name}'")
gradleBuildScript().modify { it.replace("'com.example:sample-lib:1.0'", "project(':${libProject.projectDir.name}')") }
build("clean", "assemble", "--rerun-tasks") {
checkAppBuild()
}
}
with(oldStyleAppProject) {
setupWorkingDir()
gradleBuildScript().appendText("\nallprojects { repositories { maven { url '$libLocalRepoUri' } } }")
build("assemble") {
assertSuccessful()
assertTasksExecuted(":app-js:compileKotlin2Js", ":app-jvm:compileKotlin", ":app-common:compileKotlinCommon")
assertFileExists(kotlinClassesDir("app-common") + "com/example/app/CommonAppKt.kotlin_metadata")
val jvmClassFile = projectDir.resolve(kotlinClassesDir("app-jvm") + "com/example/app/JvmAppKt.class")
checkBytecodeContains(jvmClassFile, "CommonKt.id", "MainKt.expectedFun")
val jsCompiledFilePath = kotlinClassesDir("app-js") + "app-js.js"
assertFileContains(jsCompiledFilePath, "lib.expectedFun", "lib.id")
}
}
}
@Test
fun testResourceProcessing() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
val targetsWithResources = listOf("jvm6", "wasm32", nativeHostTargetName)
val processResourcesTasks =
targetsWithResources.map { ":${it}ProcessResources" }
build(*processResourcesTasks.toTypedArray()) {
assertSuccessful()
assertTasksExecuted(*processResourcesTasks.toTypedArray())
targetsWithResources.forEach {
assertFileExists("build/processedResources/$it/main/commonMainResource.txt")
assertFileExists("build/processedResources/$it/main/${it}MainResource.txt")
}
}
}
@Test
fun testSourceSetCyclicDependencyDetection() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText("\n" + """
kotlin.sourceSets {
a
b { dependsOn a }
c { dependsOn b }
a.dependsOn(c)
}
""".trimIndent())
build("assemble") {
assertFailed()
assertContains("a -> c -> b -> a")
}
}
@Test
fun testJvmWithJavaEquivalence() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
lateinit var classesWithoutJava: Set<String>
fun getFilePathsSet(inDirectory: String): Set<String> {
val dir = projectDir.resolve(inDirectory)
return dir.walk().filter { it.isFile }.map { it.relativeTo(dir).path.replace('\\', '/') }.toSet()
}
build("assemble") {
assertSuccessful()
classesWithoutJava = getFilePathsSet("build/classes")
}
gradleBuildScript().modify { it.replace("presets.jvm", "presets.jvmWithJava") }
projectDir.resolve("src/main/java").apply {
mkdirs()
mkdir()
// Check that Java can access the dependencies (kotlin-stdlib):
resolve("JavaClassInJava.java").writeText("""
package com.example.lib;
import kotlin.sequences.Sequence;
class JavaClassInJava {
Sequence<String> makeSequence() { throw new UnsupportedOperationException(); }
}
""".trimIndent())
// Add a Kotlin source file in the Java source root and check that it is compiled:
resolve("KotlinClassInJava.kt").writeText("""
package com.example.lib
class KotlinClassInJava
""".trimIndent())
}
build("clean", "assemble") {
assertSuccessful()
val expectedClasses =
classesWithoutJava +
"kotlin/jvm6/main/com/example/lib/KotlinClassInJava.class" +
"java/main/com/example/lib/JavaClassInJava.class"
val actualClasses = getFilePathsSet("build/classes")
Assert.assertEquals(expectedClasses, actualClasses)
}
}
@Test
fun testLibWithTests() = with(Project("new-mpp-lib-with-tests", gradleVersion)) {
build("check") {
assertSuccessful()
assertTasksExecuted(
// compilation tasks:
":compileKotlinJs",
":compileTestKotlinJs",
":compileKotlinJvmWithoutJava",
":compileTestKotlinJvmWithoutJava",
":compileKotlinJvmWithJava",
":compileJava",
":compileTestKotlinJvmWithJava",
":compileTestJava",
// test tasks:
":jsTest", // does not run any actual tests for now
":jvmWithoutJavaTest",
":test"
)
val expectedKotlinOutputFiles = listOf(
kotlinClassesDir(sourceSet = "js/main") + "new-mpp-lib-with-tests.js",
kotlinClassesDir(sourceSet = "js/test") + "new-mpp-lib-with-tests_test.js",
*kotlinClassesDir(sourceSet = "jvmWithJava/main").let {
arrayOf(
it + "com/example/lib/JavaClassUsageKt.class",
it + "com/example/lib/CommonKt.class",
it + "META-INF/new-mpp-lib-with-tests.kotlin_module"
)
},
*kotlinClassesDir(sourceSet = "jvmWithJava/test").let {
arrayOf(
it + "com/example/lib/TestCommonCode.class",
it + "com/example/lib/TestWithJava.class",
it + "META-INF/new-mpp-lib-with-tests.kotlin_module" // Note: same name as in main
)
},
*kotlinClassesDir(sourceSet = "jvmWithoutJava/main").let {
arrayOf(
it + "com/example/lib/CommonKt.class",
it + "com/example/lib/MainKt.class",
it + "META-INF/new-mpp-lib-with-tests.kotlin_module"
)
},
*kotlinClassesDir(sourceSet = "jvmWithoutJava/test").let {
arrayOf(
it + "com/example/lib/TestCommonCode.class",
it + "com/example/lib/TestWithoutJava.class",
it + "META-INF/new-mpp-lib-with-tests.kotlin_module" // Note: same name as in main
)
}
)
expectedKotlinOutputFiles.forEach { assertFileExists(it) }
}
}
@Test
fun testLanguageSettingsApplied() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText(
"\n" + """
kotlin.sourceSets.jvm6Main.languageSettings {
languageVersion = '1.3'
apiVersion = '1.3'
enableLanguageFeature('InlineClasses')
progressiveMode = true
}
""".trimIndent()
)
build("compileKotlinJvm6") {
assertSuccessful()
assertContains("-language-version 1.3", "-api-version 1.3", "-XXLanguage:+InlineClasses", " -progressive")
}
}
@Test
fun testLanguageSettingsConsistency() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText(
"\n" + """
kotlin.sourceSets {
foo { }
bar { dependsOn foo }
}
""".trimIndent()
)
fun testMonotonousCheck(sourceSetConfigurationChange: String, expectedErrorHint: String) {
gradleBuildScript().appendText("\nkotlin.sourceSets.foo.${sourceSetConfigurationChange}")
build("tasks") {
assertFailed()
assertContains(expectedErrorHint)
}
gradleBuildScript().appendText("\nkotlin.sourceSets.bar.${sourceSetConfigurationChange}")
build("tasks") {
assertSuccessful()
}
}
testMonotonousCheck("languageSettings.languageVersion = '1.4'", SourceSetConsistencyChecks.languageVersionCheckHint)
testMonotonousCheck("languageSettings.enableLanguageFeature('InlineClasses')", SourceSetConsistencyChecks.unstableFeaturesHint)
// check that enabling a bugfix feature and progressive mode or advancing API level
// don't require doing the same for dependent source sets:
gradleBuildScript().appendText(
"\n" + """
kotlin.sourceSets.foo.languageSettings {
apiVersion = '1.3'
enableLanguageFeature('SoundSmartcastForEnumEntries')
progressiveMode = true
}
""".trimIndent()
)
build("tasks") {
assertSuccessful()
}
}
@Test
fun testResolveMppLibDependencyToMetadata() {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
val appProject = Project("sample-app", gradleVersion, "new-mpp-lib-and-app")
libProject.build("publish") { assertSuccessful() }
val localRepo = libProject.projectDir.resolve("repo")
val localRepoUri = localRepo.toURI()
with(appProject) {
setupWorkingDir()
val pathPrefix = "metadataDependency: "
gradleBuildScript().appendText(
"\n" + """
repositories { maven { url '$localRepoUri' } }
kotlin.sourceSets {
commonMain {
dependencies {
// add these dependencies to check that they are resolved to metadata
api 'com.example:sample-lib:1.0'
compileOnly 'com.example:sample-lib:1.0'
runtimeOnly 'com.example:sample-lib:1.0'
}
}
}
task('printMetadataFiles') {
doFirst {
['Api', 'Implementation', 'CompileOnly', 'RuntimeOnly'].each { kind ->
def configuration = configurations.getByName("commonMain${'$'}kind" + '$METADATA_CONFIGURATION_NAME_SUFFIX')
configuration.files.each { println '$pathPrefix' + configuration.name + '->' + it.name }
}
}
}
""".trimIndent()
)
val metadataDependencyRegex = "$pathPrefix(.*?)->(.*)".toRegex()
build("printMetadataFiles") {
assertSuccessful()
val expectedFileName = "sample-lib-${KotlinMultiplatformPlugin.METADATA_TARGET_NAME}-1.0.jar"
val paths = metadataDependencyRegex.findAll(output).map { it.groupValues[1] to it.groupValues[2] }.toSet()
Assert.assertEquals(
listOf("Api", "Implementation", "CompileOnly", "RuntimeOnly").map {
"commonMain$it$METADATA_CONFIGURATION_NAME_SUFFIX" to expectedFileName
}.toSet(),
paths
)
}
}
}
@Test
fun testResolveMppProjectDependencyToMetadata() {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
val appProject = Project("sample-app", gradleVersion, "new-mpp-lib-and-app")
val pathPrefix = "metadataDependency: "
with(appProject) {
setupWorkingDir()
libProject.setupWorkingDir()
libProject.projectDir.copyRecursively(projectDir.resolve(libProject.projectDir.name))
projectDir.resolve("settings.gradle").appendText("\ninclude '${libProject.projectDir.name}'")
gradleBuildScript().modify {
it.replace("'com.example:sample-lib:1.0'", "project(':${libProject.projectDir.name}')") +
"\n" + """
task('printMetadataFiles') {
doFirst {
configurations.getByName('commonMainImplementation$METADATA_CONFIGURATION_NAME_SUFFIX')
.files.each { println '$pathPrefix' + it.name }
}
}
""".trimIndent()
}
build("printMetadataFiles") {
assertSuccessful()
assertContains(pathPrefix + "sample-lib-metadata-1.0.jar")
}
}
}
@Test
fun testPublishWithoutGradleMetadata() {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
with (libProject) {
setupWorkingDir()
projectDir.resolve("settings.gradle").modify { it.replace("enableFeaturePreview", "// enableFeaturePreview") }
build("publish") {
assertSuccessful()
val groupRepoDir = "repo/com/example"
// No root publication:
assertNoSuchFile("$groupRepoDir/sample-lib")
// Check that the platform publications have the metadata dependency
listOf("jvm6", "nodejs", "wasm32", nativeHostTargetName.toLowerCase()).forEach { targetAppendix ->
val targetPomPath = "$groupRepoDir/sample-lib-$targetAppendix/1.0/sample-lib-$targetAppendix-1.0.pom"
assertFileContains(
targetPomPath,
"<groupId>com.example</groupId>",
"<artifactId>sample-lib-metadata</artifactId>",
"<version>1.0</version>"
)
}
}
}
}
@Test
fun testDependenciesOnMppLibraryPartsWithNoMetadata() {
val repoDir = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
projectDir.resolve("settings.gradle").modify { it.replace("enableFeaturePreview", "// enableFeaturePreview") }
build("publish") { assertSuccessful() }
projectDir.resolve("repo")
}
with(Project("sample-app", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().modify {
it.replace("implementation 'com.example:sample-lib:1.0'", "implementation 'com.example:sample-lib-metadata:1.0'") + "\n" + """
repositories { maven { url '${repoDir.toURI()}' } }
dependencies {
allJvmImplementation 'com.example:sample-lib-jvm6:1.0'
nodeJsMainImplementation 'com.example:sample-lib-nodejs:1.0'
wasm32MainImplementation 'com.example:sample-lib-wasm32:1.0'
${nativeHostTargetName}MainImplementation 'com.example:sample-lib-${nativeHostTargetName.toLowerCase()}:1.0'
}
""".trimIndent()
}
build("assemble") {
assertSuccessful()
assertTasksExecuted(listOf("Jvm6", "NodeJs", "Wasm32", nativeHostTargetName.capitalize()).map { ":compileKotlin$it" })
}
}
}
@Test
fun testPublishingOnlySupportedNativeTargets() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
val (publishedVariant, nonPublishedVariant) = when {
HostManager.hostIsMac -> "macos64" to "linux64"
HostManager.hostIsLinux -> "linux64" to "macos64"
HostManager.hostIsMingw -> "mingw64" to "linux64"
else -> error("Unknown host")
}
build("publish") {
assertSuccessful()
assertFileExists("repo/com/example/sample-lib-$publishedVariant/1.0/sample-lib-$publishedVariant-1.0.klib")
assertNoSuchFile("repo/com/example/sample-lib-$nonPublishedVariant") // check that no artifacts are published for that variant
// but check that the module metadata contains all variants:
val gradleModuleMetadata = projectDir.resolve("repo/com/example/sample-lib/1.0/sample-lib-1.0.module").readText()
assertTrue(""""name": "linux64-api"""" in gradleModuleMetadata)
assertTrue(""""name": "mingw64-api"""" in gradleModuleMetadata)
assertTrue(""""name": "macos64-api"""" in gradleModuleMetadata)
}
}
@Test
fun testNonMppConsumersOfLibraryPublishedWithNoMetadataOptIn() {
val repoDir = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
projectDir.resolve("settings.gradle").modify { it.replace("enableFeaturePreview", "// enableFeaturePreview") }
build("publish") { assertSuccessful() }
projectDir.resolve("repo")
}
with(Project("sample-old-style-app", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText("\nallprojects { repositories { maven { url '${repoDir.toURI()}' } } }")
gradleBuildScript("app-common").modify { it.replace("com.example:sample-lib:", "com.example:sample-lib-metadata:") }
gradleBuildScript("app-jvm").modify { it.replace("com.example:sample-lib:", "com.example:sample-lib-jvm6:") }
gradleBuildScript("app-js").modify { it.replace("com.example:sample-lib:", "com.example:sample-lib-nodejs:") }
build("assemble", "run") {
assertSuccessful()
assertTasksExecuted(":app-common:compileKotlinCommon", ":app-jvm:compileKotlin", ":app-jvm:run", ":app-js:compileKotlin2Js")
}
// Then run again without even reading the metadata from the repo:
projectDir.resolve("settings.gradle").modify { it.replace("enableFeaturePreview('GRADLE_METADATA')", "") }
build("assemble", "run", "--rerun-tasks") {
assertSuccessful()
assertTasksExecuted(":app-common:compileKotlinCommon", ":app-jvm:compileKotlin", ":app-jvm:run", ":app-js:compileKotlin2Js")
}
}
with(Project("sample-app-without-kotlin", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().modify {
it.replace("com.example:sample-lib:1.0", "com.example:sample-lib-jvm6:1.0") + "\n" + """
repositories { maven { url '${repoDir.toURI()}' } }
""".trimIndent()
}
build("run") {
assertSuccessful()
assertTasksExecuted(":compileJava", ":run")
}
// Then run again without even reading the metadata from the repo:
projectDir.resolve("settings.gradle").modify { it.replace("enableFeaturePreview('GRADLE_METADATA')", "") }
build("run", "--rerun-tasks") {
assertSuccessful()
assertTasksExecuted(":compileJava", ":run")
}
}
}
@Test
fun testOptionalExpectations() = with(Project("new-mpp-lib-with-tests", gradleVersion)) {
setupWorkingDir()
projectDir.resolve("src/commonMain/kotlin/Optional.kt").writeText(
"""
@file:Suppress("EXPERIMENTAL_API_USAGE_ERROR")
@OptionalExpectation
expect annotation class Optional(val value: String)
@Optional("optionalAnnotationValue")
class OptionalCommonUsage
""".trimIndent()
)
build("compileKotlinJvmWithoutJava", "compileKotlin${nativeHostTargetName.capitalize()}") {
assertSuccessful()
assertFileExists(targetClassesDir("jvmWithoutJava") + "OptionalCommonUsage.class")
}
val optionalImplText = "\n" + """
@Optional("should fail, see KT-25196")
class OptionalPlatformUsage
""".trimIndent()
projectDir.resolve("src/jvmWithoutJavaMain/kotlin/OptionalImpl.kt").writeText(optionalImplText)
build("compileKotlinJvmWithoutJava") {
assertFailed()
assertContains("Declaration annotated with '@OptionalExpectation' can only be used in common module sources", ignoreCase = true)
}
projectDir.resolve("src/${nativeHostTargetName}Main/kotlin/").also {
it.mkdirs()
it.resolve("OptionalImpl.kt").writeText(optionalImplText)
}
build("compileKotlin${nativeHostTargetName.capitalize()}") {
assertFailed()
assertContains("Declaration annotated with '@OptionalExpectation' can only be used in common module sources", ignoreCase = true)
}
}
@Test
fun testCanProduceNativeLibraries() = with(Project("new-mpp-native-libraries", gradleVersion)) {
val baseName = "native_lib"
val sharedPrefix = CompilerOutputKind.DYNAMIC.prefix(HostManager.host)
val sharedSuffix = CompilerOutputKind.DYNAMIC.suffix(HostManager.host)
val sharedPaths = listOf(
"build/bin/$nativeHostTargetName/main/debug/shared/$sharedPrefix$baseName$sharedSuffix",
"build/bin/$nativeHostTargetName/main/release/shared/$sharedPrefix$baseName$sharedSuffix"
)
val staticPrefix = CompilerOutputKind.STATIC.prefix(HostManager.host)
val staticSuffix = CompilerOutputKind.STATIC.suffix(HostManager.host)
val staticPaths = listOf(
"build/bin/$nativeHostTargetName/main/debug/static/$staticPrefix$baseName$staticSuffix",
"build/bin/$nativeHostTargetName/main/release/static/$staticPrefix$baseName$staticSuffix"
)
val headerPaths = listOf(
"build/bin/$nativeHostTargetName/main/debug/shared/$sharedPrefix${baseName}_api.h",
"build/bin/$nativeHostTargetName/main/release/shared/$sharedPrefix${baseName}_api.h",
"build/bin/$nativeHostTargetName/main/debug/static/$staticPrefix${baseName}_api.h",
"build/bin/$nativeHostTargetName/main/release/static/$staticPrefix${baseName}_api.h"
)
val taskSuffix = nativeHostTargetName.capitalize()
val linkTasks = listOf(
":linkDebugShared$taskSuffix",
":linkReleaseShared$taskSuffix",
":linkDebugStatic$taskSuffix",
":linkReleaseStatic$taskSuffix"
)
build("assemble") {
assertSuccessful()
sharedPaths.forEach { assertFileExists(it) }
staticPaths.forEach { assertFileExists(it) }
headerPaths.forEach { assertFileExists(it) }
}
build("assemble") {
assertSuccessful()
assertTasksUpToDate(linkTasks)
}
assertTrue(projectDir.resolve(headerPaths[0]).delete())
build("assemble") {
assertSuccessful()
assertTasksUpToDate(linkTasks.drop(1))
assertTasksExecuted(linkTasks[0])
}
}
@Test
fun testSourceJars() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
build("publish") {
assertSuccessful()
val groupDir = projectDir.resolve("repo/com/example/")
val targetArtifactIdAppendices = listOf("metadata", "jvm6", "nodejs", "wasm32", nativeHostTargetName)
val sourceJarSourceRoots = targetArtifactIdAppendices.associate { artifact ->
val sourcesJar = JarFile(groupDir.resolve("sample-lib-$artifact/1.0/sample-lib-$artifact-1.0-sources.jar"))
val sourcesDirs = sourcesJar.entries().asSequence().map { it.name.substringBefore("/") }.toSet() - "META-INF"
artifact to sourcesDirs
}
assertEquals(setOf("commonMain"), sourceJarSourceRoots["metadata"])
assertEquals(setOf("commonMain", "jvm6Main"), sourceJarSourceRoots["jvm6"])
assertEquals(setOf("commonMain", "nodeJsMain"), sourceJarSourceRoots["nodejs"])
assertEquals(setOf("commonMain", "wasm32Main"), sourceJarSourceRoots["wasm32"])
assertEquals(setOf("commonMain", "${nativeHostTargetName}Main"), sourceJarSourceRoots[nativeHostTargetName])
}
}
@Test
fun testConsumeMppLibraryFromNonKotlinProject() {
val libRepo = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
build("publish") { assertSuccessful() }
projectDir.resolve("repo")
}
with(Project("sample-app-without-kotlin", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText("\nrepositories { maven { url '${libRepo.toURI()}' } }")
build("assemble") {
assertSuccessful()
assertTasksExecuted(":compileJava")
assertFileExists("build/classes/java/main/A.class")
}
}
}
@Test
fun testNativeTests() = with(Project("new-mpp-native-tests", gradleVersion)) {
val testTasks = listOf("macos64Test", "linux64Test", "mingw64Test")
val hostTestTask = ":${nativeHostTargetName}Test"
build("tasks") {
assertSuccessful()
println(output)
testTasks.forEach {
// We need to create tasks for all hosts
assertTrue(output.contains("$it - "), "There is no test task '$it' in the task list.")
}
}
build("check") {
assertSuccessful()
assertTasksExecuted(hostTestTask)
}
}
@Test
fun testCinterop() = with(Project("new-mpp-native-cinterop", gradleVersion)) {
val host = nativeHostTargetName
build(":projectLibrary:build") {
assertSuccessful()
assertTasksExecuted(":projectLibrary:cinteropStdio${host.capitalize()}")
assertTrue(output.contains("Project test"), "No test output found")
assertFileExists("projectLibrary/build/classes/kotlin/$host/main/projectLibrary-cinterop-stdio.klib")
}
build(":publishedLibrary:build", ":publishedLibrary:publish") {
assertSuccessful()
assertTasksExecuted(":publishedLibrary:cinteropStdio${host.capitalize()}")
assertTrue(output.contains("Published test"), "No test output found")
assertFileExists("publishedLibrary/build/classes/kotlin/$host/main/publishedLibrary-cinterop-stdio.klib")
assertFileExists("repo/org/example/publishedLibrary-$host/1.0/publishedLibrary-$host-1.0-cinterop-stdio.klib")
}
build(":build") {
assertSuccessful()
assertTrue(output.contains("Dependent: Project print"), "No test output found")
assertTrue(output.contains("Dependent: Published print"), "No test output found")
}
}
@Test
fun testNativeCompilerDownloading() {
// The plugin shouldn't download the K/N compiler if there is no corresponding targets in the project.
with(Project("sample-old-style-app", gradleVersion, "new-mpp-lib-and-app")) {
build("tasks") {
assertSuccessful()
assertFalse(output.contains("Kotlin/Native distribution: "))
}
}
with(Project("new-mpp-native-libraries", gradleVersion)) {
build("tasks") {
assertSuccessful()
assertTrue(output.contains("Kotlin/Native distribution: "))
}
}
}
@Test
fun testPublishMultimoduleProjectWithNoMetadata() {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
val appProject = Project("sample-app", gradleVersion, "new-mpp-lib-and-app")
with(libProject) {
setupWorkingDir()
appProject.setupWorkingDir()
appProject.projectDir.copyRecursively(projectDir.resolve("sample-app"))
projectDir.resolve("settings.gradle").writeText("include 'sample-app'") // also disables feature preview 'GRADLE_METADATA'
gradleBuildScript("sample-app").modify {
it.replace("'com.example:sample-lib:1.0'", "project(':')") + "\n" + """
apply plugin: 'maven-publish'
group = "com.exampleapp"
version = "1.0"
publishing {
repositories {
maven { url "file://${'$'}{rootProject.projectDir.absolutePath.replace('\\', '/')}/repo" }
}
}
""".trimIndent()
}
gradleBuildScript().appendText("\n" + """
publishing {
publications {
jvm6 {
groupId = "foo"
artifactId = "bar"
version = "42"
}
}
}
""".trimIndent())
build("publish") {
assertSuccessful()
assertFileContains(
"repo/com/exampleapp/sample-app-nodejs/1.0/sample-app-nodejs-1.0.pom",
"<groupId>com.example</groupId>",
"<artifactId>sample-lib-nodejs</artifactId>",
"<version>1.0</version>"
)
assertFileContains(
"repo/com/exampleapp/sample-app-jvm8/1.0/sample-app-jvm8-1.0.pom",
"<groupId>foo</groupId>",
"<artifactId>bar</artifactId>",
"<version>42</version>"
)
assertFileExists("repo/foo/bar/42/bar-42.jar")
}
}
}
@Test
fun testJsDceInMpp() = with(Project("new-mpp-js-dce", gradleVersion)) {
build("runRhino") {
assertSuccessful()
assertTasksExecuted(":mainProject:runDceNodeJsKotlin")
val pathPrefix = "mainProject/build/kotlin-js-min/nodeJs/main"
assertFileExists("$pathPrefix/exampleapp.js.map")
assertFileExists("$pathPrefix/examplelib.js.map")
assertFileContains("$pathPrefix/exampleapp.js.map", "\"../../../../src/nodeJsMain/kotlin/exampleapp/main.kt\"")
assertFileExists("$pathPrefix/kotlin.js")
assertTrue(fileInWorkingDir("$pathPrefix/kotlin.js").length() < 500 * 1000, "Looks like kotlin.js file was not minified by DCE")
}
}
}